From c2460a708a3b652ce5623e28b533b92f9de0dada Mon Sep 17 00:00:00 2001 From: xdustinface Date: Tue, 19 May 2026 11:11:23 +1000 Subject: [PATCH] chore: remove FFI layer Delete the `dash-spv-ffi` and `key-wallet-ffi` crates and remove the optional `ffi` feature, `ffi` module, `build.rs`, and `cbindgen.toml` from `dash-network`. It is no longer used by anything. Keeping it around for potential future use just eats time and tokens to keep it in sync with the Rust API, make sure it's tested and working, and review all the changes happening to it. I think it makes more sense to just generate a new FFI layer if we ever come up with a use case or if someone actually requests it. If someone is actually using it they will probably look into here and are welcome to open an issue, so we can consider reverting the PR. But at the moment all the effort we put into this is better put into something else. --- .codecov.yml | 8 +- .github/ci-groups.yml | 4 - .github/workflows/build-and-test.yml | 15 +- .github/workflows/sanitizer.yml | 6 - .gitignore | 2 - .pre-commit-config.yaml | 8 - AGENTS.md | 7 +- CLAUDE.md | 22 +- CONTRIBUTING.md | 6 +- Cargo.toml | 2 +- README.md | 3 - contrib/verify_ffi.py | 72 - dash-network/Cargo.toml | 5 - dash-network/build.rs | 37 - dash-network/cbindgen.toml | 11 - dash-network/src/ffi.rs | 81 - dash-network/src/lib.rs | 3 - dash-spv-ffi/CLAUDE.md | 125 - dash-spv-ffi/Cargo.toml | 44 - dash-spv-ffi/FFI_API.md | 808 --- dash-spv-ffi/FFI_DOCS_README.md | 89 - dash-spv-ffi/README.md | 112 - dash-spv-ffi/build.rs | 32 - dash-spv-ffi/cbindgen.toml | 38 - dash-spv-ffi/examples/basic_usage.c | 42 - dash-spv-ffi/examples/wallet_manager_usage.rs | 75 - dash-spv-ffi/scripts/generate_ffi_docs.py | 380 -- dash-spv-ffi/src/bin/ffi_cli.rs | 604 --- dash-spv-ffi/src/callbacks.rs | 1373 ----- dash-spv-ffi/src/client.rs | 425 -- dash-spv-ffi/src/config.rs | 345 -- dash-spv-ffi/src/error.rs | 115 - dash-spv-ffi/src/lib.rs | 48 - dash-spv-ffi/src/platform_integration.rs | 200 - dash-spv-ffi/src/types.rs | 541 -- dash-spv-ffi/src/utils.rs | 93 - dash-spv-ffi/tests/README.md | 106 - dash-spv-ffi/tests/dashd_sync/callbacks.rs | 626 --- dash-spv-ffi/tests/dashd_sync/context.rs | 420 -- dash-spv-ffi/tests/dashd_sync/main.rs | 11 - dash-spv-ffi/tests/dashd_sync/tests_basic.rs | 79 - .../tests/dashd_sync/tests_callback.rs | 518 -- .../tests/dashd_sync/tests_restart.rs | 61 - .../tests/dashd_sync/tests_transaction.rs | 351 -- dash-spv-ffi/tests/test_client.rs | 78 - dash-spv-ffi/tests/test_config.rs | 110 - dash-spv-ffi/tests/test_error.rs | 48 - .../test_platform_integration_minimal.rs | 15 - .../tests/test_platform_integration_safety.rs | 305 -- dash-spv-ffi/tests/test_types.rs | 200 - dash-spv-ffi/tests/test_utils.rs | 39 - dash-spv-ffi/tests/test_wallet_manager.rs | 139 - .../tests/unit/test_async_operations.rs | 100 - .../tests/unit/test_client_lifecycle.rs | 343 -- dash-spv-ffi/tests/unit/test_configuration.rs | 244 - .../tests/unit/test_error_handling.rs | 173 - .../tests/unit/test_memory_management.rs | 320 -- .../tests/unit/test_type_conversions.rs | 111 - dash-spv/src/test_utils/context.rs | 2 +- ffi-c-tests/header-tests/all.c | 5 - ffi-c-tests/header-tests/dash-network.c | 3 - ffi-c-tests/header-tests/dash-spv.c | 3 - ffi-c-tests/header-tests/key-wallet.c | 3 - ffi-c-tests/validate-headers.sh | 34 - key-wallet-ffi/Cargo.toml | 48 - key-wallet-ffi/FFI_API.md | 4416 ----------------- key-wallet-ffi/FFI_DOCS_README.md | 111 - key-wallet-ffi/IMPORT_WALLET_FFI.md | 104 - key-wallet-ffi/README.md | 133 - key-wallet-ffi/build-ios.sh | 33 - key-wallet-ffi/build.rs | 32 - key-wallet-ffi/cbindgen.toml | 66 - key-wallet-ffi/examples/check_transaction.c | 131 - key-wallet-ffi/scripts/generate_ffi_docs.py | 321 -- key-wallet-ffi/src/account.rs | 570 --- key-wallet-ffi/src/account_collection.rs | 1573 ------ key-wallet-ffi/src/account_derivation.rs | 406 -- .../src/account_derivation_tests.rs | 249 - key-wallet-ffi/src/account_tests.rs | 201 - key-wallet-ffi/src/address.rs | 106 - key-wallet-ffi/src/address_pool.rs | 1178 ----- key-wallet-ffi/src/address_tests.rs | 205 - key-wallet-ffi/src/bip38.rs | 42 - key-wallet-ffi/src/derivation.rs | 625 --- key-wallet-ffi/src/derivation_tests.rs | 976 ---- key-wallet-ffi/src/error.rs | 457 -- key-wallet-ffi/src/keys.rs | 557 --- key-wallet-ffi/src/keys_tests.rs | 607 --- key-wallet-ffi/src/lib.rs | 63 - key-wallet-ffi/src/managed_account.rs | 2679 ---------- .../src/managed_account_collection.rs | 1259 ----- key-wallet-ffi/src/managed_wallet.rs | 860 ---- key-wallet-ffi/src/managed_wallet_tests.rs | 320 -- key-wallet-ffi/src/mnemonic.rs | 264 - key-wallet-ffi/src/mnemonic_tests.rs | 693 --- key-wallet-ffi/src/transaction.rs | 873 ---- key-wallet-ffi/src/transaction_checking.rs | 493 -- key-wallet-ffi/src/types.rs | 1184 ----- key-wallet-ffi/src/utils.rs | 47 - key-wallet-ffi/src/utils_tests.rs | 66 - key-wallet-ffi/src/utxo.rs | 193 - key-wallet-ffi/src/utxo_tests.rs | 570 --- key-wallet-ffi/src/wallet.rs | 827 --- key-wallet-ffi/src/wallet_manager.rs | 617 --- .../src/wallet_manager_serialization_tests.rs | 364 -- key-wallet-ffi/src/wallet_manager_tests.rs | 1117 ----- key-wallet-ffi/src/wallet_tests.rs | 364 -- key-wallet-ffi/tests/check_address.rs | 31 - key-wallet-ffi/tests/debug_addr.rs | 34 - key-wallet-ffi/tests/debug_wallet_add.rs | 41 - key-wallet-ffi/tests/ffi_tests.rs | 13 - key-wallet-ffi/tests/integration_test.rs | 210 - .../tests/test_account_collection.rs | 196 - key-wallet-ffi/tests/test_addr_checksum.rs | 31 - key-wallet-ffi/tests/test_addr_simple.rs | 42 - .../tests/test_error_conversions.rs | 209 - key-wallet-ffi/tests/test_import_wallet.rs | 74 - .../tests/test_managed_account_collection.rs | 463 -- key-wallet-ffi/tests/test_valid_addr.rs | 48 - key-wallet/CLAUDE.md | 18 +- key-wallet/README.md | 1 - 121 files changed, 18 insertions(+), 37621 deletions(-) delete mode 100755 contrib/verify_ffi.py delete mode 100644 dash-network/build.rs delete mode 100644 dash-network/cbindgen.toml delete mode 100644 dash-network/src/ffi.rs delete mode 100644 dash-spv-ffi/CLAUDE.md delete mode 100644 dash-spv-ffi/Cargo.toml delete mode 100644 dash-spv-ffi/FFI_API.md delete mode 100644 dash-spv-ffi/FFI_DOCS_README.md delete mode 100644 dash-spv-ffi/README.md delete mode 100644 dash-spv-ffi/build.rs delete mode 100644 dash-spv-ffi/cbindgen.toml delete mode 100644 dash-spv-ffi/examples/basic_usage.c delete mode 100644 dash-spv-ffi/examples/wallet_manager_usage.rs delete mode 100755 dash-spv-ffi/scripts/generate_ffi_docs.py delete mode 100644 dash-spv-ffi/src/bin/ffi_cli.rs delete mode 100644 dash-spv-ffi/src/callbacks.rs delete mode 100644 dash-spv-ffi/src/client.rs delete mode 100644 dash-spv-ffi/src/config.rs delete mode 100644 dash-spv-ffi/src/error.rs delete mode 100644 dash-spv-ffi/src/lib.rs delete mode 100644 dash-spv-ffi/src/platform_integration.rs delete mode 100644 dash-spv-ffi/src/types.rs delete mode 100644 dash-spv-ffi/src/utils.rs delete mode 100644 dash-spv-ffi/tests/README.md delete mode 100644 dash-spv-ffi/tests/dashd_sync/callbacks.rs delete mode 100644 dash-spv-ffi/tests/dashd_sync/context.rs delete mode 100644 dash-spv-ffi/tests/dashd_sync/main.rs delete mode 100644 dash-spv-ffi/tests/dashd_sync/tests_basic.rs delete mode 100644 dash-spv-ffi/tests/dashd_sync/tests_callback.rs delete mode 100644 dash-spv-ffi/tests/dashd_sync/tests_restart.rs delete mode 100644 dash-spv-ffi/tests/dashd_sync/tests_transaction.rs delete mode 100644 dash-spv-ffi/tests/test_client.rs delete mode 100644 dash-spv-ffi/tests/test_config.rs delete mode 100644 dash-spv-ffi/tests/test_error.rs delete mode 100644 dash-spv-ffi/tests/test_platform_integration_minimal.rs delete mode 100644 dash-spv-ffi/tests/test_platform_integration_safety.rs delete mode 100644 dash-spv-ffi/tests/test_types.rs delete mode 100644 dash-spv-ffi/tests/test_utils.rs delete mode 100644 dash-spv-ffi/tests/test_wallet_manager.rs delete mode 100644 dash-spv-ffi/tests/unit/test_async_operations.rs delete mode 100644 dash-spv-ffi/tests/unit/test_client_lifecycle.rs delete mode 100644 dash-spv-ffi/tests/unit/test_configuration.rs delete mode 100644 dash-spv-ffi/tests/unit/test_error_handling.rs delete mode 100644 dash-spv-ffi/tests/unit/test_memory_management.rs delete mode 100644 dash-spv-ffi/tests/unit/test_type_conversions.rs delete mode 100644 ffi-c-tests/header-tests/all.c delete mode 100644 ffi-c-tests/header-tests/dash-network.c delete mode 100644 ffi-c-tests/header-tests/dash-spv.c delete mode 100644 ffi-c-tests/header-tests/key-wallet.c delete mode 100755 ffi-c-tests/validate-headers.sh delete mode 100644 key-wallet-ffi/Cargo.toml delete mode 100644 key-wallet-ffi/FFI_API.md delete mode 100644 key-wallet-ffi/FFI_DOCS_README.md delete mode 100644 key-wallet-ffi/IMPORT_WALLET_FFI.md delete mode 100644 key-wallet-ffi/README.md delete mode 100755 key-wallet-ffi/build-ios.sh delete mode 100644 key-wallet-ffi/build.rs delete mode 100644 key-wallet-ffi/cbindgen.toml delete mode 100644 key-wallet-ffi/examples/check_transaction.c delete mode 100755 key-wallet-ffi/scripts/generate_ffi_docs.py delete mode 100644 key-wallet-ffi/src/account.rs delete mode 100644 key-wallet-ffi/src/account_collection.rs delete mode 100644 key-wallet-ffi/src/account_derivation.rs delete mode 100644 key-wallet-ffi/src/account_derivation_tests.rs delete mode 100644 key-wallet-ffi/src/account_tests.rs delete mode 100644 key-wallet-ffi/src/address.rs delete mode 100644 key-wallet-ffi/src/address_pool.rs delete mode 100644 key-wallet-ffi/src/address_tests.rs delete mode 100644 key-wallet-ffi/src/bip38.rs delete mode 100644 key-wallet-ffi/src/derivation.rs delete mode 100644 key-wallet-ffi/src/derivation_tests.rs delete mode 100644 key-wallet-ffi/src/error.rs delete mode 100644 key-wallet-ffi/src/keys.rs delete mode 100644 key-wallet-ffi/src/keys_tests.rs delete mode 100644 key-wallet-ffi/src/lib.rs delete mode 100644 key-wallet-ffi/src/managed_account.rs delete mode 100644 key-wallet-ffi/src/managed_account_collection.rs delete mode 100644 key-wallet-ffi/src/managed_wallet.rs delete mode 100644 key-wallet-ffi/src/managed_wallet_tests.rs delete mode 100644 key-wallet-ffi/src/mnemonic.rs delete mode 100644 key-wallet-ffi/src/mnemonic_tests.rs delete mode 100644 key-wallet-ffi/src/transaction.rs delete mode 100644 key-wallet-ffi/src/transaction_checking.rs delete mode 100644 key-wallet-ffi/src/types.rs delete mode 100644 key-wallet-ffi/src/utils.rs delete mode 100644 key-wallet-ffi/src/utils_tests.rs delete mode 100644 key-wallet-ffi/src/utxo.rs delete mode 100644 key-wallet-ffi/src/utxo_tests.rs delete mode 100644 key-wallet-ffi/src/wallet.rs delete mode 100644 key-wallet-ffi/src/wallet_manager.rs delete mode 100644 key-wallet-ffi/src/wallet_manager_serialization_tests.rs delete mode 100644 key-wallet-ffi/src/wallet_manager_tests.rs delete mode 100644 key-wallet-ffi/src/wallet_tests.rs delete mode 100644 key-wallet-ffi/tests/check_address.rs delete mode 100644 key-wallet-ffi/tests/debug_addr.rs delete mode 100644 key-wallet-ffi/tests/debug_wallet_add.rs delete mode 100644 key-wallet-ffi/tests/ffi_tests.rs delete mode 100644 key-wallet-ffi/tests/integration_test.rs delete mode 100644 key-wallet-ffi/tests/test_account_collection.rs delete mode 100644 key-wallet-ffi/tests/test_addr_checksum.rs delete mode 100644 key-wallet-ffi/tests/test_addr_simple.rs delete mode 100644 key-wallet-ffi/tests/test_error_conversions.rs delete mode 100644 key-wallet-ffi/tests/test_import_wallet.rs delete mode 100644 key-wallet-ffi/tests/test_managed_account_collection.rs delete mode 100644 key-wallet-ffi/tests/test_valid_addr.rs diff --git a/.codecov.yml b/.codecov.yml index ab824c071..88619c441 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -1,7 +1,7 @@ codecov: notify: - # Must match the number of coverage-uploading groups (core, spv, wallet, ffi, rpc) - after_n_builds: 5 + # Must match the number of coverage-uploading groups (core, spv, wallet, rpc) + after_n_builds: 4 coverage: status: @@ -36,10 +36,6 @@ flags: paths: - key-wallet/src/ - key-wallet-manager/src/ - ffi: - paths: - - dash-spv-ffi/src/ - - key-wallet-ffi/src/ rpc: paths: - rpc-client/src/ diff --git a/.github/ci-groups.yml b/.github/ci-groups.yml index 288e4703b..6006ea06a 100644 --- a/.github/ci-groups.yml +++ b/.github/ci-groups.yml @@ -16,10 +16,6 @@ groups: - key-wallet - key-wallet-manager - ffi: - - dash-spv-ffi - - key-wallet-ffi - rpc: - dashcore-rpc - dashcore-rpc-json diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 0422965cd..c21cf6b07 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -27,7 +27,7 @@ jobs: strategy: fail-fast: false matrix: - group: ["core", "spv", "wallet", "ffi", "rpc"] + group: ["core", "spv", "wallet", "rpc"] env: RUST_BACKTRACE: 1 steps: @@ -49,14 +49,14 @@ jobs: # Set up dashd and test data for groups that need it - name: Cache dashd and test data - if: matrix.group == 'spv' || matrix.group == 'ffi' + if: matrix.group == 'spv' uses: actions/cache@v5 with: path: .rust-dashcore-test key: rust-dashcore-test-${{ inputs.os }}-${{ env.DASHVERSION }}-${{ env.TEST_DATA_REPO }}-${{ env.TEST_DATA_VERSION }} - name: Setup dashd for integration tests - if: matrix.group == 'spv' || matrix.group == 'ffi' + if: matrix.group == 'spv' env: CACHE_DIR: ${{ github.workspace }}/.rust-dashcore-test shell: bash @@ -65,7 +65,7 @@ jobs: - name: Run tests id: tests env: - DASHD_TEST_RETAIN_DIR: ${{ (matrix.group == 'spv' || matrix.group == 'ffi') && format('{0}/dashd-test-logs', runner.temp) || '' }} + DASHD_TEST_RETAIN_DIR: ${{ matrix.group == 'spv' && format('{0}/dashd-test-logs', runner.temp) || '' }} run: > python .github/scripts/ci_config.py run-group ${{ matrix.group }} --os ${{ inputs.os }} @@ -85,15 +85,10 @@ jobs: fail_ci_if_error: true - name: Upload failed dashd test logs - if: failure() && (matrix.group == 'spv' || matrix.group == 'ffi') + if: failure() && matrix.group == 'spv' uses: actions/upload-artifact@v4 with: name: ${{ matrix.group }}-test-logs-${{ inputs.os }} path: ${{ runner.temp }}/dashd-test-logs/ retention-days: 7 if-no-files-found: ignore - - name: Validate headers - if: matrix.group == 'ffi' && inputs.os == 'ubuntu-latest' - run: | - sudo apt update && sudo apt install -y build-essential - bash ffi-c-tests/validate-headers.sh ./target/llvm-cov-target/debug/include diff --git a/.github/workflows/sanitizer.yml b/.github/workflows/sanitizer.yml index 38f3bd962..230c73577 100644 --- a/.github/workflows/sanitizer.yml +++ b/.github/workflows/sanitizer.yml @@ -8,8 +8,6 @@ on: - 'v**-dev' pull_request: paths: - - 'key-wallet-ffi/**' - - 'dash-spv-ffi/**' - 'dashcore/**' - 'dashcore_hashes/**' - 'key-wallet/**' @@ -40,10 +38,6 @@ jobs: LSAN_OPTIONS: "fast_unwind_on_malloc=0" SKIP_DASHD_TESTS: 1 run: | - # FFI crates (C interop) - cargo +nightly test -Zbuild-std --target x86_64-unknown-linux-gnu \ - -p key-wallet-ffi -p dash-spv-ffi --lib --tests - # Core crypto crates (unsafe optimizations) cargo +nightly test -Zbuild-std --target x86_64-unknown-linux-gnu \ -p dashcore -p dashcore_hashes --lib --tests diff --git a/.gitignore b/.gitignore index e8ca067f8..78c251eba 100644 --- a/.gitignore +++ b/.gitignore @@ -26,8 +26,6 @@ coordination.md # Build artifacts **/*.rs.bk -dash-spv-ffi/include/ -key-wallet-ffi/include/ *.a *.so *.dylib diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7ad1fec2f..d7d0594ee 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -62,14 +62,6 @@ repos: types: [rust] pass_filenames: false - - id: verify-ffi - name: verify FFI - description: Verify FFI documentation is up to date - entry: contrib/verify_ffi.py - language: python - pass_filenames: false - files: ^(key-wallet-ffi|dash-spv-ffi)/.*\.(rs|toml|py)$ - # ============================================================================ # SLOW CHECKS - Run on git push only. # ============================================================================ diff --git a/AGENTS.md b/AGENTS.md index c5832e1ff..36fd764f9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,15 +1,14 @@ # Repository Guidelines ## Project Structure & Module Organization -- Workspace with crates: `dash`, `hashes`, `internals`, `dash-spv`, `key-wallet`, `rpc-*`, utilities (`fuzz`, `test-utils`), and FFI crates (`*-ffi`). +- Workspace with crates: `dash`, `hashes`, `internals`, `dash-spv`, `key-wallet`, `rpc-*`, and utilities (`fuzz`, `test-utils`). - Each crate keeps sources in `src/`; unit tests live alongside code with `#[cfg(test)]`. Integration tests use `tests/` (e.g., `rpc-integration-test`). -- FFI bindings are in `*-ffi`. Shared helpers in `internals/` and `test-utils/`. +- Shared helpers in `internals/` and `test-utils/`. ## Build, Test, and Development Commands - MSRV: 1.89. Build all: `cargo build --workspace --all-features` - Test all: `cargo test --workspace --all-features` or `./contrib/test.sh` (set `DO_COV=true`, `DO_LINT=true`, `DO_FMT=true` as needed) - Targeted tests: `cargo test -p dash-spv --all-features` -- FFI iOS builds: `cd key-wallet-ffi && ./build-ios.sh` - Lint/format: `cargo clippy --workspace --all-targets -- -D warnings` and `cargo fmt --all` - Docs: `cargo doc --workspace` (add `--open` locally) @@ -28,7 +27,7 @@ - Prefer Conventional Commits: `feat:`, `fix:`, `refactor:`, `chore:`, `docs:`. Keep subject ≤72 chars with clear scope and rationale. - Target branches: feature work to `v**-dev` (development), hotfixes/docs to `master` unless directed otherwise. - Pre‑PR checks: `cargo fmt`, `cargo clippy`, `cargo test` (workspace). Update docs/CHANGELOG if user-facing. -- Include in PRs: description, linked issues, test evidence (commands/output), and notes on features/FFI impacts. +- Include in PRs: description, linked issues, test evidence (commands/output), and notes on feature impacts. ## Security & Configuration Tips - Not for consensus‑critical validation; do not rely on exact Dash Core consensus behavior. diff --git a/CLAUDE.md b/CLAUDE.md index 74a64f405..96676ed26 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,7 +10,6 @@ rust-dashcore is a Rust implementation of the Dash cryptocurrency protocol libra - Network protocol implementation - SPV (Simplified Payment Verification) client - HD wallet functionality (BIP32/BIP39/DIP9) -- FFI bindings for C and Swift integration - JSON-RPC client for Dash Core nodes **IMPORTANT**: This library should NOT be used for consensus code. The exact behavior of the consensus-critical parts of Dash Core cannot be replicated without an exact copy of the C++ code. @@ -24,11 +23,9 @@ rust-dashcore is a Rust implementation of the Dash cryptocurrency protocol libra ### SPV - `dash-spv/` - SPV client implementation -- `dash-spv-ffi/` - C-compatible FFI bindings for SPV client ### Wallet & Keys - `key-wallet/` - Comprehensive HD wallet implementation with multi-account support, address pools, and transaction management (see key-wallet/CLAUDE.md for detailed architecture) -- `key-wallet-ffi/` - C-compatible FFI bindings for wallet functionality ### RPC & Integration - `rpc-client/` - JSON-RPC client for Dash Core nodes @@ -52,12 +49,6 @@ cargo build --release cargo build -p dash-spv ``` -### FFI Library Build -```bash -# Build iOS libraries for key-wallet-ffi -cd key-wallet-ffi && ./build-ios.sh -``` - ### iOS/macOS Targets ```bash # Add iOS targets @@ -101,9 +92,9 @@ DO_FMT=true ./contrib/test.sh ### Integration Tests (dashd) -The `dash-spv` and `dash-spv-ffi` crates include integration tests that run against a real `dashd` regtest node. These tests cover SPV sync, wallet operations, restarts, disconnections, and transactions. +The `dash-spv` crate includes integration tests that run against a real `dashd` regtest node. These tests cover SPV sync, wallet operations, restarts, disconnections, and transactions. -**Setup:** `contrib/setup-dashd.py` downloads the dashd binary and regtest blockchain test data, caching them in `~/.rust-dashcore-test/`. It outputs the required environment variables. Always run this before testing `dash-spv` or `dash-spv-ffi` — integration tests catch critical bugs (restart, resync, disconnection) that unit tests miss. +**Setup:** `contrib/setup-dashd.py` downloads the dashd binary and regtest blockchain test data, caching them in `~/.rust-dashcore-test/`. It outputs the required environment variables. Always run this before testing `dash-spv`. Integration tests catch critical bugs (restart, resync, disconnection) that unit tests miss. ```bash eval $(python3 contrib/setup-dashd.py) @@ -112,7 +103,6 @@ eval $(python3 contrib/setup-dashd.py) **Running:** Always run with integration tests enabled after setting up dashd. Do not use `SKIP_DASHD_TESTS=1`. Use `DASHD_TEST_RETAIN_DIR` so test logs are available for debugging failures. ```bash DASHD_TEST_RETAIN_DIR=/tmp/dashd-test-logs cargo test -p dash-spv -DASHD_TEST_RETAIN_DIR=/tmp/dashd-test-logs cargo test -p dash-spv-ffi --test dashd_sync ``` **Debugging:** When tests fail, check the retained logs at the path specified by `DASHD_TEST_RETAIN_DIR`. Each test creates a subdirectory with SPV logs (`spv/logs/run.log`) and dashd data. @@ -121,9 +111,8 @@ DASHD_TEST_RETAIN_DIR=/tmp/dashd-test-logs cargo test -p dash-spv-ffi --test das **Key files:** - `dash-spv/tests/dashd_sync/` — test modules (basic, restart, disconnect, transaction) -- `dash-spv-ffi/tests/dashd_sync/` — FFI test modules (basic, restart, transaction, callback) - `dash-spv/src/test_utils/` — shared infrastructure (`DashdTestContext`, `DashCoreNode`) -- `.github/ci-groups.yml` — CI test group definitions (`spv` and `ffi` groups run dashd tests) +- `.github/ci-groups.yml` — CI test group definitions (`spv` group runs dashd tests) ## Development Commands @@ -162,7 +151,6 @@ cargo doc --open ### Architecture Highlights - **Workspace-based**: Multiple crates with clear separation of concerns - **Async/Await**: Modern async Rust throughout -- **FFI Support**: C and Swift bindings for cross-platform usage - **Comprehensive Testing**: Unit, integration, and fuzz testing - **MSRV**: Rust 1.89 minimum supported version @@ -172,7 +160,6 @@ cargo doc --open - **No Hardcoded Values**: Never hardcode network parameters, addresses, or keys - **Error Handling**: Use proper error types (thiserror) and propagate errors appropriately - **Async Code**: Use tokio runtime for async operations -- **Memory Safety**: Careful handling in FFI boundaries - **Feature Flags**: Use conditional compilation for optional features ### Testing Requirements @@ -188,7 +175,6 @@ cargo doc --open ## Current Status The project is actively developing: -- FFI bindings improvements - Support for Dash Core versions 0.18.0 - 0.23.x ## Security Considerations @@ -197,7 +183,6 @@ The project is actively developing: - Always validate inputs from untrusted sources - Use secure random number generation for keys - Never log or expose private keys -- Be careful with FFI memory management ## API Stability @@ -207,5 +192,4 @@ The API is currently unstable (version 0.x.x). Breaking changes may occur in min - Cannot replicate exact consensus behavior of Dash Core - Not suitable for mining or consensus validation -- FFI bindings have limited error propagation - Some Dash Core RPC methods not yet implemented diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 814ef8730..122a9bbdf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,7 +33,7 @@ changes to this document in a pull request. ## General -We welcome contributions of all kinds: bug fixes, features, tests, docs, and reviews. This codebase powers Dash protocol libraries (networking, SPV, wallet, FFI). Changes must be reviewed with security and backward‑compatibility in mind. +We welcome contributions of all kinds: bug fixes, features, tests, docs, and reviews. This codebase powers Dash protocol libraries (networking, SPV, wallet). Changes must be reviewed with security and backward‑compatibility in mind. ## Communication @@ -110,8 +110,6 @@ That's it! Hooks run automatically from now on. **On git push** (~30-90 seconds additional): - `cargo clippy` — Strict linting on entire workspace -- `verify-ffi-headers` — Ensures FFI C headers are up to date -- `verify-ffi-docs` — Ensures FFI API documentation is current **Note:** CI runs the exact same checks, so passing locally = passing in CI. @@ -179,7 +177,7 @@ Use Rust standards: `UpperCamelCase` for types/traits, `snake_case` for modules/ ### Unsafe code -Minimize `unsafe`. When required (especially across FFI boundaries), encapsulate it, document invariants, add tests, and consider Miri/sanitizers. +Minimize `unsafe`. When required, encapsulate it, document invariants, add tests, and consider Miri/sanitizers. ## Security diff --git a/Cargo.toml b/Cargo.toml index 5491e2f88..0ff8d614f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["dash", "dash-network", "hashes", "internals", "fuzz", "rpc-client", "rpc-json", "rpc-integration-test", "key-wallet", "key-wallet-manager", "key-wallet-ffi", "dash-spv", "dash-spv-ffi", "dash-network-seeds", "masternode-seeds-fetcher"] +members = ["dash", "dash-network", "hashes", "internals", "fuzz", "rpc-client", "rpc-json", "rpc-integration-test", "key-wallet", "key-wallet-manager", "dash-spv", "dash-network-seeds", "masternode-seeds-fetcher"] resolver = "2" [workspace.package] diff --git a/README.md b/README.md index 96d3921e3..00507cee7 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,6 @@ | core | dashcore, dashcore_hashes, dashcore-private | [![codecov](https://codecov.io/gh/dashpay/rust-dashcore/graph/badge.svg?flag=core)](https://codecov.io/gh/dashpay/rust-dashcore?flags[0]=core) | | spv | dash-spv | [![codecov](https://codecov.io/gh/dashpay/rust-dashcore/graph/badge.svg?flag=spv)](https://codecov.io/gh/dashpay/rust-dashcore?flags[0]=spv) | | wallet | key-wallet | [![codecov](https://codecov.io/gh/dashpay/rust-dashcore/graph/badge.svg?flag=wallet)](https://codecov.io/gh/dashpay/rust-dashcore?flags[0]=wallet) | -| ffi | dash-spv-ffi, key-wallet-ffi | [![codecov](https://codecov.io/gh/dashpay/rust-dashcore/graph/badge.svg?flag=ffi)](https://codecov.io/gh/dashpay/rust-dashcore?flags[0]=ffi) | | rpc | dashcore-rpc, dashcore-rpc-json | [![codecov](https://codecov.io/gh/dashpay/rust-dashcore/graph/badge.svg?flag=rpc)](https://codecov.io/gh/dashpay/rust-dashcore?flags[0]=rpc) | @@ -45,7 +44,6 @@ Supports (or should support) * PSBT creation, manipulation, merging and finalization * Pay-to-contract support as in Appendix A of the [Blockstream sidechains whitepaper](https://www.blockstream.com/sidechains.pdf) * JSONRPC interaction with Dash Core -* FFI bindings for C/Swift integration (dash-spv-ffi, key-wallet-ffi) * High-level wallet management with transaction building and UTXO management # Known limitations @@ -99,7 +97,6 @@ See `client/examples/` for more usage examples. This library provides comprehensive wallet functionality through multiple components: * **key-wallet**: Low-level cryptographic primitives for HD wallets, mnemonic generation, and key derivation; and high-level wallet management with transaction building, UTXO tracking, and coin selection -* **key-wallet-ffi**: C/Swift FFI bindings for mobile integration * **dash-spv**: SPV (Simplified Payment Verification) client implementation # Supported Dash Core Versions diff --git a/contrib/verify_ffi.py b/contrib/verify_ffi.py deleted file mode 100755 index abbbb374a..000000000 --- a/contrib/verify_ffi.py +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env python3 -"""Verify that FFI documentation is up to date.""" - -import subprocess -import sys -from pathlib import Path -from concurrent.futures import ThreadPoolExecutor - -FFI_CRATES = ["key-wallet-ffi", "dash-spv-ffi"] - - -def generate_ffi_docs(crate_dir: Path) -> tuple[str, int, str]: - """Generate FFI documentation for a crate.""" - print(f" Generating {crate_dir.name} docs...") - result = subprocess.run( - [sys.executable, "scripts/generate_ffi_docs.py"], - cwd=crate_dir, - capture_output=True, - text=True - ) - output = result.stdout - if result.returncode != 0 and result.stderr: - output = result.stderr - return crate_dir.name, result.returncode, output - - -def main(): - repo_root = Path(__file__).parent.parent - ffi_crate_dirs = [repo_root / crate for crate in FFI_CRATES] - - print("Regenerating FFI documentation") - - # Generate docs in parallel - with ThreadPoolExecutor(max_workers=2) as executor: - doc_futures = [executor.submit(generate_ffi_docs, crate) for crate in ffi_crate_dirs] - doc_results = [f.result() for f in doc_futures] - - # Check results and print output - for crate_name, returncode, stdout in doc_results: - if returncode != 0: - print(f"Documentation generation failed for {crate_name}", file=sys.stderr) - sys.exit(1) - if stdout: - for line in stdout.strip().split('\n'): - print(f" {line}") - - print(" Generation complete, checking for changes...") - - # Check if docs changed - docs_result = subprocess.run( - ["git", "diff", "--exit-code", "--quiet", "--", - "key-wallet-ffi/FFI_API.md", "dash-spv-ffi/FFI_API.md"], - cwd=repo_root - ) - - if docs_result.returncode != 0: - print() - print("FFI documentation is out of date!\n") - print("Documentation changes detected:") - subprocess.run( - ["git", "--no-pager", "diff", "--", - "key-wallet-ffi/FFI_API.md", "dash-spv-ffi/FFI_API.md"], - cwd=repo_root - ) - print() - sys.exit(1) - - print("FFI documentation is up to date") - - -if __name__ == "__main__": - main() diff --git a/dash-network/Cargo.toml b/dash-network/Cargo.toml index e8c5fa407..6dc3f5efa 100644 --- a/dash-network/Cargo.toml +++ b/dash-network/Cargo.toml @@ -17,13 +17,8 @@ default = [] serde = ["dep:serde"] # bincode::{Encode, Decode} impls for Network. bincode = ["dep:bincode", "dep:bincode_derive"] -# C-ABI `FFINetwork` mirror + `dashcore_network_get_name` extern fn. -ffi = [] [dependencies] serde = { version = "1.0.219", default-features = false, features = ["derive"], optional = true } bincode = { version = "2.0.1", optional = true } bincode_derive = { version = "2.0.1", optional = true } - -[build-dependencies] -cbindgen = "0.29" diff --git a/dash-network/build.rs b/dash-network/build.rs deleted file mode 100644 index 7bcf382a6..000000000 --- a/dash-network/build.rs +++ /dev/null @@ -1,37 +0,0 @@ -use std::{env, fs, path::Path}; - -fn main() { - if std::env::var("CARGO_FEATURE_FFI").is_ok() { - generate_bindings(); - } -} - -fn generate_bindings() { - let crate_name = env::var("CARGO_PKG_NAME").unwrap(); - let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); - let out_dir = env::var("OUT_DIR").unwrap(); - - println!("cargo:rerun-if-changed=cbindgen.toml"); - println!("cargo:rerun-if-changed=src/"); - - let target_dir = Path::new(&out_dir) - .ancestors() - .nth(3) // This line moves up to the target/ directory - .expect("Failed to find target dir"); - - let include_dir = target_dir.join("include").join(&crate_name); - - fs::create_dir_all(&include_dir).unwrap(); - - let output_path = include_dir.join(format!("{}.h", &crate_name)); - - let config_path = Path::new(&crate_dir).join("cbindgen.toml"); - let config = cbindgen::Config::from_file(&config_path).expect("Failed to read cbindgen.toml"); - - cbindgen::Builder::new() - .with_crate(&crate_dir) - .with_config(config) - .generate() - .expect("Unable to generate bindings") - .write_to_file(&output_path); -} diff --git a/dash-network/cbindgen.toml b/dash-network/cbindgen.toml deleted file mode 100644 index 177ad2c9c..000000000 --- a/dash-network/cbindgen.toml +++ /dev/null @@ -1,11 +0,0 @@ -language = "C" -header = "/* dashcore C bindings - Auto-generated by cbindgen */" -include_guard = "DASH_NETWORK_H" -autogen_warning = "/* Warning: This file is auto-generated by cbindgen. Do not modify manually. */" -include_version = true - -[parse.expand] -features = ["ffi"] - -[enum] -prefix_with_name = true diff --git a/dash-network/src/ffi.rs b/dash-network/src/ffi.rs deleted file mode 100644 index 532d3a6d4..000000000 --- a/dash-network/src/ffi.rs +++ /dev/null @@ -1,81 +0,0 @@ -use std::ffi; - -use crate::Network; - -/// FFI-compatible variant of [`Network`]. Converts to/from [`Network`] via [`From`]/[`Into`]. -#[repr(C)] -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum FFINetwork { - Mainnet = 0, - Testnet = 1, - Devnet = 2, - Regtest = 3, -} - -impl From for FFINetwork { - fn from(network: Network) -> Self { - match network { - Network::Mainnet => FFINetwork::Mainnet, - Network::Testnet => FFINetwork::Testnet, - Network::Devnet => FFINetwork::Devnet, - Network::Regtest => FFINetwork::Regtest, - } - } -} - -impl From for Network { - fn from(network: FFINetwork) -> Self { - match network { - FFINetwork::Mainnet => Network::Mainnet, - FFINetwork::Testnet => Network::Testnet, - FFINetwork::Devnet => Network::Devnet, - FFINetwork::Regtest => Network::Regtest, - } - } -} - -/// Return a pointer to the canonical lowercase name of `network`. -/// -/// The returned pointer is to a static null-terminated string owned by -/// `dash-network`; callers must not free it. -#[unsafe(no_mangle)] -pub extern "C" fn dashcore_network_get_name(network: FFINetwork) -> *const ffi::c_char { - match network { - FFINetwork::Mainnet => c"mainnet".as_ptr() as *const ffi::c_char, - FFINetwork::Testnet => c"testnet".as_ptr() as *const ffi::c_char, - FFINetwork::Regtest => c"regtest".as_ptr() as *const ffi::c_char, - FFINetwork::Devnet => c"devnet".as_ptr() as *const ffi::c_char, - } -} - -#[cfg(test)] -mod tests { - use std::ffi::CStr; - - use super::*; - - #[test] - fn test_network_names() { - unsafe { - let name = dashcore_network_get_name(FFINetwork::Mainnet); - assert!(!name.is_null()); - let name_str = CStr::from_ptr(name).to_str().unwrap(); - assert_eq!(name_str, "mainnet"); - - let name = dashcore_network_get_name(FFINetwork::Testnet); - assert!(!name.is_null()); - let name_str = CStr::from_ptr(name).to_str().unwrap(); - assert_eq!(name_str, "testnet"); - - let name = dashcore_network_get_name(FFINetwork::Regtest); - assert!(!name.is_null()); - let name_str = CStr::from_ptr(name).to_str().unwrap(); - assert_eq!(name_str, "regtest"); - - let name = dashcore_network_get_name(FFINetwork::Devnet); - assert!(!name.is_null()); - let name_str = CStr::from_ptr(name).to_str().unwrap(); - assert_eq!(name_str, "devnet"); - } - } -} diff --git a/dash-network/src/lib.rs b/dash-network/src/lib.rs index 8d4cc08b4..0f51a9d08 100644 --- a/dash-network/src/lib.rs +++ b/dash-network/src/lib.rs @@ -19,9 +19,6 @@ use core::fmt; -#[cfg(feature = "ffi")] -pub mod ffi; - #[cfg(feature = "bincode")] use bincode_derive::{Decode, Encode}; diff --git a/dash-spv-ffi/CLAUDE.md b/dash-spv-ffi/CLAUDE.md deleted file mode 100644 index 66ce8e7ba..000000000 --- a/dash-spv-ffi/CLAUDE.md +++ /dev/null @@ -1,125 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Overview - -dash-spv-ffi provides C-compatible FFI bindings for the Dash SPV (Simplified Payment Verification) client. It wraps the Rust dash-spv library to enable usage from C, Swift, and other languages via a stable ABI. - -## Build Commands - -### Rust Library Build -```bash -# Debug build -cargo build - -# Release build (recommended for production) -cargo build --release - -# Build for specific iOS targets -cargo build --release --target aarch64-apple-ios -cargo build --release --target aarch64-apple-ios-sim -``` - -### Header Generation -The C header (`include/dash_spv_ffi.h`) is auto-generated by `build.rs` during `cargo build` and is gitignored. - -## Testing - -### Rust Tests -```bash -# Run all tests -cargo test - -# Run specific test -cargo test test_client_lifecycle - -# Run with output -cargo test -- --nocapture - -# Run tests with real Dash node (requires DASH_SPV_IP env var) -DASH_SPV_IP=192.168.1.100 cargo test -- --ignored -``` - -### C Tests -```bash -cd tests/c_tests - -# Build and run all tests -make test - -# Run specific test -make test_basic && ./test_basic - -# Clean build artifacts -make clean -``` - -## Architecture - -### Core Components - -**FFI Wrapper Layer** (`src/`): -- `client.rs` - SPV client operations (connect, sync, broadcast) -- `config.rs` - Client configuration (network, peers, validation) -- `wallet.rs` - Wallet operations (addresses, balances, UTXOs) -- `callbacks.rs` - Async callback system for progress/events -- `types.rs` - FFI-safe type conversions -- `error.rs` - Thread-local error handling -- `platform_integration.rs` - Platform SDK integration support - -**Key Design Patterns**: -1. **Opaque Pointers**: Complex Rust types are exposed as opaque pointers (`FFIDashSpvClient*`) -2. **Explicit Memory Management**: All FFI types have corresponding `_destroy()` functions -3. **Error Handling**: Uses thread-local storage for error propagation -4. **Callbacks**: Async operations use C function pointers for progress/completion - -### FFI Safety Rules - -1. **String Handling**: - - Rust strings are returned as `*const c_char` (caller must free with `dash_string_free`) - - Input strings are `*const c_char` (borrowed, not freed) - -2. **Memory Ownership**: - - Functions returning pointers transfer ownership (caller must destroy) - - Functions taking pointers borrow (caller retains ownership) - -3. **Thread Safety**: - - Client operations are thread-safe - - Callbacks may be invoked from any thread - -### Integration with Unified SDK - -This crate can be used standalone or as part of the unified SDK: -- **Standalone**: Produces `libdash_spv_ffi.a` with `dash_spv_ffi.h` -- **Unified**: Combined with platform SDK in `DashUnifiedSDK.xcframework` - -The unified SDK merges headers and resolves type conflicts between Core and Platform layers. - -## Common Development Tasks - -### Adding New FFI Functions -1. Implement Rust function in appropriate module with `#[no_mangle] extern "C"` -2. Add cbindgen annotations for complex types -3. Run `cargo build` to regenerate header -4. Add corresponding test in `tests/unit/` -5. Add C test in `tests/c_tests/` - -### Debugging FFI Issues -- Check `dash_spv_ffi_get_last_error()` for error details -- Use `RUST_LOG=debug` for verbose logging -- Verify memory management (matching create/destroy calls) -- Test with AddressSanitizer: `RUSTFLAGS="-Z sanitizer=address" cargo test` - -### Platform-Specific Builds -- iOS: Use `--target aarch64-apple-ios` or `aarch64-apple-ios-sim` -- Android: Use appropriate NDK target -- Linux/macOS: Default target works - -## Dependencies - -Key dependencies from Cargo.toml: -- `dash-spv` - Core SPV implementation (local path) -- `dashcore` - Dash protocol types (local path) -- `tokio` - Async runtime -- `cbindgen` - C header generation (build dependency) diff --git a/dash-spv-ffi/Cargo.toml b/dash-spv-ffi/Cargo.toml deleted file mode 100644 index 19ada2d0e..000000000 --- a/dash-spv-ffi/Cargo.toml +++ /dev/null @@ -1,44 +0,0 @@ -[package] -name = "dash-spv-ffi" -version = { workspace = true } -edition = "2021" -authors = ["Dash Core Developers"] -license = "MIT" -description = "FFI bindings for the Dash SPV client" -repository = "https://github.com/dashpay/rust-dashcore" - -[lib] -name = "dash_spv_ffi" -crate-type = ["cdylib", "staticlib", "rlib"] - -[dependencies] -dash-spv = { path = "../dash-spv" } -dashcore = { path = "../dash" } -dash-network = { path = "../dash-network", features = ["ffi"] } -tokio = { version = "1", features = ["full"] } -hex = "0.4" -tracing = "0.1" -# Use key-wallet-ffi for all wallet-related FFI types -key-wallet-ffi = { path = "../key-wallet-ffi" } -# Still need these for SPV client internals (not for FFI types) -key-wallet = { path = "../key-wallet" } -key-wallet-manager = { path = "../key-wallet-manager" } -clap = { version = "4.5", features = ["derive"] } - -[dev-dependencies] -dash-spv = { path = "../dash-spv", features = ["test-utils"] } -# Tests inspect per-account transaction history end-to-end (including -# chainlocked transactions), which requires the `keep-finalized-transactions` -# feature on `key-wallet-ffi`. Cargo unifies features across the build graph at -# test time, so this enables the gated FFI accessors in test builds without -# changing the lib's default feature surface. -key-wallet-ffi = { path = "../key-wallet-ffi", features = ["keep-finalized-transactions"] } -serial_test = "3.0" -tempfile = "3.8" - -[build-dependencies] -cbindgen = "0.29" - -[[bin]] -name = "dash-spv-ffi" -path = "src/bin/ffi_cli.rs" diff --git a/dash-spv-ffi/FFI_API.md b/dash-spv-ffi/FFI_API.md deleted file mode 100644 index 5b708ea8b..000000000 --- a/dash-spv-ffi/FFI_API.md +++ /dev/null @@ -1,808 +0,0 @@ -# Dash SPV FFI API Documentation - -This document provides a comprehensive reference for all FFI (Foreign Function Interface) functions available in the dash-spv-ffi library. - -**Auto-generated**: This documentation is automatically generated from the source code. Do not edit manually. - -**Total Functions**: 39 - -## Table of Contents - -- [Client Management](#client-management) -- [Configuration](#configuration) -- [Synchronization](#synchronization) -- [Transaction Management](#transaction-management) -- [Mempool Operations](#mempool-operations) -- [Platform Integration](#platform-integration) -- [Error Handling](#error-handling) -- [Utility Functions](#utility-functions) - -## Function Reference - -### Client Management - -Functions: 3 - -| Function | Description | Module | -|----------|-------------|--------| -| `dash_spv_ffi_client_destroy` | Destroy the client and free associated resources | client | -| `dash_spv_ffi_client_new` | Create a new SPV client and return an opaque pointer | client | -| `dash_spv_ffi_client_stop` | Stop the SPV client | client | - -### Configuration - -Functions: 15 - -| Function | Description | Module | -|----------|-------------|--------| -| `dash_spv_ffi_client_update_config` | Update the running client's configuration | client | -| `dash_spv_ffi_config_add_peer` | Adds a peer address to the configuration Accepts socket addresses with or... | config | -| `dash_spv_ffi_config_destroy` | Destroys an FFIClientConfig and frees its memory # Safety - `config` must... | config | -| `dash_spv_ffi_config_get_network` | Gets the network type from the configuration # Safety - `config` must be a... | config | -| `dash_spv_ffi_config_mainnet` | No description | config | -| `dash_spv_ffi_config_new` | No description | config | -| `dash_spv_ffi_config_set_data_dir` | Sets the data directory for storing blockchain data # Safety - `config`... | config | -| `dash_spv_ffi_config_set_fetch_mempool_transactions` | Sets whether to fetch full mempool transaction data # Safety - `config`... | config | -| `dash_spv_ffi_config_set_masternode_sync_enabled` | Enables or disables masternode synchronization # Safety - `config` must be... | config | -| `dash_spv_ffi_config_set_mempool_strategy` | Sets the mempool synchronization strategy # Safety - `config` must be a... | config | -| `dash_spv_ffi_config_set_mempool_tracking` | Enables or disables mempool tracking # Safety - `config` must be a valid... | config | -| `dash_spv_ffi_config_set_restrict_to_configured_peers` | Restrict connections strictly to configured peers (disable DNS discovery and... | config | -| `dash_spv_ffi_config_set_start_from_height` | Sets the starting block height for synchronization # Safety - `config` must... | config | -| `dash_spv_ffi_config_set_user_agent` | Sets the user agent string to advertise in the P2P handshake # Safety -... | config | -| `dash_spv_ffi_config_testnet` | No description | config | - -### Synchronization - -Functions: 3 - -| Function | Description | Module | -|----------|-------------|--------| -| `dash_spv_ffi_client_get_manager_sync_progress` | Get the current manager-based sync progress | client | -| `dash_spv_ffi_client_get_sync_progress` | Get the current sync progress snapshot | client | -| `dash_spv_ffi_sync_progress_destroy` | Destroy an `FFISyncProgress` object and all its nested pointers | types | - -### Transaction Management - -Functions: 1 - -| Function | Description | Module | -|----------|-------------|--------| -| `dash_spv_ffi_client_broadcast_transaction` | Broadcasts a transaction to the Dash network via connected peers | client | - -### Mempool Operations - -Functions: 1 - -| Function | Description | Module | -|----------|-------------|--------| -| `dash_spv_ffi_mempool_progress_destroy` | Destroy an `FFIMempoolProgress` object | types | - -### Platform Integration - -Functions: 2 - -| Function | Description | Module | -|----------|-------------|--------| -| `ffi_dash_spv_get_platform_activation_height` | Gets the platform activation height from the Core chain # Safety This... | platform_integration | -| `ffi_dash_spv_get_quorum_public_key` | Gets a quorum public key from the Core chain # Safety This function is... | platform_integration | - -### Error Handling - -Functions: 1 - -| Function | Description | Module | -|----------|-------------|--------| -| `dash_spv_ffi_get_last_error` | No description | error | - -### Utility Functions - -Functions: 13 - -| Function | Description | Module | -|----------|-------------|--------| -| `dash_spv_ffi_block_headers_progress_destroy` | Destroy an `FFIBlockHeadersProgress` object | types | -| `dash_spv_ffi_blocks_progress_destroy` | Destroy an `FFIBlocksProgress` object | types | -| `dash_spv_ffi_chainlock_progress_destroy` | Destroy an `FFIChainLockProgress` object | types | -| `dash_spv_ffi_client_clear_storage` | Clear all persisted SPV storage (headers, filters, metadata, sync state) | client | -| `dash_spv_ffi_client_get_wallet_manager` | Get the wallet manager from the SPV client Returns a pointer to an... | client | -| `dash_spv_ffi_client_run` | Start the SPV client and begin syncing in the background | client | -| `dash_spv_ffi_filter_headers_progress_destroy` | Destroy an `FFIFilterHeadersProgress` object | types | -| `dash_spv_ffi_filters_progress_destroy` | Destroy an `FFIFiltersProgress` object | types | -| `dash_spv_ffi_init_logging` | Initialize logging for the SPV library | utils | -| `dash_spv_ffi_instantsend_progress_destroy` | Destroy an `FFIInstantSendProgress` object | types | -| `dash_spv_ffi_masternode_progress_destroy` | Destroy an `FFIMasternodesProgress` object | types | -| `dash_spv_ffi_version` | No description | utils | -| `dash_spv_ffi_wallet_manager_free` | Release a wallet manager obtained from `dash_spv_ffi_client_get_wallet_manager` | client | - -## Detailed Function Documentation - -### Client Management - Detailed - -#### `dash_spv_ffi_client_destroy` - -```c -dash_spv_ffi_client_destroy(client: *mut FFIDashSpvClient) -> () -``` - -**Description:** -Destroy the client and free associated resources. # Safety - `client` must be either null or a pointer obtained from `dash_spv_ffi_client_new`. - -**Safety:** -- `client` must be either null or a pointer obtained from `dash_spv_ffi_client_new`. - -**Module:** `client` - ---- - -#### `dash_spv_ffi_client_new` - -```c -dash_spv_ffi_client_new(config: *const FFIClientConfig, callbacks: FFIEventCallbacks,) -> *mut FFIDashSpvClient -``` - -**Description:** -Create a new SPV client and return an opaque pointer. # Safety - `config` must be a valid, non-null pointer for the duration of the call. - `callbacks` is taken by value (function pointers and `user_data` pointers are copied internally). The struct itself may be dropped after the call, but all `user_data` pointer targets must remain valid until `dash_spv_ffi_client_stop` or `dash_spv_ffi_client_destroy` is called. - Callback functions and `user_data` pointees must be safe to use from background threads; different callback groups may be invoked concurrently. - The returned pointer must be freed with `dash_spv_ffi_client_destroy`. - -**Safety:** -- `config` must be a valid, non-null pointer for the duration of the call. - `callbacks` is taken by value (function pointers and `user_data` pointers are copied internally). The struct itself may be dropped after the call, but all `user_data` pointer targets must remain valid until `dash_spv_ffi_client_stop` or `dash_spv_ffi_client_destroy` is called. - Callback functions and `user_data` pointees must be safe to use from background threads; different callback groups may be invoked concurrently. - The returned pointer must be freed with `dash_spv_ffi_client_destroy`. - -**Module:** `client` - ---- - -#### `dash_spv_ffi_client_stop` - -```c -dash_spv_ffi_client_stop(client: *mut FFIDashSpvClient) -> i32 -``` - -**Description:** -Stop the SPV client. # Safety - `client` must be a valid, non-null pointer to a created client. - -**Safety:** -- `client` must be a valid, non-null pointer to a created client. - -**Module:** `client` - ---- - -### Configuration - Detailed - -#### `dash_spv_ffi_client_update_config` - -```c -dash_spv_ffi_client_update_config(client: *mut FFIDashSpvClient, config: *const FFIClientConfig,) -> i32 -``` - -**Description:** -Update the running client's configuration. # Safety - `client` must be a valid pointer to an `FFIDashSpvClient`. - `config` must be a valid pointer to an `FFIClientConfig`. - The network in `config` must match the client's network; changing networks at runtime is not supported. - -**Safety:** -- `client` must be a valid pointer to an `FFIDashSpvClient`. - `config` must be a valid pointer to an `FFIClientConfig`. - The network in `config` must match the client's network; changing networks at runtime is not supported. - -**Module:** `client` - ---- - -#### `dash_spv_ffi_config_add_peer` - -```c -dash_spv_ffi_config_add_peer(config: *mut FFIClientConfig, addr: *const c_char,) -> i32 -``` - -**Description:** -Adds a peer address to the configuration Accepts socket addresses with or without port. When no port is specified, the default P2P port for the configured network is used. Supported formats: - IP with port: `192.168.1.1:9999`, `[::1]:19999` - IP without port: `127.0.0.1`, `2001:db8::1` - Hostname with port: `node.example.com:9999` - Hostname without port: `node.example.com` # Safety - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet - `addr` must be a valid null-terminated C string containing a socket address or IP-only string - The caller must ensure both pointers remain valid for the duration of this call - -**Safety:** -- `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet - `addr` must be a valid null-terminated C string containing a socket address or IP-only string - The caller must ensure both pointers remain valid for the duration of this call - -**Module:** `config` - ---- - -#### `dash_spv_ffi_config_destroy` - -```c -dash_spv_ffi_config_destroy(config: *mut FFIClientConfig) -> () -``` - -**Description:** -Destroys an FFIClientConfig and frees its memory # Safety - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet, or null - After calling this function, the config pointer becomes invalid and must not be used - This function should only be called once per config instance - -**Safety:** -- `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet, or null - After calling this function, the config pointer becomes invalid and must not be used - This function should only be called once per config instance - -**Module:** `config` - ---- - -#### `dash_spv_ffi_config_get_network` - -```c -dash_spv_ffi_config_get_network(config: *const FFIClientConfig,) -> FFINetwork -``` - -**Description:** -Gets the network type from the configuration # Safety - `config` must be a valid pointer to an FFIClientConfig or null - If null, returns FFINetwork::Mainnet as default - -**Safety:** -- `config` must be a valid pointer to an FFIClientConfig or null - If null, returns FFINetwork::Mainnet as default - -**Module:** `config` - ---- - -#### `dash_spv_ffi_config_mainnet` - -```c -dash_spv_ffi_config_mainnet() -> *mut FFIClientConfig -``` - -**Module:** `config` - ---- - -#### `dash_spv_ffi_config_new` - -```c -dash_spv_ffi_config_new(network: FFINetwork) -> *mut FFIClientConfig -``` - -**Module:** `config` - ---- - -#### `dash_spv_ffi_config_set_data_dir` - -```c -dash_spv_ffi_config_set_data_dir(config: *mut FFIClientConfig, path: *const c_char,) -> i32 -``` - -**Description:** -Sets the data directory for storing blockchain data # Safety - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet - `path` must be a valid null-terminated C string - The caller must ensure the config pointer remains valid for the duration of this call - -**Safety:** -- `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet - `path` must be a valid null-terminated C string - The caller must ensure the config pointer remains valid for the duration of this call - -**Module:** `config` - ---- - -#### `dash_spv_ffi_config_set_fetch_mempool_transactions` - -```c -dash_spv_ffi_config_set_fetch_mempool_transactions(config: *mut FFIClientConfig, fetch: bool,) -> i32 -``` - -**Description:** -Sets whether to fetch full mempool transaction data # Safety - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet - The caller must ensure the config pointer remains valid for the duration of this call - -**Safety:** -- `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet - The caller must ensure the config pointer remains valid for the duration of this call - -**Module:** `config` - ---- - -#### `dash_spv_ffi_config_set_masternode_sync_enabled` - -```c -dash_spv_ffi_config_set_masternode_sync_enabled(config: *mut FFIClientConfig, enable: bool,) -> i32 -``` - -**Description:** -Enables or disables masternode synchronization # Safety - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet - The caller must ensure the config pointer remains valid for the duration of this call - -**Safety:** -- `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet - The caller must ensure the config pointer remains valid for the duration of this call - -**Module:** `config` - ---- - -#### `dash_spv_ffi_config_set_mempool_strategy` - -```c -dash_spv_ffi_config_set_mempool_strategy(config: *mut FFIClientConfig, strategy: FFIMempoolStrategy,) -> i32 -``` - -**Description:** -Sets the mempool synchronization strategy # Safety - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet - The caller must ensure the config pointer remains valid for the duration of this call - -**Safety:** -- `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet - The caller must ensure the config pointer remains valid for the duration of this call - -**Module:** `config` - ---- - -#### `dash_spv_ffi_config_set_mempool_tracking` - -```c -dash_spv_ffi_config_set_mempool_tracking(config: *mut FFIClientConfig, enable: bool,) -> i32 -``` - -**Description:** -Enables or disables mempool tracking # Safety - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet - The caller must ensure the config pointer remains valid for the duration of this call - -**Safety:** -- `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet - The caller must ensure the config pointer remains valid for the duration of this call - -**Module:** `config` - ---- - -#### `dash_spv_ffi_config_set_restrict_to_configured_peers` - -```c -dash_spv_ffi_config_set_restrict_to_configured_peers(config: *mut FFIClientConfig, restrict_peers: bool,) -> i32 -``` - -**Description:** -Restrict connections strictly to configured peers (disable DNS discovery and peer store) # Safety - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet - -**Safety:** -- `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet - -**Module:** `config` - ---- - -#### `dash_spv_ffi_config_set_start_from_height` - -```c -dash_spv_ffi_config_set_start_from_height(config: *mut FFIClientConfig, height: u32,) -> i32 -``` - -**Description:** -Sets the starting block height for synchronization # Safety - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet - The caller must ensure the config pointer remains valid for the duration of this call - -**Safety:** -- `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet - The caller must ensure the config pointer remains valid for the duration of this call - -**Module:** `config` - ---- - -#### `dash_spv_ffi_config_set_user_agent` - -```c -dash_spv_ffi_config_set_user_agent(config: *mut FFIClientConfig, user_agent: *const c_char,) -> i32 -``` - -**Description:** -Sets the user agent string to advertise in the P2P handshake # Safety - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet - `user_agent` must be a valid null-terminated C string - The caller must ensure both pointers remain valid for the duration of this call - -**Safety:** -- `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet - `user_agent` must be a valid null-terminated C string - The caller must ensure both pointers remain valid for the duration of this call - -**Module:** `config` - ---- - -#### `dash_spv_ffi_config_testnet` - -```c -dash_spv_ffi_config_testnet() -> *mut FFIClientConfig -``` - -**Module:** `config` - ---- - -### Synchronization - Detailed - -#### `dash_spv_ffi_client_get_manager_sync_progress` - -```c -dash_spv_ffi_client_get_manager_sync_progress(client: *mut FFIDashSpvClient,) -> *mut FFISyncProgress -``` - -**Description:** -Get the current manager-based sync progress. Returns the new parallel sync system's progress with per-manager details. Use `dash_spv_ffi_sync_progress_destroy` to free the returned struct. # Safety - `client` must be a valid, non-null pointer. - -**Safety:** -- `client` must be a valid, non-null pointer. - -**Module:** `client` - ---- - -#### `dash_spv_ffi_client_get_sync_progress` - -```c -dash_spv_ffi_client_get_sync_progress(client: *mut FFIDashSpvClient,) -> *mut FFISyncProgress -``` - -**Description:** -Get the current sync progress snapshot. # Safety - `client` must be a valid, non-null pointer. - -**Safety:** -- `client` must be a valid, non-null pointer. - -**Module:** `client` - ---- - -#### `dash_spv_ffi_sync_progress_destroy` - -```c -dash_spv_ffi_sync_progress_destroy(progress: *mut FFISyncProgress) -> () -``` - -**Description:** -Destroy an `FFISyncProgress` object and all its nested pointers. # Safety - `progress` must be a pointer returned from this crate, or null. - -**Safety:** -- `progress` must be a pointer returned from this crate, or null. - -**Module:** `types` - ---- - -### Transaction Management - Detailed - -#### `dash_spv_ffi_client_broadcast_transaction` - -```c -dash_spv_ffi_client_broadcast_transaction(client: *mut FFIDashSpvClient, tx_bytes: *const u8, length: usize,) -> i32 -``` - -**Description:** -Broadcasts a transaction to the Dash network via connected peers. # Safety - `client` must be a valid, non-null pointer to an initialized FFIDashSpvClient - `tx_bytes` must be a valid, non-null pointer to the transaction data - `length` must be the length of the transaction data in bytes - -**Safety:** -- `client` must be a valid, non-null pointer to an initialized FFIDashSpvClient - `tx_bytes` must be a valid, non-null pointer to the transaction data - `length` must be the length of the transaction data in bytes - -**Module:** `client` - ---- - -### Mempool Operations - Detailed - -#### `dash_spv_ffi_mempool_progress_destroy` - -```c -dash_spv_ffi_mempool_progress_destroy(progress: *mut FFIMempoolProgress) -> () -``` - -**Description:** -Destroy an `FFIMempoolProgress` object. # Safety - `progress` must be a pointer returned from this crate, or null. - -**Safety:** -- `progress` must be a pointer returned from this crate, or null. - -**Module:** `types` - ---- - -### Platform Integration - Detailed - -#### `ffi_dash_spv_get_platform_activation_height` - -```c -ffi_dash_spv_get_platform_activation_height(client: *mut FFIDashSpvClient, out_height: *mut u32,) -> FFIResult -``` - -**Description:** -Gets the platform activation height from the Core chain # Safety This function is unsafe because: - The caller must ensure all pointers are valid - out_height must point to a valid u32 - -**Safety:** -This function is unsafe because: - The caller must ensure all pointers are valid - out_height must point to a valid u32 - -**Module:** `platform_integration` - ---- - -#### `ffi_dash_spv_get_quorum_public_key` - -```c -ffi_dash_spv_get_quorum_public_key(client: *mut FFIDashSpvClient, quorum_type: u32, quorum_hash: *const u8, core_chain_locked_height: u32, out_pubkey: *mut u8, out_pubkey_size: usize,) -> FFIResult -``` - -**Description:** -Gets a quorum public key from the Core chain # Safety This function is unsafe because: - The caller must ensure all pointers are valid - quorum_hash must point to a 32-byte array - out_pubkey must point to a buffer of at least out_pubkey_size bytes - out_pubkey_size must be at least 48 bytes - -**Safety:** -This function is unsafe because: - The caller must ensure all pointers are valid - quorum_hash must point to a 32-byte array - out_pubkey must point to a buffer of at least out_pubkey_size bytes - out_pubkey_size must be at least 48 bytes - -**Module:** `platform_integration` - ---- - -### Error Handling - Detailed - -#### `dash_spv_ffi_get_last_error` - -```c -dash_spv_ffi_get_last_error() -> *const c_char -``` - -**Module:** `error` - ---- - -### Utility Functions - Detailed - -#### `dash_spv_ffi_block_headers_progress_destroy` - -```c -dash_spv_ffi_block_headers_progress_destroy(progress: *mut FFIBlockHeadersProgress,) -> () -``` - -**Description:** -Destroy an `FFIBlockHeadersProgress` object. # Safety - `progress` must be a pointer returned from this crate, or null. - -**Safety:** -- `progress` must be a pointer returned from this crate, or null. - -**Module:** `types` - ---- - -#### `dash_spv_ffi_blocks_progress_destroy` - -```c -dash_spv_ffi_blocks_progress_destroy(progress: *mut FFIBlocksProgress) -> () -``` - -**Description:** -Destroy an `FFIBlocksProgress` object. # Safety - `progress` must be a pointer returned from this crate, or null. - -**Safety:** -- `progress` must be a pointer returned from this crate, or null. - -**Module:** `types` - ---- - -#### `dash_spv_ffi_chainlock_progress_destroy` - -```c -dash_spv_ffi_chainlock_progress_destroy(progress: *mut FFIChainLockProgress,) -> () -``` - -**Description:** -Destroy an `FFIChainLockProgress` object. # Safety - `progress` must be a pointer returned from this crate, or null. - -**Safety:** -- `progress` must be a pointer returned from this crate, or null. - -**Module:** `types` - ---- - -#### `dash_spv_ffi_client_clear_storage` - -```c -dash_spv_ffi_client_clear_storage(client: *mut FFIDashSpvClient) -> i32 -``` - -**Description:** -Clear all persisted SPV storage (headers, filters, metadata, sync state). # Safety - `client` must be a valid, non-null pointer. - -**Safety:** -- `client` must be a valid, non-null pointer. - -**Module:** `client` - ---- - -#### `dash_spv_ffi_client_get_wallet_manager` - -```c -dash_spv_ffi_client_get_wallet_manager(client: *mut FFIDashSpvClient,) -> *mut FFIWalletManager -``` - -**Description:** -Get the wallet manager from the SPV client Returns a pointer to an `FFIWalletManager` wrapper that clones the underlying `Arc>`. This allows direct interaction with the wallet manager without going back through the client for each call. # Safety The caller must ensure that: - The client pointer is valid - The returned pointer is released exactly once using `dash_spv_ffi_wallet_manager_free` # Returns A pointer to the wallet manager wrapper, or NULL if the client is not initialized. - -**Safety:** -The caller must ensure that: - The client pointer is valid - The returned pointer is released exactly once using `dash_spv_ffi_wallet_manager_free` - -**Module:** `client` - ---- - -#### `dash_spv_ffi_client_run` - -```c -dash_spv_ffi_client_run(client: *mut FFIDashSpvClient) -> i32 -``` - -**Description:** -Start the SPV client and begin syncing in the background. Uses the event callbacks provided at client creation time. Returns immediately after spawning the sync task. # Safety - `client` must be a valid, non-null pointer to a created client. # Returns 0 on success, error code on failure. - -**Safety:** -- `client` must be a valid, non-null pointer to a created client. - -**Module:** `client` - ---- - -#### `dash_spv_ffi_filter_headers_progress_destroy` - -```c -dash_spv_ffi_filter_headers_progress_destroy(progress: *mut FFIFilterHeadersProgress,) -> () -``` - -**Description:** -Destroy an `FFIFilterHeadersProgress` object. # Safety - `progress` must be a pointer returned from this crate, or null. - -**Safety:** -- `progress` must be a pointer returned from this crate, or null. - -**Module:** `types` - ---- - -#### `dash_spv_ffi_filters_progress_destroy` - -```c -dash_spv_ffi_filters_progress_destroy(progress: *mut FFIFiltersProgress) -> () -``` - -**Description:** -Destroy an `FFIFiltersProgress` object. # Safety - `progress` must be a pointer returned from this crate, or null. - -**Safety:** -- `progress` must be a pointer returned from this crate, or null. - -**Module:** `types` - ---- - -#### `dash_spv_ffi_init_logging` - -```c -dash_spv_ffi_init_logging(level: *const c_char, enable_console: bool, log_dir: *const c_char, max_files: usize,) -> i32 -``` - -**Description:** -Initialize logging for the SPV library. # Arguments - `level`: Log level string (null uses RUST_LOG env var or defaults to INFO). Valid values: "error", "warn", "info", "debug", "trace" - `enable_console`: Whether to output logs to console (stderr) - `log_dir`: Directory for log files (null to disable file logging) - `max_files`: Maximum archived log files to retain (ignored if log_dir is null) # Safety - `level` and `log_dir` may be null or point to valid, NUL-terminated C strings. - -**Safety:** -- `level` and `log_dir` may be null or point to valid, NUL-terminated C strings. - -**Module:** `utils` - ---- - -#### `dash_spv_ffi_instantsend_progress_destroy` - -```c -dash_spv_ffi_instantsend_progress_destroy(progress: *mut FFIInstantSendProgress,) -> () -``` - -**Description:** -Destroy an `FFIInstantSendProgress` object. # Safety - `progress` must be a pointer returned from this crate, or null. - -**Safety:** -- `progress` must be a pointer returned from this crate, or null. - -**Module:** `types` - ---- - -#### `dash_spv_ffi_masternode_progress_destroy` - -```c -dash_spv_ffi_masternode_progress_destroy(progress: *mut FFIMasternodesProgress,) -> () -``` - -**Description:** -Destroy an `FFIMasternodesProgress` object. # Safety - `progress` must be a pointer returned from this crate, or null. - -**Safety:** -- `progress` must be a pointer returned from this crate, or null. - -**Module:** `types` - ---- - -#### `dash_spv_ffi_version` - -```c -dash_spv_ffi_version() -> *const c_char -``` - -**Module:** `utils` - ---- - -#### `dash_spv_ffi_wallet_manager_free` - -```c -dash_spv_ffi_wallet_manager_free(manager: *mut FFIWalletManager) -> () -``` - -**Description:** -Release a wallet manager obtained from `dash_spv_ffi_client_get_wallet_manager`. This simply forwards to `wallet_manager_free` in key-wallet-ffi so that lifetime management is consistent between direct key-wallet usage and the SPV client pathway. # Safety - `manager` must either be null or a pointer previously returned by `dash_spv_ffi_client_get_wallet_manager`. - -**Safety:** -- `manager` must either be null or a pointer previously returned by `dash_spv_ffi_client_get_wallet_manager`. - -**Module:** `client` - ---- - -## Type Definitions - -### Core Types - -- `FFIDashSpvClient` - SPV client handle -- `FFIClientConfig` - Client configuration -- `FFISyncProgress` - Synchronization progress -- `FFIDetailedSyncProgress` - Detailed sync progress -- `FFITransaction` - Transaction information -- `FFIUnconfirmedTransaction` - Unconfirmed transaction -- `FFIEventCallbacks` - Event callback structure -- `CoreSDKHandle` - Platform SDK integration handle - -### Enumerations - -- `FFINetwork` - Network type (Mainnet, Testnet, Regtest, Devnet) -- `FFIValidationMode` - Validation mode (None, Basic, Full) -- `FFIMempoolStrategy` - Mempool strategy (FetchAll, BloomFilter, Selective) - -## Memory Management - -### Important Rules - -1. **Ownership Transfer**: Functions returning pointers transfer ownership to the caller -2. **Cleanup Required**: All returned pointers must be freed using the appropriate `_destroy` function -3. **Thread Safety**: The SPV client is thread-safe -4. **Error Handling**: Check return codes and use `dash_spv_ffi_get_last_error()` for details -5. **Shared Ownership**: `dash_spv_ffi_client_get_wallet_manager()` returns `FFIWalletManager*` that must be released with `dash_spv_ffi_wallet_manager_free()` - -## Usage Examples - -### Basic SPV Client Usage - -```c -// Create configuration -FFIClientConfig* config = dash_spv_ffi_config_testnet(); - -// Build event callbacks (zero-init for no-op defaults) -FFIEventCallbacks callbacks = { 0 }; - -// Create client with callbacks -FFIDashSpvClient* client = dash_spv_ffi_client_new(config, callbacks); - -// Start syncing (uses callbacks provided at creation) -int32_t result = dash_spv_ffi_client_run(client); -if (result != 0) { - const char* error = dash_spv_ffi_get_last_error(); - // Handle error -} - -// Get wallet manager (shares ownership with the client) -FFIWalletManager* wallet_manager = dash_spv_ffi_client_get_wallet_manager(client); - -// Clean up -dash_spv_ffi_client_destroy(client); -dash_spv_ffi_config_destroy(config); -``` - -### Event Callbacks - -```c -void on_headers(uint32_t tip_height, void* user_data) { - printf("Headers stored up to height %u\n", tip_height); -} - -void on_tx(const char* wallet_id, uint32_t account_index, - const uint8_t (*txid)[32], int64_t amount, - const char* addresses, void* user_data) { - printf("Transaction: %lld duffs\n", (long long)amount); -} - -// Build callbacks struct and pass to client_new() -FFIEventCallbacks callbacks = { 0 }; -callbacks.sync.on_block_headers_stored = on_headers; -callbacks.wallet.on_transaction_received = on_tx; -FFIDashSpvClient* client = dash_spv_ffi_client_new(config, callbacks); - -// Start syncing (uses callbacks provided at creation) -dash_spv_ffi_client_run(client); -``` diff --git a/dash-spv-ffi/FFI_DOCS_README.md b/dash-spv-ffi/FFI_DOCS_README.md deleted file mode 100644 index 26afe966f..000000000 --- a/dash-spv-ffi/FFI_DOCS_README.md +++ /dev/null @@ -1,89 +0,0 @@ -# FFI Documentation Guide - -## Overview - -The `FFI_API.md` file contains comprehensive documentation for all FFI functions in the dash-spv-ffi library. This documentation is automatically generated from the source code to ensure it stays up-to-date. - -## Keeping Documentation Updated - -### Automatic Verification - -A GitHub Action automatically verifies that the FFI documentation is up-to-date on every push and pull request. If the documentation is out of sync, the CI will fail and provide instructions on how to update it. - -### Manual Updates - -To update the FFI documentation after making changes to FFI functions: - -```bash -# Using Make -make update-docs - -# Or directly with Python -cd dash-spv-ffi -python3 scripts/generate_ffi_docs.py -``` - -### Checking Documentation - -To verify the documentation is current without updating: - -```bash -# Using Make -make check-docs - -# Or directly with the script -bash scripts/check_ffi_docs.sh -``` - -## Documentation Structure - -The `FFI_API.md` file includes: - -1. **Table of Contents** - Quick navigation to different sections -2. **Function Reference** - Categorized list of all functions -3. **Detailed Documentation** - Full signatures and descriptions -4. **Type Definitions** - Core FFI types used -5. **Memory Management** - Important rules for FFI usage -6. **Usage Examples** - Sample code for common operations - -## Categories - -Functions are automatically categorized into: - -- Client Management -- Configuration -- Synchronization -- Wallet Operations -- Address Monitoring -- Transaction Management -- Balance & UTXOs -- Mempool Operations -- Platform Integration -- Event Callbacks -- Error Handling -- Utility Functions - -## Adding New FFI Functions - -When adding new FFI functions: - -1. Add the function with `#[no_mangle]` and `extern "C"` attributes -2. Include doc comments with `///` -3. Add safety documentation if the function is `unsafe` -4. Run `make update-docs` to regenerate documentation -5. Commit both the code changes and updated `FFI_API.md` - -## CI/CD Integration - -The documentation verification is integrated into the CI pipeline: - -1. **On Push/PR**: Verifies documentation is up-to-date -2. **On Failure**: Comments on PR with update instructions -3. **Required Check**: Must pass before merging - -## Tools - -- `scripts/generate_ffi_docs.py` - Python script that parses Rust files and generates documentation -- `scripts/check_ffi_docs.sh` - Bash script to verify documentation is current -- `.github/workflows/verify-ffi-docs.yml` - GitHub Action for CI verification -- `Makefile` - Convenient commands for documentation tasks diff --git a/dash-spv-ffi/README.md b/dash-spv-ffi/README.md deleted file mode 100644 index 747c40696..000000000 --- a/dash-spv-ffi/README.md +++ /dev/null @@ -1,112 +0,0 @@ -# Dash SPV FFI - -This crate provides C-compatible FFI bindings for the Dash SPV client library. - -## Features - -- Complete FFI wrapper for DashSpvClient -- Configuration management -- Wallet operations (watch addresses, balance queries, UTXO management) -- Async operation support via callbacks -- Comprehensive error handling -- Memory-safe abstractions - -## Building - -### Standalone Build - -```bash -cargo build --release -``` - -This will generate: -- Static library: `target/release/libdash_spv_ffi.a` -- Dynamic library: `target/release/libdash_spv_ffi.so` (or `.dylib` on macOS) -- C header: `include/dash_spv_ffi.h` - -## Usage - -See `examples/basic_usage.c` for a simple example of using the FFI bindings. - -### Basic Example - -```c -#include "dash_spv_ffi.h" - -// Initialize logging -dash_spv_ffi_init_logging("info", true, NULL, 0); - -// Create configuration -FFIClientConfig* config = dash_spv_ffi_config_testnet(); -dash_spv_ffi_config_set_data_dir(config, "/path/to/data"); - -// Create client -FFIDashSpvClient* client = dash_spv_ffi_client_new(config); -if (client == NULL) { - const char* error = dash_spv_ffi_get_last_error(); - // Handle error -} - -// Start the client and begin syncing in the background -if (dash_spv_ffi_client_run(client) != 0) { - // Handle error -} - -// ... use the client ... - -// Clean up -dash_spv_ffi_client_destroy(client); -dash_spv_ffi_config_destroy(config); -``` - -## API Documentation - -### Configuration - -- `dash_spv_ffi_config_new(network)` - Create new config -- `dash_spv_ffi_config_mainnet()` - Create mainnet config -- `dash_spv_ffi_config_testnet()` - Create testnet config -- `dash_spv_ffi_config_set_data_dir(config, path)` - Set data directory -- `dash_spv_ffi_config_set_validation_mode(config, mode)` - Set validation mode -- `dash_spv_ffi_config_set_max_peers(config, max)` - Set maximum peers -- `dash_spv_ffi_config_add_peer(config, addr)` - Add a peer address. Accepts `"ip:port"`, `[ipv6]:port`, or IP-only (defaults to the network port). -- `dash_spv_ffi_config_destroy(config)` - Free config memory - -### Client Operations - -- `dash_spv_ffi_client_new(config)` - Create new client -- `dash_spv_ffi_client_run(client)` - Start the client and begin syncing in the background -- `dash_spv_ffi_client_stop(client)` - Stop the client -- `dash_spv_ffi_client_get_sync_progress(client)` - Get sync progress -- `dash_spv_ffi_client_get_stats(client)` - Get client statistics -- `dash_spv_ffi_client_destroy(client)` - Free client memory - -### Wallet Operations - -- `dash_spv_ffi_client_get_address_balance(client, address)` - Get address balance -- `dash_spv_ffi_client_get_utxos(client)` - Get all UTXOs -- `dash_spv_ffi_client_get_utxos_for_address(client, address)` - Get UTXOs for address - -### Error Handling - -- `dash_spv_ffi_get_last_error()` - Get last error message -- `dash_spv_ffi_clear_error()` - Clear last error - -### Memory Management - -All created objects must be explicitly destroyed: -- Config: `dash_spv_ffi_config_destroy()` -- Client: `dash_spv_ffi_client_destroy()` -- Progress: `dash_spv_ffi_sync_progress_destroy()` -- Stats: `dash_spv_ffi_spv_stats_destroy()` -- Balance: `dash_spv_ffi_balance_destroy()` -- Arrays: `dash_spv_ffi_array_destroy()` -- Strings: `dash_spv_ffi_string_destroy()` - -## Thread Safety - -The FFI bindings are thread-safe. The client uses internal synchronization to ensure safe concurrent access. - -## License - -MIT diff --git a/dash-spv-ffi/build.rs b/dash-spv-ffi/build.rs deleted file mode 100644 index 29549edbb..000000000 --- a/dash-spv-ffi/build.rs +++ /dev/null @@ -1,32 +0,0 @@ -use std::path::Path; -use std::{env, fs}; - -fn main() { - let crate_name = env::var("CARGO_PKG_NAME").unwrap(); - let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); - let out_dir = env::var("OUT_DIR").unwrap(); - - println!("cargo:rerun-if-changed=cbindgen.toml"); - println!("cargo:rerun-if-changed=src/"); - - let target_dir = Path::new(&out_dir) - .ancestors() - .nth(3) // This line moves up to the target/ directory - .expect("Failed to find target dir"); - - let include_dir = target_dir.join("include").join(&crate_name); - - fs::create_dir_all(&include_dir).unwrap(); - - let output_path = include_dir.join(format!("{}.h", &crate_name)); - - let config_path = Path::new(&crate_dir).join("cbindgen.toml"); - let config = cbindgen::Config::from_file(&config_path).expect("Failed to read cbindgen.toml"); - - cbindgen::Builder::new() - .with_crate(&crate_dir) - .with_config(config) - .generate() - .expect("Unable to generate bindings") - .write_to_file(&output_path); -} diff --git a/dash-spv-ffi/cbindgen.toml b/dash-spv-ffi/cbindgen.toml deleted file mode 100644 index ebb6cbc9c..000000000 --- a/dash-spv-ffi/cbindgen.toml +++ /dev/null @@ -1,38 +0,0 @@ -language = "C" -header = "/* dash-spv-ffi C bindings - Auto-generated by cbindgen */" -include_guard = "DASH_SPV_FFI_H" -autogen_warning = "/* Warning: This file is auto-generated by cbindgen. Do not modify manually. */" -include_version = true -cpp_compat = true -includes = ["../key-wallet-ffi/key-wallet-ffi.h", "../dash-network/dash-network.h"] - -[export] -include = ["FFI"] -exclude = ["Option_BlockCallback", "Option_TransactionCallback", "Option_BalanceCallback"] -prefix = "" - -[export.body] -# Forward-declare FFIClientConfig to avoid aliasing to internal ClientConfig in C headers -"FFIClientConfig" = "" - -[export.rename] -"FFIValidationMode" = "DashSpvValidationMode" -"FFIErrorCode" = "DashSpvErrorCode" - -[fn] -prefix = "" -postfix = "" - -[struct] -rename_fields = "None" - -[enum] -prefix_with_name = true -rename_variants = "None" - -[parse] -parse_deps = false -include = [] - -[macro_expansion] -bitflags = false diff --git a/dash-spv-ffi/examples/basic_usage.c b/dash-spv-ffi/examples/basic_usage.c deleted file mode 100644 index deae9d3ec..000000000 --- a/dash-spv-ffi/examples/basic_usage.c +++ /dev/null @@ -1,42 +0,0 @@ -#include -#include -#include "../include/dash_spv_ffi.h" - -int main() { - // Initialize logging - if (dash_spv_ffi_init_logging("info", true, NULL, 0) != 0) { - fprintf(stderr, "Failed to initialize logging\n"); - return 1; - } - - // Create a configuration for testnet - FFIClientConfig* config = dash_spv_ffi_config_testnet(); - if (config == NULL) { - fprintf(stderr, "Failed to create config\n"); - return 1; - } - - // Set data directory - if (dash_spv_ffi_config_set_data_dir(config, "/tmp/dash-spv-test") != 0) { - fprintf(stderr, "Failed to set data dir\n"); - dash_spv_ffi_config_destroy(config); - return 1; - } - - // Create the client - FFIDashSpvClient* client = dash_spv_ffi_client_new(config); - if (client == NULL) { - const char* error = dash_spv_ffi_get_last_error(); - fprintf(stderr, "Failed to create client: %s\n", error); - dash_spv_ffi_config_destroy(config); - return 1; - } - - printf("Successfully created Dash SPV client!\n"); - - // Clean up - dash_spv_ffi_client_destroy(client); - dash_spv_ffi_config_destroy(config); - - return 0; -} diff --git a/dash-spv-ffi/examples/wallet_manager_usage.rs b/dash-spv-ffi/examples/wallet_manager_usage.rs deleted file mode 100644 index fdacaceda..000000000 --- a/dash-spv-ffi/examples/wallet_manager_usage.rs +++ /dev/null @@ -1,75 +0,0 @@ -/// Example demonstrating the simplified FFIWalletManager usage -/// -/// The refactored design removes unnecessary indirection by: -/// 1. FFIWalletManager directly contains Arc> -/// 2. No longer requires going through the client for each operation -/// 3. Cleaner and more efficient access to wallet functionality -use dash_spv_ffi::*; -use key_wallet_ffi::{wallet_manager_wallet_count, FFIError}; - -fn main() { - unsafe { - // Create a config for testnet - let config = dash_spv_ffi_config_testnet(); - if config.is_null() { - panic!("Failed to create config"); - } - - // Create an SPV client - let client = dash_spv_ffi_client_new(config, FFIEventCallbacks::default()); - if client.is_null() { - panic!("Failed to create client"); - } - - // Get the wallet manager - this returns a strongly typed pointer that - // shares the Arc with the SPV client, allowing direct interaction - let wallet_manager = dash_spv_ffi_client_get_wallet_manager(client); - if wallet_manager.is_null() { - panic!("Failed to get wallet manager"); - } - - // Now we can use the wallet manager directly - // No need to go through client -> inner -> spv_client -> wallet() - - // Get the number of wallets (should be 0 initially) - let mut error = std::mem::zeroed::(); - let wallet_count = wallet_manager_wallet_count( - wallet_manager as *const key_wallet_ffi::FFIWalletManager, - &mut error, - ); - println!("Number of wallets: {}", wallet_count); - - // Note: To get total balance, you would need to iterate through wallets - // For now, just show the wallet count - println!("Currently managing {} wallets", wallet_count); - - // Example of processing a transaction (with mock data) - // In real usage, you would have actual transaction hex - /* - let tx_hex = "01000000..."; // Transaction hex string - let mut error = std::mem::zeroed(); - let affected = wallet_manager_process_transaction( - wallet_manager, - tx_hex.as_ptr() as *const i8, - FFINetwork::Testnet, - 100000, // block height - &mut error - ); - - if affected >= 0 { - println!("Transaction affected {} wallets", affected); - } else { - println!("Failed to process transaction"); - } - */ - - // Clean up - // The wallet manager can now be independently destroyed - // It maintains its own Arc reference to the underlying wallet - dash_spv_ffi_wallet_manager_free(wallet_manager); - dash_spv_ffi_client_destroy(client); - dash_spv_ffi_config_destroy(config); - - println!("Example completed successfully!"); - } -} diff --git a/dash-spv-ffi/scripts/generate_ffi_docs.py b/dash-spv-ffi/scripts/generate_ffi_docs.py deleted file mode 100755 index d26411183..000000000 --- a/dash-spv-ffi/scripts/generate_ffi_docs.py +++ /dev/null @@ -1,380 +0,0 @@ -#!/usr/bin/env python3 -""" -Generate FFI API documentation for dash-spv-ffi -""" - -import os -import re -import sys -from pathlib import Path -from dataclasses import dataclass -from typing import List, Optional, Dict -import subprocess - -@dataclass -class FFIFunction: - name: str - signature: str - module: str - doc_comment: Optional[str] = None - safety_comment: Optional[str] = None - params: List[str] = None - return_type: str = None - -def extract_ffi_functions(file_path: Path) -> List[FFIFunction]: - """Extract all #[no_mangle] extern "C" functions from a Rust file. - - Uses a lightweight parser to correctly capture parameters with nested parentheses - (e.g., function pointer types) and the return type. - """ - functions: List[FFIFunction] = [] - - with open(file_path, 'r', encoding='utf-8') as f: - content = f.read() - - # Iterate over all #[no_mangle] attribute occurrences - for m in re.finditer(r'(?m)^\s*#\[no_mangle\]\s*$', content): - idx = m.end() - # Find the next extern "C" fn declaration - fn_match = re.search(r'\bextern\s+"C"\s+fn\s+([A-Za-z0-9_]+)\s*\(', content[idx:], re.S) - if not fn_match: - continue - name = fn_match.group(1) - abs_start = idx + fn_match.start() - paren_start = content.find('(', abs_start) - if paren_start == -1: - continue - - # Scan for the matching closing parenthesis with nesting - depth = 0 - i = paren_start - while i < len(content): - ch = content[i] - if ch == '(': - depth += 1 - elif ch == ')': - depth -= 1 - if depth == 0: - break - i += 1 - if depth != 0: - continue # Unbalanced; skip - paren_end = i - - params_raw = content[paren_start + 1:paren_end] - - # Find return type between paren_end and the opening brace '{' - brace_idx = content.find('{', paren_end) - header_tail = content[paren_end:brace_idx if brace_idx != -1 else len(content)] - ret_match = re.search(r'->\s*([^\n{]+)', header_tail) - return_type = ret_match.group(1).strip() if ret_match else '()' - - # Collect contiguous doc comments above #[no_mangle] - # Walk backwards line-by-line accumulating lines starting with '///' - doc_lines_rev: List[str] = [] - # Position of start of the #[no_mangle] line - line_start = content.rfind('\n', 0, m.start()) + 1 - j = line_start - 1 - while j > 0: - prev_nl = content.rfind('\n', 0, j) - line = content[prev_nl + 1:j] - if line.strip().startswith('///'): - # Strip leading /// and whitespace - doc_lines_rev.append(line.strip()[3:].strip()) - j = prev_nl - continue - # Allow a single blank line between doc comment blocks - if line.strip() == '' and doc_lines_rev: - j = prev_nl - continue - break - doc_lines = list(reversed(doc_lines_rev)) if doc_lines_rev else [] - - # Extract safety sub-section from doc lines - safety_comment = None - if doc_lines: - joined = '\n'.join(doc_lines) - if '# Safety' in joined: - safety_lines: List[str] = [] - in_safety = False - for dl in doc_lines: - if dl.strip().startswith('# Safety'): - in_safety = True - continue - if in_safety and dl.strip().startswith('#'): - break - if in_safety: - safety_lines.append(dl) - safety_comment = ' '.join(safety_lines).strip() if safety_lines else None - - params_clean = re.sub(r'\s+', ' ', params_raw.strip()) - module_name = file_path.stem - - functions.append(FFIFunction( - name=name, - signature=f"{name}({params_clean}) -> {return_type}", - module=module_name, - doc_comment=' '.join(doc_lines) if doc_lines else None, - safety_comment=safety_comment, - params=params_clean, - return_type=return_type, - )) - - return functions - -def categorize_functions(functions: List[FFIFunction]) -> Dict[str, List[FFIFunction]]: - """Categorize functions by their module/purpose.""" - categories = { - 'Client Management': [], - 'Configuration': [], - 'Synchronization': [], - 'Wallet Operations': [], - 'Address Monitoring': [], - 'Transaction Management': [], - 'Balance & UTXOs': [], - 'Mempool Operations': [], - 'Platform Integration': [], - 'Event Callbacks': [], - 'Error Handling': [], - 'Utility Functions': [], - } - - for func in functions: - name = func.name.lower() - - if 'client_new' in name or 'client_start' in name or 'client_stop' in name or 'client_destroy' in name: - categories['Client Management'].append(func) - elif 'config' in name: - categories['Configuration'].append(func) - elif 'sync' in name: - categories['Synchronization'].append(func) - elif 'wallet' in name and 'manager' not in name: - categories['Wallet Operations'].append(func) - elif 'watch' in name or 'unwatch' in name or 'address' in name and 'balance' not in name: - categories['Address Monitoring'].append(func) - elif 'transaction' in name or 'broadcast' in name or 'tx' in name: - categories['Transaction Management'].append(func) - elif 'balance' in name or 'utxo' in name: - categories['Balance & UTXOs'].append(func) - elif 'mempool' in name: - categories['Mempool Operations'].append(func) - elif 'platform' in name or 'quorum' in name or 'core_handle' in name: - categories['Platform Integration'].append(func) - elif 'callback' in name or 'event' in name: - categories['Event Callbacks'].append(func) - elif 'error' in name or 'last_error' in name: - categories['Error Handling'].append(func) - else: - categories['Utility Functions'].append(func) - - # Remove empty categories - return {k: v for k, v in categories.items() if v} - -def generate_markdown(functions: List[FFIFunction]) -> str: - """Generate markdown documentation from FFI functions.""" - - categories = categorize_functions(functions) - - md = [] - md.append("# Dash SPV FFI API Documentation") - md.append("") - md.append("This document provides a comprehensive reference for all FFI (Foreign Function Interface) functions available in the dash-spv-ffi library.") - md.append("") - md.append("**Auto-generated**: This documentation is automatically generated from the source code. Do not edit manually.") - md.append("") - md.append(f"**Total Functions**: {len(functions)}") - md.append("") - - # Table of Contents - md.append("## Table of Contents") - md.append("") - for category in categories.keys(): - anchor = category.lower().replace(' ', '-').replace('&', 'and') - md.append(f"- [{category}](#{anchor})") - md.append("") - - # Function Reference - md.append("## Function Reference") - md.append("") - - for category, funcs in categories.items(): - if not funcs: - continue - - anchor = category.lower().replace(' ', '-').replace('&', 'and') - md.append(f"### {category}") - md.append("") - md.append(f"Functions: {len(funcs)}") - md.append("") - - # Create a table for each category - md.append("| Function | Description | Module |") - md.append("|----------|-------------|--------|") - - for func in sorted(funcs, key=lambda f: f.name): - desc = func.doc_comment.split('.')[0] if func.doc_comment else "No description" - desc = desc.replace('|', '\\|') # Escape pipes in description - if len(desc) > 80: - # Truncate at last complete word before 77 chars to avoid mid-word breaks - truncate_pos = desc.rfind(' ', 0, 77) - if truncate_pos > 60: # Only if we find a space reasonably close - desc = desc[:truncate_pos] + "..." - else: - desc = desc[:77] + "..." - md.append(f"| `{func.name}` | {desc} | {func.module} |") - - md.append("") - - # Detailed Function Documentation - md.append("## Detailed Function Documentation") - md.append("") - - for category, funcs in categories.items(): - if not funcs: - continue - - md.append(f"### {category} - Detailed") - md.append("") - - for func in sorted(funcs, key=lambda f: f.name): - md.append(f"#### `{func.name}`") - md.append("") - md.append("```c") - md.append(func.signature) - md.append("```") - md.append("") - - if func.doc_comment: - md.append("**Description:**") - md.append(func.doc_comment) - md.append("") - - if func.safety_comment: - md.append("**Safety:**") - md.append(func.safety_comment) - md.append("") - - md.append(f"**Module:** `{func.module}`") - md.append("") - md.append("---") - md.append("") - - # Type Definitions - md.append("## Type Definitions") - md.append("") - md.append("### Core Types") - md.append("") - md.append("- `FFIDashSpvClient` - SPV client handle") - md.append("- `FFIClientConfig` - Client configuration") - md.append("- `FFISyncProgress` - Synchronization progress") - md.append("- `FFIDetailedSyncProgress` - Detailed sync progress") - md.append("- `FFITransaction` - Transaction information") - md.append("- `FFIUnconfirmedTransaction` - Unconfirmed transaction") - md.append("- `FFIEventCallbacks` - Event callback structure") - md.append("- `CoreSDKHandle` - Platform SDK integration handle") - md.append("") - - md.append("### Enumerations") - md.append("") - md.append("- `FFINetwork` - Network type (Mainnet, Testnet, Regtest, Devnet)") - md.append("- `FFIValidationMode` - Validation mode (None, Basic, Full)") - md.append("- `FFIMempoolStrategy` - Mempool strategy (FetchAll, BloomFilter, Selective)") - md.append("") - - # Memory Management - md.append("## Memory Management") - md.append("") - md.append("### Important Rules") - md.append("") - md.append("1. **Ownership Transfer**: Functions returning pointers transfer ownership to the caller") - md.append("2. **Cleanup Required**: All returned pointers must be freed using the appropriate `_destroy` function") - md.append("3. **Thread Safety**: The SPV client is thread-safe") - md.append("4. **Error Handling**: Check return codes and use `dash_spv_ffi_get_last_error()` for details") - md.append( - "5. **Shared Ownership**: `dash_spv_ffi_client_get_wallet_manager()` returns `FFIWalletManager*` " - "that must be released with `dash_spv_ffi_wallet_manager_free()`" - ) - md.append("") - - # Usage Examples - md.append("## Usage Examples") - md.append("") - md.append("### Basic SPV Client Usage") - md.append("") - md.append("```c") - md.append("// Create configuration") - md.append("FFIClientConfig* config = dash_spv_ffi_config_testnet();") - md.append("") - md.append("// Build event callbacks (zero-init for no-op defaults)") - md.append("FFIEventCallbacks callbacks = { 0 };") - md.append("") - md.append("// Create client with callbacks") - md.append("FFIDashSpvClient* client = dash_spv_ffi_client_new(config, callbacks);") - md.append("") - md.append("// Start syncing (uses callbacks provided at creation)") - md.append("int32_t result = dash_spv_ffi_client_run(client);") - md.append("if (result != 0) {") - md.append(" const char* error = dash_spv_ffi_get_last_error();") - md.append(" // Handle error") - md.append("}") - md.append("") - md.append("// Get wallet manager (shares ownership with the client)") - md.append("FFIWalletManager* wallet_manager = dash_spv_ffi_client_get_wallet_manager(client);") - md.append("") - md.append("// Clean up") - md.append("dash_spv_ffi_client_destroy(client);") - md.append("dash_spv_ffi_config_destroy(config);") - md.append("```") - md.append("") - - md.append("### Event Callbacks") - md.append("") - md.append("```c") - md.append("void on_headers(uint32_t tip_height, void* user_data) {") - md.append(" printf(\"Headers stored up to height %u\\n\", tip_height);") - md.append("}") - md.append("") - md.append("void on_tx(const char* wallet_id, uint32_t account_index,") - md.append(" const uint8_t (*txid)[32], int64_t amount,") - md.append(" const char* addresses, void* user_data) {") - md.append(" printf(\"Transaction: %lld duffs\\n\", (long long)amount);") - md.append("}") - md.append("") - md.append("// Build callbacks struct and pass to client_new()") - md.append("FFIEventCallbacks callbacks = { 0 };") - md.append("callbacks.sync.on_block_headers_stored = on_headers;") - md.append("callbacks.wallet.on_transaction_received = on_tx;") - md.append("FFIDashSpvClient* client = dash_spv_ffi_client_new(config, callbacks);") - md.append("") - md.append("// Start syncing (uses callbacks provided at creation)") - md.append("dash_spv_ffi_client_run(client);") - md.append("```") - md.append("") - - return '\n'.join(md) - -def main(): - # Find all Rust source files - src_dir = Path(__file__).parent.parent / "src" - - all_functions = [] - - for rust_file in src_dir.rglob("*.rs"): - functions = extract_ffi_functions(rust_file) - all_functions.extend(functions) - - # Generate markdown - markdown = generate_markdown(all_functions) - - # Write to file - output_file = Path(__file__).parent.parent / "FFI_API.md" - with open(output_file, 'w', encoding='utf-8') as f: - f.write(markdown) - - print(f"Generated FFI documentation with {len(all_functions)} functions") - print(f"Output: {output_file}") - - return 0 - -if __name__ == "__main__": - sys.exit(main()) diff --git a/dash-spv-ffi/src/bin/ffi_cli.rs b/dash-spv-ffi/src/bin/ffi_cli.rs deleted file mode 100644 index fa32ddb87..000000000 --- a/dash-spv-ffi/src/bin/ffi_cli.rs +++ /dev/null @@ -1,604 +0,0 @@ -use std::ffi::{CStr, CString}; -use std::os::raw::{c_char, c_void}; -use std::ptr; - -use clap::{Arg, ArgAction, Command}; -use dash_network::ffi::FFINetwork; -use dash_spv_ffi::*; -use key_wallet_ffi::types::FFIBalance; -use key_wallet_ffi::wallet_manager::wallet_manager_add_wallet_from_mnemonic; -use key_wallet_ffi::FFIError; - -fn ffi_string_to_rust(s: *const c_char) -> String { - if s.is_null() { - return String::new(); - } - unsafe { CStr::from_ptr(s) }.to_str().unwrap_or_default().to_owned() -} - -// ============================================================================ -// Sync Event Callbacks -// ============================================================================ - -extern "C" fn on_sync_start(manager_id: FFIManagerId, _user_data: *mut c_void) { - let manager_name = match manager_id { - FFIManagerId::Headers => "Headers", - FFIManagerId::FilterHeaders => "FilterHeaders", - FFIManagerId::Filters => "Filters", - FFIManagerId::Blocks => "Blocks", - FFIManagerId::Masternodes => "Masternodes", - FFIManagerId::ChainLocks => "ChainLocks", - FFIManagerId::InstantSend => "InstantSend", - FFIManagerId::Mempool => "Mempool", - }; - println!("[Sync] Manager started: {}", manager_name); -} - -extern "C" fn on_block_headers_stored(tip_height: u32, _user_data: *mut c_void) { - println!("[Sync] Block headers stored, tip: {}", tip_height); -} - -extern "C" fn on_block_header_sync_complete(tip_height: u32, _user_data: *mut c_void) { - println!("[Sync] Block header sync complete at height: {}", tip_height); -} - -extern "C" fn on_filter_headers_stored( - start_height: u32, - end_height: u32, - tip_height: u32, - _user_data: *mut c_void, -) { - println!("[Sync] Filter headers stored: {}-{}, tip: {}", start_height, end_height, tip_height); -} - -extern "C" fn on_filter_headers_sync_complete(tip_height: u32, _user_data: *mut c_void) { - println!("[Sync] Filter headers sync complete at height: {}", tip_height); -} - -extern "C" fn on_filters_stored(start_height: u32, end_height: u32, _user_data: *mut c_void) { - println!("[Sync] Filters stored: {}-{}", start_height, end_height); -} - -extern "C" fn on_filters_sync_complete(tip_height: u32, _user_data: *mut c_void) { - println!("[Sync] Filters sync complete at height: {}", tip_height); -} - -extern "C" fn on_blocks_needed(blocks: *const FFIBlockNeeded, count: u32, _user_data: *mut c_void) { - println!("[Sync] Blocks needed: {}", count); - if !blocks.is_null() && count > 0 { - let blocks_slice = unsafe { std::slice::from_raw_parts(blocks, count as usize) }; - for block in blocks_slice.iter() { - println!(" - height: {}, hash: {}", block.height, hex::encode(block.hash)); - } - } -} - -extern "C" fn on_block_processed( - height: u32, - _hash: *const [u8; 32], - new_address_count: u32, - _confirmed_txids: *const [u8; 32], - confirmed_txid_count: u32, - _user_data: *mut c_void, -) { - println!( - "[Sync] Block processed: height={}, new_addresses={}, confirmed_txs={}", - height, new_address_count, confirmed_txid_count - ); -} - -extern "C" fn on_masternode_state_updated(height: u32, _user_data: *mut c_void) { - println!("[Sync] Masternode state updated at height: {}", height); -} - -extern "C" fn on_chainlock_received( - height: u32, - hash: *const [u8; 32], - signature: *const [u8; 96], - validated: bool, - _user_data: *mut c_void, -) { - let hash_hex = unsafe { hex::encode(*hash) }; - let signature_hex = unsafe { hex::encode(*signature) }; - println!( - "[Sync] ChainLock received: height={}, hash={}, signature={}, validated={}", - height, hash_hex, signature_hex, validated - ); -} - -extern "C" fn on_instantlock_received( - txid: *const [u8; 32], - _instantlock_data: *const u8, - instantlock_len: usize, - validated: bool, - _user_data: *mut c_void, -) { - let txid_hex = unsafe { hex::encode(*txid) }; - println!( - "[Sync] InstantLock received: txid={}, validated={}, data_len={}", - txid_hex, validated, instantlock_len - ); -} - -extern "C" fn on_manager_error( - manager_id: FFIManagerId, - error: *const c_char, - _user_data: *mut c_void, -) { - let error_str = ffi_string_to_rust(error); - println!("[Sync] Manager error: {:?} - {}", manager_id, error_str); -} - -extern "C" fn on_sync_complete(header_tip: u32, cycle: u32, _user_data: *mut c_void) { - println!("[Sync] Sync complete at height: {} (cycle {})", header_tip, cycle); -} - -// ============================================================================ -// Network Event Callbacks -// ============================================================================ - -extern "C" fn on_peer_connected(address: *const c_char, _user_data: *mut c_void) { - let addr = ffi_string_to_rust(address); - println!("[Network] Peer connected: {}", addr); -} - -extern "C" fn on_peer_disconnected(address: *const c_char, _user_data: *mut c_void) { - let addr = ffi_string_to_rust(address); - println!("[Network] Peer disconnected: {}", addr); -} - -extern "C" fn on_peers_updated(connected_count: u32, best_height: u32, _user_data: *mut c_void) { - println!("[Network] Peers: {} connected, best height: {}", connected_count, best_height); -} - -// ============================================================================ -// Wallet Event Callbacks -// ============================================================================ - -fn short_wallet(wallet_id: *const c_char) -> String { - let s = ffi_string_to_rust(wallet_id); - if s.len() > 8 { - s[..8].to_string() - } else { - s - } -} - -fn read_balance(balance: *const FFIBalance) -> FFIBalance { - if balance.is_null() { - tracing::warn!("read_balance: null pointer, returning zero balance"); - return FFIBalance::default(); - } - unsafe { *balance } -} - -#[allow(clippy::too_many_arguments)] -extern "C" fn on_transaction_detected( - wallet_id: *const c_char, - record: *const FFITransactionRecord, - balance: *const FFIBalance, - _account_balances: *const dash_spv_ffi::FFIAccountBalance, - account_balances_count: u32, - _addresses_derived: *const dash_spv_ffi::FFIDerivedAddress, - addresses_derived_count: u32, - _user_data: *mut c_void, -) { - let wallet_short = short_wallet(wallet_id); - if record.is_null() { - println!("[Wallet] TX detected: wallet={}..., record=null", wallet_short); - return; - } - let r = unsafe { &*record }; - let b = read_balance(balance); - let txid_hex = hex::encode(r.txid); - println!( - "[Wallet] TX detected: wallet={}..., txid={}, account_kind={:?}, account_index={}, amount={} duffs, balance[confirmed={}, unconfirmed={}], changed_accounts={}, derived={}", - wallet_short, - txid_hex, - r.account_type.kind, - r.account_type.index, - r.net_amount, - b.confirmed, - b.unconfirmed, - account_balances_count, - addresses_derived_count, - ); -} - -extern "C" fn on_transaction_instant_locked( - wallet_id: *const c_char, - txid: *const [u8; 32], - _islock_data: *const u8, - islock_len: usize, - balance: *const FFIBalance, - _account_balances: *const dash_spv_ffi::FFIAccountBalance, - account_balances_count: u32, - _user_data: *mut c_void, -) { - let wallet_short = short_wallet(wallet_id); - if txid.is_null() { - println!("[Wallet] TX instant-locked: wallet={}..., txid=null", wallet_short); - return; - } - let txid_bytes = unsafe { &*txid }; - let b = read_balance(balance); - let txid_hex = hex::encode(txid_bytes); - println!( - "[Wallet] TX instant-locked: wallet={}..., txid={}, islock_len={}, balance[confirmed={}, unconfirmed={}], changed_accounts={}", - wallet_short, - txid_hex, - islock_len, - b.confirmed, - b.unconfirmed, - account_balances_count, - ); -} - -#[allow(clippy::too_many_arguments)] -extern "C" fn on_wallet_block_processed( - wallet_id: *const c_char, - height: u32, - _inserted: *const FFITransactionRecord, - inserted_count: u32, - _updated: *const FFITransactionRecord, - updated_count: u32, - _matured: *const FFITransactionRecord, - matured_count: u32, - balance: *const FFIBalance, - _account_balances: *const dash_spv_ffi::FFIAccountBalance, - account_balances_count: u32, - _addresses_derived: *const dash_spv_ffi::FFIDerivedAddress, - addresses_derived_count: u32, - cl_height: u32, - cl_hash: *const [u8; 32], - _cl_signature: *const [u8; 96], - _user_data: *mut c_void, -) { - let wallet_short = short_wallet(wallet_id); - let b = read_balance(balance); - let chainlocked = if cl_hash.is_null() { - "no".to_string() - } else { - format!("yes@{}", cl_height) - }; - println!( - "[Wallet] Block processed: wallet={}..., height={}, chainlock={}, inserted={}, updated={}, matured={}, balance[confirmed={}, unconfirmed={}, immature={}, locked={}], changed_accounts={}, derived={}", - wallet_short, - height, - chainlocked, - inserted_count, - updated_count, - matured_count, - b.confirmed, - b.unconfirmed, - b.immature, - b.locked, - account_balances_count, - addresses_derived_count, - ); -} - -extern "C" fn on_wallet_chain_lock_processed( - wallet_id: *const c_char, - cl_height: u32, - _cl_hash: *const [u8; 32], - _cl_signature: *const [u8; 96], - _finalized: *const dash_spv_ffi::FFIChainlockedTxid, - finalized_count: u32, - _user_data: *mut c_void, -) { - let wallet_short = short_wallet(wallet_id); - println!( - "[Wallet] ChainLock processed: wallet={}..., cl_height={}, finalized={}", - wallet_short, cl_height, finalized_count, - ); -} - -extern "C" fn on_sync_height_advanced( - wallet_id: *const c_char, - height: u32, - _user_data: *mut c_void, -) { - let wallet_short = short_wallet(wallet_id); - println!("[Wallet] Sync height advanced: wallet={}..., height={}", wallet_short, height); -} - -// ============================================================================ -// Progress Callback -// ============================================================================ - -extern "C" fn on_progress_update(progress: *const FFISyncProgress, _user_data: *mut c_void) { - if progress.is_null() { - return; - } - let p = unsafe { &*progress }; - - let state_str = match p.state { - FFISyncState::WaitForEvents => "WaitForEvents", - FFISyncState::WaitingForConnections => "WaitingForConnections", - FFISyncState::Syncing => "Syncing", - FFISyncState::Synced => "Synced", - FFISyncState::Error => "Error", - }; - - print!("[Progress] {:.1}% {} ", p.percentage * 100.0, state_str); - - if !p.headers.is_null() { - let h = unsafe { &*p.headers }; - print!("headers:{}/{} ", h.tip_height + h.buffered, h.target_height); - } - if !p.filter_headers.is_null() { - let fh = unsafe { &*p.filter_headers }; - print!("filter headers:{}/{} ", fh.current_height, fh.target_height); - } - if !p.filters.is_null() { - let f = unsafe { &*p.filters }; - print!("filters:{}/{} stored: {} ", f.committed_height, f.target_height, f.stored_height); - } - if !p.blocks.is_null() { - let f = unsafe { &*p.blocks }; - print!("blocks: last: {}, transactions: {} ", f.last_processed, f.transactions); - } - if !p.masternodes.is_null() { - let mn = unsafe { &*p.masternodes }; - print!("masternodes:{}/{} ", mn.current_height, mn.target_height); - } - - println!(); -} - -// ============================================================================ -// Error Callback -// ============================================================================ - -extern "C" fn on_error(error: *const c_char, _user_data: *mut c_void) { - let msg = if error.is_null() { - "unknown error".to_string() - } else { - unsafe { std::ffi::CStr::from_ptr(error) } - .to_str() - .unwrap_or("invalid error string") - .to_string() - }; - eprintln!("[FATAL] {}", msg); - std::process::exit(1); -} - -fn main() { - let matches = Command::new("dash-spv-ffi") - .about("Run SPV sync via FFI using event callbacks") - .arg( - Arg::new("network") - .long("network") - .short('n') - .value_parser(clap::builder::PossibleValuesParser::new([ - "mainnet", "testnet", "regtest", - ])) - .default_value("mainnet"), - ) - .arg( - Arg::new("peer") - .long("peer") - .short('p') - .action(ArgAction::Append) - .help("Peer address host:port (repeatable)"), - ) - .arg( - Arg::new("log-level") - .long("log-level") - .value_parser(["error", "warn", "info", "debug", "trace"]) - .default_value("info") - .help("Tracing log level"), - ) - .arg( - Arg::new("start-height") - .long("start-height") - .value_parser(clap::value_parser!(u32)) - .help("Start syncing from nearest checkpoint at height"), - ) - .arg( - Arg::new("no-masternodes") - .long("no-masternodes") - .action(ArgAction::SetTrue) - .help("Disable masternode list synchronization"), - ) - .arg( - Arg::new("data-dir") - .short('d') - .long("data-dir") - .value_name("DIR") - .help("Data directory for storage (default: unique directory in /tmp)"), - ) - .arg( - Arg::new("mnemonic-file") - .long("mnemonic-file") - .value_name("PATH") - .help("Path to file containing BIP39 mnemonic phrase"), - ) - .get_matches(); - - // Map network - let network = match matches.get_one::("network").map(|s| s.as_str()) { - Some("mainnet") => FFINetwork::Mainnet, - Some("testnet") => FFINetwork::Testnet, - Some("regtest") => FFINetwork::Regtest, - _ => FFINetwork::Mainnet, - }; - - unsafe { - // Build config - let cfg = dash_spv_ffi_config_new(network); - if cfg.is_null() { - eprintln!( - "Failed to allocate config: {}", - ffi_string_to_rust(dash_spv_ffi_get_last_error()) - ); - std::process::exit(1); - } - - // Determine and set data directory - let data_dir = matches - .get_one::("data-dir") - .map(|s| s.to_string()) - .unwrap_or_else(|| ".tmp/ffi-cli".to_string()); - - let storage_dir_c = CString::new(data_dir.as_str()).unwrap(); - let rc = dash_spv_ffi_config_set_data_dir(cfg, storage_dir_c.as_ptr()); - if rc != FFIErrorCode::Success as i32 { - eprintln!( - "Failed to set data dir: {}", - ffi_string_to_rust(dash_spv_ffi_get_last_error()) - ); - std::process::exit(1); - } - println!("Storage directory: {}", data_dir); - - // Initialize tracing/logging via FFI with file logging to data_dir/logs - let level = matches.get_one::("log-level").map(String::as_str).unwrap_or("info"); - let level_c = CString::new(level).unwrap(); - let log_dir = format!("{}/logs", data_dir); - let log_dir_c = CString::new(log_dir.as_str()).unwrap(); - let _ = dash_spv_ffi_init_logging(level_c.as_ptr(), false, log_dir_c.as_ptr(), 5); - println!("Log directory: {}", log_dir); - - if let Some(height) = matches.get_one::("start-height") { - let _ = dash_spv_ffi_config_set_start_from_height(cfg, *height); - } - - if matches.get_flag("no-masternodes") { - let _ = dash_spv_ffi_config_set_masternode_sync_enabled(cfg, false); - } - - if let Some(peers) = matches.get_many::("peer") { - for p in peers { - let c = CString::new(p.as_str()).unwrap(); - let rc = dash_spv_ffi_config_add_peer(cfg, c.as_ptr()); - if rc != FFIErrorCode::Success as i32 { - eprintln!( - "Invalid peer {}: {}", - p, - ffi_string_to_rust(dash_spv_ffi_get_last_error()) - ); - } - } - } - - // Read mnemonic file if provided - let mnemonic_phrase = matches.get_one::("mnemonic-file").map(|path| { - std::fs::read_to_string(path) - .unwrap_or_else(|e| { - eprintln!("Failed to read mnemonic file '{}': {}", path, e); - std::process::exit(1); - }) - .trim() - .to_string() - }); - - // Build all event callbacks in a single struct - let callbacks = FFIEventCallbacks { - sync: FFISyncEventCallbacks { - on_sync_start: Some(on_sync_start), - on_block_headers_stored: Some(on_block_headers_stored), - on_block_header_sync_complete: Some(on_block_header_sync_complete), - on_filter_headers_stored: Some(on_filter_headers_stored), - on_filter_headers_sync_complete: Some(on_filter_headers_sync_complete), - on_filters_stored: Some(on_filters_stored), - on_filters_sync_complete: Some(on_filters_sync_complete), - on_blocks_needed: Some(on_blocks_needed), - on_block_processed: Some(on_block_processed), - on_masternode_state_updated: Some(on_masternode_state_updated), - on_chainlock_received: Some(on_chainlock_received), - on_instantlock_received: Some(on_instantlock_received), - on_manager_error: Some(on_manager_error), - on_sync_complete: Some(on_sync_complete), - user_data: ptr::null_mut(), - }, - network: FFINetworkEventCallbacks { - on_peer_connected: Some(on_peer_connected), - on_peer_disconnected: Some(on_peer_disconnected), - on_peers_updated: Some(on_peers_updated), - user_data: ptr::null_mut(), - }, - progress: FFIProgressCallback { - on_progress: Some(on_progress_update), - user_data: ptr::null_mut(), - }, - wallet: FFIWalletEventCallbacks { - on_transaction_detected: Some(on_transaction_detected), - on_transaction_instant_locked: Some(on_transaction_instant_locked), - on_block_processed: Some(on_wallet_block_processed), - on_sync_height_advanced: Some(on_sync_height_advanced), - on_chain_lock_processed: Some(on_wallet_chain_lock_processed), - user_data: ptr::null_mut(), - }, - error: FFIClientErrorCallback { - on_error: Some(on_error), - user_data: ptr::null_mut(), - }, - }; - - // Create client with event callbacks - let client = dash_spv_ffi_client_new(cfg, callbacks); - if client.is_null() { - eprintln!( - "Client create failed: {}", - ffi_string_to_rust(dash_spv_ffi_get_last_error()) - ); - std::process::exit(1); - } - - // Add wallet from mnemonic if provided - if let Some(ref mnemonic) = mnemonic_phrase { - let wallet_manager = dash_spv_ffi_client_get_wallet_manager(client); - if wallet_manager.is_null() { - eprintln!( - "Failed to get wallet manager: {}", - ffi_string_to_rust(dash_spv_ffi_get_last_error()) - ); - std::process::exit(1); - } - - let mnemonic_c = CString::new(mnemonic.as_str()).unwrap(); - let mut error = FFIError::default(); - let success = wallet_manager_add_wallet_from_mnemonic( - wallet_manager as *mut _, - mnemonic_c.as_ptr(), - &mut error, - ); - - if !success { - eprintln!("Failed to add wallet from mnemonic: {:?}", error); - std::process::exit(1); - } - - println!("Wallet created from mnemonic"); - dash_spv_ffi_wallet_manager_free(wallet_manager); - } - - println!("Event and progress callbacks configured, starting sync..."); - - // Run client - starts sync in background and returns immediately - let rc = dash_spv_ffi_client_run(client); - if rc != FFIErrorCode::Success as i32 { - eprintln!("Client run failed: {}", ffi_string_to_rust(dash_spv_ffi_get_last_error())); - std::process::exit(1); - } - - println!("Client running. Press Ctrl+C to shutdown..."); - - // Wait for Ctrl+C signal using tokio - tokio::runtime::Runtime::new() - .expect("Failed to create tokio runtime") - .block_on(tokio::signal::ctrl_c()) - .expect("Failed to listen for Ctrl+C"); - - println!("Shutting down..."); - - // Cleanup - dash_spv_ffi_client_stop(client); - dash_spv_ffi_client_destroy(client); - dash_spv_ffi_config_destroy(cfg); - - println!("Done."); - } -} diff --git a/dash-spv-ffi/src/callbacks.rs b/dash-spv-ffi/src/callbacks.rs deleted file mode 100644 index 62e25f8e0..000000000 --- a/dash-spv-ffi/src/callbacks.rs +++ /dev/null @@ -1,1373 +0,0 @@ -//! FFI callback types for event notifications. -//! -//! This module provides several callback structs, each with one callback per event variant: -//! - `FFIProgressCallback` - Sync progress updates -//! - `FFISyncEventCallbacks` - Sync coordinator events -//! - `FFINetworkEventCallbacks` - Network manager events -//! - `FFIWalletEventCallbacks` - Wallet manager events - -use crate::{dash_spv_ffi_sync_progress_destroy, FFISyncProgress}; -use dash_spv::network::NetworkEvent; -use dash_spv::sync::{SyncEvent, SyncProgress}; -use dash_spv::EventHandler; -use dashcore::hashes::Hash; -use key_wallet::account::AccountType; -use key_wallet::WalletCoreBalance; -use key_wallet_ffi::managed_account::{FFIAccountType, FFITransactionRecord}; -use key_wallet_ffi::types::FFIBalance; -use key_wallet_manager::WalletEvent; -use std::collections::BTreeMap; -use std::ffi::CString; -use std::os::raw::{c_char, c_void}; -use std::ptr; - -// ============================================================================ -// Sync Event Types (for FFISyncEventCallbacks) -// ============================================================================ - -/// Identifies which sync manager generated an event. -#[repr(C)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum FFIManagerId { - Headers = 0, - FilterHeaders = 1, - Filters = 2, - Blocks = 3, - Masternodes = 4, - ChainLocks = 5, - InstantSend = 6, - Mempool = 7, -} - -impl From for FFIManagerId { - fn from(id: dash_spv::sync::ManagerIdentifier) -> Self { - match id { - dash_spv::sync::ManagerIdentifier::BlockHeader => FFIManagerId::Headers, - dash_spv::sync::ManagerIdentifier::FilterHeader => FFIManagerId::FilterHeaders, - dash_spv::sync::ManagerIdentifier::Filter => FFIManagerId::Filters, - dash_spv::sync::ManagerIdentifier::Block => FFIManagerId::Blocks, - dash_spv::sync::ManagerIdentifier::Masternode => FFIManagerId::Masternodes, - dash_spv::sync::ManagerIdentifier::ChainLock => FFIManagerId::ChainLocks, - dash_spv::sync::ManagerIdentifier::InstantSend => FFIManagerId::InstantSend, - dash_spv::sync::ManagerIdentifier::Mempool => FFIManagerId::Mempool, - } - } -} - -// ============================================================================ -// Progress Callback -// ============================================================================ - -/// Callback for sync progress updates. -/// -/// Called whenever the sync progress changes. The progress pointer is only -/// valid for the duration of the callback. The caller must NOT free the -/// progress pointer - it will be freed automatically after the callback returns. -pub type OnProgressUpdateCallback = - Option; - -/// Progress callback configuration. -#[repr(C)] -#[derive(Clone)] -pub struct FFIProgressCallback { - /// Callback function for progress updates. - pub on_progress: OnProgressUpdateCallback, - /// User data passed to the callback. - pub user_data: *mut c_void, -} - -unsafe impl Send for FFIProgressCallback {} -unsafe impl Sync for FFIProgressCallback {} - -impl Default for FFIProgressCallback { - fn default() -> Self { - Self { - on_progress: None, - user_data: std::ptr::null_mut(), - } - } -} - -impl FFIProgressCallback { - /// Dispatch a progress update to the callback. - /// - /// Creates an FFISyncProgress from the Rust progress, calls the callback, - /// then cleans up all allocated memory. - pub fn dispatch(&self, progress: &dash_spv::sync::SyncProgress) { - if let Some(cb) = self.on_progress { - // Clone the progress to get an owned SyncProgress for conversion - let owned_progress = progress.clone(); - let ffi_progress = Box::new(FFISyncProgress::from(owned_progress)); - let ptr = Box::into_raw(ffi_progress); - - // Call the callback - cb(ptr as *const FFISyncProgress, self.user_data); - - // Clean up the progress and all its nested pointers - unsafe { - dash_spv_ffi_sync_progress_destroy(ptr); - } - } - } -} - -// ============================================================================ -// FFISyncEventCallbacks - One callback per SyncEvent variant -// ============================================================================ - -/// Callback for SyncEvent::SyncStart -pub type OnSyncStartCallback = - Option; - -/// Callback for SyncEvent::BlockHeadersStored -pub type OnBlockHeadersStoredCallback = - Option; - -/// Callback for SyncEvent::BlockHeaderSyncComplete -pub type OnBlockHeaderSyncCompleteCallback = - Option; - -/// Callback for SyncEvent::FilterHeadersStored -pub type OnFilterHeadersStoredCallback = Option< - extern "C" fn(start_height: u32, end_height: u32, tip_height: u32, user_data: *mut c_void), ->; - -/// Callback for SyncEvent::FilterHeadersSyncComplete -pub type OnFilterHeadersSyncCompleteCallback = - Option; - -/// Callback for SyncEvent::FiltersStored -pub type OnFiltersStoredCallback = - Option; - -/// Callback for SyncEvent::FiltersSyncComplete -pub type OnFiltersSyncCompleteCallback = - Option; - -/// A block that needs to be downloaded (height + hash). -#[repr(C)] -#[derive(Debug, Clone, Copy)] -pub struct FFIBlockNeeded { - /// Block height - pub height: u32, - /// Block hash (32 bytes) - pub hash: [u8; 32], -} - -/// Callback for SyncEvent::BlocksNeeded -/// -/// The `blocks` pointer points to an array of `FFIBlockNeeded` structs. -/// The pointer is borrowed and only valid for the duration of the callback. -/// Callers must memcpy/duplicate any data they need to retain after the -/// callback returns. -pub type OnBlocksNeededCallback = - Option; - -/// Callback for SyncEvent::BlockProcessed -/// -/// The `hash` pointer is borrowed and only valid for the duration of the -/// callback. Callers must memcpy/duplicate it to retain the value after -/// the callback returns. -pub type OnBlockProcessedCallback = Option< - extern "C" fn( - height: u32, - hash: *const [u8; 32], - new_address_count: u32, - confirmed_txids: *const [u8; 32], - confirmed_txid_count: u32, - user_data: *mut c_void, - ), ->; - -/// Callback for SyncEvent::MasternodeStateUpdated -pub type OnMasternodeStateUpdatedCallback = - Option; - -/// Callback for SyncEvent::ChainLockReceived -/// -/// The `hash` and `signature` pointers are borrowed and only valid for the -/// duration of the callback. Callers must memcpy/duplicate them to retain -/// the values after the callback returns. -pub type OnChainLockReceivedCallback = Option< - extern "C" fn( - height: u32, - hash: *const [u8; 32], - signature: *const [u8; 96], - validated: bool, - user_data: *mut c_void, - ), ->; - -/// Callback for SyncEvent::InstantLockReceived -/// -/// The `txid` pointer is borrowed and only valid for the duration of the callback. -/// The `instantlock_data` pointer points to the consensus-serialized InstantLock -/// bytes and is only valid for the duration of the callback. -/// Callers must memcpy/duplicate any data they need to retain. -pub type OnInstantLockReceivedCallback = Option< - extern "C" fn( - txid: *const [u8; 32], - instantlock_data: *const u8, - instantlock_len: usize, - validated: bool, - user_data: *mut c_void, - ), ->; - -/// Callback for SyncEvent::ManagerError -/// -/// The `error` string pointer is borrowed and only valid for the duration -/// of the callback. Callers must copy the string if they need to retain it -/// after the callback returns. -pub type OnManagerErrorCallback = - Option; - -/// Callback for SyncEvent::SyncComplete -pub type OnSyncCompleteCallback = - Option; - -/// Sync event callbacks - one callback per SyncEvent variant. -/// -/// Set only the callbacks you're interested in; unset callbacks will be ignored. -/// -/// All pointer parameters passed to callbacks (strings, hashes, arrays) are -/// borrowed and only valid for the duration of the callback invocation. -/// Callers must memcpy/duplicate any data they need to retain. -#[repr(C)] -#[derive(Clone)] -pub struct FFISyncEventCallbacks { - pub on_sync_start: OnSyncStartCallback, - pub on_block_headers_stored: OnBlockHeadersStoredCallback, - pub on_block_header_sync_complete: OnBlockHeaderSyncCompleteCallback, - pub on_filter_headers_stored: OnFilterHeadersStoredCallback, - pub on_filter_headers_sync_complete: OnFilterHeadersSyncCompleteCallback, - pub on_filters_stored: OnFiltersStoredCallback, - pub on_filters_sync_complete: OnFiltersSyncCompleteCallback, - pub on_blocks_needed: OnBlocksNeededCallback, - pub on_block_processed: OnBlockProcessedCallback, - pub on_masternode_state_updated: OnMasternodeStateUpdatedCallback, - pub on_chainlock_received: OnChainLockReceivedCallback, - pub on_instantlock_received: OnInstantLockReceivedCallback, - pub on_manager_error: OnManagerErrorCallback, - pub on_sync_complete: OnSyncCompleteCallback, - pub user_data: *mut c_void, -} - -// SAFETY: FFISyncEventCallbacks is safe to send between threads because: -// 1. All callback function pointers are extern "C" functions with no captured state -// 2. The user_data pointer is treated as opaque and managed by the caller -// 3. The caller is responsible for ensuring user_data points to thread-safe memory -unsafe impl Send for FFISyncEventCallbacks {} - -// SAFETY: FFISyncEventCallbacks is safe to share between threads because: -// 1. The struct is immutable after construction -// 2. Function pointers are inherently thread-safe -// 3. Thread safety of user_data is the caller's responsibility -unsafe impl Sync for FFISyncEventCallbacks {} - -impl Default for FFISyncEventCallbacks { - fn default() -> Self { - Self { - on_sync_start: None, - on_block_headers_stored: None, - on_block_header_sync_complete: None, - on_filter_headers_stored: None, - on_filter_headers_sync_complete: None, - on_filters_stored: None, - on_filters_sync_complete: None, - on_blocks_needed: None, - on_block_processed: None, - on_masternode_state_updated: None, - on_chainlock_received: None, - on_instantlock_received: None, - on_manager_error: None, - on_sync_complete: None, - user_data: std::ptr::null_mut(), - } - } -} - -impl FFISyncEventCallbacks { - /// Dispatch a SyncEvent to the appropriate callback. - pub fn dispatch(&self, event: &dash_spv::sync::SyncEvent) { - use dash_spv::sync::SyncEvent; - - match event { - SyncEvent::SyncStart { - identifier, - } => { - if let Some(cb) = self.on_sync_start { - cb((*identifier).into(), self.user_data); - } - } - SyncEvent::BlockHeadersStored { - tip_height, - } => { - if let Some(cb) = self.on_block_headers_stored { - cb(*tip_height, self.user_data); - } - } - SyncEvent::BlockHeaderSyncComplete { - tip_height, - } => { - if let Some(cb) = self.on_block_header_sync_complete { - cb(*tip_height, self.user_data); - } - } - SyncEvent::FilterHeadersStored { - start_height, - end_height, - tip_height, - } => { - if let Some(cb) = self.on_filter_headers_stored { - cb(*start_height, *end_height, *tip_height, self.user_data); - } - } - SyncEvent::FilterHeadersSyncComplete { - tip_height, - } => { - if let Some(cb) = self.on_filter_headers_sync_complete { - cb(*tip_height, self.user_data); - } - } - SyncEvent::FiltersStored { - start_height, - end_height, - } => { - if let Some(cb) = self.on_filters_stored { - cb(*start_height, *end_height, self.user_data); - } - } - SyncEvent::FiltersSyncComplete { - tip_height, - } => { - if let Some(cb) = self.on_filters_sync_complete { - cb(*tip_height, self.user_data); - } - } - SyncEvent::BlocksNeeded { - blocks, - } => { - if let Some(cb) = self.on_blocks_needed { - let ffi_blocks: Vec = blocks - .keys() - .map(|key| FFIBlockNeeded { - height: key.height(), - hash: *key.hash().as_byte_array(), - }) - .collect(); - cb(ffi_blocks.as_ptr(), ffi_blocks.len() as u32, self.user_data); - } - } - SyncEvent::BlockProcessed { - block_hash, - height, - new_addresses, - confirmed_txids, - .. - } => { - if let Some(cb) = self.on_block_processed { - let hash_bytes = block_hash.as_byte_array(); - let txid_bytes: Vec<[u8; 32]> = - confirmed_txids.iter().map(|txid| *txid.as_byte_array()).collect(); - let total_new_addresses: usize = new_addresses.values().map(|v| v.len()).sum(); - cb( - *height, - hash_bytes as *const [u8; 32], - total_new_addresses as u32, - txid_bytes.as_ptr(), - txid_bytes.len() as u32, - self.user_data, - ); - } - } - SyncEvent::MasternodeStateUpdated { - height, - .. - } => { - if let Some(cb) = self.on_masternode_state_updated { - cb(*height, self.user_data); - } - } - SyncEvent::ChainLockReceived { - chain_lock, - validated, - } => { - if let Some(cb) = self.on_chainlock_received { - let hash_bytes = chain_lock.block_hash.as_byte_array(); - let sig_bytes = chain_lock.signature.as_bytes(); - cb( - chain_lock.block_height, - hash_bytes as *const [u8; 32], - sig_bytes as *const [u8; 96], - *validated, - self.user_data, - ); - } - } - SyncEvent::InstantLockReceived { - instant_lock, - validated, - } => { - if let Some(cb) = self.on_instantlock_received { - let txid_bytes = instant_lock.txid.as_byte_array(); - let serialized = dashcore::consensus::serialize(instant_lock); - cb( - txid_bytes as *const [u8; 32], - serialized.as_ptr(), - serialized.len(), - *validated, - self.user_data, - ); - } - } - SyncEvent::ManagerError { - manager, - error, - } => { - if let Some(cb) = self.on_manager_error { - let c_error = CString::new(error.as_str()).unwrap_or_default(); - cb((*manager).into(), c_error.as_ptr(), self.user_data); - } - } - SyncEvent::SyncComplete { - header_tip, - cycle, - } => { - if let Some(cb) = self.on_sync_complete { - cb(*header_tip, *cycle, self.user_data); - } - } - } - } -} - -// ============================================================================ -// FFINetworkEventCallbacks - One callback per NetworkEvent variant -// ============================================================================ - -/// Callback for NetworkEvent::PeerConnected -/// -/// The `address` string pointer is borrowed and only valid for the duration -/// of the callback. Callers must copy the string if they need to retain it -/// after the callback returns. -pub type OnPeerConnectedCallback = - Option; - -/// Callback for NetworkEvent::PeerDisconnected -/// -/// The `address` string pointer is borrowed and only valid for the duration -/// of the callback. Callers must copy the string if they need to retain it -/// after the callback returns. -pub type OnPeerDisconnectedCallback = - Option; - -/// Callback for NetworkEvent::PeersUpdated -pub type OnPeersUpdatedCallback = - Option; - -/// Network event callbacks - one callback per NetworkEvent variant. -/// -/// Set only the callbacks you're interested in; unset callbacks will be ignored. -/// -/// All pointer parameters passed to callbacks (strings, addresses) are -/// borrowed and only valid for the duration of the callback invocation. -/// Callers must copy any data they need to retain. -#[repr(C)] -#[derive(Clone)] -pub struct FFINetworkEventCallbacks { - pub on_peer_connected: OnPeerConnectedCallback, - pub on_peer_disconnected: OnPeerDisconnectedCallback, - pub on_peers_updated: OnPeersUpdatedCallback, - pub user_data: *mut c_void, -} - -// SAFETY: Same rationale as FFISyncEventCallbacks -unsafe impl Send for FFINetworkEventCallbacks {} -unsafe impl Sync for FFINetworkEventCallbacks {} - -impl Default for FFINetworkEventCallbacks { - fn default() -> Self { - Self { - on_peer_connected: None, - on_peer_disconnected: None, - on_peers_updated: None, - user_data: std::ptr::null_mut(), - } - } -} - -impl FFINetworkEventCallbacks { - /// Dispatch a NetworkEvent to the appropriate callback. - pub fn dispatch(&self, event: &dash_spv::network::NetworkEvent) { - use dash_spv::network::NetworkEvent; - - match event { - NetworkEvent::PeerConnected { - address, - } => { - if let Some(cb) = self.on_peer_connected { - let c_addr = CString::new(address.to_string()).unwrap_or_default(); - cb(c_addr.as_ptr(), self.user_data); - } - } - NetworkEvent::PeerDisconnected { - address, - } => { - if let Some(cb) = self.on_peer_disconnected { - let c_addr = CString::new(address.to_string()).unwrap_or_default(); - cb(c_addr.as_ptr(), self.user_data); - } - } - NetworkEvent::PeersUpdated { - connected_count, - best_height, - .. - } => { - if let Some(cb) = self.on_peers_updated { - cb(*connected_count as u32, best_height.unwrap_or(0), self.user_data); - } - } - } - } -} - -// ============================================================================ -// FFIAccountBalance - Per-account balance entry -// ============================================================================ - -/// Per-account balance pair carried on wallet events. -/// -/// Wallet events deliver an array of these — one entry per account whose -/// balance changed during the event. Accounts whose balance was unchanged -/// are omitted to keep the payload small (most transactions touch only -/// 1–2 accounts). -/// -/// `account_type` follows the same memory rules as the equivalent field on -/// [`FFITransactionRecord`]: the embedded `identity_user` / `identity_friend` -/// pointers (non-null only for Dashpay variants) are owned by the -/// `FFIAccountType` and freed when the array is dropped after the callback -/// returns. Consumers that need to retain the data past the callback must -/// copy the contents. -#[repr(C)] -pub struct FFIAccountBalance { - /// Owning-account descriptor (discriminant + indices + identity ids). - pub account_type: FFIAccountType, - /// Balance for the account after the event. - pub balance: FFIBalance, -} - -impl FFIAccountBalance { - fn from_map(map: &BTreeMap) -> Vec { - map.iter() - .map(|(account_type, balance)| FFIAccountBalance { - account_type: FFIAccountType::from(account_type), - balance: FFIBalance::from(*balance), - }) - .collect() - } -} - -// ============================================================================ -// FFIDerivedAddress - One address derived during gap-limit maintenance -// ============================================================================ - -/// Pool the derived address belongs to. -/// -/// Mirrors `key_wallet::managed_account::address_pool::AddressPoolType` -/// 1:1 — kept distinct from the existing `FFIAddressPoolType` (which -/// collapses Absent / AbsentHardened into a single `Single` variant) so -/// event consumers can distinguish hardened single-pool variants -/// (Provider operator keys, etc.) from non-hardened ones. -#[repr(C)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum FFIDerivedAddressPoolType { - External = 0, - Internal = 1, - Absent = 2, - AbsentHardened = 3, -} - -impl From - for FFIDerivedAddressPoolType -{ - fn from(t: key_wallet::managed_account::address_pool::AddressPoolType) -> Self { - use key_wallet::managed_account::address_pool::AddressPoolType as P; - match t { - P::External => FFIDerivedAddressPoolType::External, - P::Internal => FFIDerivedAddressPoolType::Internal, - P::Absent => FFIDerivedAddressPoolType::Absent, - P::AbsentHardened => FFIDerivedAddressPoolType::AbsentHardened, - } - } -} - -/// One address derived as a side effect of gap-limit maintenance during -/// transaction or block processing. -/// -/// Wallet events deliver an array of these so persisters can mirror the -/// on-disk address pool transactionally with the tx/block records that -/// triggered the derivation. Without this, UTXOs landing on freshly -/// derived addresses orphan their parent address row at the persister. -/// -/// `account_type` follows the same memory rules as on -/// [`FFIAccountBalance`]: the embedded `identity_user` / `identity_friend` -/// pointers are owned by the `FFIAccountType` and freed when the array is -/// dropped after the callback returns. `address` is a heap-allocated -/// null-terminated UTF-8 string, owned by this struct and freed on drop. -/// Consumers that need to retain the data past the callback must copy -/// every owning field — not just retain pointers. -#[repr(C)] -pub struct FFIDerivedAddress { - /// Owning-account descriptor (discriminant + indices + identity ids). - pub account_type: FFIAccountType, - /// Pool within the account that derived this address. - pub pool_type: FFIDerivedAddressPoolType, - /// Derivation index within the pool. Combined with `account_type` - /// and `pool_type`, this fully determines the derivation path — - /// consumers that need a rendered path can recompute it - /// deterministically. - pub derivation_index: u32, - /// Heap-allocated null-terminated UTF-8 string. Owned by this - /// struct; freed when the struct is dropped. - pub address: *mut c_char, - /// 33-byte compressed ECDSA public key (inline, no allocation). - pub public_key: [u8; 33], -} - -impl FFIDerivedAddress { - fn from_slice(addresses: &[key_wallet_manager::DerivedAddress]) -> Vec { - addresses - .iter() - .map(|d| { - let address_str = d.address.to_string(); - let c_address = CString::new(address_str).unwrap_or_else(|_| CString::default()); - FFIDerivedAddress { - account_type: FFIAccountType::from(&d.account_type), - pool_type: FFIDerivedAddressPoolType::from(d.pool_type), - derivation_index: d.derivation_index, - address: c_address.into_raw(), - public_key: d.public_key.inner.serialize(), - } - }) - .collect() - } -} - -impl Drop for FFIDerivedAddress { - fn drop(&mut self) { - if !self.address.is_null() { - // SAFETY: `address` was constructed via `CString::into_raw` in - // `FFIDerivedAddress::from_slice`, so reclaiming via - // `CString::from_raw` is the matching free. - let _ = unsafe { CString::from_raw(self.address) }; - self.address = std::ptr::null_mut(); - } - // `account_type` has its own Drop impl that frees the - // identity_user / identity_friend allocations when applicable. - } -} - -// ============================================================================ -// FFIWalletEventCallbacks - One callback per WalletEvent variant -// ============================================================================ - -/// Callback for `WalletEvent::TransactionDetected`. -/// -/// Fires when a wallet-relevant transaction is first seen off-chain — either -/// in the mempool, or directly via an InstantSend lock (in that case the -/// record's `context` is `InstantSend(..)`). -/// -/// All pointer parameters are borrowed and only valid for the duration of the -/// callback. `balance` is the wallet's balance *after* the transaction was -/// recorded. `account_balances` is an array of size `account_balances_count` -/// containing one entry per account whose balance changed (typically 1–2 -/// entries for a normal transaction); accounts whose balance is unchanged -/// are omitted. The array is null with a zero count when no per-account -/// balance changed. -/// -/// `addresses_derived` is an array of size `addresses_derived_count` of -/// addresses derived as a side effect of gap-limit maintenance while -/// processing this transaction, attributed to the same account as -/// `record`. Empty in the common case (null pointer with zero count). -/// Persisters should write these rows transactionally with `record` so -/// UTXOs landing on freshly-derived addresses retain a parent row. -pub type OnTransactionDetectedCallback = Option< - extern "C" fn( - wallet_id: *const c_char, - record: *const FFITransactionRecord, - balance: *const FFIBalance, - account_balances: *const FFIAccountBalance, - account_balances_count: u32, - addresses_derived: *const FFIDerivedAddress, - addresses_derived_count: u32, - user_data: *mut c_void, - ), ->; - -/// Callback for `WalletEvent::TransactionInstantLocked`. -/// -/// Fires when an InstantSend lock is applied to a previously-seen off-chain -/// wallet-relevant transaction. Consumers already hold the full record from -/// the prior `TransactionDetected`; only the txid, the consensus-serialized -/// `InstantLock` bytes, and the post-change balance are delivered. -/// -/// All pointer parameters are borrowed and only valid for the duration of -/// the callback. `balance` is the wallet's balance *after* the change. -/// `account_balances` follows the same contract as on -/// [`OnTransactionDetectedCallback`]. -pub type OnTransactionInstantLockedCallback = Option< - extern "C" fn( - wallet_id: *const c_char, - txid: *const [u8; 32], - islock_data: *const u8, - islock_len: usize, - balance: *const FFIBalance, - account_balances: *const FFIAccountBalance, - account_balances_count: u32, - user_data: *mut c_void, - ), ->; - -/// Callback for `WalletEvent::BlockProcessed`. -/// -/// Fires once per wallet affected by a processed block. The three record -/// arrays bucket what happened in this block: `inserted` is records first -/// stored, `updated` is previously-known records confirmed, `matured` is -/// older coinbase records whose maturity threshold was just crossed. Empty -/// arrays are passed as null with a zero count. `balance` is the wallet's -/// balance *after* the block was processed. `account_balances` follows the -/// same contract as on [`OnTransactionDetectedCallback`]. -/// -/// `addresses_derived` is an array of size `addresses_derived_count` of -/// addresses derived as a side effect of gap-limit maintenance across -/// every record in the block, deduplicated by -/// `(account_type, pool_type, derivation_index)`. Empty in the common -/// case (null pointer with zero count). Persisters should write these -/// rows transactionally with the inserted/updated records. -/// -/// `cl_hash` and `cl_signature` are non-null iff the processed block is -/// covered by the wallet's chainlock at processing time. When non-null, -/// every record in this event has an `InChainLockedBlock` context and -/// the carried chainlock is the proof that established it (`cl_height -/// >= height` by construction). When null, the block is above the -/// wallet's finality boundary and records are `InBlock`. -/// -/// All array pointers and their contents are borrowed and only valid for the -/// duration of the callback. -pub type OnWalletBlockProcessedCallback = Option< - extern "C" fn( - wallet_id: *const c_char, - height: u32, - inserted: *const FFITransactionRecord, - inserted_count: u32, - updated: *const FFITransactionRecord, - updated_count: u32, - matured: *const FFITransactionRecord, - matured_count: u32, - balance: *const FFIBalance, - account_balances: *const FFIAccountBalance, - account_balances_count: u32, - addresses_derived: *const FFIDerivedAddress, - addresses_derived_count: u32, - cl_height: u32, - cl_hash: *const [u8; 32], - cl_signature: *const [u8; 96], - user_data: *mut c_void, - ), ->; - -/// Callback for `WalletEvent::SyncHeightAdvanced`. -/// -/// Fires once per wallet when the filter pipeline commits a batch — the -/// wallet has been scanned up to `height`. Consumers can persist this as a -/// checkpoint atomically with any records/balance already persisted from -/// prior `BlockProcessed` events inside the batch. -pub type OnSyncHeightAdvancedCallback = - Option; - -/// One net-new chainlock-finalized txid, scoped to the account it was -/// promoted on. `WalletEvent::ChainLockProcessed` delivers an -/// array of these — one entry per (account, txid) pair promoted by -/// the chainlock. -/// -/// `account_type` follows the same memory rules as on -/// [`FFIAccountBalance`]: the embedded `identity_user` / -/// `identity_friend` pointers (non-null only for Dashpay variants) -/// are owned by the `FFIAccountType` and freed when the array is -/// dropped after the callback returns. -#[repr(C)] -pub struct FFIChainlockedTxid { - /// Owning-account descriptor. - pub account_type: FFIAccountType, - /// Promoted transaction id. - pub txid: [u8; 32], -} - -impl FFIChainlockedTxid { - fn from_map(map: &BTreeMap>) -> Vec { - let mut out = Vec::new(); - for (account_type, txids) in map { - for txid in txids { - out.push(FFIChainlockedTxid { - account_type: FFIAccountType::from(account_type), - txid: *txid.as_byte_array(), - }); - } - } - out - } -} - -/// Callback for `WalletEvent::ChainLockProcessed`. -/// -/// Fires once per wallet whenever the wallet's -/// `last_applied_chain_lock` advances forward by height (or moves from -/// `None` to `Some`). Carries the full signing proof so durable -/// consumers can persist the chainlock alongside the height — important -/// for SDKs that need to reconstruct chainlock-derived state across -/// restarts (e.g. building a `ChainAssetLockProof` for an `InBlock` -/// asset-lock TX from the persisted chainlock). -/// -/// `finalized` carries the per-(account, txid) promotions when the -/// same chainlock also flipped one or more `InBlock` records to -/// `InChainLockedBlock`. `finalized_count == 0` (and `finalized == -/// NULL`) when the chainlock advanced the wallet's metadata without -/// promoting any record — consumers that persist the chainlock proof -/// must still observe these empty-promotion events. -/// -/// All pointers are borrowed and only valid for the duration of the -/// callback. -pub type OnWalletChainLockProcessedCallback = Option< - extern "C" fn( - wallet_id: *const c_char, - cl_height: u32, - cl_hash: *const [u8; 32], - cl_signature: *const [u8; 96], - finalized: *const FFIChainlockedTxid, - finalized_count: u32, - user_data: *mut c_void, - ), ->; - -/// Wallet event callbacks - one callback per WalletEvent variant. -/// -/// Set only the callbacks you're interested in; unset callbacks will be ignored. -/// -/// All pointer parameters passed to callbacks (wallet IDs, txids, records, -/// balances) are borrowed and only valid for the duration of the callback -/// invocation. Callers must copy any data they need to retain. -#[repr(C)] -#[derive(Clone)] -pub struct FFIWalletEventCallbacks { - pub on_transaction_detected: OnTransactionDetectedCallback, - pub on_transaction_instant_locked: OnTransactionInstantLockedCallback, - pub on_block_processed: OnWalletBlockProcessedCallback, - pub on_sync_height_advanced: OnSyncHeightAdvancedCallback, - pub on_chain_lock_processed: OnWalletChainLockProcessedCallback, - pub user_data: *mut c_void, -} - -// SAFETY: Same rationale as FFISyncEventCallbacks -unsafe impl Send for FFIWalletEventCallbacks {} -unsafe impl Sync for FFIWalletEventCallbacks {} - -impl Default for FFIWalletEventCallbacks { - fn default() -> Self { - Self { - on_transaction_detected: None, - on_transaction_instant_locked: None, - on_block_processed: None, - on_sync_height_advanced: None, - on_chain_lock_processed: None, - user_data: std::ptr::null_mut(), - } - } -} - -// ============================================================================ -// FFIClientErrorCallback - Fatal client-level errors -// ============================================================================ - -/// Callback for fatal client errors (e.g. start failure, monitor thread crash). -/// -/// The `error` string pointer is borrowed and only valid for the duration -/// of the callback. Callers must copy the string if they need to retain it -/// after the callback returns. -pub type OnClientErrorCallback = - Option; - -/// Client error callback configuration. -#[repr(C)] -#[derive(Clone)] -pub struct FFIClientErrorCallback { - pub on_error: OnClientErrorCallback, - pub user_data: *mut c_void, -} - -unsafe impl Send for FFIClientErrorCallback {} -unsafe impl Sync for FFIClientErrorCallback {} - -impl Default for FFIClientErrorCallback { - fn default() -> Self { - Self { - on_error: None, - user_data: std::ptr::null_mut(), - } - } -} - -impl FFIClientErrorCallback { - /// Dispatch a client error to the callback. - pub fn dispatch(&self, error: &str) { - if let Some(cb) = self.on_error { - let c_error = CString::new(error).unwrap_or_default(); - cb(c_error.as_ptr(), self.user_data); - } - } -} - -// ============================================================================ -// FFIEventCallbacks - All callbacks in a single C-compatible struct -// ============================================================================ - -/// All event callbacks grouped into a single struct. -/// -/// Pass this to `dash_spv_ffi_client_new`. Any callback group left at its -/// default (all function pointers null) will simply not receive events. -#[repr(C)] -#[derive(Clone, Default)] -pub struct FFIEventCallbacks { - pub sync: FFISyncEventCallbacks, - pub network: FFINetworkEventCallbacks, - pub progress: FFIProgressCallback, - pub wallet: FFIWalletEventCallbacks, - pub error: FFIClientErrorCallback, -} - -unsafe impl Send for FFIEventCallbacks {} -unsafe impl Sync for FFIEventCallbacks {} - -impl EventHandler for FFIEventCallbacks { - fn on_sync_event(&self, event: &SyncEvent) { - self.sync.dispatch(event); - } - - fn on_network_event(&self, event: &NetworkEvent) { - self.network.dispatch(event); - } - - fn on_progress(&self, progress: &SyncProgress) { - self.progress.dispatch(progress); - } - - fn on_wallet_event(&self, event: &WalletEvent) { - self.wallet.dispatch(event); - } - - fn on_error(&self, error: &str) { - self.error.dispatch(error); - } -} - -impl FFIWalletEventCallbacks { - /// Dispatch a WalletEvent to the appropriate callback. - pub fn dispatch(&self, event: &WalletEvent) { - match event { - WalletEvent::TransactionDetected { - wallet_id, - record, - balance, - account_balances, - addresses_derived, - } => { - if let Some(cb) = self.on_transaction_detected { - let wallet_id_hex = hex::encode(wallet_id); - let c_wallet_id = CString::new(wallet_id_hex).unwrap_or_default(); - let ffi_record = FFITransactionRecord::from(record.as_ref()); - let ffi_balance = FFIBalance::from(*balance); - let ffi_account_balances = FFIAccountBalance::from_map(account_balances); - let ffi_addresses_derived = FFIDerivedAddress::from_slice(addresses_derived); - let account_balances_ptr = if ffi_account_balances.is_empty() { - ptr::null() - } else { - ffi_account_balances.as_ptr() - }; - let addresses_derived_ptr = if ffi_addresses_derived.is_empty() { - ptr::null() - } else { - ffi_addresses_derived.as_ptr() - }; - - cb( - c_wallet_id.as_ptr(), - &ffi_record as *const FFITransactionRecord, - &ffi_balance as *const FFIBalance, - account_balances_ptr, - ffi_account_balances.len() as u32, - addresses_derived_ptr, - ffi_addresses_derived.len() as u32, - self.user_data, - ); - - drop(ffi_account_balances); - drop(ffi_addresses_derived); - } - } - WalletEvent::TransactionInstantLocked { - wallet_id, - txid, - instant_lock, - balance, - account_balances, - } => { - if let Some(cb) = self.on_transaction_instant_locked { - let wallet_id_hex = hex::encode(wallet_id); - let c_wallet_id = CString::new(wallet_id_hex).unwrap_or_default(); - let txid_bytes = *txid.as_byte_array(); - let islock_bytes = dashcore::consensus::serialize(instant_lock); - let ffi_balance = FFIBalance::from(*balance); - let ffi_account_balances = FFIAccountBalance::from_map(account_balances); - let account_balances_ptr = if ffi_account_balances.is_empty() { - ptr::null() - } else { - ffi_account_balances.as_ptr() - }; - - cb( - c_wallet_id.as_ptr(), - &txid_bytes as *const [u8; 32], - islock_bytes.as_ptr(), - islock_bytes.len(), - &ffi_balance as *const FFIBalance, - account_balances_ptr, - ffi_account_balances.len() as u32, - self.user_data, - ); - - drop(ffi_account_balances); - } - } - WalletEvent::BlockProcessed { - wallet_id, - height, - inserted, - updated, - matured, - balance, - account_balances, - addresses_derived, - chain_lock, - } => { - if let Some(cb) = self.on_block_processed { - let wallet_id_hex = hex::encode(wallet_id); - let c_wallet_id = CString::new(wallet_id_hex).unwrap_or_default(); - let ffi_inserted: Vec = - inserted.iter().map(FFITransactionRecord::from).collect(); - let ffi_updated: Vec = - updated.iter().map(FFITransactionRecord::from).collect(); - let ffi_matured: Vec = - matured.iter().map(FFITransactionRecord::from).collect(); - let ffi_balance = FFIBalance::from(*balance); - let ffi_account_balances = FFIAccountBalance::from_map(account_balances); - let ffi_addresses_derived = FFIDerivedAddress::from_slice(addresses_derived); - - // Pass a null pointer when an array is empty so C/Swift - // consumers that null-check before reading don't see a - // non-null dangling pointer paired with a zero count. - let inserted_ptr = if ffi_inserted.is_empty() { - ptr::null() - } else { - ffi_inserted.as_ptr() - }; - let updated_ptr = if ffi_updated.is_empty() { - ptr::null() - } else { - ffi_updated.as_ptr() - }; - let matured_ptr = if ffi_matured.is_empty() { - ptr::null() - } else { - ffi_matured.as_ptr() - }; - let account_balances_ptr = if ffi_account_balances.is_empty() { - ptr::null() - } else { - ffi_account_balances.as_ptr() - }; - let addresses_derived_ptr = if ffi_addresses_derived.is_empty() { - ptr::null() - } else { - ffi_addresses_derived.as_ptr() - }; - // Null pointers (and `cl_height=0`) when the block isn't - // chainlocked; non-null hash + signature pointers borrow - // from `chain_lock` for the duration of the callback. - let (cl_height_arg, cl_hash_arg, cl_signature_arg) = match chain_lock { - Some(cl) => ( - cl.block_height, - cl.block_hash.as_byte_array() as *const [u8; 32], - cl.signature.as_bytes() as *const [u8; 96], - ), - None => (0, ptr::null(), ptr::null()), - }; - - cb( - c_wallet_id.as_ptr(), - *height, - inserted_ptr, - ffi_inserted.len() as u32, - updated_ptr, - ffi_updated.len() as u32, - matured_ptr, - ffi_matured.len() as u32, - &ffi_balance as *const FFIBalance, - account_balances_ptr, - ffi_account_balances.len() as u32, - addresses_derived_ptr, - ffi_addresses_derived.len() as u32, - cl_height_arg, - cl_hash_arg, - cl_signature_arg, - self.user_data, - ); - - drop(ffi_inserted); - drop(ffi_updated); - drop(ffi_matured); - drop(ffi_account_balances); - drop(ffi_addresses_derived); - } - } - WalletEvent::SyncHeightAdvanced { - wallet_id, - height, - } => { - if let Some(cb) = self.on_sync_height_advanced { - let wallet_id_hex = hex::encode(wallet_id); - let c_wallet_id = CString::new(wallet_id_hex).unwrap_or_default(); - cb(c_wallet_id.as_ptr(), *height, self.user_data); - } - } - WalletEvent::ChainLockProcessed { - wallet_id, - chain_lock, - locked_transactions, - } => { - if let Some(cb) = self.on_chain_lock_processed { - let wallet_id_hex = hex::encode(wallet_id); - let c_wallet_id = CString::new(wallet_id_hex).unwrap_or_default(); - let ffi_finalized = FFIChainlockedTxid::from_map(locked_transactions); - let finalized_ptr = if ffi_finalized.is_empty() { - ptr::null() - } else { - ffi_finalized.as_ptr() - }; - - cb( - c_wallet_id.as_ptr(), - chain_lock.block_height, - chain_lock.block_hash.as_byte_array() as *const [u8; 32], - chain_lock.signature.as_bytes() as *const [u8; 96], - finalized_ptr, - ffi_finalized.len() as u32, - self.user_data, - ); - - drop(ffi_finalized); - } - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use dashcore::hashes::Hash; - use dashcore::{Address, BlockHash, ChainLock, Network, Txid}; - use key_wallet_manager::{FilterMatchKey, WalletId}; - use std::collections::{BTreeMap, BTreeSet}; - use std::sync::atomic::{AtomicU32, Ordering}; - - /// `BlocksNeeded` dispatch must pass exactly one entry per - /// `FilterMatchKey` to the FFI callback (i.e. iterate keys, not - /// inflated by the per-block wallet attribution). - #[test] - fn test_blocks_needed_dispatch_passes_unique_keys_count() { - static COUNT: AtomicU32 = AtomicU32::new(u32::MAX); - extern "C" fn cb(_blocks: *const FFIBlockNeeded, count: u32, _user: *mut c_void) { - COUNT.store(count, Ordering::SeqCst); - } - - let callbacks = FFISyncEventCallbacks { - on_blocks_needed: Some(cb), - ..FFISyncEventCallbacks::default() - }; - - let mut blocks: BTreeMap> = BTreeMap::new(); - // Two distinct blocks, each attributed to two wallets. The dispatch - // must report 2 (unique keys), not 4. - blocks.insert( - FilterMatchKey::new(10, BlockHash::from_byte_array([1u8; 32])), - BTreeSet::from([[1u8; 32], [2u8; 32]]), - ); - blocks.insert( - FilterMatchKey::new(20, BlockHash::from_byte_array([2u8; 32])), - BTreeSet::from([[1u8; 32], [2u8; 32]]), - ); - - callbacks.dispatch(&SyncEvent::BlocksNeeded { - blocks, - }); - assert_eq!(COUNT.load(Ordering::SeqCst), 2); - } - - /// `BlockProcessed` dispatch must report the total address count - /// summed across all per-wallet entries in the `new_addresses` map. - #[test] - fn test_block_processed_dispatch_sums_per_wallet_addresses() { - static NEW_ADDR_COUNT: AtomicU32 = AtomicU32::new(u32::MAX); - extern "C" fn cb( - _height: u32, - _hash: *const [u8; 32], - new_address_count: u32, - _txids: *const [u8; 32], - _txid_count: u32, - _user: *mut c_void, - ) { - NEW_ADDR_COUNT.store(new_address_count, Ordering::SeqCst); - } - - let callbacks = FFISyncEventCallbacks { - on_block_processed: Some(cb), - ..FFISyncEventCallbacks::default() - }; - - let addr_a = Address::dummy(Network::Regtest, 1); - let addr_b = Address::dummy(Network::Regtest, 2); - let addr_c = Address::dummy(Network::Regtest, 3); - let mut new_addresses: BTreeMap> = BTreeMap::new(); - // Wallet 1 contributes 2 new addresses, wallet 2 contributes 1. Total = 3. - new_addresses.insert([1u8; 32], vec![addr_a, addr_b]); - new_addresses.insert([2u8; 32], vec![addr_c]); - - callbacks.dispatch(&SyncEvent::BlockProcessed { - block_hash: BlockHash::from_byte_array([7u8; 32]), - height: 100, - wallets: BTreeSet::new(), - new_addresses, - confirmed_txids: vec![Txid::from_byte_array([9u8; 32])], - }); - assert_eq!(NEW_ADDR_COUNT.load(Ordering::SeqCst), 3); - } - - /// `ChainLockProcessed` dispatch must hand every wired field - /// through to the FFI callback unchanged: hex-encoded wallet_id, - /// height, 32-byte block hash, 96-byte signature, and the count of - /// per-(account, txid) promotions. A regression that miswires any - /// of these (e.g. height/hash swap, signature truncation, empty vs. - /// non-empty promotion handling) shows up as a single assertion - /// failure here. - #[test] - fn test_chain_lock_processed_dispatch_round_trips_every_field() { - struct Captured { - wallet_id_hex: String, - cl_height: u32, - cl_hash: [u8; 32], - cl_signature: [u8; 96], - finalized_count: u32, - } - static CAPTURED: std::sync::Mutex> = std::sync::Mutex::new(None); - - extern "C" fn cb( - wallet_id: *const c_char, - cl_height: u32, - cl_hash: *const [u8; 32], - cl_signature: *const [u8; 96], - _finalized: *const FFIChainlockedTxid, - finalized_count: u32, - _user: *mut c_void, - ) { - let wid = unsafe { std::ffi::CStr::from_ptr(wallet_id) } - .to_str() - .expect("wallet_id must be valid UTF-8 hex") - .to_string(); - *CAPTURED.lock().unwrap() = Some(Captured { - wallet_id_hex: wid, - cl_height, - cl_hash: unsafe { *cl_hash }, - cl_signature: unsafe { *cl_signature }, - finalized_count, - }); - } - - let callbacks = FFIWalletEventCallbacks { - on_chain_lock_processed: Some(cb), - ..FFIWalletEventCallbacks::default() - }; - - let chain_lock = ChainLock::dummy(777); - let expected_hash = *chain_lock.block_hash.as_byte_array(); - let expected_sig = *chain_lock.signature.as_bytes(); - let wallet_id: WalletId = [3u8; 32]; - - // Two promotions to verify `finalized_count` reflects total - // (account, txid) pairs, not the number of accounts. - let account_a = AccountType::Standard { - index: 0, - standard_account_type: key_wallet::account::StandardAccountType::BIP44Account, - }; - let account_b = AccountType::Standard { - index: 1, - standard_account_type: key_wallet::account::StandardAccountType::BIP44Account, - }; - let mut locked: BTreeMap> = BTreeMap::new(); - locked.insert(account_a, vec![Txid::from_byte_array([0xaa; 32])]); - locked.insert(account_b, vec![Txid::from_byte_array([0xbb; 32])]); - - callbacks.dispatch(&WalletEvent::ChainLockProcessed { - wallet_id, - chain_lock, - locked_transactions: locked, - }); - - let captured = CAPTURED.lock().unwrap().take().expect("callback fired"); - assert_eq!(captured.wallet_id_hex, hex::encode(wallet_id), "wallet_id hex-encoding"); - assert_eq!(captured.cl_height, 777, "cl_height"); - assert_eq!(captured.cl_hash, expected_hash, "cl_hash round-trip"); - assert_eq!(captured.cl_signature, expected_sig, "cl_signature round-trip"); - assert_eq!(captured.finalized_count, 2, "finalized_count counts (account, txid) pairs"); - } - - /// `ChainLockProcessed` with empty `locked_transactions` must still - /// fire the callback (durable consumers persist the chainlock proof - /// even when no record was promoted) with `finalized_count == 0`. - #[test] - fn test_chain_lock_processed_dispatch_fires_with_empty_promotions() { - static FIRED: AtomicU32 = AtomicU32::new(u32::MAX); - extern "C" fn cb( - _wallet_id: *const c_char, - _cl_height: u32, - _cl_hash: *const [u8; 32], - _cl_signature: *const [u8; 96], - _finalized: *const FFIChainlockedTxid, - finalized_count: u32, - _user: *mut c_void, - ) { - FIRED.store(finalized_count, Ordering::SeqCst); - } - - let callbacks = FFIWalletEventCallbacks { - on_chain_lock_processed: Some(cb), - ..FFIWalletEventCallbacks::default() - }; - - callbacks.dispatch(&WalletEvent::ChainLockProcessed { - wallet_id: [4u8; 32], - chain_lock: ChainLock::dummy(900), - locked_transactions: BTreeMap::new(), - }); - assert_eq!(FIRED.load(Ordering::SeqCst), 0); - } -} diff --git a/dash-spv-ffi/src/client.rs b/dash-spv-ffi/src/client.rs deleted file mode 100644 index 6cb94c8e2..000000000 --- a/dash-spv-ffi/src/client.rs +++ /dev/null @@ -1,425 +0,0 @@ -use crate::{ - null_check, set_last_error, FFIClientConfig, FFIErrorCode, FFIEventCallbacks, FFISyncProgress, - FFIWalletManager, -}; -// Import wallet types from key-wallet-ffi -use key_wallet_ffi::FFIWalletManager as KeyWalletFFIWalletManager; - -use dash_spv::storage::DiskStorageManager; -use dash_spv::DashSpvClient; -use tracing::dispatcher::{get_default, set_default}; - -use std::mem::forget; -use std::sync::{Arc, Mutex}; -use tokio::runtime::Runtime; -use tokio::task::JoinHandle; - -/// FFI wrapper around `DashSpvClient`. -type InnerClient = DashSpvClient< - key_wallet_manager::WalletManager, - dash_spv::network::PeerNetworkManager, - DiskStorageManager, ->; - -pub struct FFIDashSpvClient { - pub(crate) inner: InnerClient, - pub(crate) runtime: Arc, - run_task: Mutex>>, -} - -impl FFIDashSpvClient { - /// Returns the shared masternode list engine, if initialized. - pub fn masternode_list_engine( - &self, - ) -> Option>> { - self.inner.masternode_list_engine().ok() - } -} - -/// Create a new SPV client and return an opaque pointer. -/// -/// # Safety -/// - `config` must be a valid, non-null pointer for the duration of the call. -/// - `callbacks` is taken by value (function pointers and `user_data` pointers -/// are copied internally). The struct itself may be dropped after the call, -/// but all `user_data` pointer targets must remain valid until -/// `dash_spv_ffi_client_stop` or `dash_spv_ffi_client_destroy` is called. -/// - Callback functions and `user_data` pointees must be safe to use from -/// background threads; different callback groups may be invoked concurrently. -/// - The returned pointer must be freed with `dash_spv_ffi_client_destroy`. -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_client_new( - config: *const FFIClientConfig, - callbacks: FFIEventCallbacks, -) -> *mut FFIDashSpvClient { - null_check!(config, std::ptr::null_mut()); - - let config = &(*config); - // Build runtime with configurable worker threads (0 => auto) - let mut builder = tokio::runtime::Builder::new_multi_thread(); - builder.thread_name("dash-spv-worker").enable_all(); - if config.worker_threads > 0 { - builder.worker_threads(config.worker_threads as usize); - } - - // Propagate the caller's tracing subscriber to worker threads so that - // thread-local subscribers (used by tests for per-test log isolation) - // capture logs from spawned async tasks. - let dispatch = get_default(|d| d.clone()); - builder.on_thread_start(move || { - let guard = set_default(&dispatch); - forget(guard); - }); - let runtime = match builder.build() { - Ok(rt) => Arc::new(rt), - Err(e) => { - set_last_error(&format!("Failed to create runtime: {}", e)); - return std::ptr::null_mut(); - } - }; - - let client_config = config.clone_inner(); - - let client_result = runtime.block_on(async move { - // Construct concrete implementations for generics - let network = dash_spv::network::PeerNetworkManager::new(&client_config).await; - let storage = DiskStorageManager::new(&client_config).await; - let wallet = key_wallet_manager::WalletManager::< - key_wallet::wallet::managed_wallet_info::ManagedWalletInfo, - >::new(client_config.network); - let wallet = std::sync::Arc::new(tokio::sync::RwLock::new(wallet)); - - match (network, storage) { - (Ok(network), Ok(storage)) => { - DashSpvClient::new( - client_config, - network, - storage, - wallet, - vec![Arc::new(callbacks)], - ) - .await - } - (Err(e), _) => Err(e), - (_, Err(e)) => Err(dash_spv::SpvError::Storage(e)), - } - }); - - match client_result { - Ok(client) => { - let ffi_client = FFIDashSpvClient { - inner: client, - runtime, - run_task: Mutex::new(None), - }; - Box::into_raw(Box::new(ffi_client)) - } - Err(e) => { - set_last_error(&format!("Failed to create client: {}", e)); - std::ptr::null_mut() - } - } -} - -/// Maximum time to wait for the run task to exit cooperatively before aborting. -const RUN_TASK_SHUTDOWN_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5); - -impl FFIDashSpvClient { - /// Wait for the run task to finish cooperatively, aborting only on timeout. - /// - /// `DashSpvClient::stop()` must have been called first (it flips the client's - /// internal running state, which makes `run()` exit its loop and clean up - /// monitor tasks). This only falls back to `abort()` if the task doesn't - /// exit within the timeout. - fn wait_for_run_task(&self) { - let task = self.run_task.lock().unwrap().take(); - if let Some(mut task) = task { - let finished = self.runtime.block_on(async { - tokio::time::timeout(RUN_TASK_SHUTDOWN_TIMEOUT, &mut task).await - }); - match finished { - Ok(Ok(())) => {} - Ok(Err(e)) => tracing::warn!("Run task exited with join error: {}", e), - Err(_) => { - tracing::warn!( - "Run task did not exit within {:?}, aborting", - RUN_TASK_SHUTDOWN_TIMEOUT, - ); - task.abort(); - let _ = self.runtime.block_on(task); - } - } - } - } -} - -/// Update the running client's configuration. -/// -/// # Safety -/// - `client` must be a valid pointer to an `FFIDashSpvClient`. -/// - `config` must be a valid pointer to an `FFIClientConfig`. -/// - The network in `config` must match the client's network; changing networks at runtime is not supported. -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_client_update_config( - client: *mut FFIDashSpvClient, - config: *const FFIClientConfig, -) -> i32 { - null_check!(client); - null_check!(config); - - let client = &(*client); - let new_config = (&*config).clone_inner(); - - let result = client.runtime.block_on(async { client.inner.update_config(new_config).await }); - - match result { - Ok(()) => FFIErrorCode::Success as i32, - Err(e) => { - set_last_error(&e.to_string()); - FFIErrorCode::from(e) as i32 - } - } -} - -/// Stop the SPV client. -/// -/// # Safety -/// - `client` must be a valid, non-null pointer to a created client. -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_client_stop(client: *mut FFIDashSpvClient) -> i32 { - null_check!(client); - - let client = &(*client); - - // `stop()` flips the client's internal running state, making `run()` break - // out of its loop. Wait for the spawned run task only after that. - let result = client.runtime.block_on(async { client.inner.stop().await }); - client.wait_for_run_task(); - - match result { - Ok(()) => FFIErrorCode::Success as i32, - Err(e) => { - set_last_error(&e.to_string()); - FFIErrorCode::from(e) as i32 - } - } -} - -/// Start the SPV client and begin syncing in the background. -/// -/// Uses the event callbacks provided at client creation time. Returns -/// immediately after spawning the sync task. -/// -/// # Safety -/// - `client` must be a valid, non-null pointer to a created client. -/// -/// # Returns -/// 0 on success, error code on failure. -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_client_run(client: *mut FFIDashSpvClient) -> i32 { - null_check!(client); - - let client = &(*client); - - tracing::info!("dash_spv_ffi_client_run: starting sync"); - - let spv_client = client.inner.clone(); - - let task = client.runtime.spawn(async move { - tracing::debug!("Sync task: starting run"); - - if let Err(e) = spv_client.run().await { - tracing::error!("Sync task: error: {}", e); - } - - tracing::debug!("Sync task: exiting"); - }); - - *client.run_task.lock().unwrap() = Some(task); - - tracing::info!("dash_spv_ffi_client_run: background task spawned, returning"); - - FFIErrorCode::Success as i32 -} - -/// Get the current sync progress snapshot. -/// -/// # Safety -/// - `client` must be a valid, non-null pointer. -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_client_get_sync_progress( - client: *mut FFIDashSpvClient, -) -> *mut FFISyncProgress { - null_check!(client, std::ptr::null_mut()); - - let client = &(*client); - - let progress = client.runtime.block_on(async { client.inner.sync_progress().await }); - - Box::into_raw(Box::new(FFISyncProgress::from(progress))) -} - -/// Get the current manager-based sync progress. -/// -/// Returns the new parallel sync system's progress with per-manager details. -/// Use `dash_spv_ffi_sync_progress_destroy` to free the returned struct. -/// -/// # Safety -/// - `client` must be a valid, non-null pointer. -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_client_get_manager_sync_progress( - client: *mut FFIDashSpvClient, -) -> *mut FFISyncProgress { - null_check!(client, std::ptr::null_mut()); - - let client = &(*client); - - let progress = client.runtime.block_on(async { client.inner.progress().await }); - - Box::into_raw(Box::new(FFISyncProgress::from(progress))) -} - -/// Clear all persisted SPV storage (headers, filters, metadata, sync state). -/// -/// # Safety -/// - `client` must be a valid, non-null pointer. -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_client_clear_storage(client: *mut FFIDashSpvClient) -> i32 { - null_check!(client); - - let client = &(*client); - - let result = client.runtime.block_on(async { - // Try to stop before clearing to ensure no in-flight writes race the wipe. - if let Err(e) = client.inner.stop().await { - tracing::warn!("Failed to stop client before clearing storage: {}", e); - } - - client.inner.clear_storage().await - }); - - match result { - Ok(_) => FFIErrorCode::Success as i32, - Err(e) => { - set_last_error(&e.to_string()); - FFIErrorCode::from(e) as i32 - } - } -} - -/// Broadcasts a transaction to the Dash network via connected peers. -/// -/// # Safety -/// -/// - `client` must be a valid, non-null pointer to an initialized FFIDashSpvClient -/// - `tx_bytes` must be a valid, non-null pointer to the transaction data -/// - `length` must be the length of the transaction data in bytes -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_client_broadcast_transaction( - client: *mut FFIDashSpvClient, - tx_bytes: *const u8, - length: usize, -) -> i32 { - null_check!(client); - null_check!(tx_bytes); - - let tx_bytes = std::slice::from_raw_parts(tx_bytes, length); - - let tx = match dashcore::consensus::deserialize::(tx_bytes) { - Ok(t) => t, - Err(e) => { - set_last_error(&format!("Invalid transaction: {}", e)); - return FFIErrorCode::InvalidArgument as i32; - } - }; - - let client = &(*client); - - let spv_client = client.inner.clone(); - - let result = client.runtime.block_on(async { spv_client.broadcast_transaction(&tx).await }); - - match result { - Ok(_) => FFIErrorCode::Success as i32, - Err(e) => { - set_last_error(&format!("Failed to broadcast transaction: {}", e)); - FFIErrorCode::from(e) as i32 - } - } -} - -/// Destroy the client and free associated resources. -/// -/// # Safety -/// - `client` must be either null or a pointer obtained from `dash_spv_ffi_client_new`. -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_client_destroy(client: *mut FFIDashSpvClient) { - if !client.is_null() { - let client = Box::from_raw(client); - - // Stop the SPV client (run() calls stop() internally, but this - // handles the case where run() was never called or was aborted). - client.runtime.block_on(async { - let _ = client.inner.stop().await; - }); - - // Wait for the run task to finish (cooperative, with timeout fallback) - client.wait_for_run_task(); - - tracing::info!("FFI client destroyed and all tasks cleaned up"); - } -} - -// Wallet operations - -/// Get the wallet manager from the SPV client -/// -/// Returns a pointer to an `FFIWalletManager` wrapper that clones the underlying -/// `Arc>`. This allows direct interaction with the wallet -/// manager without going back through the client for each call. -/// -/// # Safety -/// -/// The caller must ensure that: -/// - The client pointer is valid -/// - The returned pointer is released exactly once using -/// `dash_spv_ffi_wallet_manager_free` -/// -/// # Returns -/// -/// A pointer to the wallet manager wrapper, or NULL if the client is not initialized. -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_client_get_wallet_manager( - client: *mut FFIDashSpvClient, -) -> *mut FFIWalletManager { - null_check!(client, std::ptr::null_mut()); - - let client = &*client; - - // Clone the Arc to the wallet manager - let wallet_arc = client.inner.wallet().clone(); - let runtime = client.runtime.clone(); - - // Create the FFIWalletManager with the cloned Arc - let manager = KeyWalletFFIWalletManager::from_arc(wallet_arc, runtime); - - Box::into_raw(Box::new(manager)) as *mut FFIWalletManager -} - -/// Release a wallet manager obtained from `dash_spv_ffi_client_get_wallet_manager`. -/// -/// This simply forwards to `wallet_manager_free` in key-wallet-ffi so that -/// lifetime management is consistent between direct key-wallet usage and the -/// SPV client pathway. -/// -/// # Safety -/// - `manager` must either be null or a pointer previously returned by -/// `dash_spv_ffi_client_get_wallet_manager`. -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_wallet_manager_free(manager: *mut FFIWalletManager) { - if manager.is_null() { - return; - } - - key_wallet_ffi::wallet_manager::wallet_manager_free(manager as *mut KeyWalletFFIWalletManager); -} diff --git a/dash-spv-ffi/src/config.rs b/dash-spv-ffi/src/config.rs deleted file mode 100644 index d6d7bf581..000000000 --- a/dash-spv-ffi/src/config.rs +++ /dev/null @@ -1,345 +0,0 @@ -use crate::{null_check, set_last_error, FFIErrorCode, FFIMempoolStrategy}; -use dash_spv::{ClientConfig, ValidationMode}; - -use dash_network::ffi::FFINetwork; -use std::ffi::CStr; -use std::net::{IpAddr, SocketAddr, ToSocketAddrs}; -use std::os::raw::c_char; - -#[repr(C)] -pub enum FFIValidationMode { - None = 0, - Basic = 1, - Full = 2, -} - -impl From for ValidationMode { - fn from(mode: FFIValidationMode) -> Self { - match mode { - FFIValidationMode::None => ValidationMode::None, - FFIValidationMode::Basic => ValidationMode::Basic, - FFIValidationMode::Full => ValidationMode::Full, - } - } -} - -#[repr(C)] -pub struct FFIClientConfig { - // Opaque pointer to avoid exposing internal ClientConfig in generated C headers - inner: *mut std::ffi::c_void, - // Tokio runtime worker thread count (0 = auto) - pub worker_threads: u32, -} - -#[no_mangle] -pub extern "C" fn dash_spv_ffi_config_new(network: FFINetwork) -> *mut FFIClientConfig { - let config = ClientConfig::new(network.into()); - let inner = Box::into_raw(Box::new(config)) as *mut std::ffi::c_void; - Box::into_raw(Box::new(FFIClientConfig { - inner, - worker_threads: 0, - })) -} - -#[no_mangle] -pub extern "C" fn dash_spv_ffi_config_mainnet() -> *mut FFIClientConfig { - let config = ClientConfig::mainnet(); - let inner = Box::into_raw(Box::new(config)) as *mut std::ffi::c_void; - Box::into_raw(Box::new(FFIClientConfig { - inner, - worker_threads: 0, - })) -} - -#[no_mangle] -pub extern "C" fn dash_spv_ffi_config_testnet() -> *mut FFIClientConfig { - let config = ClientConfig::testnet(); - let inner = Box::into_raw(Box::new(config)) as *mut std::ffi::c_void; - Box::into_raw(Box::new(FFIClientConfig { - inner, - worker_threads: 0, - })) -} - -/// Sets the data directory for storing blockchain data -/// -/// # Safety -/// - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet -/// - `path` must be a valid null-terminated C string -/// - The caller must ensure the config pointer remains valid for the duration of this call -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_config_set_data_dir( - config: *mut FFIClientConfig, - path: *const c_char, -) -> i32 { - null_check!(config); - null_check!(path); - - let config = unsafe { &mut *((*config).inner as *mut ClientConfig) }; - match CStr::from_ptr(path).to_str() { - Ok(path_str) => { - config.storage_path = path_str.into(); - FFIErrorCode::Success as i32 - } - Err(e) => { - set_last_error(&format!("Invalid UTF-8 in path: {}", e)); - FFIErrorCode::InvalidArgument as i32 - } - } -} - -// Note: dash-spv doesn't have min_peers, only max_peers - -/// Adds a peer address to the configuration -/// -/// Accepts socket addresses with or without port. When no port is specified, -/// the default P2P port for the configured network is used. -/// -/// Supported formats: -/// - IP with port: `192.168.1.1:9999`, `[::1]:19999` -/// - IP without port: `127.0.0.1`, `2001:db8::1` -/// - Hostname with port: `node.example.com:9999` -/// - Hostname without port: `node.example.com` -/// -/// # Safety -/// - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet -/// - `addr` must be a valid null-terminated C string containing a socket address or IP-only string -/// - The caller must ensure both pointers remain valid for the duration of this call -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_config_add_peer( - config: *mut FFIClientConfig, - addr: *const c_char, -) -> i32 { - null_check!(config); - null_check!(addr); - - let cfg = unsafe { &mut *((*config).inner as *mut ClientConfig) }; - let default_port = match cfg.network { - dashcore::Network::Mainnet => 9999, - dashcore::Network::Testnet => 19999, - dashcore::Network::Regtest => 19899, - dashcore::Network::Devnet => 29999, - }; - - let addr_str = match CStr::from_ptr(addr).to_str() { - Ok(s) => s.trim(), - Err(e) => { - set_last_error(&format!("Invalid UTF-8 in address: {}", e)); - return FFIErrorCode::InvalidArgument as i32; - } - }; - - // Try parsing as bare IP address and apply default port - if let Ok(ip) = addr_str.parse::() { - let sock = SocketAddr::new(ip, default_port); - cfg.peers.push(sock); - return FFIErrorCode::Success as i32; - } - - // If not, must be a hostname - reject empty or missing hostname - if addr_str.is_empty() || addr_str.starts_with(':') { - set_last_error("Empty or missing hostname"); - return FFIErrorCode::InvalidArgument as i32; - } - - let addr_with_port = if addr_str.contains(':') { - addr_str.to_string() - } else { - format!("{}:{}", addr_str, default_port) - }; - - match addr_with_port.to_socket_addrs() { - Ok(mut iter) => match iter.next() { - Some(sock) => { - cfg.peers.push(sock); - FFIErrorCode::Success as i32 - } - None => { - set_last_error(&format!("Failed to resolve address: {}", addr_str)); - FFIErrorCode::InvalidArgument as i32 - } - }, - Err(e) => { - set_last_error(&format!("Invalid address {} ({})", addr_str, e)); - FFIErrorCode::InvalidArgument as i32 - } - } -} - -/// Sets the user agent string to advertise in the P2P handshake -/// -/// # Safety -/// - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet -/// - `user_agent` must be a valid null-terminated C string -/// - The caller must ensure both pointers remain valid for the duration of this call -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_config_set_user_agent( - config: *mut FFIClientConfig, - user_agent: *const c_char, -) -> i32 { - null_check!(config); - null_check!(user_agent); - - // Validate the user_agent string - match CStr::from_ptr(user_agent).to_str() { - Ok(agent_str) => { - // Store as-is; normalization/length capping is applied at handshake build time - let cfg = unsafe { &mut *((*config).inner as *mut ClientConfig) }; - cfg.user_agent = Some(agent_str.to_string()); - FFIErrorCode::Success as i32 - } - Err(e) => { - set_last_error(&format!("Invalid UTF-8 in user agent: {}", e)); - FFIErrorCode::InvalidArgument as i32 - } - } -} - -/// Restrict connections strictly to configured peers (disable DNS discovery and peer store) -/// -/// # Safety -/// - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_config_set_restrict_to_configured_peers( - config: *mut FFIClientConfig, - restrict_peers: bool, -) -> i32 { - null_check!(config); - - let config = unsafe { &mut *((*config).inner as *mut ClientConfig) }; - config.restrict_to_configured_peers = restrict_peers; - FFIErrorCode::Success as i32 -} - -/// Enables or disables masternode synchronization -/// -/// # Safety -/// - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet -/// - The caller must ensure the config pointer remains valid for the duration of this call -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_config_set_masternode_sync_enabled( - config: *mut FFIClientConfig, - enable: bool, -) -> i32 { - null_check!(config); - - let config = unsafe { &mut *((*config).inner as *mut ClientConfig) }; - config.enable_masternodes = enable; - FFIErrorCode::Success as i32 -} - -/// Gets the network type from the configuration -/// -/// # Safety -/// - `config` must be a valid pointer to an FFIClientConfig or null -/// - If null, returns FFINetwork::Mainnet as default -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_config_get_network( - config: *const FFIClientConfig, -) -> FFINetwork { - if config.is_null() { - return FFINetwork::Mainnet; - } - - let config = unsafe { &*((*config).inner as *const ClientConfig) }; - config.network.into() -} - -/// Destroys an FFIClientConfig and frees its memory -/// -/// # Safety -/// - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet, or null -/// - After calling this function, the config pointer becomes invalid and must not be used -/// - This function should only be called once per config instance -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_config_destroy(config: *mut FFIClientConfig) { - if !config.is_null() { - // Reclaim outer struct - let cfg = Box::from_raw(config); - // Free inner ClientConfig if present - if !cfg.inner.is_null() { - let _ = Box::from_raw(cfg.inner as *mut ClientConfig); - } - } -} - -impl FFIClientConfig { - pub fn get_inner(&self) -> &ClientConfig { - unsafe { &*(self.inner as *const ClientConfig) } - } - - pub fn clone_inner(&self) -> ClientConfig { - unsafe { (*(self.inner as *const ClientConfig)).clone() } - } -} -// Mempool configuration functions - -/// Enables or disables mempool tracking -/// -/// # Safety -/// - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet -/// - The caller must ensure the config pointer remains valid for the duration of this call -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_config_set_mempool_tracking( - config: *mut FFIClientConfig, - enable: bool, -) -> i32 { - null_check!(config); - - let config = unsafe { &mut *((*config).inner as *mut ClientConfig) }; - config.enable_mempool_tracking = enable; - FFIErrorCode::Success as i32 -} - -/// Sets the mempool synchronization strategy -/// -/// # Safety -/// - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet -/// - The caller must ensure the config pointer remains valid for the duration of this call -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_config_set_mempool_strategy( - config: *mut FFIClientConfig, - strategy: FFIMempoolStrategy, -) -> i32 { - null_check!(config); - - let config = unsafe { &mut *((*config).inner as *mut ClientConfig) }; - config.mempool_strategy = strategy.into(); - FFIErrorCode::Success as i32 -} - -/// Sets whether to fetch full mempool transaction data -/// -/// # Safety -/// - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet -/// - The caller must ensure the config pointer remains valid for the duration of this call -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_config_set_fetch_mempool_transactions( - config: *mut FFIClientConfig, - fetch: bool, -) -> i32 { - null_check!(config); - - let config = unsafe { &mut *((*config).inner as *mut ClientConfig) }; - config.fetch_mempool_transactions = fetch; - FFIErrorCode::Success as i32 -} - -// Checkpoint sync configuration functions - -/// Sets the starting block height for synchronization -/// -/// # Safety -/// - `config` must be a valid pointer to an FFIClientConfig created by dash_spv_ffi_config_new/mainnet/testnet -/// - The caller must ensure the config pointer remains valid for the duration of this call -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_config_set_start_from_height( - config: *mut FFIClientConfig, - height: u32, -) -> i32 { - null_check!(config); - - let config = unsafe { &mut *((*config).inner as *mut ClientConfig) }; - config.start_from_height = Some(height); - FFIErrorCode::Success as i32 -} diff --git a/dash-spv-ffi/src/error.rs b/dash-spv-ffi/src/error.rs deleted file mode 100644 index 77a4bbb33..000000000 --- a/dash-spv-ffi/src/error.rs +++ /dev/null @@ -1,115 +0,0 @@ -use dash_spv::error::SpvError; -use std::ffi::CString; -use std::os::raw::c_char; -use std::sync::Mutex; - -// Global error storage protected by mutex for thread safety -static LAST_ERROR: Mutex> = Mutex::new(None); - -#[repr(C)] -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum FFIErrorCode { - Success = 0, - NullPointer = 1, - InvalidArgument = 2, - NetworkError = 3, - StorageError = 4, - ValidationError = 5, - SyncError = 6, - ConfigError = 7, - RuntimeError = 8, -} - -pub fn set_last_error(err: &str) { - let c_err = CString::new(err).unwrap_or_else(|_| CString::new("Unknown error").unwrap()); - if let Ok(mut guard) = LAST_ERROR.lock() { - *guard = Some(c_err); - } -} - -pub fn clear_last_error() { - if let Ok(mut guard) = LAST_ERROR.lock() { - *guard = None; - } -} - -#[no_mangle] -pub extern "C" fn dash_spv_ffi_get_last_error() -> *const c_char { - match LAST_ERROR.lock() { - Ok(guard) => guard.as_ref().map(|err| err.as_ptr()).unwrap_or(std::ptr::null()), - Err(_) => std::ptr::null(), - } -} - -impl From for FFIErrorCode { - fn from(err: SpvError) -> Self { - match err { - SpvError::ChannelFailure(_, _) => FFIErrorCode::RuntimeError, - SpvError::Network(_) => FFIErrorCode::NetworkError, - SpvError::Storage(_) => FFIErrorCode::StorageError, - SpvError::Sync(_) => FFIErrorCode::SyncError, - SpvError::Config(_) => FFIErrorCode::ConfigError, - SpvError::QuorumLookupError(_) => FFIErrorCode::ValidationError, - } - } -} - -pub fn handle_error(result: Result) -> Option { - match result { - Ok(value) => { - clear_last_error(); - Some(value) - } - Err(e) => { - set_last_error(&e.to_string()); - None - } - } -} - -pub fn handle_error_code>( - result: Result<(), E>, -) -> FFIErrorCode { - match result { - Ok(()) => { - clear_last_error(); - FFIErrorCode::Success - } - Err(e) => { - set_last_error(&e.to_string()); - e.into() - } - } -} - -#[macro_export] -macro_rules! ffi_result { - ($expr:expr) => { - match $expr { - Ok(val) => { - $crate::error::clear_last_error(); - val - } - Err(e) => { - $crate::error::set_last_error(&e.to_string()); - return $crate::error::FFIErrorCode::from(e) as i32; - } - } - }; -} - -#[macro_export] -macro_rules! null_check { - ($ptr:expr) => { - if $ptr.is_null() { - $crate::error::set_last_error("Null pointer provided"); - return $crate::error::FFIErrorCode::NullPointer as i32; - } - }; - ($ptr:expr, $ret:expr) => { - if $ptr.is_null() { - $crate::error::set_last_error("Null pointer provided"); - return $ret; - } - }; -} diff --git a/dash-spv-ffi/src/lib.rs b/dash-spv-ffi/src/lib.rs deleted file mode 100644 index 36d7d389f..000000000 --- a/dash-spv-ffi/src/lib.rs +++ /dev/null @@ -1,48 +0,0 @@ -pub mod callbacks; -pub mod client; -pub mod config; -pub mod error; -pub mod platform_integration; -pub mod types; -pub mod utils; - -pub use callbacks::*; -pub use client::*; -pub use config::*; -pub use error::*; -pub use platform_integration::*; -pub use types::*; -pub use utils::*; - -// Re-export wallet-FFI types used by `FFIWalletEventCallbacks` so consumers -// can refer to them via `dash_spv_ffi::*` without importing `key_wallet_ffi` -// directly. -pub use key_wallet_ffi::managed_account::{FFIAccountType, FFITransactionRecord}; -pub use key_wallet_ffi::types::FFIAccountKind; - -// FFINetwork is now defined in types.rs for cbindgen compatibility -// It must match the definition in key_wallet_ffi - -#[cfg(test)] -#[path = "../tests/unit/test_type_conversions.rs"] -mod test_type_conversions; - -#[cfg(test)] -#[path = "../tests/unit/test_error_handling.rs"] -mod test_error_handling; - -#[cfg(test)] -#[path = "../tests/unit/test_configuration.rs"] -mod test_configuration; - -#[cfg(test)] -#[path = "../tests/unit/test_client_lifecycle.rs"] -mod test_client_lifecycle; - -#[cfg(test)] -#[path = "../tests/unit/test_async_operations.rs"] -mod test_async_operations; - -#[cfg(test)] -#[path = "../tests/unit/test_memory_management.rs"] -mod test_memory_management; diff --git a/dash-spv-ffi/src/platform_integration.rs b/dash-spv-ffi/src/platform_integration.rs deleted file mode 100644 index ce1609788..000000000 --- a/dash-spv-ffi/src/platform_integration.rs +++ /dev/null @@ -1,200 +0,0 @@ -use crate::{set_last_error, FFIDashSpvClient, FFIErrorCode}; -use dashcore::hashes::Hash; -use dashcore::sml::llmq_type::LLMQType; -use dashcore::QuorumHash; -use std::os::raw::c_char; -use std::ptr; - -/// Handle for Core SDK that can be passed to Platform SDK -#[repr(C)] -pub struct CoreSDKHandle { - pub client: *mut FFIDashSpvClient, -} - -/// FFIResult type for error handling -#[repr(C)] -pub struct FFIResult { - pub error_code: i32, - pub error_message: *const c_char, -} - -impl FFIResult { - fn error(code: FFIErrorCode, message: &str) -> Self { - set_last_error(message); - FFIResult { - error_code: code as i32, - error_message: crate::dash_spv_ffi_get_last_error(), - } - } -} - -/// Gets a quorum public key from the Core chain -/// -/// # Safety -/// -/// This function is unsafe because: -/// - The caller must ensure all pointers are valid -/// - quorum_hash must point to a 32-byte array -/// - out_pubkey must point to a buffer of at least out_pubkey_size bytes -/// - out_pubkey_size must be at least 48 bytes -#[no_mangle] -pub unsafe extern "C" fn ffi_dash_spv_get_quorum_public_key( - client: *mut FFIDashSpvClient, - quorum_type: u32, - quorum_hash: *const u8, - core_chain_locked_height: u32, - out_pubkey: *mut u8, - out_pubkey_size: usize, -) -> FFIResult { - // Validate client pointer - if client.is_null() { - return FFIResult::error(FFIErrorCode::NullPointer, "Null client pointer"); - } - - // Validate quorum_hash pointer - if quorum_hash.is_null() { - return FFIResult::error(FFIErrorCode::NullPointer, "Null quorum_hash pointer"); - } - - // Validate output buffer pointer - if out_pubkey.is_null() { - return FFIResult::error(FFIErrorCode::NullPointer, "Null out_pubkey pointer"); - } - - // Validate buffer size - quorum public keys are 48 bytes - const QUORUM_PUBKEY_SIZE: usize = 48; - if out_pubkey_size < QUORUM_PUBKEY_SIZE { - return FFIResult::error( - FFIErrorCode::InvalidArgument, - &format!( - "Buffer too small: {} bytes provided, {} bytes required", - out_pubkey_size, QUORUM_PUBKEY_SIZE - ), - ); - } - - // Get the client reference - let client = &*client; - let spv_client = &client.inner; - - // Read the quorum hash from the input pointer - let quorum_hash_bytes = std::slice::from_raw_parts(quorum_hash, 32); - let mut hash_array = [0u8; 32]; - hash_array.copy_from_slice(quorum_hash_bytes); - - // Convert quorum type and hash for engine lookup (infallible) - let llmq_type: LLMQType = (quorum_type as u8).into(); - let quorum_hash = QuorumHash::from_byte_array(hash_array); - - // Get the masternode list engine directly for efficient access - let engine = match spv_client.masternode_list_engine() { - Ok(engine) => engine, - Err(e) => { - return FFIResult::error( - FFIErrorCode::RuntimeError, - &format!( - "Masternode list engine not initialized: {}. Core SDK may still be syncing.", - e - ), - ); - } - }; - - let engine_guard = engine.blocking_read(); - let (before, _after) = engine_guard.masternode_lists_around_height(core_chain_locked_height); - let ml = match before { - Some(ml) => ml, - None => { - return FFIResult::error( - FFIErrorCode::ValidationError, - &format!( - "No masternode list found at or before height {}", - core_chain_locked_height - ), - ); - } - }; - - let list_height = ml.known_height; - match ml.quorums.get(&llmq_type) { - Some(quorums) => match quorums.get(&quorum_hash) { - Some(quorum) => { - let pubkey_bytes: &[u8; 48] = quorum.quorum_entry.quorum_public_key.as_ref(); - std::ptr::copy_nonoverlapping( - pubkey_bytes.as_ptr(), - out_pubkey, - QUORUM_PUBKEY_SIZE, - ); - - FFIResult { - error_code: 0, - error_message: ptr::null(), - } - } - None => FFIResult::error( - FFIErrorCode::ValidationError, - &format!( - "Quorum not found: type {} at list height {} (requested {}) with hash {:x} (masternode list exists with {} quorums of this type)", - quorum_type, - list_height, - core_chain_locked_height, - quorum_hash, - quorums.len() - ), - ), - }, - None => FFIResult::error( - FFIErrorCode::ValidationError, - &format!( - "No quorums of type {} found at list height {} (requested {})", - quorum_type, list_height, core_chain_locked_height - ), - ), - } -} - -/// Gets the platform activation height from the Core chain -/// -/// # Safety -/// -/// This function is unsafe because: -/// - The caller must ensure all pointers are valid -/// - out_height must point to a valid u32 -#[no_mangle] -pub unsafe extern "C" fn ffi_dash_spv_get_platform_activation_height( - client: *mut FFIDashSpvClient, - out_height: *mut u32, -) -> FFIResult { - // Validate client pointer - if client.is_null() { - return FFIResult::error(FFIErrorCode::NullPointer, "Null client pointer"); - } - - // Validate output pointer - if out_height.is_null() { - return FFIResult::error(FFIErrorCode::NullPointer, "Null out_height pointer"); - } - - // Get the client reference - let client = &*client; - - // Get the network from the client config - let network = client.runtime.block_on(async { client.inner.network().await }); - - // Platform activation heights per network - let height = match network { - dashcore::Network::Mainnet => 1_888_888, // Mainnet (placeholder - needs verification) - dashcore::Network::Testnet => 1_289_520, // Testnet confirmed height - dashcore::Network::Devnet => 1, // Devnet starts immediately - _ => 0, // Unknown network - }; - - // Set the output value - *out_height = height; - - // Return success - FFIResult { - error_code: 0, - error_message: ptr::null(), - } -} diff --git a/dash-spv-ffi/src/types.rs b/dash-spv-ffi/src/types.rs deleted file mode 100644 index a90f9e755..000000000 --- a/dash-spv-ffi/src/types.rs +++ /dev/null @@ -1,541 +0,0 @@ -use dash_spv::client::config::MempoolStrategy; -use dash_spv::sync::{ - BlockHeadersProgress, BlocksProgress, ChainLockProgress, FilterHeadersProgress, - FiltersProgress, InstantSendProgress, MasternodesProgress, MempoolProgress, ProgressPercentage, - SyncProgress, SyncState, -}; -use std::ffi::{CStr, CString}; -use std::os::raw::c_char; - -/// Opaque handle to the wallet manager owned by the SPV client. -/// -/// This is intentionally zero-sized so it can be used purely as an FFI handle -/// while still allowing Rust to cast to the underlying key-wallet manager -/// implementation when necessary. -#[repr(C)] -pub struct FFIWalletManager { - _private: [u8; 0], -} - -#[repr(C)] -pub struct FFIString { - pub ptr: *mut c_char, - pub length: usize, -} - -impl FFIString { - pub fn new(s: &str) -> Self { - let c_string = CString::new(s).unwrap_or_else(|_| CString::new("").unwrap()); - // Compute length from the finalized CString to avoid mismatches when input contains NULs - let length = c_string.as_bytes().len(); - FFIString { - ptr: c_string.into_raw(), - length, - } - } - - /// # Safety - /// - `ptr` must be either null or point to a valid, NUL-terminated C string. - /// - The pointer must remain valid for the duration of this call. - pub unsafe fn from_ptr(ptr: *const c_char) -> Result { - if ptr.is_null() { - return Err("Null pointer".to_string()); - } - CStr::from_ptr(ptr).to_str().map(|s| s.to_string()).map_err(|e| e.to_string()) - } -} - -/// SyncState exposed by the FFI as FFISyncState. -#[repr(C)] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] -pub enum FFISyncState { - #[default] - WaitForEvents = 0, - WaitingForConnections = 1, - Syncing = 2, - Synced = 3, - Error = 4, -} - -impl From for FFISyncState { - fn from(state: SyncState) -> Self { - match state { - SyncState::WaitingForConnections => FFISyncState::WaitingForConnections, - SyncState::WaitForEvents => FFISyncState::WaitForEvents, - SyncState::Syncing => FFISyncState::Syncing, - SyncState::Synced => FFISyncState::Synced, - SyncState::Error => FFISyncState::Error, - } - } -} - -/// Progress for block headers synchronization. -#[repr(C)] -#[derive(Debug, Clone, Default)] -pub struct FFIBlockHeadersProgress { - pub state: FFISyncState, - pub tip_height: u32, - pub target_height: u32, - pub processed: u32, - pub buffered: u32, - pub percentage: f64, - pub last_activity: u64, -} - -impl From<&BlockHeadersProgress> for FFIBlockHeadersProgress { - fn from(progress: &BlockHeadersProgress) -> Self { - FFIBlockHeadersProgress { - state: progress.state().into(), - tip_height: progress.tip_height(), - target_height: progress.target_height(), - processed: progress.processed(), - buffered: progress.buffered(), - percentage: progress.percentage(), - last_activity: progress.last_activity().elapsed().as_secs(), - } - } -} - -/// Progress for filter headers synchronization. -#[repr(C)] -#[derive(Debug, Clone, Default)] -pub struct FFIFilterHeadersProgress { - pub state: FFISyncState, - pub current_height: u32, - pub target_height: u32, - pub block_header_tip_height: u32, - pub processed: u32, - pub percentage: f64, - pub last_activity: u64, -} - -impl From<&FilterHeadersProgress> for FFIFilterHeadersProgress { - fn from(progress: &FilterHeadersProgress) -> Self { - FFIFilterHeadersProgress { - state: progress.state().into(), - current_height: progress.current_height(), - target_height: progress.target_height(), - block_header_tip_height: progress.block_header_tip_height(), - processed: progress.processed(), - percentage: progress.percentage(), - last_activity: progress.last_activity().elapsed().as_secs(), - } - } -} - -/// Progress for compact block filters synchronization. -#[repr(C)] -#[derive(Debug, Clone, Default)] -pub struct FFIFiltersProgress { - pub state: FFISyncState, - pub committed_height: u32, - pub stored_height: u32, - pub target_height: u32, - pub filter_header_tip_height: u32, - pub downloaded: u32, - pub processed: u32, - pub matched: u32, - pub percentage: f64, - pub last_activity: u64, -} - -impl From<&FiltersProgress> for FFIFiltersProgress { - fn from(progress: &FiltersProgress) -> Self { - FFIFiltersProgress { - state: progress.state().into(), - committed_height: progress.committed_height(), - stored_height: progress.stored_height(), - target_height: progress.target_height(), - filter_header_tip_height: progress.filter_header_tip_height(), - downloaded: progress.downloaded(), - processed: progress.processed(), - matched: progress.matched(), - percentage: progress.percentage(), - last_activity: progress.last_activity().elapsed().as_secs(), - } - } -} - -/// Progress for full block synchronization. -#[repr(C)] -#[derive(Debug, Clone, Default)] -pub struct FFIBlocksProgress { - pub state: FFISyncState, - pub last_processed: u32, - pub requested: u32, - pub from_storage: u32, - pub downloaded: u32, - pub processed: u32, - pub relevant: u32, - pub transactions: u32, - pub last_activity: u64, -} - -impl From<&BlocksProgress> for FFIBlocksProgress { - fn from(progress: &BlocksProgress) -> Self { - FFIBlocksProgress { - state: progress.state().into(), - last_processed: progress.last_processed(), - requested: progress.requested(), - from_storage: progress.from_storage(), - downloaded: progress.downloaded(), - processed: progress.processed(), - relevant: progress.relevant(), - transactions: progress.transactions(), - last_activity: progress.last_activity().elapsed().as_secs(), - } - } -} - -/// Progress for masternode list synchronization. -#[repr(C)] -#[derive(Debug, Clone, Default)] -pub struct FFIMasternodesProgress { - pub state: FFISyncState, - pub current_height: u32, - pub target_height: u32, - pub block_header_tip_height: u32, - pub diffs_processed: u32, - pub last_activity: u64, -} - -impl From<&MasternodesProgress> for FFIMasternodesProgress { - fn from(progress: &MasternodesProgress) -> Self { - FFIMasternodesProgress { - state: progress.state().into(), - current_height: progress.current_height(), - target_height: progress.target_height(), - block_header_tip_height: progress.block_header_tip_height(), - diffs_processed: progress.diffs_processed(), - last_activity: progress.last_activity().elapsed().as_secs(), - } - } -} - -/// Progress for ChainLock synchronization. -#[repr(C)] -#[derive(Debug, Clone, Default)] -pub struct FFIChainLockProgress { - pub state: FFISyncState, - pub best_validated_height: u32, - pub valid: u32, - pub invalid: u32, - pub last_activity: u64, -} - -impl From<&ChainLockProgress> for FFIChainLockProgress { - fn from(progress: &ChainLockProgress) -> Self { - FFIChainLockProgress { - state: progress.state().into(), - best_validated_height: progress.best_validated_height(), - valid: progress.valid(), - invalid: progress.invalid(), - last_activity: progress.last_activity().elapsed().as_secs(), - } - } -} - -/// Progress for InstantSend synchronization. -#[repr(C)] -#[derive(Debug, Clone, Default)] -pub struct FFIInstantSendProgress { - pub state: FFISyncState, - pub pending: u32, - pub valid: u32, - pub invalid: u32, - pub last_activity: u64, -} - -impl From<&InstantSendProgress> for FFIInstantSendProgress { - fn from(progress: &InstantSendProgress) -> Self { - FFIInstantSendProgress { - state: progress.state().into(), - pending: progress.pending() as u32, - valid: progress.valid(), - invalid: progress.invalid(), - last_activity: progress.last_activity().elapsed().as_secs(), - } - } -} - -/// Progress for mempool transaction monitoring. -#[repr(C)] -#[derive(Debug, Clone, Default)] -pub struct FFIMempoolProgress { - pub state: FFISyncState, - pub received: u32, - pub relevant: u32, - pub tracked: u32, - pub removed: u32, - pub last_activity: u64, -} - -impl From<&MempoolProgress> for FFIMempoolProgress { - fn from(progress: &MempoolProgress) -> Self { - FFIMempoolProgress { - state: progress.state().into(), - received: progress.received(), - relevant: progress.relevant(), - tracked: progress.tracked(), - removed: progress.removed(), - last_activity: progress.last_activity().elapsed().as_secs(), - } - } -} - -/// Aggregate progress for all sync managers. -/// Provides a complete view of the parallel sync system's state. -#[repr(C)] -pub struct FFISyncProgress { - pub state: FFISyncState, - pub percentage: f64, - pub is_synced: bool, - /// Per-manager progress (null if manager not started). - pub headers: *mut FFIBlockHeadersProgress, - pub filter_headers: *mut FFIFilterHeadersProgress, - pub filters: *mut FFIFiltersProgress, - pub blocks: *mut FFIBlocksProgress, - pub masternodes: *mut FFIMasternodesProgress, - pub chainlocks: *mut FFIChainLockProgress, - pub instantsend: *mut FFIInstantSendProgress, - pub mempool: *mut FFIMempoolProgress, -} - -impl From for FFISyncProgress { - fn from(progress: SyncProgress) -> Self { - let headers = progress - .headers() - .ok() - .map(|p| Box::into_raw(Box::new(FFIBlockHeadersProgress::from(p)))) - .unwrap_or(std::ptr::null_mut()); - - let filter_headers = progress - .filter_headers() - .ok() - .map(|p| Box::into_raw(Box::new(FFIFilterHeadersProgress::from(p)))) - .unwrap_or(std::ptr::null_mut()); - - let filters = progress - .filters() - .ok() - .map(|p| Box::into_raw(Box::new(FFIFiltersProgress::from(p)))) - .unwrap_or(std::ptr::null_mut()); - - let blocks = progress - .blocks() - .ok() - .map(|p| Box::into_raw(Box::new(FFIBlocksProgress::from(p)))) - .unwrap_or(std::ptr::null_mut()); - - let masternodes = progress - .masternodes() - .ok() - .map(|p| Box::into_raw(Box::new(FFIMasternodesProgress::from(p)))) - .unwrap_or(std::ptr::null_mut()); - - let chainlocks = progress - .chainlocks() - .ok() - .map(|p| Box::into_raw(Box::new(FFIChainLockProgress::from(p)))) - .unwrap_or(std::ptr::null_mut()); - - let instantsend = progress - .instantsend() - .ok() - .map(|p| Box::into_raw(Box::new(FFIInstantSendProgress::from(p)))) - .unwrap_or(std::ptr::null_mut()); - - let mempool = progress - .mempool() - .ok() - .map(|p| Box::into_raw(Box::new(FFIMempoolProgress::from(p)))) - .unwrap_or(std::ptr::null_mut()); - - Self { - state: progress.state().into(), - percentage: progress.percentage(), - is_synced: progress.is_synced(), - headers, - filter_headers, - filters, - blocks, - masternodes, - chainlocks, - instantsend, - mempool, - } - } -} - -/// # Safety -/// - `s.ptr` must be a pointer previously returned by `FFIString::new` or compatible. -/// - It must not be used after this call. -pub unsafe fn dash_spv_ffi_string_destroy(s: FFIString) { - if !s.ptr.is_null() { - let _ = CString::from_raw(s.ptr); - } -} - -#[repr(C)] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum FFIMempoolStrategy { - FetchAll = 0, - BloomFilter = 1, -} - -impl From for FFIMempoolStrategy { - fn from(strategy: MempoolStrategy) -> Self { - match strategy { - MempoolStrategy::FetchAll => FFIMempoolStrategy::FetchAll, - MempoolStrategy::BloomFilter => FFIMempoolStrategy::BloomFilter, - } - } -} - -impl From for MempoolStrategy { - fn from(strategy: FFIMempoolStrategy) -> Self { - match strategy { - FFIMempoolStrategy::FetchAll => MempoolStrategy::FetchAll, - FFIMempoolStrategy::BloomFilter => MempoolStrategy::BloomFilter, - } - } -} - -// ============================================================================ -// Destroy functions for new manager progress types -// ============================================================================ - -/// Destroy an `FFIBlockHeadersProgress` object. -/// -/// # Safety -/// - `progress` must be a pointer returned from this crate, or null. -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_block_headers_progress_destroy( - progress: *mut FFIBlockHeadersProgress, -) { - if !progress.is_null() { - let _ = Box::from_raw(progress); - } -} - -/// Destroy an `FFIFilterHeadersProgress` object. -/// -/// # Safety -/// - `progress` must be a pointer returned from this crate, or null. -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_filter_headers_progress_destroy( - progress: *mut FFIFilterHeadersProgress, -) { - if !progress.is_null() { - let _ = Box::from_raw(progress); - } -} - -/// Destroy an `FFIFiltersProgress` object. -/// -/// # Safety -/// - `progress` must be a pointer returned from this crate, or null. -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_filters_progress_destroy(progress: *mut FFIFiltersProgress) { - if !progress.is_null() { - let _ = Box::from_raw(progress); - } -} - -/// Destroy an `FFIBlocksProgress` object. -/// -/// # Safety -/// - `progress` must be a pointer returned from this crate, or null. -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_blocks_progress_destroy(progress: *mut FFIBlocksProgress) { - if !progress.is_null() { - let _ = Box::from_raw(progress); - } -} - -/// Destroy an `FFIMasternodesProgress` object. -/// -/// # Safety -/// - `progress` must be a pointer returned from this crate, or null. -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_masternode_progress_destroy( - progress: *mut FFIMasternodesProgress, -) { - if !progress.is_null() { - let _ = Box::from_raw(progress); - } -} - -/// Destroy an `FFIChainLockProgress` object. -/// -/// # Safety -/// - `progress` must be a pointer returned from this crate, or null. -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_chainlock_progress_destroy( - progress: *mut FFIChainLockProgress, -) { - if !progress.is_null() { - let _ = Box::from_raw(progress); - } -} - -/// Destroy an `FFIInstantSendProgress` object. -/// -/// # Safety -/// - `progress` must be a pointer returned from this crate, or null. -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_instantsend_progress_destroy( - progress: *mut FFIInstantSendProgress, -) { - if !progress.is_null() { - let _ = Box::from_raw(progress); - } -} - -/// Destroy an `FFIMempoolProgress` object. -/// -/// # Safety -/// - `progress` must be a pointer returned from this crate, or null. -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_mempool_progress_destroy(progress: *mut FFIMempoolProgress) { - if !progress.is_null() { - let _ = Box::from_raw(progress); - } -} - -/// Destroy an `FFISyncProgress` object and all its nested pointers. -/// -/// # Safety -/// - `progress` must be a pointer returned from this crate, or null. -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_sync_progress_destroy(progress: *mut FFISyncProgress) { - if !progress.is_null() { - let p = Box::from_raw(progress); - - // Free all nested progress pointers - if !p.headers.is_null() { - dash_spv_ffi_block_headers_progress_destroy(p.headers); - } - if !p.filter_headers.is_null() { - dash_spv_ffi_filter_headers_progress_destroy(p.filter_headers); - } - if !p.filters.is_null() { - dash_spv_ffi_filters_progress_destroy(p.filters); - } - if !p.blocks.is_null() { - dash_spv_ffi_blocks_progress_destroy(p.blocks); - } - if !p.masternodes.is_null() { - dash_spv_ffi_masternode_progress_destroy(p.masternodes); - } - if !p.chainlocks.is_null() { - dash_spv_ffi_chainlock_progress_destroy(p.chainlocks); - } - if !p.instantsend.is_null() { - dash_spv_ffi_instantsend_progress_destroy(p.instantsend); - } - if !p.mempool.is_null() { - dash_spv_ffi_mempool_progress_destroy(p.mempool); - } - } -} diff --git a/dash-spv-ffi/src/utils.rs b/dash-spv-ffi/src/utils.rs deleted file mode 100644 index e314051b5..000000000 --- a/dash-spv-ffi/src/utils.rs +++ /dev/null @@ -1,93 +0,0 @@ -use std::ffi::CStr; -use std::os::raw::c_char; -use std::path::PathBuf; -use std::sync::OnceLock; - -use crate::{set_last_error, FFIErrorCode}; -use dash_spv::{LogFileConfig, LoggingConfig}; - -/// Static storage for the logging guard to keep it alive for the FFI lifetime. -/// The guard must remain alive for log flushing to work correctly. -static LOGGING_GUARD: OnceLock = OnceLock::new(); - -/// Initialize logging for the SPV library. -/// -/// # Arguments -/// - `level`: Log level string (null uses RUST_LOG env var or defaults to INFO). -/// Valid values: "error", "warn", "info", "debug", "trace" -/// - `enable_console`: Whether to output logs to console (stderr) -/// - `log_dir`: Directory for log files (null to disable file logging) -/// - `max_files`: Maximum archived log files to retain (ignored if log_dir is null) -/// -/// # Safety -/// - `level` and `log_dir` may be null or point to valid, NUL-terminated C strings. -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_init_logging( - level: *const c_char, - enable_console: bool, - log_dir: *const c_char, - max_files: usize, -) -> i32 { - let level_filter = if level.is_null() { - None - } else { - match CStr::from_ptr(level).to_str() { - Ok(s) => match s.parse() { - Ok(lf) => Some(lf), - Err(_) => { - set_last_error(&format!( - "Invalid log level '{}'. Valid: error, warn, info, debug, trace", - s - )); - return FFIErrorCode::InvalidArgument as i32; - } - }, - Err(e) => { - set_last_error(&format!("Invalid UTF-8 in log level: {}", e)); - return FFIErrorCode::InvalidArgument as i32; - } - } - }; - - let file_config = if log_dir.is_null() { - None - } else { - match CStr::from_ptr(log_dir).to_str() { - Ok(s) => Some(LogFileConfig { - log_dir: PathBuf::from(s), - max_files, - }), - Err(e) => { - set_last_error(&format!("Invalid UTF-8 in log directory: {}", e)); - return FFIErrorCode::InvalidArgument as i32; - } - } - }; - - let config = LoggingConfig { - level: level_filter, - console: enable_console, - file: file_config, - thread_local: false, - }; - - match dash_spv::init_logging(config) { - Ok(guard) => { - // Store guard in static to keep it alive for log flushing. - // OnceLock::set returns Err if already set (first init wins). - if LOGGING_GUARD.set(guard).is_err() { - tracing::warn!("Logging already initialized, ignoring subsequent init"); - } - FFIErrorCode::Success as i32 - } - Err(e) => { - set_last_error(&format!("Failed to initialize logging: {}", e)); - FFIErrorCode::RuntimeError as i32 - } - } -} - -#[no_mangle] -pub extern "C" fn dash_spv_ffi_version() -> *const c_char { - concat!(env!("CARGO_PKG_VERSION"), "\0").as_ptr() as *const c_char -} diff --git a/dash-spv-ffi/tests/README.md b/dash-spv-ffi/tests/README.md deleted file mode 100644 index 54bc12481..000000000 --- a/dash-spv-ffi/tests/README.md +++ /dev/null @@ -1,106 +0,0 @@ -# Dash SPV FFI Test Suite - -This directory contains a comprehensive test suite for the dash-spv-ffi crate, covering all aspects of the FFI bindings. - -## Test Categories - -### 1. Unit Tests (`unit/`) -Located in the source tree and included via `src/lib.rs`. - -- **test_type_conversions.rs**: Tests FFI type conversions, string handling, array operations, and edge cases -- **test_error_handling.rs**: Tests error propagation, thread-local error storage, and error code mappings -- **test_configuration.rs**: Tests configuration creation, validation, and parameter handling -- **test_client_lifecycle.rs**: Tests client creation, destruction, state management, and concurrent operations -- **test_async_operations.rs**: Tests callback mechanisms, event handling, and async operation patterns -- **test_wallet_operations.rs**: Tests address/script watching, balance queries, transaction operations -- **test_memory_management.rs**: Tests memory allocation, deallocation, alignment, and leak prevention - -### 2. Integration Tests (`integration/`) -End-to-end tests that verify complete workflows. - -- **test_full_workflow.rs**: Tests complete sync workflows, wallet monitoring, transaction broadcast -- **test_cross_language.rs**: Tests C compatibility, struct alignment, calling conventions - -### 3. Performance Tests (`performance/`) -Benchmarks and performance measurements. - -- **test_benchmarks.rs**: Measures performance of string/array allocation, type conversions, concurrent operations - -### 4. Security Tests (`security/`) -Security-focused tests for vulnerability prevention. - -- **test_security.rs**: Tests buffer overflow protection, null pointer handling, input validation, DoS resistance - -### 5. C Test Suite (`c_tests/`) -Native C tests to verify the FFI interface from C perspective. - -- **test_basic.c**: Basic functionality tests (config, client creation, error handling) -- **test_advanced.c**: Advanced features (wallet ops, concurrency, callbacks) -- **test_integration.c**: Integration scenarios (full workflow, persistence, transactions) -- **Makefile**: Build system for C tests - -## Running the Tests - -### Rust Tests -```bash -# Run all Rust tests -cargo test -p dash-spv-ffi - -# Run specific test category -cargo test -p dash-spv-ffi test_type_conversions -cargo test -p dash-spv-ffi test_memory_management - -# Run with output -cargo test -p dash-spv-ffi -- --nocapture -``` - -### C Tests -```bash -cd tests/c_tests - -# Build Rust library first -make rust-lib - -# Generate C header -make header - -# Build and run all C tests -make test - -# Run individual C test -make test_basic -./test_basic -``` - -## Test Coverage - -The test suite covers: - -1. **API Surface**: All public FFI functions -2. **Error Conditions**: Null pointers, invalid inputs, error propagation -3. **Memory Safety**: Allocation, deallocation, alignment, leaks -4. **Thread Safety**: Concurrent access, race conditions -5. **Cross-Language**: C compatibility, struct layout, calling conventions -6. **Performance**: Throughput, latency, scalability -7. **Security**: Input validation, buffer overflows, DoS resistance -8. **Integration**: Real-world usage patterns, persistence, network operations - -## Adding New Tests - -When adding new functionality to dash-spv-ffi: - -1. Add unit tests in the appropriate `unit/test_*.rs` file -2. Add integration tests if the feature involves multiple components -3. Add C tests to verify the C API works correctly -4. Add performance benchmarks for performance-critical operations -5. Add security tests for any input validation or unsafe operations - -## Test Dependencies - -- `serial_test`: Ensures tests run serially to avoid conflicts -- `tempfile`: Creates temporary directories for test data -- `env_logger`: Optional logging for debugging - -## Known Limitations - -Some tests may fail in environments without network access or when dash-spv services are unavailable. These tests are designed to handle such failures gracefully. diff --git a/dash-spv-ffi/tests/dashd_sync/callbacks.rs b/dash-spv-ffi/tests/dashd_sync/callbacks.rs deleted file mode 100644 index a36b944ef..000000000 --- a/dash-spv-ffi/tests/dashd_sync/callbacks.rs +++ /dev/null @@ -1,626 +0,0 @@ -//! FFI callback implementations and tracker for integration tests. - -use std::ffi::CStr; -use std::os::raw::{c_char, c_void}; -use std::slice; -use std::sync::atomic::{AtomicU32, AtomicU64, Ordering}; -use std::sync::{Arc, Mutex}; -use std::time::Duration; - -use dash_spv_ffi::*; -use key_wallet_ffi::types::FFIBalance; - -/// Tracks callback invocations for verification. -/// -/// Fields are updated atomically from FFI callbacks and read in test assertions. -#[derive(Default)] -pub(super) struct CallbackTracker { - // Sync event tracking - pub(super) sync_start_count: AtomicU32, - pub(super) block_headers_stored_count: AtomicU32, - pub(super) block_header_sync_complete_count: AtomicU32, - pub(super) filter_headers_stored_count: AtomicU32, - pub(super) filter_headers_sync_complete_count: AtomicU32, - pub(super) filters_stored_count: AtomicU32, - pub(super) filters_sync_complete_count: AtomicU32, - pub(super) blocks_needed_count: AtomicU32, - pub(super) block_processed_count: AtomicU32, - pub(super) masternode_state_updated_count: AtomicU32, - pub(super) chainlock_received_count: AtomicU32, - pub(super) instantlock_received_count: AtomicU32, - pub(super) manager_error_count: AtomicU32, - pub(super) sync_complete_count: AtomicU32, - - // Network event tracking - pub(super) peer_connected_count: AtomicU32, - pub(super) peer_disconnected_count: AtomicU32, - pub(super) peers_updated_count: AtomicU32, - - // Wallet event tracking - pub(super) transaction_received_count: AtomicU32, - pub(super) transaction_instant_send_locked_count: AtomicU32, - pub(super) block_processed_wallet_count: AtomicU32, - pub(super) block_processed_wallet_record_count: AtomicU32, - pub(super) synced_height_updated_count: AtomicU32, - /// Highest synced-height value observed from any `SyncedHeightUpdated`. - pub(super) last_synced_height: AtomicU32, - - // Data from callbacks - pub(super) last_header_tip: AtomicU32, - pub(super) last_filter_tip: AtomicU32, - pub(super) last_connected_peer_count: AtomicU32, - pub(super) last_best_height: AtomicU32, - pub(super) connected_peers: Mutex>, - pub(super) errors: Mutex>, - - // Per-record (txid, net_amount) seen via the off-chain wallet callback. - pub(super) received_transactions: Mutex>, - // Per-record (txid, net_amount) seen via the block-processed callback. - pub(super) block_received_transactions: Mutex>, - - // `FFIAccountKind` discriminants captured from wallet callbacks. Lets - // tests assert that account-type delivery is well-formed and matches the - // expected account. - pub(super) received_account_types: Mutex>, - pub(super) block_account_types: Mutex>, - - // `account_index` values captured alongside `FFIAccountKind`, paired - // positionally with the corresponding `*_account_types` entries. - pub(super) received_account_indices: Mutex>, - pub(super) block_account_indices: Mutex>, - - // Per-record bucketing observed on `BlockProcessed` changes, in delivery - // order. Each entry is `true` when the record was delivered via the - // `inserted` array, `false` when delivered via `updated`. Lets tests - // assert that confirmation of a previously-known mempool transaction - // lands in `updated` rather than `inserted`. - pub(super) block_record_inserted: Mutex>, - - // Number of changed-account entries observed on the most recent wallet - // event. Lets tests assert that per-account balance diffs are wired - // through and arrive non-empty for state-changing events. - pub(super) last_changed_account_count: AtomicU32, - /// Highest changed-account count observed across all wallet events so a - /// single state-changing event can be detected without racing the - /// "last" snapshot. - pub(super) max_changed_account_count: AtomicU32, - - // Balance data from the most recent wallet event. - pub(super) last_confirmed: AtomicU64, - pub(super) last_unconfirmed: AtomicU64, - - // Raw IS lock bytes captured from the most recent - // `on_transaction_instant_send_locked` callback. Lets tests verify the - // payload is non-empty and round-trips through `InstantLock` deserialisation. - pub(super) last_islock_bytes: Mutex>>, - - // Lifecycle ordering via global sequence counter - pub(super) sequence_counter: AtomicU32, - pub(super) sync_start_seq: AtomicU32, - pub(super) header_complete_seq: AtomicU32, - pub(super) filter_header_complete_seq: AtomicU32, - pub(super) filters_sync_complete_seq: AtomicU32, - pub(super) sync_complete_seq: AtomicU32, - - // Filter header range validation: (start, end, tip) - pub(super) filter_header_ranges: Mutex>, - - // Block processed heights - pub(super) processed_block_heights: Mutex>, - - // Completion tracking - pub(super) last_sync_cycle: AtomicU32, - - // Baseline for `wait_for_sync`: captured before the client starts so that - // a SyncComplete firing between client start and `wait_for_sync` entry is - // not missed. - pub(super) sync_count_baseline: AtomicU32, -} - -impl CallbackTracker { - /// Assert that no errors were recorded during sync. - pub(super) fn assert_no_errors(&self) { - let errors = self.errors.lock().unwrap(); - assert!(errors.is_empty(), "Unexpected sync errors: {:?}", *errors); - } - - /// Polls until the given counter exceeds `baseline`, with a 10s timeout. - /// - /// Wallet event callbacks travel on a separate broadcast channel from sync - /// events, so `wait_for_sync` completing does not guarantee they have fired. - pub(super) fn wait_for_callback(&self, counter: &AtomicU32, baseline: u32, name: &str) { - let timeout = std::time::Instant::now() + Duration::from_secs(10); - while counter.load(Ordering::SeqCst) <= baseline { - assert!( - std::time::Instant::now() < timeout, - "Timed out waiting for {} callback (stuck at baseline {})", - name, - baseline - ); - std::thread::sleep(Duration::from_millis(100)); - } - } -} - -/// Extract the `CallbackTracker` reference from a `user_data` pointer. -/// Returns `None` if the pointer is null. -/// -/// # Safety -/// -/// The pointer must point to a valid, live `CallbackTracker` -/// (e.g. obtained via `Arc::as_ptr`). -unsafe fn tracker_from(user_data: *mut c_void) -> Option<&'static CallbackTracker> { - if user_data.is_null() { - None - } else { - Some(&*(user_data as *const CallbackTracker)) - } -} - -/// Convert a nullable C string pointer to an owned `String`. -/// Returns `"Unknown"` if the pointer is null. -/// -/// # Safety -/// -/// The pointer must point to a valid, null-terminated C string if non-null. -unsafe fn cstr_or_unknown(ptr: *const c_char) -> String { - if ptr.is_null() { - "Unknown".to_string() - } else { - CStr::from_ptr(ptr).to_string_lossy().into_owned() - } -} - -extern "C" fn on_sync_start(manager_id: FFIManagerId, user_data: *mut c_void) { - let Some(tracker) = (unsafe { tracker_from(user_data) }) else { - return; - }; - let seq = tracker.sequence_counter.fetch_add(1, Ordering::SeqCst); - tracker.sync_start_seq.store(seq, Ordering::SeqCst); - tracker.sync_start_count.fetch_add(1, Ordering::SeqCst); - tracing::debug!("on_sync_start: manager={:?}, seq={}", manager_id, seq); -} - -extern "C" fn on_block_headers_stored(tip_height: u32, user_data: *mut c_void) { - let Some(tracker) = (unsafe { tracker_from(user_data) }) else { - return; - }; - tracker.last_header_tip.store(tip_height, Ordering::SeqCst); - tracker.block_headers_stored_count.fetch_add(1, Ordering::SeqCst); - tracing::debug!("on_block_headers_stored: tip={}", tip_height); -} - -extern "C" fn on_block_header_sync_complete(tip_height: u32, user_data: *mut c_void) { - let Some(tracker) = (unsafe { tracker_from(user_data) }) else { - return; - }; - tracker.last_header_tip.store(tip_height, Ordering::SeqCst); - let seq = tracker.sequence_counter.fetch_add(1, Ordering::SeqCst); - tracker.header_complete_seq.store(seq, Ordering::SeqCst); - tracker.block_header_sync_complete_count.fetch_add(1, Ordering::SeqCst); - tracing::info!("on_block_header_sync_complete: tip={}, seq={}", tip_height, seq); -} - -extern "C" fn on_filter_headers_stored( - start_height: u32, - end_height: u32, - tip_height: u32, - user_data: *mut c_void, -) { - let Some(tracker) = (unsafe { tracker_from(user_data) }) else { - return; - }; - tracker.last_filter_tip.store(tip_height, Ordering::SeqCst); - tracker.filter_header_ranges.lock().unwrap_or_else(|e| e.into_inner()).push(( - start_height, - end_height, - tip_height, - )); - tracker.filter_headers_stored_count.fetch_add(1, Ordering::SeqCst); - tracing::debug!( - "on_filter_headers_stored: start={}, end={}, tip={}", - start_height, - end_height, - tip_height - ); -} - -extern "C" fn on_filter_headers_sync_complete(tip_height: u32, user_data: *mut c_void) { - let Some(tracker) = (unsafe { tracker_from(user_data) }) else { - return; - }; - tracker.last_filter_tip.store(tip_height, Ordering::SeqCst); - let seq = tracker.sequence_counter.fetch_add(1, Ordering::SeqCst); - tracker.filter_header_complete_seq.store(seq, Ordering::SeqCst); - tracker.filter_headers_sync_complete_count.fetch_add(1, Ordering::SeqCst); - tracing::info!("on_filter_headers_sync_complete: tip={}, seq={}", tip_height, seq); -} - -extern "C" fn on_filters_stored(start_height: u32, end_height: u32, user_data: *mut c_void) { - let Some(tracker) = (unsafe { tracker_from(user_data) }) else { - return; - }; - tracker.filters_stored_count.fetch_add(1, Ordering::SeqCst); - tracing::debug!("on_filters_stored: {}-{}", start_height, end_height); -} - -extern "C" fn on_filters_sync_complete(tip_height: u32, user_data: *mut c_void) { - let Some(tracker) = (unsafe { tracker_from(user_data) }) else { - return; - }; - tracker.last_filter_tip.store(tip_height, Ordering::SeqCst); - let seq = tracker.sequence_counter.fetch_add(1, Ordering::SeqCst); - tracker.filters_sync_complete_seq.store(seq, Ordering::SeqCst); - tracker.filters_sync_complete_count.fetch_add(1, Ordering::SeqCst); - tracing::info!("on_filters_sync_complete: tip={}, seq={}", tip_height, seq); -} - -extern "C" fn on_blocks_needed( - _blocks: *const dash_spv_ffi::FFIBlockNeeded, - count: u32, - user_data: *mut c_void, -) { - let Some(tracker) = (unsafe { tracker_from(user_data) }) else { - return; - }; - tracker.blocks_needed_count.fetch_add(1, Ordering::SeqCst); - tracing::debug!("on_blocks_needed: count={}", count); -} - -extern "C" fn on_block_processed( - height: u32, - _hash: *const [u8; 32], - new_address_count: u32, - _confirmed_txids: *const [u8; 32], - confirmed_txid_count: u32, - user_data: *mut c_void, -) { - let Some(tracker) = (unsafe { tracker_from(user_data) }) else { - return; - }; - tracker.processed_block_heights.lock().unwrap_or_else(|e| e.into_inner()).push(height); - tracker.block_processed_count.fetch_add(1, Ordering::SeqCst); - tracing::debug!( - "on_block_processed: height={}, new_addresses={}, confirmed_txs={}", - height, - new_address_count, - confirmed_txid_count - ); -} - -extern "C" fn on_masternode_state_updated(height: u32, user_data: *mut c_void) { - let Some(tracker) = (unsafe { tracker_from(user_data) }) else { - return; - }; - tracker.masternode_state_updated_count.fetch_add(1, Ordering::SeqCst); - tracing::debug!("on_masternode_state_updated: height={}", height); -} - -extern "C" fn on_chainlock_received( - height: u32, - _hash: *const [u8; 32], - _signature: *const [u8; 96], - validated: bool, - user_data: *mut c_void, -) { - let Some(tracker) = (unsafe { tracker_from(user_data) }) else { - return; - }; - tracker.chainlock_received_count.fetch_add(1, Ordering::SeqCst); - tracing::info!("on_chainlock_received: height={}, validated={}", height, validated); -} - -extern "C" fn on_instantlock_received( - _txid: *const [u8; 32], - _instantlock_data: *const u8, - _instantlock_len: usize, - validated: bool, - user_data: *mut c_void, -) { - let Some(tracker) = (unsafe { tracker_from(user_data) }) else { - return; - }; - tracker.instantlock_received_count.fetch_add(1, Ordering::SeqCst); - tracing::debug!("on_instantlock_received: validated={}", validated); -} - -extern "C" fn on_manager_error( - manager_id: FFIManagerId, - error: *const c_char, - user_data: *mut c_void, -) { - let Some(tracker) = (unsafe { tracker_from(user_data) }) else { - return; - }; - let error_str = unsafe { cstr_or_unknown(error) }; - tracing::error!("on_manager_error: manager={:?}, error={}", manager_id, error_str); - tracker.errors.lock().unwrap_or_else(|e| e.into_inner()).push(error_str); - tracker.manager_error_count.fetch_add(1, Ordering::SeqCst); -} - -extern "C" fn on_sync_complete(header_tip: u32, cycle: u32, user_data: *mut c_void) { - let Some(tracker) = (unsafe { tracker_from(user_data) }) else { - return; - }; - tracker.last_header_tip.store(header_tip, Ordering::SeqCst); - tracker.last_sync_cycle.store(cycle, Ordering::SeqCst); - let seq = tracker.sequence_counter.fetch_add(1, Ordering::SeqCst); - tracker.sync_complete_seq.store(seq, Ordering::SeqCst); - tracker.sync_complete_count.fetch_add(1, Ordering::SeqCst); - tracing::info!("on_sync_complete: header_tip={}, cycle={}, seq={}", header_tip, cycle, seq); -} - -extern "C" fn on_peer_connected(address: *const c_char, user_data: *mut c_void) { - let Some(tracker) = (unsafe { tracker_from(user_data) }) else { - return; - }; - let addr_str = unsafe { cstr_or_unknown(address) }; - tracing::info!("on_peer_connected: {}", addr_str); - tracker.connected_peers.lock().unwrap_or_else(|e| e.into_inner()).push(addr_str); - tracker.peer_connected_count.fetch_add(1, Ordering::SeqCst); -} - -extern "C" fn on_peer_disconnected(address: *const c_char, user_data: *mut c_void) { - let Some(tracker) = (unsafe { tracker_from(user_data) }) else { - return; - }; - tracker.peer_disconnected_count.fetch_add(1, Ordering::SeqCst); - let addr_str = unsafe { cstr_or_unknown(address) }; - tracing::info!("on_peer_disconnected: {}", addr_str); -} - -extern "C" fn on_peers_updated(connected_count: u32, best_height: u32, user_data: *mut c_void) { - let Some(tracker) = (unsafe { tracker_from(user_data) }) else { - return; - }; - tracker.last_connected_peer_count.store(connected_count, Ordering::SeqCst); - tracker.last_best_height.store(best_height, Ordering::SeqCst); - tracker.peers_updated_count.fetch_add(1, Ordering::SeqCst); - tracing::debug!("on_peers_updated: connected={}, best_height={}", connected_count, best_height); -} - -fn record_balance(tracker: &CallbackTracker, balance: *const FFIBalance) { - if balance.is_null() { - return; - } - let b = unsafe { *balance }; - tracker.last_confirmed.store(b.confirmed, Ordering::SeqCst); - tracker.last_unconfirmed.store(b.unconfirmed, Ordering::SeqCst); -} - -/// Capture the size of the per-account balance diff delivered with a wallet -/// event. Stores both the most recent and the running max so tests can wait -/// on a non-zero observation without racing the "last" snapshot. -fn record_account_balances( - tracker: &CallbackTracker, - account_balances: *const FFIAccountBalance, - count: u32, -) { - tracker.last_changed_account_count.store(count, Ordering::SeqCst); - tracker.max_changed_account_count.fetch_max(count, Ordering::SeqCst); - if account_balances.is_null() || count == 0 { - return; - } - // Borrow check: the array and its `FFIAccountType` entries are owned by - // the caller's dispatch and freed when control returns. We only read. - let slice = unsafe { slice::from_raw_parts(account_balances, count as usize) }; - for entry in slice { - tracing::debug!( - " account_balance: kind={:?}, idx={}, total={}", - entry.account_type.kind, - entry.account_type.index, - entry.balance.total - ); - } -} - -#[allow(clippy::too_many_arguments)] -extern "C" fn on_transaction_detected( - wallet_id: *const c_char, - record: *const FFITransactionRecord, - balance: *const FFIBalance, - account_balances: *const FFIAccountBalance, - account_balances_count: u32, - _addresses_derived: *const dash_spv_ffi::FFIDerivedAddress, - _addresses_derived_count: u32, - user_data: *mut c_void, -) { - let Some(tracker) = (unsafe { tracker_from(user_data) }) else { - return; - }; - record_account_balances(tracker, account_balances, account_balances_count); - let mut account_log = None; - if !record.is_null() { - let r = unsafe { &*record }; - tracker - .received_transactions - .lock() - .unwrap_or_else(|e| e.into_inner()) - .push((r.txid, r.net_amount)); - tracker - .received_account_types - .lock() - .unwrap_or_else(|e| e.into_inner()) - .push(r.account_type.kind); - tracker - .received_account_indices - .lock() - .unwrap_or_else(|e| e.into_inner()) - .push(r.account_type.index); - account_log = Some((r.account_type.kind, r.account_type.index)); - } - // Store the balance before bumping the counter so a test that waits on the - // counter and then reads `last_unconfirmed` is guaranteed to observe the - // balance for the same callback invocation. - record_balance(tracker, balance); - tracker.transaction_received_count.fetch_add(1, Ordering::SeqCst); - let wallet_str = unsafe { cstr_or_unknown(wallet_id) }; - tracing::info!("on_transaction_detected: wallet={}, account={:?}", wallet_str, account_log); -} - -#[allow(clippy::too_many_arguments)] -extern "C" fn on_transaction_instant_locked( - _wallet_id: *const c_char, - _txid: *const [u8; 32], - islock_data: *const u8, - islock_len: usize, - balance: *const FFIBalance, - account_balances: *const FFIAccountBalance, - account_balances_count: u32, - user_data: *mut c_void, -) { - let Some(tracker) = (unsafe { tracker_from(user_data) }) else { - return; - }; - record_account_balances(tracker, account_balances, account_balances_count); - if !islock_data.is_null() && islock_len > 0 { - let bytes = unsafe { slice::from_raw_parts(islock_data, islock_len) }.to_vec(); - *tracker.last_islock_bytes.lock().unwrap_or_else(|e| e.into_inner()) = Some(bytes); - } - record_balance(tracker, balance); - tracker.transaction_instant_send_locked_count.fetch_add(1, Ordering::SeqCst); - tracing::debug!("on_transaction_instant_locked"); -} - -#[allow(clippy::too_many_arguments)] -extern "C" fn on_wallet_block_processed( - wallet_id: *const c_char, - height: u32, - inserted: *const FFITransactionRecord, - inserted_count: u32, - updated: *const FFITransactionRecord, - updated_count: u32, - _matured: *const FFITransactionRecord, - matured_count: u32, - balance: *const FFIBalance, - account_balances: *const FFIAccountBalance, - account_balances_count: u32, - _addresses_derived: *const dash_spv_ffi::FFIDerivedAddress, - _addresses_derived_count: u32, - _cl_height: u32, - _cl_hash: *const [u8; 32], - _cl_signature: *const [u8; 96], - user_data: *mut c_void, -) { - let Some(tracker) = (unsafe { tracker_from(user_data) }) else { - return; - }; - record_account_balances(tracker, account_balances, account_balances_count); - // Append all per-record state before bumping either counter so that a - // test waiting on `block_processed_wallet_count` (the per-callback counter) - // is guaranteed to also observe the matching `block_processed_wallet_record_count` - // and the underlying vectors. Tests should always wait on - // `block_processed_wallet_count` and read the record counter afterwards. - let mut sink = tracker.block_received_transactions.lock().unwrap_or_else(|e| e.into_inner()); - let mut types = tracker.block_account_types.lock().unwrap_or_else(|e| e.into_inner()); - let mut indices = tracker.block_account_indices.lock().unwrap_or_else(|e| e.into_inner()); - let mut bucket = tracker.block_record_inserted.lock().unwrap_or_else(|e| e.into_inner()); - let mut records_added = 0u32; - if !inserted.is_null() && inserted_count > 0 { - let slice = unsafe { slice::from_raw_parts(inserted, inserted_count as usize) }; - for r in slice { - sink.push((r.txid, r.net_amount)); - types.push(r.account_type.kind); - indices.push(r.account_type.index); - bucket.push(true); - records_added += 1; - } - } - if !updated.is_null() && updated_count > 0 { - let slice = unsafe { slice::from_raw_parts(updated, updated_count as usize) }; - for r in slice { - sink.push((r.txid, r.net_amount)); - types.push(r.account_type.kind); - indices.push(r.account_type.index); - bucket.push(false); - records_added += 1; - } - } - drop(sink); - drop(types); - drop(indices); - drop(bucket); - if records_added > 0 { - tracker.block_processed_wallet_record_count.fetch_add(records_added, Ordering::SeqCst); - } - record_balance(tracker, balance); - tracker.block_processed_wallet_count.fetch_add(1, Ordering::SeqCst); - let wallet_str = unsafe { cstr_or_unknown(wallet_id) }; - tracing::info!( - "on_wallet_block_processed: wallet={}, height={}, inserted={}, updated={}, matured={}", - wallet_str, - height, - inserted_count, - updated_count, - matured_count - ); -} - -extern "C" fn on_sync_height_advanced( - wallet_id: *const c_char, - height: u32, - user_data: *mut c_void, -) { - let Some(tracker) = (unsafe { tracker_from(user_data) }) else { - return; - }; - // Store the height before bumping the counter so a test that waits on the - // counter and then reads `last_synced_height` is guaranteed to observe the - // height for the same callback invocation. - tracker.last_synced_height.store(height, Ordering::SeqCst); - tracker.synced_height_updated_count.fetch_add(1, Ordering::SeqCst); - let wallet_str = unsafe { cstr_or_unknown(wallet_id) }; - tracing::info!("on_sync_height_advanced: wallet={}, height={}", wallet_str, height); -} - -/// Create sync callbacks with all event handlers wired to the tracker. -/// -/// The `user_data` pointer borrows the tracker Arc. The caller must ensure the -/// Arc outlives all callback invocations (i.e. stop the client before dropping it). -pub(super) fn create_sync_callbacks(tracker: &Arc) -> FFISyncEventCallbacks { - FFISyncEventCallbacks { - on_sync_start: Some(on_sync_start), - on_block_headers_stored: Some(on_block_headers_stored), - on_block_header_sync_complete: Some(on_block_header_sync_complete), - on_filter_headers_stored: Some(on_filter_headers_stored), - on_filter_headers_sync_complete: Some(on_filter_headers_sync_complete), - on_filters_stored: Some(on_filters_stored), - on_filters_sync_complete: Some(on_filters_sync_complete), - on_blocks_needed: Some(on_blocks_needed), - on_block_processed: Some(on_block_processed), - on_masternode_state_updated: Some(on_masternode_state_updated), - on_chainlock_received: Some(on_chainlock_received), - on_instantlock_received: Some(on_instantlock_received), - on_manager_error: Some(on_manager_error), - on_sync_complete: Some(on_sync_complete), - user_data: Arc::as_ptr(tracker) as *mut c_void, - } -} - -/// Create network event callbacks wired to the tracker. -/// -/// The `user_data` pointer borrows the tracker Arc. The caller must ensure the -/// Arc outlives all callback invocations. -pub(super) fn create_network_callbacks(tracker: &Arc) -> FFINetworkEventCallbacks { - FFINetworkEventCallbacks { - on_peer_connected: Some(on_peer_connected), - on_peer_disconnected: Some(on_peer_disconnected), - on_peers_updated: Some(on_peers_updated), - user_data: Arc::as_ptr(tracker) as *mut c_void, - } -} - -/// Create wallet event callbacks wired to the tracker. -/// -/// The `user_data` pointer borrows the tracker Arc. The caller must ensure the -/// Arc outlives all callback invocations. -pub(super) fn create_wallet_callbacks(tracker: &Arc) -> FFIWalletEventCallbacks { - FFIWalletEventCallbacks { - on_transaction_detected: Some(on_transaction_detected), - on_transaction_instant_locked: Some(on_transaction_instant_locked), - on_block_processed: Some(on_wallet_block_processed), - on_sync_height_advanced: Some(on_sync_height_advanced), - on_chain_lock_processed: None, - user_data: Arc::as_ptr(tracker) as *mut c_void, - } -} diff --git a/dash-spv-ffi/tests/dashd_sync/context.rs b/dash-spv-ffi/tests/dashd_sync/context.rs deleted file mode 100644 index b2bfa6060..000000000 --- a/dash-spv-ffi/tests/dashd_sync/context.rs +++ /dev/null @@ -1,420 +0,0 @@ -//! FFI test context for integration tests. - -use std::collections::HashSet; -use std::ffi::{CStr, CString}; -use std::path::PathBuf; -use std::sync::atomic::Ordering; -use std::sync::Arc; -use std::time::Duration; - -use super::callbacks::{ - create_network_callbacks, create_sync_callbacks, create_wallet_callbacks, CallbackTracker, -}; -use dash_network::ffi::FFINetwork; -use dash_spv::logging::{LogFileConfig, LoggingConfig, LoggingGuard}; -use dash_spv::test_utils::{retain_test_dir, SYNC_TIMEOUT}; -use dash_spv_ffi::client::{ - dash_spv_ffi_client_destroy, dash_spv_ffi_client_get_wallet_manager, dash_spv_ffi_client_new, - dash_spv_ffi_client_run, dash_spv_ffi_client_stop, dash_spv_ffi_wallet_manager_free, - FFIDashSpvClient, -}; -use dash_spv_ffi::config::{ - dash_spv_ffi_config_add_peer, dash_spv_ffi_config_destroy, dash_spv_ffi_config_new, - dash_spv_ffi_config_set_data_dir, dash_spv_ffi_config_set_masternode_sync_enabled, - dash_spv_ffi_config_set_restrict_to_configured_peers, FFIClientConfig, -}; -use dash_spv_ffi::types::FFIWalletManager as FFIWalletManagerOpaque; -use dash_spv_ffi::FFIEventCallbacks; -use dashcore::hashes::Hash; -use dashcore::{Address, Txid}; -use key_wallet_ffi::managed_account::{ - managed_core_account_free, managed_core_account_free_transactions, - managed_core_account_get_transaction_count, managed_core_account_get_transactions, - managed_wallet_get_account, FFIManagedCoreAccount, FFITransactionRecord, -}; -use key_wallet_ffi::managed_wallet::{ - managed_wallet_get_next_bip44_receive_address, managed_wallet_info_free, -}; -use key_wallet_ffi::types::FFIAccountKind; -use key_wallet_ffi::wallet::wallet_free_const; -use key_wallet_ffi::wallet_manager::{ - wallet_manager_add_wallet_from_mnemonic, wallet_manager_get_managed_wallet_info, -}; -use key_wallet_ffi::{ - wallet_manager_free_string, wallet_manager_free_wallet_ids, wallet_manager_get_wallet, - wallet_manager_get_wallet_balance, wallet_manager_get_wallet_ids, FFIError, FFIWalletManager, -}; -use tempfile::TempDir; - -/// State that stays fixed across client restarts (temp dir, logging, config). -struct FixedState { - _temp_dir: TempDir, - _log_guard: LoggingGuard, - storage_dir: PathBuf, - config: *mut FFIClientConfig, -} - -impl Drop for FixedState { - fn drop(&mut self) { - retain_test_dir(&self.storage_dir, "spv"); - unsafe { - dash_spv_ffi_config_destroy(self.config); - } - } -} - -/// Per-session FFI state (client, wallet_manager, tracker). Recreated on restart. -struct SessionState { - client: *mut FFIDashSpvClient, - wallet_manager: *mut FFIWalletManagerOpaque, - tracker: Arc, -} - -impl Drop for SessionState { - fn drop(&mut self) { - unsafe { - dash_spv_ffi_client_stop(self.client); - dash_spv_ffi_wallet_manager_free(self.wallet_manager); - dash_spv_ffi_client_destroy(self.client); - } - } -} - -/// Shared FFI test context. -/// -/// Split into `FixedState` (stays fixed across restarts) and `SessionState` -/// (recreated on restart). -pub(super) struct FFITestContext { - fixed: FixedState, - session: SessionState, -} - -impl FFITestContext { - /// Create a new FFI test context connected to the given peer. - /// - /// # Safety - /// - /// Calls FFI functions that allocate and configure opaque pointers. - pub(super) unsafe fn new(peer_addr: std::net::SocketAddr) -> Self { - let temp_dir = TempDir::new().expect("Failed to create temp dir"); - let storage_dir = temp_dir.path().to_path_buf(); - let log_dir = storage_dir.join("logs"); - - let log_guard = dash_spv::init_logging(LoggingConfig { - level: Some(dash_spv::LevelFilter::DEBUG), - console: std::env::var("DASHD_TEST_LOG").is_ok(), - file: Some(LogFileConfig { - log_dir, - max_files: 1, - }), - thread_local: true, - }) - .expect("Failed to initialize test logging"); - - let config = dash_spv_ffi_config_new(FFINetwork::Regtest); - assert!(!config.is_null(), "Failed to create FFI config"); - - let path = CString::new(temp_dir.path().to_str().unwrap()).unwrap(); - let result = dash_spv_ffi_config_set_data_dir(config, path.as_ptr()); - assert_eq!(result, 0, "Failed to set data dir"); - - let result = dash_spv_ffi_config_set_masternode_sync_enabled(config, false); - assert_eq!(result, 0, "Failed to disable masternode sync"); - - let peer_str = CString::new(peer_addr.to_string()).unwrap(); - let result = dash_spv_ffi_config_add_peer(config, peer_str.as_ptr()); - assert_eq!(result, 0, "Failed to add peer"); - - let result = dash_spv_ffi_config_set_restrict_to_configured_peers(config, true); - assert_eq!(result, 0, "Failed to restrict peers"); - - let tracker = Arc::new(CallbackTracker::default()); - let callbacks = FFIEventCallbacks { - sync: create_sync_callbacks(&tracker), - network: create_network_callbacks(&tracker), - wallet: create_wallet_callbacks(&tracker), - ..FFIEventCallbacks::default() - }; - - let client = dash_spv_ffi_client_new(config, callbacks); - assert!(!client.is_null(), "Failed to create FFI client"); - - let wallet_manager = dash_spv_ffi_client_get_wallet_manager(client); - assert!(!wallet_manager.is_null(), "Failed to get wallet manager"); - - FFITestContext { - fixed: FixedState { - _temp_dir: temp_dir, - _log_guard: log_guard, - storage_dir, - config, - }, - session: SessionState { - client, - wallet_manager, - tracker, - }, - } - } - - /// The callback tracker. - pub(super) fn tracker(&self) -> &Arc { - &self.session.tracker - } - - /// Add a wallet from mnemonic via FFI. - /// - /// # Safety - /// - /// Calls FFI wallet functions through raw pointers held by the context. - pub(super) unsafe fn add_wallet(&self, mnemonic: &str) -> Vec { - let mnemonic_c = CString::new(mnemonic).unwrap(); - let mut error = FFIError::default(); - let wm = self.session.wallet_manager as *mut FFIWalletManager; - - let success = wallet_manager_add_wallet_from_mnemonic(wm, mnemonic_c.as_ptr(), &mut error); - if !success { - let error_msg = if !error.message.is_null() { - CStr::from_ptr(error.message).to_str().unwrap_or("Unknown error") - } else { - "No error message" - }; - panic!("Failed to add wallet from mnemonic: code={:?}, msg={}", error.code, error_msg); - } - - let mut wallet_ids_ptr: *mut u8 = std::ptr::null_mut(); - let mut wallet_count: usize = 0; - let success = - wallet_manager_get_wallet_ids(wm, &mut wallet_ids_ptr, &mut wallet_count, &mut error); - assert!(success && wallet_count > 0, "Failed to get wallet IDs"); - - let wallet_id = std::slice::from_raw_parts(wallet_ids_ptr, 32).to_vec(); - wallet_manager_free_wallet_ids(wallet_ids_ptr, wallet_count); - wallet_id - } - - /// Get wallet balance via FFI. Returns (confirmed, unconfirmed). - /// - /// # Safety - /// - /// Calls FFI wallet functions through raw pointers held by the context. - pub(super) unsafe fn get_wallet_balance(&self, wallet_id: &[u8]) -> (u64, u64) { - let mut confirmed: u64 = 0; - let mut unconfirmed: u64 = 0; - let mut error = FFIError::default(); - let wm = self.session.wallet_manager as *mut FFIWalletManager; - - let success = wallet_manager_get_wallet_balance( - wm, - wallet_id.as_ptr(), - &mut confirmed, - &mut unconfirmed, - &mut error, - ); - assert!(success, "Failed to get wallet balance"); - (confirmed, unconfirmed) - } - - /// Run the client (callbacks were registered at creation time). - /// - /// # Safety - /// - /// Calls FFI client functions through raw pointers held by the context. - pub(super) unsafe fn run(&self) { - self.snapshot_sync_baseline(); - let result = dash_spv_ffi_client_run(self.session.client); - assert_eq!(result, 0, "Failed to run FFI client"); - } - - /// Captures the current `sync_complete_count` as the baseline for the next - /// `wait_for_sync` call. Called automatically by the `run_*` methods before - /// starting the client, and by `wait_for_sync` after each successful wait. - fn snapshot_sync_baseline(&self) { - let current = self.session.tracker.sync_complete_count.load(Ordering::SeqCst); - self.session.tracker.sync_count_baseline.store(current, Ordering::SeqCst); - } - - /// Polls until a new `SyncComplete` event fires with both header and filter - /// tips at or above `expected_height`. - pub(super) fn wait_for_sync(&self, expected_height: u32) { - let baseline = self.session.tracker.sync_count_baseline.load(Ordering::SeqCst); - let start = std::time::Instant::now(); - - loop { - let sync_fired = - self.session.tracker.sync_complete_count.load(Ordering::SeqCst) > baseline; - let current_header = self.session.tracker.last_header_tip.load(Ordering::SeqCst); - let current_filter = self.session.tracker.last_filter_tip.load(Ordering::SeqCst); - - if sync_fired && current_header >= expected_height && current_filter >= expected_height - { - self.snapshot_sync_baseline(); - break; - } - - assert!( - start.elapsed() < SYNC_TIMEOUT, - "Sync did not complete within {:?} (headers={}/{}, filters={}/{})", - SYNC_TIMEOUT, - current_header, - expected_height, - current_filter, - expected_height, - ); - - std::thread::sleep(Duration::from_millis(50)); - } - } - - /// Get a receive address for the given wallet via FFI. - /// - /// # Safety - /// - /// Calls FFI wallet functions through raw pointers held by the context. - pub(super) unsafe fn get_receive_address(&self, wallet_id: &[u8]) -> Address { - let mut error = FFIError::default(); - let wm = self.session.wallet_manager as *mut FFIWalletManager; - - let ffi_wallet = wallet_manager_get_wallet(wm, wallet_id.as_ptr(), &mut error); - assert!(!ffi_wallet.is_null(), "Failed to get FFI wallet"); - - let ffi_info = wallet_manager_get_managed_wallet_info(wm, wallet_id.as_ptr(), &mut error); - assert!(!ffi_info.is_null(), "Failed to get FFI managed wallet info"); - - let addr_ptr = - managed_wallet_get_next_bip44_receive_address(ffi_info, ffi_wallet, 0, &mut error); - assert!(!addr_ptr.is_null(), "Failed to get receive address"); - - let addr_str = CStr::from_ptr(addr_ptr).to_str().unwrap(); - let address = addr_str.parse::>().unwrap().assume_checked(); - wallet_manager_free_string(addr_ptr); - - managed_wallet_info_free(ffi_info); - wallet_free_const(ffi_wallet); - - address - } - - /// Get the BIP44 account 0 for a wallet, call the provided closure, then free the account. - /// - /// # Safety - /// - /// Calls FFI managed account functions through raw pointers. - unsafe fn with_bip44_account( - &self, - wallet_id: &[u8], - f: impl FnOnce(*const FFIManagedCoreAccount) -> T, - ) -> T { - let wm = self.session.wallet_manager as *const FFIWalletManager; - let result = - managed_wallet_get_account(wm, wallet_id.as_ptr(), 0, FFIAccountKind::StandardBIP44); - assert!( - result.error_code == 0 && !result.account.is_null(), - "Failed to get BIP44 account 0" - ); - let value = f(result.account); - managed_core_account_free(result.account); - value - } - - /// Get the number of transactions in the BIP44 account 0 for a wallet. - /// - /// # Safety - /// - /// Calls FFI managed account functions through raw pointers. - pub(super) unsafe fn transaction_count(&self, wallet_id: &[u8]) -> usize { - self.with_bip44_account(wallet_id, |account| { - managed_core_account_get_transaction_count(account) as usize - }) - } - - /// Check whether the BIP44 account 0 contains a specific transaction. - /// - /// # Safety - /// - /// Calls FFI managed account functions through raw pointers. - pub(super) unsafe fn has_transaction(&self, wallet_id: &[u8], txid: &Txid) -> bool { - self.with_bip44_account(wallet_id, |account| { - let mut txs_ptr: *mut FFITransactionRecord = std::ptr::null_mut(); - let mut count: usize = 0; - let ok = managed_core_account_get_transactions(account, &mut txs_ptr, &mut count); - assert!(ok, "Failed to get transactions"); - - let found = if count > 0 && !txs_ptr.is_null() { - let txs = std::slice::from_raw_parts(txs_ptr, count); - let target = txid.to_byte_array(); - txs.iter().any(|t| t.txid == target) - } else { - false - }; - - managed_core_account_free_transactions(txs_ptr, count); - found - }) - } - - /// Collect all transaction IDs from BIP44 account 0 as hex strings (display order). - /// - /// # Safety - /// - /// Calls FFI managed account functions through raw pointers. - pub(super) unsafe fn wallet_txids(&self, wallet_id: &[u8]) -> HashSet { - self.with_bip44_account(wallet_id, |account| { - let mut txs_ptr: *mut FFITransactionRecord = std::ptr::null_mut(); - let mut count: usize = 0; - let ok = managed_core_account_get_transactions(account, &mut txs_ptr, &mut count); - assert!(ok, "Failed to get transactions"); - - let mut txids = HashSet::new(); - if count > 0 && !txs_ptr.is_null() { - let txs = std::slice::from_raw_parts(txs_ptr, count); - for t in txs { - // Reverse bytes for display order (internal is little-endian) - let txid = Txid::from_byte_array(t.txid); - txids.insert(txid.to_string()); - } - } - - managed_core_account_free_transactions(txs_ptr, count); - txids - }) - } - - /// Stop the client and recreate it with the same config and storage. - /// - /// Resets the tracker and returns the new context. The wallet must be - /// re-added after calling this. - /// - /// # Safety - /// - /// Calls FFI client functions through raw pointers held by the context. - pub(super) unsafe fn restart(self) -> Self { - let fixed = self.fixed; - // Drop the session (stops client, frees wallet manager, destroys client) - drop(self.session); - - // Recreate client from same config (same storage dir and peers) - let tracker = Arc::new(CallbackTracker::default()); - let callbacks = FFIEventCallbacks { - sync: create_sync_callbacks(&tracker), - network: create_network_callbacks(&tracker), - wallet: create_wallet_callbacks(&tracker), - ..FFIEventCallbacks::default() - }; - - let client = dash_spv_ffi_client_new(fixed.config, callbacks); - assert!(!client.is_null(), "Failed to recreate FFI client"); - - let wallet_manager = dash_spv_ffi_client_get_wallet_manager(client); - assert!(!wallet_manager.is_null(), "Failed to get wallet manager after restart"); - - FFITestContext { - fixed, - session: SessionState { - client, - wallet_manager, - tracker, - }, - } - } -} diff --git a/dash-spv-ffi/tests/dashd_sync/main.rs b/dash-spv-ffi/tests/dashd_sync/main.rs deleted file mode 100644 index db8424262..000000000 --- a/dash-spv-ffi/tests/dashd_sync/main.rs +++ /dev/null @@ -1,11 +0,0 @@ -//! FFI sync tests using dashd. -//! -//! These tests mirror Rust SPV sync tests but use FFI bindings -//! with the event-based API (dash_spv_ffi_client_run + event callbacks). - -mod callbacks; -mod context; -mod tests_basic; -mod tests_callback; -mod tests_restart; -mod tests_transaction; diff --git a/dash-spv-ffi/tests/dashd_sync/tests_basic.rs b/dash-spv-ffi/tests/dashd_sync/tests_basic.rs deleted file mode 100644 index b742e2c06..000000000 --- a/dash-spv-ffi/tests/dashd_sync/tests_basic.rs +++ /dev/null @@ -1,79 +0,0 @@ -use std::collections::HashSet; -use std::sync::atomic::Ordering; - -use dash_spv::test_utils::{DashdTestContext, TestChain}; - -use super::context::FFITestContext; - -#[test] -fn test_wallet_sync_via_ffi() { - let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap(); - let Some(dashd) = rt.block_on(DashdTestContext::new(TestChain::Full)) else { - eprintln!("Skipping test (dashd context unavailable)"); - return; - }; - - unsafe { - let ctx = FFITestContext::new(dashd.addr); - - let wallet_id = ctx.add_wallet(&dashd.wallet.mnemonic); - tracing::info!("Added wallet, ID: {}", hex::encode(&wallet_id)); - - ctx.run(); - tracing::info!("FFI client running"); - - ctx.wait_for_sync(dashd.initial_height); - - ctx.tracker().assert_no_errors(); - - // Validate sync heights - let final_header = ctx.tracker().last_header_tip.load(Ordering::SeqCst); - let final_filter = ctx.tracker().last_filter_tip.load(Ordering::SeqCst); - - assert_eq!(final_header, dashd.initial_height, "Header height mismatch"); - assert_eq!(final_filter, dashd.initial_height, "Filter header height mismatch"); - assert_eq!( - ctx.tracker().last_sync_cycle.load(Ordering::SeqCst), - 0, - "Initial sync should be cycle 0" - ); - tracing::info!("Heights match: headers={}, filters={}", final_header, final_filter); - - // Validate wallet balance - let (confirmed, _unconfirmed) = ctx.get_wallet_balance(&wallet_id); - let expected_balance = (dashd.wallet.balance * 100_000_000.0).round() as u64; - tracing::info!( - "Balance: confirmed={} satoshis, expected={} satoshis", - confirmed, - expected_balance - ); - - assert_eq!(confirmed, expected_balance, "Balance mismatch"); - - // Validate transaction set against dashd baseline - let spv_txids = ctx.wallet_txids(&wallet_id); - let expected_txids: HashSet = dashd - .wallet - .transactions - .iter() - .filter_map(|tx| tx.get("txid").and_then(|v| v.as_str()).map(String::from)) - .collect(); - - let missing: Vec<_> = expected_txids.difference(&spv_txids).collect(); - let extra: Vec<_> = spv_txids.difference(&expected_txids).collect(); - - assert!( - missing.is_empty(), - "SPV wallet is missing {} transactions: {:?}", - missing.len(), - missing - ); - assert!( - extra.is_empty(), - "SPV wallet has {} unexpected transactions: {:?}", - extra.len(), - extra - ); - tracing::info!("Transaction set validated: {} transactions match", spv_txids.len()); - } -} diff --git a/dash-spv-ffi/tests/dashd_sync/tests_callback.rs b/dash-spv-ffi/tests/dashd_sync/tests_callback.rs deleted file mode 100644 index 4acd4b53c..000000000 --- a/dash-spv-ffi/tests/dashd_sync/tests_callback.rs +++ /dev/null @@ -1,518 +0,0 @@ -use std::sync::atomic::Ordering; -use std::time::Duration; - -use dash_spv::test_utils::{DashdTestContext, TestChain}; -use dash_spv_ffi::FFIAccountKind; -use dashcore::hashes::Hash; -use dashcore::Amount; - -use super::context::FFITestContext; - -#[test] -fn test_all_callbacks_during_sync() { - let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap(); - // TODO: This should doesn't need a full chain but its currently flaky with the minimal chain - // will be fixed once the flakiness is resolved. - let Some(dashd) = rt.block_on(DashdTestContext::new(TestChain::Full)) else { - return; - }; - - unsafe { - let ctx = FFITestContext::new(dashd.addr); - let tracker = ctx.tracker().clone(); - - ctx.add_wallet(&dashd.wallet.mnemonic); - ctx.run(); - tracing::info!("FFI client running with all callback types"); - - ctx.wait_for_sync(dashd.initial_height); - - // Validate sync event callbacks - let sync_start = tracker.sync_start_count.load(Ordering::SeqCst); - let headers_stored = tracker.block_headers_stored_count.load(Ordering::SeqCst); - let header_complete = tracker.block_header_sync_complete_count.load(Ordering::SeqCst); - let filter_headers_stored = tracker.filter_headers_stored_count.load(Ordering::SeqCst); - let filter_header_complete = - tracker.filter_headers_sync_complete_count.load(Ordering::SeqCst); - let filters_stored = tracker.filters_stored_count.load(Ordering::SeqCst); - let filters_sync_complete = tracker.filters_sync_complete_count.load(Ordering::SeqCst); - let blocks_needed = tracker.blocks_needed_count.load(Ordering::SeqCst); - let block_processed = tracker.block_processed_count.load(Ordering::SeqCst); - let sync_complete = tracker.sync_complete_count.load(Ordering::SeqCst); - - tracing::info!("Callback Summary"); - tracing::info!( - "Sync: start={}, headers_stored={}, header_complete={}, filter_headers={}, \ - filter_complete={}, filters_stored={}, filters_sync={}, blocks_needed={}, \ - block_processed={}, sync_complete={}", - sync_start, - headers_stored, - header_complete, - filter_headers_stored, - filter_header_complete, - filters_stored, - filters_sync_complete, - blocks_needed, - block_processed, - sync_complete - ); - - assert!(sync_start > 0, "on_sync_start should have been called"); - assert!(headers_stored > 0, "on_block_headers_stored should have been called"); - assert_eq!(header_complete, 1, "on_block_header_sync_complete should be called once"); - assert!(filter_headers_stored > 0, "on_filter_headers_stored should have been called"); - assert_eq!( - filter_header_complete, 1, - "on_filter_headers_sync_complete should be called once" - ); - assert!(filters_stored > 0, "on_filters_stored should have been called"); - assert!(filters_sync_complete > 0, "on_filters_sync_complete should have been called"); - assert!(blocks_needed > 0, "on_blocks_needed should have been called"); - assert!(block_processed > 0, "on_block_processed should have been called"); - assert_eq!(sync_complete, 1, "on_sync_complete should be called once"); - - // Validate network event callbacks - let peer_connected = tracker.peer_connected_count.load(Ordering::SeqCst); - let peers_updated = tracker.peers_updated_count.load(Ordering::SeqCst); - let last_peer_count = tracker.last_connected_peer_count.load(Ordering::SeqCst); - let last_best_height = tracker.last_best_height.load(Ordering::SeqCst); - - tracing::info!( - "Network: peer_connected={}, peers_updated={}, last_peer_count={}, best_height={}", - peer_connected, - peers_updated, - last_peer_count, - last_best_height - ); - - assert!(peer_connected > 0, "on_peer_connected should have been called"); - assert!(peers_updated > 0, "on_peers_updated should have been called"); - assert!(last_peer_count > 0, "at least one peer should be tracked"); - assert!(last_best_height > 0, "best height from peers should be positive"); - - let connected_peers = tracker.connected_peers.lock().unwrap(); - assert!(!connected_peers.is_empty(), "connected_peers should contain at least one entry"); - let dashd_addr = dashd.addr.to_string(); - assert!( - connected_peers.iter().any(|p| p.contains(&dashd_addr)), - "connected_peers should contain the dashd address {}: {:?}", - dashd_addr, - *connected_peers - ); - drop(connected_peers); - - // Wait for wallet callbacks (they travel on a separate channel from sync events). - // Wait on `block_processed_wallet_count` because it is bumped last in the - // callback, after all per-record state has been written. Reading the - // record counter afterwards is therefore guaranteed to see the matching - // increment. - tracker.wait_for_callback(&tracker.block_processed_wallet_count, 0, "block_processed"); - - // Validate wallet event callbacks (test wallet has transactions) - let block_records = tracker.block_processed_wallet_record_count.load(Ordering::SeqCst); - let block_changes = tracker.block_processed_wallet_count.load(Ordering::SeqCst); - let received = tracker.transaction_received_count.load(Ordering::SeqCst); - let instant_send_locked = - tracker.transaction_instant_send_locked_count.load(Ordering::SeqCst); - - tracing::info!( - "Wallet: received={}, instant_send_locked={}, block_changes={}, block_records={}", - received, - instant_send_locked, - block_changes, - block_records - ); - - assert!( - block_records > 0, - "on_block_processed should deliver records for a wallet with transactions" - ); - assert!( - block_changes > 0, - "on_block_processed should fire for blocks containing wallet records" - ); - assert_eq!( - received, 0, - "on_transaction_detected must not fire during historical block sync" - ); - assert_eq!( - instant_send_locked, 0, - "on_transaction_instant_send_locked should not fire during initial sync" - ); - - // Validate SyncedHeightUpdated callback (atomicity boundary for persistence flush). - // Wait explicitly for the callback because it travels on the same wallet - // broadcast channel as `BlockProcessed` but is dispatched separately, - // so observing block-processed records does not guarantee it has fired yet. - tracker.wait_for_callback(&tracker.synced_height_updated_count, 0, "synced_height_updated"); - let synced_height_fired = tracker.synced_height_updated_count.load(Ordering::SeqCst); - let last_synced_height = tracker.last_synced_height.load(Ordering::SeqCst); - assert!( - synced_height_fired > 0, - "on_synced_height_updated should fire at least once during sync" - ); - assert!( - last_synced_height >= dashd.initial_height, - "last_synced_height ({}) should be at least initial_height ({}) after sync", - last_synced_height, - dashd.initial_height - ); - - // Validate sync cycle (initial sync is cycle 0) - let last_sync_cycle = tracker.last_sync_cycle.load(Ordering::SeqCst); - assert_eq!(last_sync_cycle, 0, "Initial sync should be cycle 0"); - - // Validate callback lifecycle ordering - let sync_start_seq = tracker.sync_start_seq.load(Ordering::SeqCst); - let header_complete_seq = tracker.header_complete_seq.load(Ordering::SeqCst); - let filter_header_complete_seq = tracker.filter_header_complete_seq.load(Ordering::SeqCst); - let filters_sync_complete_seq = tracker.filters_sync_complete_seq.load(Ordering::SeqCst); - let sync_complete_seq = tracker.sync_complete_seq.load(Ordering::SeqCst); - - tracing::info!( - "Sequence ordering: sync_start={}, header_complete={}, filter_header_complete={}, \ - filters_sync_complete={}, sync_complete={}", - sync_start_seq, - header_complete_seq, - filter_header_complete_seq, - filters_sync_complete_seq, - sync_complete_seq - ); - - assert!( - sync_start_seq < header_complete_seq, - "sync_start ({}) should precede header_complete ({})", - sync_start_seq, - header_complete_seq - ); - assert!( - header_complete_seq < filter_header_complete_seq, - "header_complete ({}) should precede filter_header_complete ({})", - header_complete_seq, - filter_header_complete_seq - ); - assert!( - filter_header_complete_seq < filters_sync_complete_seq, - "filter_header_complete ({}) should precede filters_sync_complete ({})", - filter_header_complete_seq, - filters_sync_complete_seq - ); - assert!( - filters_sync_complete_seq < sync_complete_seq, - "filters_sync_complete ({}) should precede sync_complete ({})", - filters_sync_complete_seq, - sync_complete_seq - ); - - // Validate filter header ranges - let filter_ranges = tracker.filter_header_ranges.lock().unwrap(); - assert!(!filter_ranges.is_empty(), "filter header ranges should be recorded"); - for &(start, end, tip) in filter_ranges.iter() { - assert!( - start <= end, - "filter header range start ({}) should be <= end ({})", - start, - end - ); - assert!(end <= tip, "filter header range end ({}) should be <= tip ({})", end, tip); - } - drop(filter_ranges); - - // Validate block processed heights - let block_heights = tracker.processed_block_heights.lock().unwrap(); - assert!(!block_heights.is_empty(), "block processed heights should be recorded"); - for &h in block_heights.iter() { - assert!( - h >= 1 && h <= dashd.initial_height, - "block processed height {} should be within [1, {}]", - h, - dashd.initial_height - ); - } - drop(block_heights); - - // Validate final state - let final_header = tracker.last_header_tip.load(Ordering::SeqCst); - let final_filter = tracker.last_filter_tip.load(Ordering::SeqCst); - assert_eq!(final_header, dashd.initial_height, "Final header tip mismatch"); - assert_eq!(final_filter, dashd.initial_height, "Final filter tip mismatch"); - - // Validate best height matches initial height - assert_eq!( - last_best_height, dashd.initial_height, - "best height from peers should match initial height" - ); - - // Validate transaction data from initial sync. Historical sync only - // touches the block-processed callback (off-chain callback must - // remain silent during initial sync), so assert against that bucket - // explicitly. - let block_received = tracker.block_received_transactions.lock().unwrap(); - assert!(!block_received.is_empty(), "should have received block records during sync"); - assert!( - block_received.iter().any(|&(_, amount)| amount != 0), - "at least one block-record net_amount should be non-zero" - ); - drop(block_received); - - // Every record observed during initial sync is a fresh insertion - // (no prior mempool sighting), so each must arrive in the `inserted` - // bucket of `BlockProcessed`. - let bucket = tracker.block_record_inserted.lock().unwrap(); - assert!(!bucket.is_empty(), "block records should be captured"); - assert!( - bucket.iter().all(|inserted| *inserted), - "every block record during historical sync should arrive via `inserted`, got: {:?}", - *bucket - ); - drop(bucket); - - // Validate the BIP-44 account discriminant + index reach the FFI - // boundary intact: every change observed during historical sync - // belongs to the default BIP-44 account (index 0) of the test wallet. - let account_types = tracker.block_account_types.lock().unwrap(); - let account_indices = tracker.block_account_indices.lock().unwrap(); - assert!(!account_types.is_empty(), "block account types should be captured"); - assert_eq!( - account_types.len(), - account_indices.len(), - "block account types and indices must be paired 1:1" - ); - assert!( - account_types.iter().all(|t| *t == FFIAccountKind::StandardBIP44), - "every block change should carry FFIAccountKind::StandardBIP44, got: {:?}", - *account_types - ); - assert!( - account_indices.iter().all(|i| *i == 0), - "every BIP-44 change should carry account_index = 0, got: {:?}", - *account_indices - ); - drop(account_indices); - drop(account_types); - - // Masternodes are disabled in test config, so these should not fire - let masternode_updated = tracker.masternode_state_updated_count.load(Ordering::SeqCst); - assert_eq!( - masternode_updated, 0, - "masternode callbacks should not fire with masternodes disabled" - ); - - tracker.assert_no_errors(); - } -} - -/// Verify wallet and network callbacks fire correctly after initial sync completes. -/// -/// After initial sync, sends DASH to the wallet and mines a block. Verifies that -/// on_transaction_detected and on_balance_updated callbacks fire. Then disconnects -/// dashd peers and verifies on_peer_disconnected fires, followed by on_peer_connected -/// after automatic reconnection. -#[test] -fn test_callbacks_post_sync_transactions_and_disconnect() { - let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap(); - let Some(dashd) = rt.block_on(DashdTestContext::new(TestChain::Minimal)) else { - return; - }; - if !dashd.supports_mining { - eprintln!("Skipping test (dashd RPC miner not available)"); - return; - } - - unsafe { - let ctx = FFITestContext::new(dashd.addr); - let tracker = ctx.tracker().clone(); - - let wallet_id = ctx.add_wallet(&dashd.wallet.mnemonic); - ctx.run(); - - // Wait for initial sync - ctx.wait_for_sync(dashd.initial_height); - tracing::info!("Initial sync complete"); - - // Record callback counts before post-sync operations - let received_before = tracker.transaction_received_count.load(Ordering::SeqCst); - let block_changes_before = tracker.block_processed_wallet_count.load(Ordering::SeqCst); - let block_records_before = - tracker.block_processed_wallet_record_count.load(Ordering::SeqCst); - - // Send DASH to the wallet. Wait for the off-chain callback before - // mining so the SPV node observes the transaction in the mempool. - // If we mine immediately, the block path can deliver the transaction - // first and the off-chain callback would never fire. - let receive_address = ctx.get_receive_address(&wallet_id); - let send_amount = Amount::from_sat(100_000_000); - let txid = dashd.node.send_to_address(&receive_address, send_amount); - tracing::info!("Sent {} to wallet, txid: {}", send_amount, txid); - - tracker.wait_for_callback( - &tracker.transaction_received_count, - received_before, - "transaction_received", - ); - - // The off-chain callback updates `last_unconfirmed` with the - // post-event balance. Snapshot it now, before mining. After - // confirmation the block-processed callback overwrites the same - // field back toward zero, so this is the only window in which the - // unconfirmed-balance update is observable. - let unconfirmed_after_mempool = tracker.last_unconfirmed.load(Ordering::SeqCst); - assert!( - unconfirmed_after_mempool > 0, - "balance.unconfirmed should be positive after mempool receipt, got {}", - unconfirmed_after_mempool - ); - - let miner_address = dashd.node.get_new_address_from_wallet("default"); - dashd.node.generate_blocks(1, &miner_address); - - // Wait for incremental sync to complete - ctx.wait_for_sync(dashd.initial_height + 1); - - // Wait for the block-processed callback. The per-callback counter is - // bumped last in the callback, so observing it incremented guarantees - // the per-record vectors and counters have already been updated. - tracker.wait_for_callback( - &tracker.block_processed_wallet_count, - block_changes_before, - "block_processed", - ); - - // Verify on_transaction_detected fired for the new transaction - let received_after = tracker.transaction_received_count.load(Ordering::SeqCst); - assert!( - received_after > received_before, - "on_transaction_detected should fire for post-sync transaction: {} -> {}", - received_before, - received_after - ); - tracing::info!( - "Off-chain transaction callback verified: {} -> {}", - received_before, - received_after - ); - - // Verify the sent txid appears in the off-chain callback data with a - // non-zero net_amount. Asserting against the off-chain bucket (rather - // than the union of off-chain + block records) ensures the off-chain - // callback specifically delivered the txid — a broken off-chain - // callback that pushed the wrong txid wouldn't be masked by the - // block path. The SPV wallet and dashd share the same mnemonic so - // the transaction is an internal transfer (wallet owns both inputs - // and outputs); net_amount therefore equals approximately -fee, not - // the nominal send amount. - let sent_txid_bytes = *txid.as_byte_array(); - let received_txs = tracker.received_transactions.lock().unwrap(); - let sent_entry = received_txs.iter().find(|&&(id, _)| id == sent_txid_bytes); - assert!(sent_entry.is_some(), "sent txid should appear in transaction callback data"); - let &(_, net_amount) = sent_entry.unwrap(); - // Internal transfer: net_amount = received - sent = (send_amount + change) - input = -fee. - // The fee must be negative, non-zero, and small (< 0.001 DASH). - assert!( - net_amount < 0 && net_amount > -100_000, - "internal transfer net_amount should equal -fee (small negative), got: {}", - net_amount - ); - drop(received_txs); - - // Verify the off-chain callback delivered the BIP-44 account - // discriminant + index 0 (default test account). - let received_types = tracker.received_account_types.lock().unwrap(); - let received_indices = tracker.received_account_indices.lock().unwrap(); - assert!( - received_types.iter().all(|t| *t == FFIAccountKind::StandardBIP44), - "off-chain callback should deliver FFIAccountKind::StandardBIP44, got: {:?}", - *received_types - ); - assert!( - received_indices.iter().all(|i| *i == 0), - "off-chain BIP-44 callback should deliver account_index = 0, got: {:?}", - *received_indices - ); - drop(received_indices); - drop(received_types); - - // The post-sync block confirms a transaction that was already known - // from the mempool, so the corresponding `BlockProcessed` change must - // arrive in the `updated` bucket rather than `inserted`. Slice by - // the pre-captured index so only post-sync entries are checked, - // avoiding masking by any `updated` entry that might appear during - // initial sync. - let block_bucket = tracker.block_record_inserted.lock().unwrap(); - assert!( - block_bucket.len() >= block_records_before as usize, - "block_record_inserted length ({}) < block_records_before ({}): counter/vector mismatch", - block_bucket.len(), - block_records_before - ); - let new_bucket = &block_bucket[block_records_before as usize..]; - assert!( - new_bucket.iter().any(|inserted| !inserted), - "post-sync block confirming a known mempool tx should arrive in the \ - `updated` bucket, got: {:?}", - new_bucket - ); - drop(block_bucket); - - let block_records_after = - tracker.block_processed_wallet_record_count.load(Ordering::SeqCst); - tracing::info!( - "Block-processed record callback verified: {} -> {}", - block_records_before, - block_records_after - ); - - // Verify balance data from the most recent wallet event reflects a positive - // confirmed balance. - let last_confirmed = tracker.last_confirmed.load(Ordering::SeqCst); - assert!(last_confirmed > 0, "last_confirmed should be positive after receiving funds"); - tracing::info!("Balance data verified: last_confirmed={}", last_confirmed); - - // Record connect count before disconnect - let connect_before = tracker.peer_connected_count.load(Ordering::SeqCst); - - // Disconnect peers via dashd and verify on_peer_disconnected fires - let disconnect_before = tracker.peer_disconnected_count.load(Ordering::SeqCst); - dashd.node.disconnect_all_peers(); - - // Wait for disconnect callback - let deadline = std::time::Instant::now() + Duration::from_secs(15); - while tracker.peer_disconnected_count.load(Ordering::SeqCst) <= disconnect_before - && std::time::Instant::now() < deadline - { - std::thread::sleep(Duration::from_millis(200)); - } - - let disconnect_after = tracker.peer_disconnected_count.load(Ordering::SeqCst); - assert!( - disconnect_after > disconnect_before, - "on_peer_disconnected should fire after disconnect: {} -> {}", - disconnect_before, - disconnect_after - ); - tracing::info!( - "Disconnect callback verified: {} -> {}", - disconnect_before, - disconnect_after - ); - - // Wait for automatic reconnection (on_peer_connected should fire again) - let deadline = std::time::Instant::now() + Duration::from_secs(30); - while tracker.peer_connected_count.load(Ordering::SeqCst) <= connect_before - && std::time::Instant::now() < deadline - { - std::thread::sleep(Duration::from_millis(200)); - } - - let connect_after = tracker.peer_connected_count.load(Ordering::SeqCst); - assert!( - connect_after > connect_before, - "on_peer_connected should fire after reconnection: {} -> {}", - connect_before, - connect_after - ); - tracing::info!("Reconnect callback verified: {} -> {}", connect_before, connect_after); - - tracker.assert_no_errors(); - } -} diff --git a/dash-spv-ffi/tests/dashd_sync/tests_restart.rs b/dash-spv-ffi/tests/dashd_sync/tests_restart.rs deleted file mode 100644 index d882c0feb..000000000 --- a/dash-spv-ffi/tests/dashd_sync/tests_restart.rs +++ /dev/null @@ -1,61 +0,0 @@ -use std::sync::atomic::Ordering; - -use dash_spv::test_utils::{DashdTestContext, TestChain}; - -use super::context::FFITestContext; - -/// Verify FFI client restart preserves consistent state across stop/recreate cycles. -#[test] -fn test_ffi_restart_consistency() { - let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap(); - let Some(dashd) = rt.block_on(DashdTestContext::new(TestChain::Full)) else { - eprintln!("Skipping test (dashd context unavailable)"); - return; - }; - - unsafe { - // First sync - tracing::info!("First FFI sync"); - let ctx = FFITestContext::new(dashd.addr); - let wallet_id = ctx.add_wallet(&dashd.wallet.mnemonic); - - ctx.run(); - ctx.wait_for_sync(dashd.initial_height); - - let (first_balance, _) = ctx.get_wallet_balance(&wallet_id); - let first_header = ctx.tracker().last_header_tip.load(Ordering::SeqCst); - - ctx.tracker().assert_no_errors(); - assert_eq!( - ctx.tracker().last_sync_cycle.load(Ordering::SeqCst), - 0, - "First sync should be cycle 0" - ); - - tracing::info!("First sync: balance={}, header_tip={}", first_balance, first_header); - - // Restart with same storage - tracing::info!("Restarting FFI client"); - let ctx = ctx.restart(); - let wallet_id = ctx.add_wallet(&dashd.wallet.mnemonic); - - ctx.run(); - ctx.wait_for_sync(dashd.initial_height); - - let (second_balance, _) = ctx.get_wallet_balance(&wallet_id); - let second_header = ctx.tracker().last_header_tip.load(Ordering::SeqCst); - - ctx.tracker().assert_no_errors(); - assert_eq!( - ctx.tracker().last_sync_cycle.load(Ordering::SeqCst), - 0, - "Restart sync should be cycle 0 (fresh client)" - ); - - tracing::info!("Second sync: balance={}, header_tip={}", second_balance, second_header); - - // Verify state is identical - assert_eq!(first_balance, second_balance, "Balance mismatch after restart"); - assert_eq!(first_header, second_header, "Header tip mismatch after restart"); - } -} diff --git a/dash-spv-ffi/tests/dashd_sync/tests_transaction.rs b/dash-spv-ffi/tests/dashd_sync/tests_transaction.rs deleted file mode 100644 index 263048205..000000000 --- a/dash-spv-ffi/tests/dashd_sync/tests_transaction.rs +++ /dev/null @@ -1,351 +0,0 @@ -use std::sync::atomic::Ordering; - -use dash_spv::test_utils::{DashdTestContext, TestChain}; -use dash_spv_ffi::FFIAccountKind; -use dashcore::hashes::Hash; -use dashcore::Amount; - -use super::context::FFITestContext; - -/// Verify incremental sync works via FFI by generating blocks after initial sync. -/// -/// Generates a single block (with a wallet transaction) and a batch of blocks, -/// verifying deterministic cycle counting and wallet balance updates. -#[test] -fn test_ffi_sync_then_generate_blocks() { - let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap(); - let Some(dashd) = rt.block_on(DashdTestContext::new(TestChain::Minimal)) else { - eprintln!("Skipping test (dashd context unavailable)"); - return; - }; - if !dashd.supports_mining { - eprintln!("Skipping test (dashd RPC miner not available)"); - return; - } - - unsafe { - let ctx = FFITestContext::new(dashd.addr); - let wallet_id = ctx.add_wallet(&dashd.wallet.mnemonic); - - ctx.run(); - ctx.wait_for_sync(dashd.initial_height); - - assert_eq!( - ctx.tracker().last_sync_cycle.load(Ordering::SeqCst), - 0, - "Initial sync should be cycle 0" - ); - - let (initial_balance, _) = ctx.get_wallet_balance(&wallet_id); - let initial_tx_count = ctx.transaction_count(&wallet_id); - tracing::info!( - "Initial state: balance={} satoshis, tx_count={}", - initial_balance, - initial_tx_count - ); - - let miner_address = dashd.node.get_new_address_from_wallet("default"); - - // Generate a block containing a wallet transaction and wait for sync. - let cycle_before = ctx.tracker().last_sync_cycle.load(Ordering::SeqCst); - let block_changes_before = - ctx.tracker().block_processed_wallet_count.load(Ordering::SeqCst); - let block_records_before = - ctx.tracker().block_processed_wallet_record_count.load(Ordering::SeqCst); - let receive_address = ctx.get_receive_address(&wallet_id); - let send_amount = Amount::from_sat(100_000_000); - let txid = dashd.node.send_to_address(&receive_address, send_amount); - tracing::info!("Sent {} to FFI wallet, txid: {}", send_amount, txid); - - dashd.node.generate_blocks(1, &miner_address); - let height_after_one = dashd.initial_height + 1; - ctx.wait_for_sync(height_after_one); - - let cycle_after_first = ctx.tracker().last_sync_cycle.load(Ordering::SeqCst); - assert_eq!( - cycle_after_first, - cycle_before + 1, - "Single block should produce exactly one sync cycle: before={}, after={}", - cycle_before, - cycle_after_first - ); - - // Wait for wallet callback (travels on a separate channel from sync events). - // The per-callback counter is bumped last in the callback, so observing - // it incremented guarantees the per-record vectors are also updated. - ctx.tracker().wait_for_callback( - &ctx.tracker().block_processed_wallet_count, - block_changes_before, - "block_processed", - ); - - // Verify the transaction was received via the block-processed callback - let received_txs = ctx.tracker().block_received_transactions.lock().unwrap(); - let txid_bytes = *txid.as_byte_array(); - assert!( - received_txs.iter().any(|&(txid, _)| txid == txid_bytes), - "Block-processed callback should have received txid {}", - txid - ); - drop(received_txs); - - // Verify per-record bucketing was captured for the post-sync block. - let bucket = ctx.tracker().block_record_inserted.lock().unwrap(); - assert!( - bucket.len() >= block_records_before as usize, - "block_record_inserted length ({}) < block_records_before ({}): counter/vector mismatch", - bucket.len(), - block_records_before - ); - let new_bucket = &bucket[block_records_before as usize..]; - assert!( - !new_bucket.is_empty(), - "block_record_inserted should be populated for the post-sync block" - ); - drop(bucket); - - // Verify the BIP-44 account discriminant + index were delivered for - // the post-sync block records. - let types = ctx.tracker().block_account_types.lock().unwrap(); - let indices = ctx.tracker().block_account_indices.lock().unwrap(); - assert!( - types.len() >= block_records_before as usize, - "block_account_types length ({}) < block_records_before ({}): counter/vector mismatch", - types.len(), - block_records_before - ); - assert_eq!( - types.len(), - indices.len(), - "block account types and indices must be paired 1:1" - ); - let new_types = &types[block_records_before as usize..]; - let new_indices = &indices[block_records_before as usize..]; - assert!( - new_types.iter().all(|t| *t == FFIAccountKind::StandardBIP44), - "post-sync block changes should carry FFIAccountKind::StandardBIP44, got: {:?}", - new_types - ); - assert!( - new_indices.iter().all(|i| *i == 0), - "post-sync BIP-44 changes should carry account_index = 0, got: {:?}", - new_indices - ); - drop(indices); - drop(types); - - // Verify via wallet query as well - assert!( - ctx.has_transaction(&wallet_id, &txid), - "Wallet should contain transaction {}", - txid - ); - - // Verify balance changed from the transaction - let (balance_after_tx, _) = ctx.get_wallet_balance(&wallet_id); - assert!( - balance_after_tx < initial_balance, - "Balance should decrease by fees: initial={}, after_tx={}", - initial_balance, - balance_after_tx - ); - let fees = initial_balance - balance_after_tx; - assert!(fees < 1_000_000, "Fees ({}) should be reasonable", fees); - - // Generate multiple blocks at once and verify the cycle advances - let cycle_before_batch = ctx.tracker().last_sync_cycle.load(Ordering::SeqCst); - dashd.node.generate_blocks(5, &miner_address); - let expected_final_height = dashd.initial_height + 6; - ctx.wait_for_sync(expected_final_height); - - let cycle_after_batch = ctx.tracker().last_sync_cycle.load(Ordering::SeqCst); - assert!( - cycle_after_batch > cycle_before_batch, - "Sync cycle should advance after batch: before={}, after={}", - cycle_before_batch, - cycle_after_batch - ); - - let final_tx_count = ctx.transaction_count(&wallet_id); - assert!( - final_tx_count > initial_tx_count, - "Transaction count should have increased: {} -> {}", - initial_tx_count, - final_tx_count - ); - - ctx.tracker().assert_no_errors(); - } -} - -/// Verify that multiple transactions sent in quick succession and mined in a single block -/// are all detected by the SPV client via FFI. -#[test] -fn test_ffi_multiple_transactions_in_single_block() { - let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap(); - let Some(dashd) = rt.block_on(DashdTestContext::new(TestChain::Minimal)) else { - eprintln!("Skipping test (dashd context unavailable)"); - return; - }; - if !dashd.supports_mining { - eprintln!("Skipping test (dashd RPC miner not available)"); - return; - } - - unsafe { - let ctx = FFITestContext::new(dashd.addr); - let wallet_id = ctx.add_wallet(&dashd.wallet.mnemonic); - - ctx.run(); - ctx.wait_for_sync(dashd.initial_height); - - let baseline_tx_count = ctx.transaction_count(&wallet_id); - let (baseline_balance, _) = ctx.get_wallet_balance(&wallet_id); - tracing::info!("Baseline: tx_count={}, balance={}", baseline_tx_count, baseline_balance); - - // Send 3 transactions of different amounts to the SPV wallet - let receive_address = ctx.get_receive_address(&wallet_id); - let amounts = [ - Amount::from_sat(50_000_000), - Amount::from_sat(75_000_000), - Amount::from_sat(120_000_000), - ]; - let mut txids = Vec::new(); - for amount in &amounts { - let txid = dashd.node.send_to_address(&receive_address, *amount); - tracing::info!("Sent {} to FFI wallet, txid: {}", amount, txid); - txids.push(txid); - } - - // Mine a single block to confirm all 3 - let miner_address = dashd.node.get_new_address_from_wallet("default"); - dashd.node.generate_blocks(1, &miner_address); - let expected_height = dashd.initial_height + 1; - ctx.wait_for_sync(expected_height); - - let final_tx_count = ctx.transaction_count(&wallet_id); - let (final_balance, _) = ctx.get_wallet_balance(&wallet_id); - - assert_eq!( - final_tx_count, - baseline_tx_count + 3, - "Expected 3 new transactions, got {}", - final_tx_count - baseline_tx_count - ); - - // Since dashd and SPV share the same wallet, sends are internal transfers. - // The only balance change is the transaction fees deducted by dashd. - let fees_paid = baseline_balance - final_balance; - assert!( - final_balance < baseline_balance, - "Balance should decrease by fees for internal transfers" - ); - assert!(fees_paid < 1_000_000, "Total fees ({}) should be reasonable", fees_paid); - - for txid in &txids { - assert!( - ctx.has_transaction(&wallet_id, txid), - "Wallet should contain transaction {}", - txid - ); - } - - ctx.tracker().assert_no_errors(); - tracing::info!( - "All 3 transactions found: tx_count {} -> {}, balance {} -> {} (fees={})", - baseline_tx_count, - final_tx_count, - baseline_balance, - final_balance, - fees_paid - ); - } -} - -/// Verify that transactions sent one per block over several blocks are each detected -/// incrementally by the SPV client via FFI. -#[test] -fn test_ffi_multiple_transactions_across_blocks() { - let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap(); - let Some(dashd) = rt.block_on(DashdTestContext::new(TestChain::Minimal)) else { - eprintln!("Skipping test (dashd context unavailable)"); - return; - }; - if !dashd.supports_mining { - eprintln!("Skipping test (dashd RPC miner not available)"); - return; - } - - unsafe { - let ctx = FFITestContext::new(dashd.addr); - let wallet_id = ctx.add_wallet(&dashd.wallet.mnemonic); - - ctx.run(); - ctx.wait_for_sync(dashd.initial_height); - - let baseline_tx_count = ctx.transaction_count(&wallet_id); - let (baseline_balance, _) = ctx.get_wallet_balance(&wallet_id); - tracing::info!("Baseline: tx_count={}, balance={}", baseline_tx_count, baseline_balance); - - // Send 1 tx per block, 3 iterations - let amounts = [ - Amount::from_sat(30_000_000), - Amount::from_sat(60_000_000), - Amount::from_sat(90_000_000), - ]; - let miner_address = dashd.node.get_new_address_from_wallet("default"); - let mut current_height = dashd.initial_height; - let mut txids = Vec::new(); - - for (i, amount) in amounts.iter().enumerate() { - let receive_address = ctx.get_receive_address(&wallet_id); - let txid = dashd.node.send_to_address(&receive_address, *amount); - tracing::info!("Iteration {}: sent {} to FFI wallet, txid: {}", i, amount, txid); - txids.push(txid); - - dashd.node.generate_blocks(1, &miner_address); - current_height += 1; - ctx.wait_for_sync(current_height); - - let tx_count = ctx.transaction_count(&wallet_id); - assert_eq!( - tx_count, - baseline_tx_count + i + 1, - "After iteration {}, expected {} transactions, got {}", - i, - baseline_tx_count + i + 1, - tx_count - ); - tracing::info!("Iteration {}: tx_count={}", i, tx_count); - } - - // Final verification - let (final_balance, _) = ctx.get_wallet_balance(&wallet_id); - - // Internal transfers: only fees are deducted - let fees_paid = baseline_balance - final_balance; - assert!( - final_balance < baseline_balance, - "Balance should decrease by fees for internal transfers" - ); - assert!(fees_paid < 1_000_000, "Total fees ({}) should be reasonable", fees_paid); - - for txid in &txids { - assert!( - ctx.has_transaction(&wallet_id, txid), - "Wallet should contain transaction {}", - txid - ); - } - - ctx.tracker().assert_no_errors(); - tracing::info!( - "All iterations complete: tx_count {} -> {}, balance {} -> {} (fees={})", - baseline_tx_count, - baseline_tx_count + amounts.len(), - baseline_balance, - final_balance, - fees_paid - ); - } -} diff --git a/dash-spv-ffi/tests/test_client.rs b/dash-spv-ffi/tests/test_client.rs deleted file mode 100644 index b2eaeb85a..000000000 --- a/dash-spv-ffi/tests/test_client.rs +++ /dev/null @@ -1,78 +0,0 @@ -#[cfg(test)] -mod tests { - use dash_network::ffi::FFINetwork; - use dash_spv_ffi::*; - use serial_test::serial; - use std::ffi::CString; - use tempfile::TempDir; - - fn create_test_config() -> (*mut FFIClientConfig, TempDir) { - let temp_dir = TempDir::new().unwrap(); - let config = dash_spv_ffi_config_new(FFINetwork::Regtest); - - unsafe { - let path = CString::new(temp_dir.path().to_str().unwrap()).unwrap(); - dash_spv_ffi_config_set_data_dir(config, path.as_ptr()); - } - - (config, temp_dir) - } - - #[test] - #[serial] - fn test_client_creation() { - unsafe { - let (config, _temp_dir) = create_test_config(); - - let client = dash_spv_ffi_client_new(config, FFIEventCallbacks::default()); - assert!(!client.is_null()); - - dash_spv_ffi_client_destroy(client); - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - #[serial] - fn test_client_null_config() { - unsafe { - let client = dash_spv_ffi_client_new(std::ptr::null(), FFIEventCallbacks::default()); - assert!(client.is_null()); - } - } - - #[test] - #[serial] - fn test_client_lifecycle() { - unsafe { - let (config, _temp_dir) = create_test_config(); - let client = dash_spv_ffi_client_new(config, FFIEventCallbacks::default()); - - // Pass default (no-op) callbacks. Start/stop may fail without network. - // Run twice on the same pointer: the client's internal watch must - // re-arm after stop() so a second run() works. - let _result = dash_spv_ffi_client_run(client); - let _result = dash_spv_ffi_client_stop(client); - let _result = dash_spv_ffi_client_run(client); - let _result = dash_spv_ffi_client_stop(client); - - dash_spv_ffi_client_destroy(client); - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - #[serial] - fn test_client_null_checks() { - unsafe { - let result = dash_spv_ffi_client_run(std::ptr::null_mut()); - assert_eq!(result, FFIErrorCode::NullPointer as i32); - - let result = dash_spv_ffi_client_stop(std::ptr::null_mut()); - assert_eq!(result, FFIErrorCode::NullPointer as i32); - - let progress = dash_spv_ffi_client_get_sync_progress(std::ptr::null_mut()); - assert!(progress.is_null()); - } - } -} diff --git a/dash-spv-ffi/tests/test_config.rs b/dash-spv-ffi/tests/test_config.rs deleted file mode 100644 index 651d7201d..000000000 --- a/dash-spv-ffi/tests/test_config.rs +++ /dev/null @@ -1,110 +0,0 @@ -#[cfg(test)] -mod tests { - use dash_network::ffi::FFINetwork; - use dash_spv_ffi::*; - use serial_test::serial; - use std::ffi::CString; - - #[test] - #[serial] - fn test_config_creation() { - unsafe { - let config = dash_spv_ffi_config_new(FFINetwork::Testnet); - assert!(!config.is_null()); - - let network = dash_spv_ffi_config_get_network(config); - assert_eq!(network as i32, FFINetwork::Testnet as i32); - - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - #[serial] - fn test_config_mainnet() { - unsafe { - let config = dash_spv_ffi_config_mainnet(); - assert!(!config.is_null()); - - let network = dash_spv_ffi_config_get_network(config); - assert_eq!(network as i32, FFINetwork::Mainnet as i32); - - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - #[serial] - fn test_config_testnet() { - unsafe { - let config = dash_spv_ffi_config_testnet(); - assert!(!config.is_null()); - - let network = dash_spv_ffi_config_get_network(config); - assert_eq!(network as i32, FFINetwork::Testnet as i32); - - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - #[serial] - fn test_config_set_data_dir() { - unsafe { - let config = dash_spv_ffi_config_new(FFINetwork::Testnet); - - let path = CString::new("/tmp/dash-spv-test").unwrap(); - let result = dash_spv_ffi_config_set_data_dir(config, path.as_ptr()); - assert_eq!(result, FFIErrorCode::Success as i32); - - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - #[serial] - fn test_config_null_checks() { - unsafe { - let result = dash_spv_ffi_config_set_data_dir(std::ptr::null_mut(), std::ptr::null()); - assert_eq!(result, FFIErrorCode::NullPointer as i32); - - let config = dash_spv_ffi_config_new(FFINetwork::Testnet); - let result = dash_spv_ffi_config_set_data_dir(config, std::ptr::null()); - assert_eq!(result, FFIErrorCode::NullPointer as i32); - - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - #[serial] - fn test_config_peers() { - unsafe { - let config = dash_spv_ffi_config_new(FFINetwork::Testnet); - - let peer_addr = CString::new("127.0.0.1:9999").unwrap(); - let result = dash_spv_ffi_config_add_peer(config, peer_addr.as_ptr()); - assert_eq!(result, FFIErrorCode::Success as i32); - - let invalid_addr = CString::new("not-an-address").unwrap(); - let result = dash_spv_ffi_config_add_peer(config, invalid_addr.as_ptr()); - assert_eq!(result, FFIErrorCode::InvalidArgument as i32); - - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - #[serial] - fn test_config_user_agent() { - unsafe { - let config = dash_spv_ffi_config_new(FFINetwork::Testnet); - - let agent = CString::new("TestAgent/1.0").unwrap(); - let result = dash_spv_ffi_config_set_user_agent(config, agent.as_ptr()); - assert_eq!(result, FFIErrorCode::Success as i32); - - dash_spv_ffi_config_destroy(config); - } - } -} diff --git a/dash-spv-ffi/tests/test_error.rs b/dash-spv-ffi/tests/test_error.rs deleted file mode 100644 index 7d3d3c785..000000000 --- a/dash-spv-ffi/tests/test_error.rs +++ /dev/null @@ -1,48 +0,0 @@ -#[cfg(test)] -mod tests { - use dash_spv_ffi::*; - use serial_test::serial; - use std::ffi::CStr; - - #[test] - #[serial] - fn test_error_handling() { - clear_last_error(); - - let error_ptr = dash_spv_ffi_get_last_error(); - assert!(error_ptr.is_null()); - - set_last_error("Test error message"); - - let error_ptr = dash_spv_ffi_get_last_error(); - assert!(!error_ptr.is_null()); - - unsafe { - let error_str = CStr::from_ptr(error_ptr).to_str().unwrap(); - assert_eq!(error_str, "Test error message"); - } - } - - #[test] - #[serial] - fn test_handle_error() { - let ok_result: Result = Ok(42); - let handled = handle_error(ok_result); - assert_eq!(handled, Some(42)); - - let err_ptr = dash_spv_ffi_get_last_error(); - assert!(err_ptr.is_null()); - - let err_result: Result = Err("Test error".to_string()); - let handled = handle_error(err_result); - assert!(handled.is_none()); - - let err_ptr = dash_spv_ffi_get_last_error(); - assert!(!err_ptr.is_null()); - - unsafe { - let error_str = CStr::from_ptr(err_ptr).to_str().unwrap(); - assert_eq!(error_str, "Test error"); - } - } -} diff --git a/dash-spv-ffi/tests/test_platform_integration_minimal.rs b/dash-spv-ffi/tests/test_platform_integration_minimal.rs deleted file mode 100644 index 824d6e1b8..000000000 --- a/dash-spv-ffi/tests/test_platform_integration_minimal.rs +++ /dev/null @@ -1,15 +0,0 @@ -//! Minimal platform integration test to verify FFI functions - -use dash_spv_ffi::*; -use std::ptr; - -#[test] -fn test_basic_null_checks() { - unsafe { - // Test error code - let mut height: u32 = 0; - let result = - ffi_dash_spv_get_platform_activation_height(ptr::null_mut(), &mut height as *mut u32); - assert_eq!(result.error_code, FFIErrorCode::NullPointer as i32); - } -} diff --git a/dash-spv-ffi/tests/test_platform_integration_safety.rs b/dash-spv-ffi/tests/test_platform_integration_safety.rs deleted file mode 100644 index 91416e857..000000000 --- a/dash-spv-ffi/tests/test_platform_integration_safety.rs +++ /dev/null @@ -1,305 +0,0 @@ -//! Comprehensive safety tests for platform_integration FFI functions -//! -//! Tests focus on: -//! - Null pointer handling -//! - Buffer overflow prevention -//! - Memory safety (double-free, use-after-free) -//! - Thread safety -//! - Error propagation - -use dash_spv_ffi::*; -use serial_test::serial; -use std::ffi::CStr; -use std::ptr; -use std::sync::{Arc, Mutex}; -use std::thread; - -/// Helper to create a mock FFI client for testing -unsafe fn create_mock_client() -> *mut FFIDashSpvClient { - // For now, we'll use a null pointer since we're testing error cases - // In a real implementation, this would create a valid mock client - ptr::null_mut() -} - -/// Helper to check FFI error result -fn assert_ffi_error(result: FFIResult, expected_code: FFIErrorCode) { - assert_eq!( - result.error_code, expected_code as i32, - "Expected error code {}, got {}", - expected_code as i32, result.error_code - ); -} - -#[test] -#[serial] -fn test_get_core_handle_null_safety() { - unsafe { - // Test 2: Getting last error after null pointer operation - let error = dash_spv_ffi_get_last_error(); - if !error.is_null() { - let error_str = CStr::from_ptr(error); - assert!( - error_str.to_string_lossy().contains("null") - || error_str.to_string_lossy().contains("Null"), - "Error should mention null pointer" - ); - // Note: Error strings are managed internally by the FFI layer - } - } -} - -#[test] -#[serial] -fn test_get_quorum_public_key_null_pointer_safety() { - unsafe { - let quorum_hash = [0u8; 32]; - let mut output_buffer = [0u8; 48]; - - // Test 1: Null client - let result = ffi_dash_spv_get_quorum_public_key( - ptr::null_mut(), - 0, - quorum_hash.as_ptr(), - 0, - output_buffer.as_mut_ptr(), - output_buffer.len(), - ); - assert_ffi_error(result, FFIErrorCode::NullPointer); - - // Test 2: Null quorum hash - let mock_client = create_mock_client(); - if !mock_client.is_null() { - let result = ffi_dash_spv_get_quorum_public_key( - mock_client, - 0, - ptr::null(), - 0, - output_buffer.as_mut_ptr(), - output_buffer.len(), - ); - assert_ffi_error(result, FFIErrorCode::NullPointer); - } - - // Test 3: Null output buffer - let result = ffi_dash_spv_get_quorum_public_key( - create_mock_client(), - 0, - quorum_hash.as_ptr(), - 0, - ptr::null_mut(), - 48, - ); - assert_ffi_error(result, FFIErrorCode::NullPointer); - } -} - -#[test] -#[serial] -fn test_get_quorum_public_key_buffer_size_validation() { - unsafe { - let quorum_hash = [0u8; 32]; - let mock_client = create_mock_client(); - - // Test 1: Buffer too small (47 bytes instead of 48) - let mut small_buffer = [0u8; 47]; - let result = ffi_dash_spv_get_quorum_public_key( - mock_client, - 0, - quorum_hash.as_ptr(), - 0, - small_buffer.as_mut_ptr(), - small_buffer.len(), - ); - // Should fail with InvalidArgument or similar - assert!(result.error_code != 0, "Should fail with small buffer"); - - // Test 2: Correct buffer size (48 bytes) - let mut correct_buffer = [0u8; 48]; - let _result = ffi_dash_spv_get_quorum_public_key( - mock_client, - 0, - quorum_hash.as_ptr(), - 0, - correct_buffer.as_mut_ptr(), - correct_buffer.len(), - ); - // Will fail due to null client, but not due to buffer size - - // Test 3: Larger buffer (should be fine) - let mut large_buffer = [0u8; 100]; - let _result = ffi_dash_spv_get_quorum_public_key( - mock_client, - 0, - quorum_hash.as_ptr(), - 0, - large_buffer.as_mut_ptr(), - large_buffer.len(), - ); - // Will fail due to null client, but not due to buffer size - } -} - -#[test] -#[serial] -fn test_get_platform_activation_height_safety() { - unsafe { - let mut height: u32 = 0; - - // Test 1: Null client - let result = - ffi_dash_spv_get_platform_activation_height(ptr::null_mut(), &mut height as *mut u32); - assert_ffi_error(result, FFIErrorCode::NullPointer); - - // Test 2: Null output pointer - let mock_client = create_mock_client(); - let result = ffi_dash_spv_get_platform_activation_height(mock_client, ptr::null_mut()); - assert_ffi_error(result, FFIErrorCode::NullPointer); - } -} - -#[test] -#[serial] -fn test_thread_safety_concurrent_access() { - // Test concurrent access to FFI functions - let barrier = Arc::new(std::sync::Barrier::new(3)); - let results = Arc::new(Mutex::new(Vec::new())); - - let mut handles = vec![]; - - for i in 0..3 { - let barrier_clone = barrier.clone(); - let results_clone = results.clone(); - - let handle = thread::spawn(move || { - unsafe { - // Synchronize thread start - barrier_clone.wait(); - - // Each thread tries to get platform activation height - let mut height: u32 = 0; - let result = ffi_dash_spv_get_platform_activation_height( - ptr::null_mut(), // Using null for test - &mut height as *mut u32, - ); - - // Store result - results_clone.lock().unwrap().push((i, result.error_code)); - } - }); - - handles.push(handle); - } - - // Wait for all threads - for handle in handles { - handle.join().unwrap(); - } - - // Verify all threads got consistent error codes - let results_vec = results.lock().unwrap(); - assert_eq!(results_vec.len(), 3); - let expected_error = FFIErrorCode::NullPointer as i32; - for (thread_id, error_code) in results_vec.iter() { - assert_eq!(*error_code, expected_error, "Thread {} got unexpected error code", thread_id); - } -} - -#[test] -#[serial] -fn test_memory_safety_patterns() { - unsafe { - // Test 1: Buffer overflow prevention - let quorum_hash = [0u8; 32]; - let mut tiny_buffer = [0u8; 1]; // Way too small - - let result = ffi_dash_spv_get_quorum_public_key( - ptr::null_mut(), - 0, - quorum_hash.as_ptr(), - 0, - tiny_buffer.as_mut_ptr(), - tiny_buffer.len(), // Correctly report size - ); - - // Should fail safely without buffer overflow - assert_ne!(result.error_code, 0); - } -} - -#[test] -#[serial] -fn test_error_propagation_thread_local() { - unsafe { - // Trigger an error - let result = ffi_dash_spv_get_platform_activation_height(ptr::null_mut(), ptr::null_mut()); - assert_ne!(result.error_code, 0); - - // Get the error message - let error = dash_spv_ffi_get_last_error(); - assert!(!error.is_null(), "Should have error message"); - - if !error.is_null() { - let error_str = CStr::from_ptr(error); - let error_string = error_str.to_string_lossy(); - - // Verify error message is meaningful - assert!(!error_string.is_empty(), "Error message should not be empty"); - - // Note: Error strings are managed internally - } - } -} - -#[test] -#[serial] -fn test_boundary_conditions() { - unsafe { - // Test various boundary conditions - - // Test 1: Zero-length buffer - let quorum_hash = [0u8; 32]; - let result = ffi_dash_spv_get_quorum_public_key( - ptr::null_mut(), - 0, - quorum_hash.as_ptr(), - 0, - ptr::null_mut(), - 0, // Zero length - ); - assert_ne!(result.error_code, 0); - - // Test 2: Maximum values - let result = ffi_dash_spv_get_quorum_public_key( - ptr::null_mut(), - u32::MAX, // Max quorum type - quorum_hash.as_ptr(), - u32::MAX, // Max height - ptr::null_mut(), - 0, - ); - assert_ne!(result.error_code, 0); - } -} - -/// Test error string lifecycle management -#[test] -#[serial] -fn test_error_string_lifecycle() { - unsafe { - // Trigger an error to generate an error string - let _ = ffi_dash_spv_get_platform_activation_height(ptr::null_mut(), ptr::null_mut()); - - let error = dash_spv_ffi_get_last_error(); - if !error.is_null() { - // Verify we can read the string - let error_cstr = CStr::from_ptr(error); - let error_string = error_cstr.to_string_lossy(); - assert!(!error_string.is_empty()); - - // The error string is managed internally and should not be freed by the caller - // Multiple calls should return the same pointer until cleared - let error2 = dash_spv_ffi_get_last_error(); - assert_eq!(error, error2, "Should return same error pointer"); - } - } -} diff --git a/dash-spv-ffi/tests/test_types.rs b/dash-spv-ffi/tests/test_types.rs deleted file mode 100644 index 4b807fa9a..000000000 --- a/dash-spv-ffi/tests/test_types.rs +++ /dev/null @@ -1,200 +0,0 @@ -#[cfg(test)] -mod tests { - use dash_network::ffi::FFINetwork; - use dash_spv::sync::{ - BlockHeadersProgress, BlocksProgress, ChainLockProgress, FilterHeadersProgress, - FiltersProgress, InstantSendProgress, MasternodesProgress, SyncProgress, SyncState, - }; - use dash_spv_ffi::*; - #[test] - fn test_ffi_string_new_and_destroy() { - let test_str = "Hello, FFI!"; - let ffi_string = FFIString::new(test_str); - - assert!(!ffi_string.ptr.is_null()); - - unsafe { - let recovered = FFIString::from_ptr(ffi_string.ptr); - assert_eq!(recovered.unwrap(), test_str); - - dash_spv_ffi_string_destroy(ffi_string); - } - } - - #[test] - fn test_ffi_string_null_handling() { - unsafe { - let result = FFIString::from_ptr(std::ptr::null()); - assert!(result.is_err()); - } - } - - #[test] - fn test_ffi_network_conversion() { - assert_eq!(dashcore::Network::Mainnet, FFINetwork::Mainnet.into()); - assert_eq!(dashcore::Network::Testnet, FFINetwork::Testnet.into()); - assert_eq!(dashcore::Network::Regtest, FFINetwork::Regtest.into()); - assert_eq!(dashcore::Network::Devnet, FFINetwork::Devnet.into()); - - assert_eq!(FFINetwork::Mainnet, dashcore::Network::Mainnet.into()); - assert_eq!(FFINetwork::Testnet, dashcore::Network::Testnet.into()); - assert_eq!(FFINetwork::Regtest, dashcore::Network::Regtest.into()); - assert_eq!(FFINetwork::Devnet, dashcore::Network::Devnet.into()); - } - - #[test] - fn test_sync_progress_conversion() { - let mut progress = SyncProgress::default(); - - let mut headers = BlockHeadersProgress::default(); - headers.set_state(SyncState::Syncing); - headers.update_tip_height(100); - headers.update_target_height(200); - headers.add_processed(20); - headers.update_buffered(5); - progress.update_headers(headers); - - let mut filter_headers = FilterHeadersProgress::default(); - filter_headers.set_state(SyncState::WaitingForConnections); - filter_headers.update_current_height(150); - filter_headers.update_target_height(200); - filter_headers.update_block_header_tip_height(180); - filter_headers.add_processed(30); - progress.update_filter_headers(filter_headers); - - let mut filters = FiltersProgress::default(); - filters.set_state(SyncState::WaitForEvents); - filters.update_stored_height(150); - filters.update_committed_height(120); - filters.update_target_height(200); - filters.update_filter_header_tip_height(150); - filters.add_downloaded(40); - filters.add_processed(35); - filters.add_matched(10); - progress.update_filters(filters); - - let mut blocks = BlocksProgress::default(); - blocks.set_state(SyncState::Syncing); - blocks.update_last_processed(400); - blocks.add_requested(50); - blocks.add_from_storage(20); - blocks.add_downloaded(15); - blocks.add_processed(12); - blocks.add_relevant(8); - blocks.add_transactions(25); - progress.update_blocks(blocks); - - let mut masternodes = MasternodesProgress::default(); - masternodes.set_state(SyncState::Synced); - masternodes.update_current_height(500); - masternodes.update_target_height(550); - masternodes.update_block_header_tip_height(560); - masternodes.add_diffs_processed(3); - progress.update_masternodes(masternodes); - - let mut chainlocks = ChainLockProgress::default(); - chainlocks.set_state(SyncState::Error); - chainlocks.update_best_validated_height(600); - chainlocks.add_valid(10); - chainlocks.add_invalid(2); - progress.update_chainlocks(chainlocks); - - let mut instantsend = InstantSendProgress::default(); - instantsend.set_state(SyncState::WaitForEvents); - instantsend.update_pending(700); - instantsend.add_valid(200); - instantsend.add_invalid(15); - progress.update_instantsend(instantsend); - - let ffi_progress = FFISyncProgress::from(progress); - - assert_eq!(ffi_progress.state, FFISyncState::Syncing); - assert_eq!(ffi_progress.percentage, 0.625); - - // Verify headers progress - assert!(!ffi_progress.headers.is_null()); - unsafe { - let headers = &*ffi_progress.headers; - assert_eq!(headers.state, FFISyncState::Syncing); - assert_eq!(headers.tip_height, 100); - assert_eq!(headers.target_height, 200); - assert_eq!(headers.processed, 20); - assert_eq!(headers.buffered, 5); - } - - // Verify filter_headers progress - assert!(!ffi_progress.filter_headers.is_null()); - unsafe { - let filter_headers = &*ffi_progress.filter_headers; - assert_eq!(filter_headers.state, FFISyncState::WaitingForConnections); - assert_eq!(filter_headers.current_height, 150); - assert_eq!(filter_headers.target_height, 200); - assert_eq!(filter_headers.block_header_tip_height, 180); - assert_eq!(filter_headers.processed, 30); - } - - // Verify filters progress - assert!(!ffi_progress.filters.is_null()); - unsafe { - let filters = &*ffi_progress.filters; - assert_eq!(filters.state, FFISyncState::WaitForEvents); - assert_eq!(filters.stored_height, 150); - assert_eq!(filters.committed_height, 120); - assert_eq!(filters.target_height, 200); - assert_eq!(filters.filter_header_tip_height, 150); - assert_eq!(filters.downloaded, 40); - assert_eq!(filters.processed, 35); - assert_eq!(filters.matched, 10); - } - - // Verify blocks progress - assert!(!ffi_progress.blocks.is_null()); - unsafe { - let blocks = &*ffi_progress.blocks; - assert_eq!(blocks.state, FFISyncState::Syncing); - assert_eq!(blocks.last_processed, 400); - assert_eq!(blocks.requested, 50); - assert_eq!(blocks.from_storage, 20); - assert_eq!(blocks.downloaded, 15); - assert_eq!(blocks.processed, 12); - assert_eq!(blocks.relevant, 8); - assert_eq!(blocks.transactions, 25); - } - - // Verify masternodes progress - assert!(!ffi_progress.masternodes.is_null()); - unsafe { - let masternodes = &*ffi_progress.masternodes; - assert_eq!(masternodes.state, FFISyncState::Synced); - assert_eq!(masternodes.current_height, 500); - assert_eq!(masternodes.target_height, 550); - assert_eq!(masternodes.block_header_tip_height, 560); - assert_eq!(masternodes.diffs_processed, 3); - } - - // Verify chainlocks progress - assert!(!ffi_progress.chainlocks.is_null()); - unsafe { - let chainlocks = &*ffi_progress.chainlocks; - assert_eq!(chainlocks.state, FFISyncState::Error); - assert_eq!(chainlocks.best_validated_height, 600); - assert_eq!(chainlocks.valid, 10); - assert_eq!(chainlocks.invalid, 2); - } - - // Verify instantsend progress - assert!(!ffi_progress.instantsend.is_null()); - unsafe { - let instantsend = &*ffi_progress.instantsend; - assert_eq!(instantsend.state, FFISyncState::WaitForEvents); - assert_eq!(instantsend.pending, 700); - assert_eq!(instantsend.valid, 200); - assert_eq!(instantsend.invalid, 15); - } - - // Cleanup all allocated memory - unsafe { - dash_spv_ffi_sync_progress_destroy(Box::into_raw(Box::new(ffi_progress))); - } - } -} diff --git a/dash-spv-ffi/tests/test_utils.rs b/dash-spv-ffi/tests/test_utils.rs deleted file mode 100644 index 2e27bf316..000000000 --- a/dash-spv-ffi/tests/test_utils.rs +++ /dev/null @@ -1,39 +0,0 @@ -#[cfg(test)] -mod tests { - use dash_spv_ffi::*; - use serial_test::serial; - use std::ffi::{CStr, CString}; - - #[test] - #[serial] - fn test_init_logging() { - unsafe { - let level = CString::new("debug").unwrap(); - let result = dash_spv_ffi_init_logging(level.as_ptr(), true, std::ptr::null(), 0); - // May fail if already initialized, but should handle gracefully - assert!( - result == FFIErrorCode::Success as i32 - || result == FFIErrorCode::RuntimeError as i32 - ); - - // Test with null level pointer (should use RUST_LOG or default to INFO) - let result = dash_spv_ffi_init_logging(std::ptr::null(), true, std::ptr::null(), 0); - assert!( - result == FFIErrorCode::Success as i32 - || result == FFIErrorCode::RuntimeError as i32 - ); - } - } - - #[test] - fn test_version() { - unsafe { - let version_ptr = dash_spv_ffi_version(); - assert!(!version_ptr.is_null()); - - let version = CStr::from_ptr(version_ptr).to_str().unwrap(); - assert!(!version.is_empty()); - assert!(version.contains(".")); - } - } -} diff --git a/dash-spv-ffi/tests/test_wallet_manager.rs b/dash-spv-ffi/tests/test_wallet_manager.rs deleted file mode 100644 index b6ecb0d7a..000000000 --- a/dash-spv-ffi/tests/test_wallet_manager.rs +++ /dev/null @@ -1,139 +0,0 @@ -#[cfg(test)] -mod tests { - use dash_network::ffi::FFINetwork; - use dash_spv_ffi::*; - use key_wallet::wallet::initialization::WalletAccountCreationOptions; - use key_wallet::wallet::managed_wallet_info::ManagedWalletInfo; - use key_wallet_ffi::{ - wallet_manager::{ - wallet_manager_free_wallet_ids, wallet_manager_get_wallet_ids, - wallet_manager_import_wallet_from_bytes, wallet_manager_wallet_count, - }, - FFIError, FFIWalletManager, - }; - use key_wallet_manager::WalletManager; - use std::ffi::{CStr, CString}; - use tempfile::TempDir; - - #[test] - fn test_get_wallet_manager() { - unsafe { - // Create a config - let config = dash_spv_ffi_config_testnet(); - assert!(!config.is_null()); - - let temp_dir = TempDir::new().unwrap(); - dash_spv_ffi_config_set_data_dir( - config, - CString::new(temp_dir.path().to_str().unwrap()).unwrap().as_ptr(), - ); - - // Create a client - let client = dash_spv_ffi_client_new(config, FFIEventCallbacks::default()); - assert!(!client.is_null()); - - // Get wallet manager - let wallet_manager = dash_spv_ffi_client_get_wallet_manager(client); - assert!(!wallet_manager.is_null()); - let wallet_manager_ptr = wallet_manager as *mut FFIWalletManager; - assert_eq!((*wallet_manager_ptr).network(), FFINetwork::Testnet); - - // Get wallet count (should be 0 initially) - let mut error = FFIError::default(); - let count = wallet_manager_wallet_count( - wallet_manager as *const FFIWalletManager, - &mut error as *mut FFIError, - ); - assert_eq!(count, 0); - - // Clean up - dash_spv_ffi_wallet_manager_free(wallet_manager); - dash_spv_ffi_client_destroy(client); - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - fn test_wallet_manager_shared_via_client_imports_wallet() { - unsafe { - let config = dash_spv_ffi_config_testnet(); - assert!(!config.is_null()); - - let temp_dir = TempDir::new().unwrap(); - dash_spv_ffi_config_set_data_dir( - config, - CString::new(temp_dir.path().to_str().unwrap()).unwrap().as_ptr(), - ); - - let client = dash_spv_ffi_client_new(config, FFIEventCallbacks::default()); - assert!(!client.is_null()); - - let wallet_manager = dash_spv_ffi_client_get_wallet_manager(client); - assert!(!wallet_manager.is_null()); - let wallet_manager_ptr = wallet_manager as *mut key_wallet_ffi::FFIWalletManager; - assert_eq!((*wallet_manager_ptr).network(), FFINetwork::Testnet); - - // Prepare a serialized wallet using the native manager so we can import it - let mut native_manager = - WalletManager::::new((*config).get_inner().network); - let (serialized_wallet, expected_wallet_id) = native_manager - .create_wallet_from_mnemonic_return_serialized_bytes( - "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", - 0, - WalletAccountCreationOptions::Default, - false, - false, - ) - .expect("wallet serialization should succeed"); - - // Import the serialized wallet through the FFI pointer we retrieved from the client - let mut error = FFIError::default(); - let mut imported_wallet_id = [0u8; 32]; - let import_ok = wallet_manager_import_wallet_from_bytes( - wallet_manager_ptr, - serialized_wallet.as_ptr(), - serialized_wallet.len(), - imported_wallet_id.as_mut_ptr(), - &mut error as *mut FFIError, - ); - assert!(import_ok, "import should succeed: {:?}", error); - assert_eq!(imported_wallet_id, expected_wallet_id); - - // Fetch wallet IDs through FFI to confirm the manager sees the new wallet - let mut ids_ptr: *mut u8 = std::ptr::null_mut(); - let mut id_count: usize = 0; - let ids_ok = wallet_manager_get_wallet_ids( - wallet_manager_ptr as *const FFIWalletManager, - &mut ids_ptr, - &mut id_count, - &mut error as *mut FFIError, - ); - assert!(ids_ok, "get_wallet_ids should succeed: {:?}", error); - assert_eq!(id_count, 1); - assert!(!ids_ptr.is_null()); - - let ids_slice = std::slice::from_raw_parts(ids_ptr, id_count * 32); - assert_eq!(&ids_slice[..32], &expected_wallet_id); - wallet_manager_free_wallet_ids(ids_ptr, id_count); - - // Call the describe helper through FFI to ensure the shared instance reports correctly - let mut description_error = FFIError::default(); - let description_ptr = key_wallet_ffi::wallet_manager_describe( - wallet_manager_ptr as *const FFIWalletManager, - &mut description_error as *mut FFIError, - ); - assert!(!description_ptr.is_null(), "describe should succeed: {:?}", description_error); - let description = CStr::from_ptr(description_ptr).to_string_lossy().into_owned(); - key_wallet_ffi::wallet_manager_free_string(description_ptr); - assert!( - description.contains("WalletManager: 1 wallet"), - "description should mention the imported wallet, got: {}", - description - ); - - dash_spv_ffi_wallet_manager_free(wallet_manager); - dash_spv_ffi_client_destroy(client); - dash_spv_ffi_config_destroy(config); - } - } -} diff --git a/dash-spv-ffi/tests/unit/test_async_operations.rs b/dash-spv-ffi/tests/unit/test_async_operations.rs deleted file mode 100644 index 0a3d9613e..000000000 --- a/dash-spv-ffi/tests/unit/test_async_operations.rs +++ /dev/null @@ -1,100 +0,0 @@ -#[cfg(test)] -mod tests { - use crate::*; - use dash_network::ffi::FFINetwork; - use serial_test::serial; - use std::ffi::CString; - use std::os::raw::c_void; - use std::sync::atomic::{AtomicBool, Ordering}; - use std::sync::Arc; - use tempfile::TempDir; - - fn create_test_client() -> (*mut FFIDashSpvClient, *mut FFIClientConfig, TempDir) { - let temp_dir = TempDir::new().unwrap(); - unsafe { - let config = dash_spv_ffi_config_new(FFINetwork::Regtest); - assert!(!config.is_null(), "Failed to create config"); - - let path = CString::new(temp_dir.path().to_str().unwrap()).unwrap(); - dash_spv_ffi_config_set_data_dir(config, path.as_ptr()); - - let client = dash_spv_ffi_client_new(config, FFIEventCallbacks::default()); - assert!(!client.is_null(), "Failed to create client"); - - (client, config, temp_dir) - } - } - - #[test] - #[serial] - fn test_sync_event_callbacks() { - unsafe { - let (client, config, _temp_dir) = create_test_client(); - assert!(!client.is_null()); - - let sync_started = Arc::new(AtomicBool::new(false)); - let headers_stored = Arc::new(AtomicBool::new(false)); - let sync_complete = Arc::new(AtomicBool::new(false)); - - struct EventData { - sync_started: Arc, - headers_stored: Arc, - sync_complete: Arc, - } - - let event_data = EventData { - sync_started: sync_started.clone(), - headers_stored: headers_stored.clone(), - sync_complete: sync_complete.clone(), - }; - - extern "C" fn on_sync_start(_manager_id: FFIManagerId, user_data: *mut c_void) { - let data = unsafe { &*(user_data as *const EventData) }; - data.sync_started.store(true, Ordering::SeqCst); - } - - extern "C" fn on_block_headers_stored(_tip_height: u32, user_data: *mut c_void) { - let data = unsafe { &*(user_data as *const EventData) }; - data.headers_stored.store(true, Ordering::SeqCst); - } - - extern "C" fn on_sync_complete(_header_tip: u32, _cycle: u32, user_data: *mut c_void) { - let data = unsafe { &*(user_data as *const EventData) }; - data.sync_complete.store(true, Ordering::SeqCst); - } - - let sync_callbacks = FFISyncEventCallbacks { - on_sync_start: Some(on_sync_start), - on_block_headers_stored: Some(on_block_headers_stored), - on_block_header_sync_complete: None, - on_filter_headers_stored: None, - on_filter_headers_sync_complete: None, - on_filters_stored: None, - on_filters_sync_complete: None, - on_blocks_needed: None, - on_block_processed: None, - on_masternode_state_updated: None, - on_chainlock_received: None, - on_instantlock_received: None, - on_manager_error: None, - on_sync_complete: Some(on_sync_complete), - user_data: &event_data as *const _ as *mut c_void, - }; - - // Build an FFIEventCallbacks with sync callbacks set - let callbacks = FFIEventCallbacks { - sync: sync_callbacks, - ..FFIEventCallbacks::default() - }; - - // Verify the struct is properly constructed (callbacks are now - // passed directly to run(), no separate set call needed) - assert!(callbacks.sync.on_sync_start.is_some()); - assert!(callbacks.sync.on_block_headers_stored.is_some()); - assert!(callbacks.sync.on_sync_complete.is_some()); - - dash_spv_ffi_client_destroy(client); - dash_spv_ffi_config_destroy(config); - } - } -} diff --git a/dash-spv-ffi/tests/unit/test_client_lifecycle.rs b/dash-spv-ffi/tests/unit/test_client_lifecycle.rs deleted file mode 100644 index bf50abeb7..000000000 --- a/dash-spv-ffi/tests/unit/test_client_lifecycle.rs +++ /dev/null @@ -1,343 +0,0 @@ -// Note: Many tests in this file are marked with #[ignore] because they call -// dash_spv_ffi_client_run() which hangs indefinitely when using regtest -// network with no configured peers. These tests should be run with a proper -// test network setup or mocked networking layer. - -#[cfg(test)] -mod tests { - use crate::*; - use dash_network::ffi::FFINetwork; - use serial_test::serial; - use std::ffi::CString; - use std::sync::mpsc; - use std::sync::{Arc as StdArc, Mutex as StdMutex}; - use std::thread; - use std::time::Duration; - use tempfile::TempDir; - - fn create_test_config_with_dir() -> (*mut FFIClientConfig, TempDir) { - let temp_dir = TempDir::new().unwrap(); - unsafe { - let config = dash_spv_ffi_config_new(FFINetwork::Regtest); - let path = CString::new(temp_dir.path().to_str().unwrap()).unwrap(); - dash_spv_ffi_config_set_data_dir(config, path.as_ptr()); - (config, temp_dir) - } - } - - #[test] - #[serial] - fn test_client_creation_with_invalid_config() { - unsafe { - // Test with null config - let client = dash_spv_ffi_client_new(std::ptr::null(), FFIEventCallbacks::default()); - assert!(client.is_null()); - - // Check error was set - let error_ptr = dash_spv_ffi_get_last_error(); - assert!(!error_ptr.is_null()); - } - } - - #[test] - #[serial] - fn test_multiple_client_instances() { - unsafe { - let mut clients = vec![]; - let mut temp_dirs = vec![]; - - // Create multiple clients with different data directories - for i in 0..3 { - let (config, temp_dir) = create_test_config_with_dir(); - let client = dash_spv_ffi_client_new(config, FFIEventCallbacks::default()); - assert!(!client.is_null(), "Failed to create client {}", i); - - clients.push(client); - temp_dirs.push(temp_dir); - dash_spv_ffi_config_destroy(config); - } - - // Clean up all clients - for client in clients { - dash_spv_ffi_client_destroy(client); - } - } - } - - #[test] - #[serial] - #[ignore] // Requires network - client_start hangs without peers - fn test_client_start_stop_restart() { - unsafe { - let (config, _temp_dir) = create_test_config_with_dir(); - let client = dash_spv_ffi_client_new(config, FFIEventCallbacks::default()); - assert!(!client.is_null()); - - // Start - let _result = dash_spv_ffi_client_run(client); - // May fail in test environment, but should handle gracefully - - // Stop - let _result = dash_spv_ffi_client_stop(client); - - // Restart - let _result = dash_spv_ffi_client_run(client); - let _result = dash_spv_ffi_client_stop(client); - - dash_spv_ffi_client_destroy(client); - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - #[serial] - #[ignore] // Requires network - fn test_client_destruction_while_operations_pending() { - unsafe { - let (config, _temp_dir) = create_test_config_with_dir(); - let client = dash_spv_ffi_client_new(config, FFIEventCallbacks::default()); - assert!(!client.is_null()); - - // Start a sync operation in background - // Start sync (non-blocking) - dash_spv_ffi_client_run(client); - - // Immediately destroy client (should handle pending operations) - dash_spv_ffi_client_destroy(client); - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - #[serial] - #[ignore] // Requires network - client_start hangs without peers - fn test_client_with_no_peers() { - unsafe { - let temp_dir = TempDir::new().unwrap(); - let config = dash_spv_ffi_config_new(FFINetwork::Regtest); - let path = CString::new(temp_dir.path().to_str().unwrap()).unwrap(); - dash_spv_ffi_config_set_data_dir(config, path.as_ptr()); - - // Don't add any peers - let client = dash_spv_ffi_client_new(config, FFIEventCallbacks::default()); - assert!(!client.is_null()); - - // Try to start (should handle no peers gracefully) - let _result = dash_spv_ffi_client_run(client); - - dash_spv_ffi_client_destroy(client); - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - #[serial] - fn test_client_resource_cleanup() { - // Test that resources are properly cleaned up - let _initial_thread_count = thread::current().id(); - - unsafe { - for _ in 0..5 { - let (config, _temp_dir) = create_test_config_with_dir(); - let client = dash_spv_ffi_client_new(config, FFIEventCallbacks::default()); - assert!(!client.is_null()); - - // Do some operations - let progress = dash_spv_ffi_client_get_sync_progress(client); - - dash_spv_ffi_sync_progress_destroy(progress); - - dash_spv_ffi_client_destroy(client); - dash_spv_ffi_config_destroy(config); - } - } - - // Give time for cleanup - thread::sleep(Duration::from_millis(100)); - - // Thread count should be reasonable (not growing indefinitely) - let _final_thread_count = thread::current().id(); - // Can't directly compare thread counts, but test passes if no panic/leak - } - - #[test] - #[serial] - fn test_client_null_operations() { - unsafe { - // Test all client operations with null - assert_eq!( - dash_spv_ffi_client_run(std::ptr::null_mut()), - FFIErrorCode::NullPointer as i32 - ); - - assert_eq!( - dash_spv_ffi_client_stop(std::ptr::null_mut()), - FFIErrorCode::NullPointer as i32 - ); - - assert!(dash_spv_ffi_client_get_sync_progress(std::ptr::null_mut()).is_null()); - - // Test destroy with null (should be safe) - dash_spv_ffi_client_destroy(std::ptr::null_mut()); - } - } - - #[test] - #[serial] - #[ignore] // Requires network - client_start hangs without peers - fn test_client_state_consistency() { - unsafe { - let (config, _temp_dir) = create_test_config_with_dir(); - let client = dash_spv_ffi_client_new(config, FFIEventCallbacks::default()); - assert!(!client.is_null()); - - // Get initial state - let progress1 = dash_spv_ffi_client_get_sync_progress(client); - - let progress = &*progress1; - let headers = &*progress.headers; - let filter_headers = &*progress.filter_headers; - - // Basic consistency checks - assert!( - headers.tip_height <= filter_headers.target_height - || filter_headers.current_height == 0 - ); - // headers_downloaded is u64, always >= 0 - - dash_spv_ffi_sync_progress_destroy(progress1); - dash_spv_ffi_client_destroy(client); - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - #[serial] - fn test_client_error_callback_fires_on_start_failure() { - let (tx, rx) = mpsc::channel::(); - let tx_ptr = Box::into_raw(Box::new(tx)); - - extern "C" fn on_error( - error: *const std::os::raw::c_char, - user_data: *mut std::os::raw::c_void, - ) { - let tx = unsafe { &*(user_data as *const mpsc::Sender) }; - let error_str = unsafe { std::ffi::CStr::from_ptr(error) }.to_str().unwrap().to_owned(); - let _ = tx.send(error_str); - } - - unsafe { - let (config, _temp_dir) = create_test_config_with_dir(); - let callbacks = FFIEventCallbacks { - error: FFIClientErrorCallback { - on_error: Some(on_error), - user_data: tx_ptr as *mut std::os::raw::c_void, - }, - ..FFIEventCallbacks::default() - }; - let client = dash_spv_ffi_client_new(config, callbacks); - assert!(!client.is_null()); - - // Call run() twice — the second run's sync thread will call - // start() on the already-running client, triggering "already running" - let run_result = dash_spv_ffi_client_run(client); - assert_eq!(run_result, FFIErrorCode::Success as i32); - - // Brief wait for the first run's sync thread to complete start() - thread::sleep(Duration::from_millis(200)); - - let _run_result2 = dash_spv_ffi_client_run(client); - - // Wait for the error callback to fire (with timeout) - let error_msg = rx - .recv_timeout(Duration::from_secs(5)) - .expect("Error callback should have been called on start failure"); - assert!( - error_msg.contains("already running"), - "Expected 'already running' error, got: {}", - error_msg - ); - - dash_spv_ffi_client_stop(client); - - // Free the sender only after stop has joined all threads, - // so no background thread can call on_error with a dangling user_data. - drop(Box::from_raw(tx_ptr)); - - dash_spv_ffi_client_destroy(client); - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - #[serial] - fn test_client_error_callback_dispatch() { - let error_store: StdArc>> = StdArc::new(StdMutex::new(None)); - let error_store_raw = StdArc::into_raw(error_store.clone()); - - extern "C" fn on_error( - error: *const std::os::raw::c_char, - user_data: *mut std::os::raw::c_void, - ) { - assert!(!error.is_null()); - let store = unsafe { StdArc::from_raw(user_data as *const StdMutex>) }; - let error_str = unsafe { std::ffi::CStr::from_ptr(error) }.to_str().unwrap().to_owned(); - *store.lock().unwrap() = Some(error_str); - let _ = StdArc::into_raw(store); - } - - let callback = FFIClientErrorCallback { - on_error: Some(on_error), - user_data: error_store_raw as *mut std::os::raw::c_void, - }; - - callback.dispatch("test error message"); - - let received = error_store.lock().unwrap(); - assert_eq!(received.as_deref(), Some("test error message")); - drop(received); - - unsafe { drop(StdArc::from_raw(error_store_raw)) }; - } - - #[test] - #[serial] - fn test_client_run_null_client() { - unsafe { - assert_eq!( - dash_spv_ffi_client_run(std::ptr::null_mut()), - FFIErrorCode::NullPointer as i32 - ); - } - } - - #[test] - #[serial] - fn test_client_error_callback_no_callback_set() { - // Dispatch with no callback set should not panic - let callback = FFIClientErrorCallback::default(); - callback.dispatch("should not panic"); - } - - #[test] - #[serial] - fn test_client_repeated_creation_destruction() { - // Stress test client creation/destruction - for _ in 0..10 { - unsafe { - let (config, _temp_dir) = create_test_config_with_dir(); - let client = dash_spv_ffi_client_new(config, FFIEventCallbacks::default()); - assert!(!client.is_null()); - - // Do a quick operation - let progress = dash_spv_ffi_client_get_sync_progress(client); - if !progress.is_null() { - dash_spv_ffi_sync_progress_destroy(progress); - } - - dash_spv_ffi_client_destroy(client); - dash_spv_ffi_config_destroy(config); - } - } - } -} diff --git a/dash-spv-ffi/tests/unit/test_configuration.rs b/dash-spv-ffi/tests/unit/test_configuration.rs deleted file mode 100644 index 513638333..000000000 --- a/dash-spv-ffi/tests/unit/test_configuration.rs +++ /dev/null @@ -1,244 +0,0 @@ -#[cfg(test)] -mod tests { - use crate::*; - use dash_network::ffi::FFINetwork; - use serial_test::serial; - use std::ffi::CString; - - #[test] - #[serial] - fn test_config_with_invalid_network() { - unsafe { - // Test creating config with each valid network - let networks = - [FFINetwork::Mainnet, FFINetwork::Testnet, FFINetwork::Regtest, FFINetwork::Devnet]; - for net in networks { - let config = dash_spv_ffi_config_new(net); - assert!(!config.is_null()); - let retrieved_net = dash_spv_ffi_config_get_network(config); - assert_eq!(retrieved_net as i32, net as i32); - dash_spv_ffi_config_destroy(config); - } - } - } - - #[test] - #[serial] - fn test_extremely_long_paths() { - unsafe { - let config = dash_spv_ffi_config_testnet(); - - // Test with very long path (near filesystem limits) - let long_path = format!("/tmp/{}", "x".repeat(4000)); - let c_path = CString::new(long_path.clone()).unwrap(); - let result = dash_spv_ffi_config_set_data_dir(config, c_path.as_ptr()); - assert_eq!(result, FFIErrorCode::Success as i32); - - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - #[serial] - fn test_invalid_peer_addresses() { - unsafe { - let config = dash_spv_ffi_config_testnet(); - - // Test various invalid addresses - let invalid_addrs = [ - "", // empty string - "256.256.256.256:9999", // invalid IP octets - "127.0.0.1:99999", // port too high - "127.0.0.1:-1", // negative port - ":9999", // missing hostname - "localhost:", // missing port - ":", // missing hostname and port - ":::", // invalid IPv6 - "localhost:abc", // non-numeric port - ]; - - for addr in &invalid_addrs { - let c_addr = CString::new(*addr).unwrap(); - let result = dash_spv_ffi_config_add_peer(config, c_addr.as_ptr()); - assert_eq!( - result, - FFIErrorCode::InvalidArgument as i32, - "Expected '{}' to be invalid", - addr - ); - - // Check error message - let error_ptr = dash_spv_ffi_get_last_error(); - assert!(!error_ptr.is_null()); - } - - // Test valid addresses including IP-only forms (port inferred from network) - let valid_addrs = [ - "127.0.0.1:9999", - "192.168.1.1:8333", - "[::1]:9999", - "[2001:db8::1]:8333", - "127.0.0.1", // IP-only v4 - "2001:db8::1", // IP-only v6 - "localhost:9999", // Hostname with port - "localhost", // Hostname without port (uses default) - ]; - - for addr in &valid_addrs { - let c_addr = CString::new(*addr).unwrap(); - let result = dash_spv_ffi_config_add_peer(config, c_addr.as_ptr()); - assert_eq!(result, FFIErrorCode::Success as i32); - } - - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - #[serial] - fn test_adding_maximum_peers() { - unsafe { - let config = dash_spv_ffi_config_testnet(); - - // Add many peers - for i in 0..1000 { - let addr = format!("192.168.1.{}:9999", (i % 254) + 1); - let c_addr = CString::new(addr).unwrap(); - let result = dash_spv_ffi_config_add_peer(config, c_addr.as_ptr()); - assert_eq!(result, FFIErrorCode::Success as i32); - } - - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - #[serial] - fn test_config_with_special_characters_in_paths() { - unsafe { - let config = dash_spv_ffi_config_testnet(); - - // Test paths with spaces - let path_with_spaces = "/tmp/path with spaces/dash spv"; - let c_path = CString::new(path_with_spaces).unwrap(); - let result = dash_spv_ffi_config_set_data_dir(config, c_path.as_ptr()); - assert_eq!(result, FFIErrorCode::Success as i32); - - // Test paths with unicode - let unicode_path = "/tmp/путь/目录/dossier"; - let c_path = CString::new(unicode_path).unwrap(); - let result = dash_spv_ffi_config_set_data_dir(config, c_path.as_ptr()); - assert_eq!(result, FFIErrorCode::Success as i32); - - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - #[serial] - fn test_relative_vs_absolute_paths() { - unsafe { - let config = dash_spv_ffi_config_testnet(); - - // Test relative path - let rel_path = "./data/dash-spv"; - let c_path = CString::new(rel_path).unwrap(); - let result = dash_spv_ffi_config_set_data_dir(config, c_path.as_ptr()); - assert_eq!(result, FFIErrorCode::Success as i32); - - // Test absolute path - let abs_path = "/tmp/dash-spv-test"; - let c_path = CString::new(abs_path).unwrap(); - let result = dash_spv_ffi_config_set_data_dir(config, c_path.as_ptr()); - assert_eq!(result, FFIErrorCode::Success as i32); - - // Test home directory expansion (won't actually expand in FFI) - let home_path = "~/dash-spv"; - let c_path = CString::new(home_path).unwrap(); - let result = dash_spv_ffi_config_set_data_dir(config, c_path.as_ptr()); - assert_eq!(result, FFIErrorCode::Success as i32); - - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - #[serial] - fn test_config_all_settings() { - unsafe { - let config = dash_spv_ffi_config_new(FFINetwork::Regtest); - - // Set all possible configuration options - let data_dir = CString::new("/tmp/test-dash-spv").unwrap(); - assert_eq!( - dash_spv_ffi_config_set_data_dir(config, data_dir.as_ptr()), - FFIErrorCode::Success as i32 - ); - - let peer = CString::new("127.0.0.1:9999").unwrap(); - assert_eq!( - dash_spv_ffi_config_add_peer(config, peer.as_ptr()), - FFIErrorCode::Success as i32 - ); - - let user_agent = CString::new("TestAgent/1.0").unwrap(); - assert_eq!( - dash_spv_ffi_config_set_user_agent(config, user_agent.as_ptr()), - FFIErrorCode::Success as i32 - ); - - assert_eq!( - dash_spv_ffi_config_set_restrict_to_configured_peers(config, true), - FFIErrorCode::Success as i32 - ); - - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - #[serial] - fn test_config_null_handling() { - unsafe { - // Test all functions with null config - assert_eq!( - dash_spv_ffi_config_set_data_dir(std::ptr::null_mut(), std::ptr::null()), - FFIErrorCode::NullPointer as i32 - ); - - assert_eq!( - dash_spv_ffi_config_add_peer(std::ptr::null_mut(), std::ptr::null()), - FFIErrorCode::NullPointer as i32 - ); - - assert_eq!( - dash_spv_ffi_config_set_user_agent(std::ptr::null_mut(), std::ptr::null()), - FFIErrorCode::NullPointer as i32 - ); - - // Test getters with null - let net = dash_spv_ffi_config_get_network(std::ptr::null()); - assert_eq!(net as i32, FFINetwork::Mainnet as i32); // Returns default - - // Test destroy with null (should be safe) - dash_spv_ffi_config_destroy(std::ptr::null_mut()); - } - } - - #[test] - #[serial] - fn test_config_edge_case_values() { - unsafe { - let config = dash_spv_ffi_config_testnet(); - - // Test empty strings - let empty = CString::new("").unwrap(); - assert_eq!( - dash_spv_ffi_config_set_data_dir(config, empty.as_ptr()), - FFIErrorCode::Success as i32 - ); - - dash_spv_ffi_config_destroy(config); - } - } -} diff --git a/dash-spv-ffi/tests/unit/test_error_handling.rs b/dash-spv-ffi/tests/unit/test_error_handling.rs deleted file mode 100644 index 353cabe0a..000000000 --- a/dash-spv-ffi/tests/unit/test_error_handling.rs +++ /dev/null @@ -1,173 +0,0 @@ -#[cfg(test)] -mod tests { - use crate::*; - use dash_network::ffi::FFINetwork; - use serial_test::serial; - use std::ffi::CStr; - use std::sync::{Arc, Barrier}; - use std::thread; - - #[test] - #[serial] - fn test_concurrent_error_handling() { - // Test thread safety of error handling - // Note: The implementation uses a global mutex, not thread-local storage - let barrier = Arc::new(Barrier::new(10)); - let mut handles = vec![]; - - for i in 0..10 { - let barrier_clone = barrier.clone(); - let handle = thread::spawn(move || { - // Wait for all threads to start - barrier_clone.wait(); - - // Each thread sets its own error - let error_msg = format!("Error from thread {}", i); - set_last_error(&error_msg); - - // Small delay to reduce contention - thread::sleep(std::time::Duration::from_millis(10)); - - // Read the global error - it could be from any thread - let error_ptr = dash_spv_ffi_get_last_error(); - if !error_ptr.is_null() { - unsafe { - let c_str = CStr::from_ptr(error_ptr); - // Verify it's a valid UTF-8 string - if let Ok(error_str) = c_str.to_str() { - // The error could be from any thread due to global mutex - assert!( - error_str.contains("Error from thread") || error_str.is_empty() - ); - } - } - } - }); - handles.push(handle); - } - - for handle in handles { - handle.join().unwrap(); - } - } - - #[test] - #[serial] - fn test_error_message_truncation() { - // Test very long error message - let long_error = "X".repeat(10000); - set_last_error(&long_error); - - let error_ptr = dash_spv_ffi_get_last_error(); - assert!(!error_ptr.is_null()); - - unsafe { - let error_str = CStr::from_ptr(error_ptr).to_str().unwrap(); - // Should handle long strings without truncation - assert_eq!(error_str.len(), 10000); - assert!(error_str.chars().all(|c| c == 'X')); - } - } - - #[test] - fn test_all_error_code_mappings() { - // Test all error codes have correct values - assert_eq!(FFIErrorCode::Success as i32, 0); - assert_eq!(FFIErrorCode::NullPointer as i32, 1); - assert_eq!(FFIErrorCode::InvalidArgument as i32, 2); - assert_eq!(FFIErrorCode::NetworkError as i32, 3); - assert_eq!(FFIErrorCode::StorageError as i32, 4); - assert_eq!(FFIErrorCode::ValidationError as i32, 5); - assert_eq!(FFIErrorCode::SyncError as i32, 6); - assert_eq!(FFIErrorCode::ConfigError as i32, 7); - assert_eq!(FFIErrorCode::RuntimeError as i32, 8); - - // Test conversions from SpvError - use dash_spv::{NetworkError, SpvError, StorageError}; - - let net_err = SpvError::Network(NetworkError::ConnectionFailed("test".to_string())); - assert_eq!(FFIErrorCode::from(net_err) as i32, FFIErrorCode::NetworkError as i32); - - let storage_err = SpvError::Storage(StorageError::NotFound("test".to_string())); - assert_eq!(FFIErrorCode::from(storage_err) as i32, FFIErrorCode::StorageError as i32); - - let config_err = SpvError::Config("test".to_string()); - assert_eq!(FFIErrorCode::from(config_err) as i32, FFIErrorCode::ConfigError as i32); - } - - #[test] - #[serial] - fn test_error_clearing_between_operations() { - // Set an error - set_last_error("First error"); - assert!(!dash_spv_ffi_get_last_error().is_null()); - - // Clear it - clear_last_error(); - assert!(dash_spv_ffi_get_last_error().is_null()); - - // Set another error - set_last_error("Second error"); - let error_ptr = dash_spv_ffi_get_last_error(); - assert!(!error_ptr.is_null()); - - unsafe { - let error_str = CStr::from_ptr(error_ptr).to_str().unwrap(); - assert_eq!(error_str, "Second error"); - } - - // Clear using public API - clear_last_error(); - assert!(dash_spv_ffi_get_last_error().is_null()); - } - - #[test] - #[serial] - fn test_null_pointer_error_handling() { - // Test null_check! macro behavior - unsafe { - // Test with config functions - let result = dash_spv_ffi_config_set_data_dir(std::ptr::null_mut(), std::ptr::null()); - assert_eq!(result, FFIErrorCode::NullPointer as i32); - - // Check error was set - let error_ptr = dash_spv_ffi_get_last_error(); - assert!(!error_ptr.is_null()); - let error_str = CStr::from_ptr(error_ptr).to_str().unwrap(); - assert_eq!(error_str, "Null pointer provided"); - } - } - - #[test] - fn test_invalid_enum_handling() { - // Use a valid enum value to avoid UB in Rust tests. If invalid raw inputs - // need to be tested, do so from a C test or add a raw-int FFI entrypoint. - unsafe { - let config = dash_spv_ffi_config_new(FFINetwork::Mainnet); - assert!(!config.is_null()); - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - #[serial] - fn test_handle_error_helper() { - // Test Ok case - let ok_result: Result = Ok(42); - let handled = handle_error(ok_result); - assert_eq!(handled, Some(42)); - assert!(dash_spv_ffi_get_last_error().is_null()); - - // Test Err case - let err_result: Result = Err("Test error".to_string()); - let handled = handle_error(err_result); - assert!(handled.is_none()); - - let error_ptr = dash_spv_ffi_get_last_error(); - assert!(!error_ptr.is_null()); - unsafe { - let error_str = CStr::from_ptr(error_ptr).to_str().unwrap(); - assert_eq!(error_str, "Test error"); - } - } -} diff --git a/dash-spv-ffi/tests/unit/test_memory_management.rs b/dash-spv-ffi/tests/unit/test_memory_management.rs deleted file mode 100644 index 04194c2c9..000000000 --- a/dash-spv-ffi/tests/unit/test_memory_management.rs +++ /dev/null @@ -1,320 +0,0 @@ -#[cfg(test)] -mod tests { - use crate::*; - use dash_network::ffi::FFINetwork; - use serial_test::serial; - use std::ffi::{CStr, CString}; - use std::os::raw::{c_char, c_void}; - use std::sync::{Arc, Mutex}; - use std::thread; - use std::time::{Duration, Instant}; - use tempfile::TempDir; - - #[test] - #[serial] - fn test_string_memory_lifecycle() { - unsafe { - // Test FFIString allocation and deallocation - let test_string = "Hello, FFI Memory Test!"; - let ffi_string = FFIString::new(test_string); - assert!(!ffi_string.ptr.is_null()); - - // Verify contents - let recovered = FFIString::from_ptr(ffi_string.ptr).unwrap(); - assert_eq!(recovered, test_string); - - // Clean up - dash_spv_ffi_string_destroy(ffi_string); - - // Test with empty string - let empty = FFIString::new(""); - assert!(!empty.ptr.is_null()); - dash_spv_ffi_string_destroy(empty); - - // Test with very large string - let large_string = "X".repeat(1_000_000); - let large_ffi = FFIString::new(&large_string); - assert!(!large_ffi.ptr.is_null()); - dash_spv_ffi_string_destroy(large_ffi); - } - } - - #[test] - #[serial] - fn test_client_memory_lifecycle() { - unsafe { - let temp_dir = TempDir::new().unwrap(); - let config = dash_spv_ffi_config_new(FFINetwork::Regtest); - let path = CString::new(temp_dir.path().to_str().unwrap()).unwrap(); - dash_spv_ffi_config_set_data_dir(config, path.as_ptr()); - - // Create and destroy multiple clients - for _ in 0..10 { - let client = dash_spv_ffi_client_new(config, FFIEventCallbacks::default()); - assert!(!client.is_null()); - - // Perform some operations - let progress = dash_spv_ffi_client_get_sync_progress(client); - if !progress.is_null() { - dash_spv_ffi_sync_progress_destroy(progress); - } - - dash_spv_ffi_client_destroy(client); - } - - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - #[serial] - fn test_concurrent_memory_operations() { - let barrier = Arc::new(std::sync::Barrier::new(10)); - let mut handles = vec![]; - - for i in 0..10 { - let barrier_clone = barrier.clone(); - let handle = thread::spawn(move || { - barrier_clone.wait(); - - unsafe { - // Each thread creates and destroys strings - for j in 0..100 { - let s = format!("Thread {} iteration {}", i, j); - let ffi = FFIString::new(&s); - - // Simulate some work - thread::sleep(Duration::from_micros(10)); - - dash_spv_ffi_string_destroy(ffi); - } - } - }); - handles.push(handle); - } - - for handle in handles { - handle.join().unwrap(); - } - } - - #[test] - #[serial] - fn test_memory_stress_large_allocations() { - unsafe { - // Test with progressively larger allocations - let sizes = [1_000, 10_000, 100_000, 1_000_000, 10_000_000]; - - for &size in &sizes { - // String allocation - let large_string = "X".repeat(size); - let ffi_string = FFIString::new(&large_string); - assert!(!ffi_string.ptr.is_null()); - - // Verify we can read it back - let recovered = FFIString::from_ptr(ffi_string.ptr).unwrap(); - assert_eq!(recovered.len(), size); - - dash_spv_ffi_string_destroy(ffi_string); - } - } - } - - #[test] - #[serial] - fn test_double_free_prevention() { - unsafe { - // Test that double-free doesn't cause issues - // Note: This relies on the implementation handling null pointers gracefully - - // Test with string - let ffi_string = FFIString::new("test"); - let _ptr = ffi_string.ptr; - dash_spv_ffi_string_destroy(ffi_string); - - // Second destroy should handle gracefully - let null_string = FFIString { - ptr: std::ptr::null_mut(), - length: 0, - }; - dash_spv_ffi_string_destroy(null_string); - } - } - - #[test] - #[serial] - fn test_callback_memory_management() { - // Test that callbacks don't leak memory - let data = Arc::new(Mutex::new(Vec::::new())); - let data_clone = data.clone(); - - extern "C" fn memory_test_callback( - _progress: f64, - msg: *const c_char, - user_data: *mut c_void, - ) { - let data = unsafe { &*(user_data as *const Arc>>) }; - if !msg.is_null() { - let msg_str = unsafe { CStr::from_ptr(msg).to_str().unwrap() }; - data.lock().unwrap().push(msg_str.to_string()); - } - } - - // Simulate multiple callback invocations - for i in 0..1000 { - let msg = CString::new(format!("Progress: {}", i)).unwrap(); - memory_test_callback(i as f64, msg.as_ptr(), &data_clone as *const _ as *mut c_void); - } - - // Verify we captured all messages - assert_eq!(data.lock().unwrap().len(), 1000); - } - - #[test] - #[serial] - fn test_recursive_structure_cleanup() { - unsafe { - // Test cleanup of structures containing pointers to other structures - let temp_dir = TempDir::new().unwrap(); - let config = dash_spv_ffi_config_new(FFINetwork::Regtest); - let path = CString::new(temp_dir.path().to_str().unwrap()).unwrap(); - dash_spv_ffi_config_set_data_dir(config, path.as_ptr()); - - let client = dash_spv_ffi_client_new(config, FFIEventCallbacks::default()); - assert!(!client.is_null()); - - // Get structures that contain FFIString and other pointers - let progress = dash_spv_ffi_client_get_sync_progress(client); - if !progress.is_null() { - // SyncProgress might contain strings or other allocated data - dash_spv_ffi_sync_progress_destroy(progress); - } - - dash_spv_ffi_client_destroy(client); - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - #[serial] - fn test_memory_pool_behavior() { - unsafe { - // Test rapid allocation/deallocation patterns - let start = Instant::now(); - let mut allocations = Vec::new(); - - // Rapid allocation phase - for i in 0..10000 { - let s = format!("String number {}", i); - let ffi = FFIString::new(&s); - allocations.push(ffi); - } - - // Rapid deallocation phase - for ffi in allocations { - dash_spv_ffi_string_destroy(ffi); - } - - let duration = start.elapsed(); - println!("Allocation/deallocation of 10000 strings took: {:?}", duration); - - // Test interleaved allocation/deallocation - for i in 0..5000 { - let s1 = FFIString::new(&format!("First {}", i)); - let s2 = FFIString::new(&format!("Second {}", i)); - dash_spv_ffi_string_destroy(s1); - let s3 = FFIString::new(&format!("Third {}", i)); - dash_spv_ffi_string_destroy(s2); - dash_spv_ffi_string_destroy(s3); - } - } - } - - #[test] - #[serial] - fn test_zero_size_allocations() { - unsafe { - // Test edge case of zero-size allocations - let empty_string = FFIString::new(""); - assert!(!empty_string.ptr.is_null()); - let recovered = FFIString::from_ptr(empty_string.ptr).unwrap(); - assert_eq!(recovered, ""); - dash_spv_ffi_string_destroy(empty_string); - } - } - - #[test] - #[serial] - fn test_memory_corruption_detection() { - unsafe { - // Test that we can detect potential memory corruption scenarios - // This test verifies our memory handling is robust - - // Create multiple strings with specific patterns - let patterns = vec!["AAAAAAAAAA", "BBBBBBBBBB", "CCCCCCCCCC", "DDDDDDDDDD"]; - - let mut ffi_strings = Vec::new(); - for pattern in &patterns { - let ffi = FFIString::new(pattern); - ffi_strings.push(ffi); - } - - // Verify all strings are still intact - for (i, ffi) in ffi_strings.iter().enumerate() { - let recovered = FFIString::from_ptr(ffi.ptr).unwrap(); - assert_eq!(recovered, patterns[i]); - } - - // Clean up in reverse order - while let Some(ffi) = ffi_strings.pop() { - dash_spv_ffi_string_destroy(ffi); - } - } - } - - #[test] - #[serial] - fn test_long_running_memory_stability() { - unsafe { - // Simulate long-running application with periodic allocations - let duration = Duration::from_millis(100); - let start = Instant::now(); - let mut cycle = 0; - - while start.elapsed() < duration { - // Allocate some memory - let strings: Vec<_> = (0..10) - .map(|i| FFIString::new(&format!("Cycle {} String {}", cycle, i))) - .collect(); - - // Do some work - thread::sleep(Duration::from_micros(100)); - - // Clean up - for s in strings { - dash_spv_ffi_string_destroy(s); - } - - cycle += 1; - } - - println!("Completed {} allocation cycles", cycle); - } - } - - #[test] - #[serial] - fn test_cross_thread_memory_sharing() { - // Test that memory allocated in one thread can be safely used in another - unsafe { - let string = FFIString::new("Allocated in thread 1"); - - // Verify we can read the data - let s = FFIString::from_ptr(string.ptr).unwrap(); - assert_eq!(s, "Allocated in thread 1"); - - // Clean up - dash_spv_ffi_string_destroy(string); - } - } -} diff --git a/dash-spv-ffi/tests/unit/test_type_conversions.rs b/dash-spv-ffi/tests/unit/test_type_conversions.rs deleted file mode 100644 index 127702774..000000000 --- a/dash-spv-ffi/tests/unit/test_type_conversions.rs +++ /dev/null @@ -1,111 +0,0 @@ -#[cfg(test)] -mod tests { - use crate::*; - use dash_network::ffi::FFINetwork; - - #[test] - fn test_ffi_string_utf8_edge_cases() { - // Test empty string - let empty = FFIString::new(""); - unsafe { - let recovered = FFIString::from_ptr(empty.ptr).unwrap(); - assert_eq!(recovered, ""); - dash_spv_ffi_string_destroy(empty); - } - - // Test with emojis - let emoji_str = "Hello 👋 World 🌍!"; - let emoji = FFIString::new(emoji_str); - unsafe { - let recovered = FFIString::from_ptr(emoji.ptr).unwrap(); - assert_eq!(recovered, emoji_str); - dash_spv_ffi_string_destroy(emoji); - } - - // Test with special characters - let special = "Tab\tNewline\nCarriage\rReturn"; - let special_ffi = FFIString::new(special); - unsafe { - let recovered = FFIString::from_ptr(special_ffi.ptr).unwrap(); - assert_eq!(recovered, special); - dash_spv_ffi_string_destroy(special_ffi); - } - - // Test with very long string - let long_str = "a".repeat(10000); - let long_ffi = FFIString::new(&long_str); - unsafe { - let recovered = FFIString::from_ptr(long_ffi.ptr).unwrap(); - assert_eq!(recovered, long_str); - dash_spv_ffi_string_destroy(long_ffi); - } - } - - #[test] - fn test_ffi_string_null_handling() { - unsafe { - // Test null pointer - let result = FFIString::from_ptr(std::ptr::null()); - assert!(result.is_err()); - assert_eq!(result.unwrap_err(), "Null pointer"); - - // Test destroying null (should be safe) - dash_spv_ffi_string_destroy(FFIString { - ptr: std::ptr::null_mut(), - length: 0, - }); - } - } - - #[test] - fn test_network_conversions() { - // Test all network conversions - let networks = [ - (FFINetwork::Mainnet, dashcore::Network::Mainnet), - (FFINetwork::Testnet, dashcore::Network::Testnet), - (FFINetwork::Regtest, dashcore::Network::Regtest), - (FFINetwork::Devnet, dashcore::Network::Devnet), - ]; - - for (ffi_net, dash_net) in networks.iter() { - let converted: dashcore::Network = (*ffi_net).into(); - assert_eq!(converted, *dash_net); - - let back: FFINetwork = (*dash_net).into(); - assert_eq!(back as i32, *ffi_net as i32); - } - } - - #[test] - fn test_concurrent_ffi_string_creation() { - use std::sync::atomic::{AtomicUsize, Ordering}; - use std::sync::Arc; - use std::thread; - - let counter = Arc::new(AtomicUsize::new(0)); - let mut handles = vec![]; - - for i in 0..10 { - let counter_clone = counter.clone(); - let handle = thread::spawn(move || { - for j in 0..100 { - let s = format!("Thread {} iteration {}", i, j); - let ffi = FFIString::new(&s); - unsafe { - let recovered = FFIString::from_ptr(ffi.ptr).unwrap(); - assert_eq!(recovered, s); - dash_spv_ffi_string_destroy(ffi); - } - counter_clone.fetch_add(1, Ordering::SeqCst); - } - }); - handles.push(handle); - } - - for handle in handles { - handle.join().unwrap(); - } - - assert_eq!(counter.load(Ordering::SeqCst), 1000); - } -} diff --git a/dash-spv/src/test_utils/context.rs b/dash-spv/src/test_utils/context.rs index fbb66b4ed..82a2c3bef 100644 --- a/dash-spv/src/test_utils/context.rs +++ b/dash-spv/src/test_utils/context.rs @@ -2,7 +2,7 @@ //! //! Provides `DashdTestContext` which encapsulates the common setup logic for //! launching a dashd node with a pre-built blockchain and loading wallet data. -//! Used by both `dash-spv` and `dash-spv-ffi` integration tests. +//! Used by `dash-spv` integration tests. use std::net::SocketAddr; diff --git a/ffi-c-tests/header-tests/all.c b/ffi-c-tests/header-tests/all.c deleted file mode 100644 index 0a23b0c35..000000000 --- a/ffi-c-tests/header-tests/all.c +++ /dev/null @@ -1,5 +0,0 @@ -#include "dash-spv-ffi/dash-spv-ffi.h" -#include "key-wallet-ffi/key-wallet-ffi.h" -#include "dash-network/dash-network.h" - -int main() { return 0; } diff --git a/ffi-c-tests/header-tests/dash-network.c b/ffi-c-tests/header-tests/dash-network.c deleted file mode 100644 index cd1657070..000000000 --- a/ffi-c-tests/header-tests/dash-network.c +++ /dev/null @@ -1,3 +0,0 @@ -#include "dash-network/dash-network.h" - -int main() { return 0; } diff --git a/ffi-c-tests/header-tests/dash-spv.c b/ffi-c-tests/header-tests/dash-spv.c deleted file mode 100644 index 4ae27378e..000000000 --- a/ffi-c-tests/header-tests/dash-spv.c +++ /dev/null @@ -1,3 +0,0 @@ -#include "dash-spv-ffi/dash-spv-ffi.h" - -int main() { return 0; } diff --git a/ffi-c-tests/header-tests/key-wallet.c b/ffi-c-tests/header-tests/key-wallet.c deleted file mode 100644 index 94dd4076c..000000000 --- a/ffi-c-tests/header-tests/key-wallet.c +++ /dev/null @@ -1,3 +0,0 @@ -#include "key-wallet-ffi/key-wallet-ffi.h" - -int main() { return 0; } diff --git a/ffi-c-tests/validate-headers.sh b/ffi-c-tests/validate-headers.sh deleted file mode 100755 index 4dfdc86d2..000000000 --- a/ffi-c-tests/validate-headers.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/bash -set -euo pipefail - -RED='\033[0;31m' -GREEN='\033[0;32m' -NC='\033[0m' - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -HEADER_TESTS_DIR="$SCRIPT_DIR/header-tests" - -if [ $# -lt 1 ]; then - echo "Usage: $0 " - exit 1 -fi - -INCLUDE_DIR="$1" - -if [ ! -d "$INCLUDE_DIR" ]; then - echo "Error: INCLUDE_DIR '$INCLUDE_DIR' does not exist or is not a directory." - exit 1 -fi - -EXIT_CODE=0 - -for file in "$HEADER_TESTS_DIR"/*.c; do - if gcc -c "$file" -I"$INCLUDE_DIR" -o /dev/null; then - echo -e "${GREEN}Passed: $file${NC}" - else - echo -e "${RED}Failed: $file${NC}" - EXIT_CODE=1 - fi -done - -exit $EXIT_CODE diff --git a/key-wallet-ffi/Cargo.toml b/key-wallet-ffi/Cargo.toml deleted file mode 100644 index 203ec8f65..000000000 --- a/key-wallet-ffi/Cargo.toml +++ /dev/null @@ -1,48 +0,0 @@ -[package] -name = "key-wallet-ffi" -version = { workspace = true } -authors = ["The Dash Core Developers"] -edition = "2021" -description = "FFI bindings for key-wallet library" -keywords = ["dash", "wallet", "ffi", "bindings"] -readme = "README.md" -license = "CC0-1.0" - -[lib] -name = "key_wallet_ffi" -crate-type = ["cdylib", "staticlib", "lib"] - -[features] -default = ["bincode", "eddsa", "bls", "bip38"] -bip38 = ["key-wallet/bip38"] -bincode = ["key-wallet/bincode", "key-wallet-manager/bincode"] -eddsa = ["dashcore/eddsa", "key-wallet/eddsa"] -bls = ["dashcore/bls", "key-wallet/bls"] -# Forward to `key-wallet/keep-finalized-transactions` (via key-wallet-manager). -# With this on, every processed transaction (including chainlocked ones) -# stays in the in-memory `transactions` map for the wallet's lifetime. -# With it off (the default), records of chainlocked transactions are -# dropped and only their txids are kept (in `finalized_txids`) for dedup. -# See `key-wallet`'s feature documentation for details. -keep-finalized-transactions = [ - "key-wallet/keep-finalized-transactions", - "key-wallet-manager/keep-finalized-transactions", -] - -[dependencies] -key-wallet = { path = "../key-wallet" } -key-wallet-manager = { path = "../key-wallet-manager" } -dashcore = { path = "../dash" } -dash-network = { path = "../dash-network", features = ["ffi"] } -secp256k1 = { version = "0.30.0", features = ["global-context"] } -tokio = { version = "1.32", features = ["rt-multi-thread", "sync"] } -libc = "0.2" -hex = "0.4" - -[build-dependencies] -cbindgen = "0.29" - -[dev-dependencies] -key-wallet = { path = "../key-wallet", features = ["test-utils"] } -key-wallet-manager = { path = "../key-wallet-manager", features = ["test-utils"] } -hex = "0.4" diff --git a/key-wallet-ffi/FFI_API.md b/key-wallet-ffi/FFI_API.md deleted file mode 100644 index 60018fdfd..000000000 --- a/key-wallet-ffi/FFI_API.md +++ /dev/null @@ -1,4416 +0,0 @@ -# Key-Wallet FFI API Documentation - -This document provides a comprehensive reference for all FFI (Foreign Function Interface) functions available in the key-wallet-ffi library. - -**Auto-generated**: This documentation is automatically generated from the source code. Do not edit manually. - -**Total Functions**: 258 - -## Table of Contents - -- [Initialization](#initialization) -- [Error Handling](#error-handling) -- [Wallet Manager](#wallet-manager) -- [Wallet Operations](#wallet-operations) -- [Account Management](#account-management) -- [Address Management](#address-management) -- [Transaction Management](#transaction-management) -- [Key Management](#key-management) -- [Mnemonic Operations](#mnemonic-operations) -- [Utility Functions](#utility-functions) - -## Function Reference - -### Initialization - -Functions: 2 - -| Function | Description | Module | -|----------|-------------|--------| -| `key_wallet_ffi_initialize` | Initialize the library | lib | -| `key_wallet_ffi_version` | Get library version Returns a static string that should NOT be freed by the... | lib | - -### Error Handling - -Functions: 4 - -| Function | Description | Module | -|----------|-------------|--------| -| `account_result_free_error` | Free an account result's error message (if any) Note: This does NOT free the... | account | -| `error_message_free` | Free an error message # Safety - `message` must be a valid pointer to a C... | error | -| `managed_core_account_result_free_error` | Free a managed account result's error message (if any) Note: This does NOT... | managed_account | -| `managed_platform_account_result_free_error` | Free a managed platform account result's error message (if any) Note: This... | managed_account | - -### Wallet Manager - -Functions: 20 - -| Function | Description | Module | -|----------|-------------|--------| -| `wallet_manager_add_wallet_from_mnemonic` | Add a wallet from mnemonic to the manager (backward compatibility) # Safety... | wallet_manager | -| `wallet_manager_add_wallet_from_mnemonic_return_serialized_bytes` | No description | wallet_manager | -| `wallet_manager_add_wallet_from_mnemonic_with_options` | Add a wallet from mnemonic to the manager with options # Safety -... | wallet_manager | -| `wallet_manager_create` | Create a new wallet manager # Safety `error` must be a valid pointer to an... | wallet_manager | -| `wallet_manager_current_height` | Get current height for a network # Safety - `manager` must be a valid... | wallet_manager | -| `wallet_manager_describe` | Describe the wallet manager for a given network and return a newly allocated... | wallet_manager | -| `wallet_manager_free` | Free wallet manager # Safety - `manager` must be a valid pointer to an... | wallet_manager | -| `wallet_manager_free_addresses` | Free address array # Safety - `addresses` must be a valid pointer to an... | wallet_manager | -| `wallet_manager_free_string` | Free a string previously returned by wallet manager APIs | wallet_manager | -| `wallet_manager_free_wallet_bytes` | No description | wallet_manager | -| `wallet_manager_free_wallet_ids` | Free wallet IDs buffer # Safety - `wallet_ids` must be a valid pointer to... | wallet_manager | -| `wallet_manager_get_managed_wallet_info` | Get managed wallet info from the manager Returns a reference to the managed... | wallet_manager | -| `wallet_manager_get_wallet` | Get a wallet from the manager Returns a reference to the wallet if found #... | wallet_manager | -| `wallet_manager_get_wallet_balance` | Get wallet balance Returns the confirmed and unconfirmed balance for a... | wallet_manager | -| `wallet_manager_get_wallet_ids` | Get wallet IDs # Safety - `manager` must be a valid pointer to an... | wallet_manager | -| `wallet_manager_import_wallet_from_bytes` | No description | wallet_manager | -| `wallet_manager_network` | Get the network for this wallet manager # Safety - `manager` must be a... | wallet_manager | -| `wallet_manager_process_transaction` | Process a transaction through all wallets Checks a transaction against all... | wallet_manager | -| `wallet_manager_set_transaction_label` | Set or clear a label on a transaction record in the shared wallet manager... | managed_account | -| `wallet_manager_wallet_count` | Get wallet count # Safety - `manager` must be a valid pointer to an... | wallet_manager | - -### Wallet Operations - -Functions: 63 - -| Function | Description | Module | -|----------|-------------|--------| -| `account_get_parent_wallet_id` | Get the parent wallet ID of an account # Safety - `account` must be a... | account | -| `bls_account_get_parent_wallet_id` | No description | account | -| `eddsa_account_get_parent_wallet_id` | No description | account | -| `ffi_managed_wallet_free` | Free a managed wallet (FFIManagedWalletInfo type) # Safety -... | transaction_checking | -| `key_wallet_derive_address_from_key` | Derive an address from a private key # Safety - `private_key` must be a... | derivation | -| `key_wallet_derive_address_from_seed` | Derive an address from a seed at a specific derivation path # Safety -... | derivation | -| `key_wallet_derive_private_key_from_seed` | Derive a private key from a seed at a specific derivation path # Safety -... | derivation | -| `managed_core_account_get_parent_wallet_id` | Get the parent wallet ID of a managed account Note: ManagedAccount doesn't... | managed_account | -| `managed_wallet_check_transaction` | Check if a transaction belongs to the wallet This function checks a... | transaction_checking | -| `managed_wallet_free` | Free managed wallet info # Safety - `managed_wallet` must be a valid... | managed_wallet | -| `managed_wallet_generate_addresses_to_index` | Generate addresses up to a specific index in a pool This ensures that... | address_pool | -| `managed_wallet_get_account` | Get a managed account from a managed wallet This function gets a... | managed_account | -| `managed_wallet_get_account_collection` | Get managed account collection for a specific network from wallet manager #... | managed_account_collection | -| `managed_wallet_get_account_count` | Get number of accounts in a managed wallet # Safety - `manager` must be a... | managed_account | -| `managed_wallet_get_address_pool_info` | Get address pool information for an account # Safety - `managed_wallet`... | address_pool | -| `managed_wallet_get_balance` | Get wallet balance from managed wallet info Returns the balance breakdown... | managed_wallet | -| `managed_wallet_get_bip_44_external_address_range` | Get BIP44 external (receive) addresses in the specified range Returns... | managed_wallet | -| `managed_wallet_get_bip_44_internal_address_range` | Get BIP44 internal (change) addresses in the specified range Returns... | managed_wallet | -| `managed_wallet_get_dashpay_external_account` | Get a managed DashPay external account by composite key # Safety - Pointers... | managed_account | -| `managed_wallet_get_dashpay_receiving_account` | Get a managed DashPay receiving funds account by composite key # Safety -... | managed_account | -| `managed_wallet_get_next_bip44_change_address` | Get the next unused change address Generates the next unused change address... | managed_wallet | -| `managed_wallet_get_next_bip44_receive_address` | Get the next unused receive address Generates the next unused receive... | managed_wallet | -| `managed_wallet_get_platform_payment_account` | Get a managed platform payment account from a managed wallet Platform... | managed_account | -| `managed_wallet_get_top_up_account_with_registration_index` | Get a managed IdentityTopUp account with a specific registration index This... | managed_account | -| `managed_wallet_get_utxos` | Get all UTXOs from managed wallet info # Safety - `managed_info` must be a... | utxo | -| `managed_wallet_info_free` | Free managed wallet info returned by wallet_manager_get_managed_wallet_info ... | managed_wallet | -| `managed_wallet_last_processed_height` | Get current last processed height from wallet info # Safety -... | managed_wallet | -| `managed_wallet_mark_address_used` | Mark an address as used in the pool This updates the pool's tracking of... | address_pool | -| `managed_wallet_set_gap_limit` | Set the gap limit for an address pool The gap limit determines how many... | address_pool | -| `wallet_add_account` | Add an account to the wallet without xpub # Safety This function... | wallet | -| `wallet_add_account_with_string_xpub` | Add an account to the wallet with xpub as string # Safety This function... | wallet | -| `wallet_add_account_with_xpub_bytes` | Add an account to the wallet with xpub as byte array # Safety This... | wallet | -| `wallet_add_dashpay_external_account_with_xpub_bytes` | Add a DashPay external (watch-only) account with xpub bytes # Safety -... | wallet | -| `wallet_add_dashpay_receiving_account` | Add a DashPay receiving funds account # Safety - `wallet` must be a valid... | wallet | -| `wallet_add_platform_payment_account` | Add a Platform Payment account (DIP-17) to the wallet Platform Payment... | wallet | -| `wallet_build_and_sign_asset_lock_transaction` | Build and sign an asset lock transaction for Core to Platform transfers | transaction | -| `wallet_build_and_sign_transaction` | Build and sign a transaction using the wallet's managed info This is the... | transaction | -| `wallet_check_transaction` | Check if a transaction belongs to the wallet using ManagedWalletInfo #... | transaction | -| `wallet_create_from_mnemonic` | Create a new wallet from mnemonic (backward compatibility - single network) ... | wallet | -| `wallet_create_from_mnemonic_with_options` | Create a new wallet from mnemonic with options # Safety - `mnemonic` must... | wallet | -| `wallet_create_from_seed` | Create a new wallet from seed (backward compatibility) # Safety - `seed`... | wallet | -| `wallet_create_from_seed_with_options` | Create a new wallet from seed with options # Safety - `seed` must be a... | wallet | -| `wallet_create_random` | Create a new random wallet (backward compatibility) # Safety - `error`... | wallet | -| `wallet_create_random_with_options` | Create a new random wallet with options # Safety - `account_options` must... | wallet | -| `wallet_derive_extended_private_key` | Derive extended private key at a specific path Returns an opaque... | keys | -| `wallet_derive_extended_public_key` | Derive extended public key at a specific path Returns an opaque... | keys | -| `wallet_derive_private_key` | Derive private key at a specific path Returns an opaque FFIPrivateKey... | keys | -| `wallet_derive_private_key_as_wif` | Derive private key at a specific path and return as WIF string # Safety -... | keys | -| `wallet_derive_public_key` | Derive public key at a specific path Returns an opaque FFIPublicKey pointer... | keys | -| `wallet_derive_public_key_as_hex` | Derive public key at a specific path and return as hex string # Safety -... | keys | -| `wallet_free` | Free a wallet # Safety - `wallet` must be a valid pointer to an FFIWallet... | wallet | -| `wallet_free_const` | Free a const wallet handle This is a const-safe wrapper for wallet_free()... | wallet | -| `wallet_get_account` | Get an account handle for a specific account type Returns a result... | account | -| `wallet_get_account_collection` | Get account collection for a specific network from wallet # Safety -... | account_collection | -| `wallet_get_account_count` | Get number of accounts # Safety - `wallet` must be a valid pointer to an... | account | -| `wallet_get_account_xpriv` | Get extended private key for account # Safety - `wallet` must be a valid... | keys | -| `wallet_get_account_xpub` | Get extended public key for account # Safety - `wallet` must be a valid... | keys | -| `wallet_get_id` | Get wallet ID (32-byte hash) # Safety - `wallet` must be a valid pointer... | wallet | -| `wallet_get_top_up_account_with_registration_index` | Get an IdentityTopUp account handle with a specific registration index This... | account | -| `wallet_get_utxos` | Get all UTXOs (deprecated - use managed_wallet_get_utxos instead) # Safety ... | utxo | -| `wallet_get_xpub` | Get extended public key for account # Safety - `wallet` must be a valid... | wallet | -| `wallet_has_mnemonic` | Check if wallet has mnemonic # Safety - `wallet` must be a valid pointer... | wallet | -| `wallet_is_watch_only` | Check if wallet is watch-only # Safety - `wallet` must be a valid pointer... | wallet | - -### Account Management - -Functions: 108 - -| Function | Description | Module | -|----------|-------------|--------| -| `account_collection_count` | Get the total number of accounts in the collection # Safety - `collection`... | account_collection | -| `account_collection_free` | Free an account collection handle # Safety - `collection` must be a valid... | account_collection | -| `account_collection_get_bip32_account` | Get a BIP32 account by index from the collection # Safety - `collection`... | account_collection | -| `account_collection_get_bip32_indices` | Get all BIP32 account indices # Safety - `collection` must be a valid... | account_collection | -| `account_collection_get_bip44_account` | Get a BIP44 account by index from the collection # Safety - `collection`... | account_collection | -| `account_collection_get_bip44_indices` | Get all BIP44 account indices # Safety - `collection` must be a valid... | account_collection | -| `account_collection_get_coinjoin_account` | Get a CoinJoin account by index from the collection # Safety -... | account_collection | -| `account_collection_get_coinjoin_indices` | Get all CoinJoin account indices # Safety - `collection` must be a valid... | account_collection | -| `account_collection_get_identity_invitation` | Get the identity invitation account if it exists # Safety - `collection`... | account_collection | -| `account_collection_get_identity_registration` | Get the identity registration account if it exists # Safety - `collection`... | account_collection | -| `account_collection_get_identity_topup` | Get an identity topup account by registration index # Safety -... | account_collection | -| `account_collection_get_identity_topup_indices` | Get all identity topup registration indices # Safety - `collection` must... | account_collection | -| `account_collection_get_identity_topup_not_bound` | Get the identity topup not bound account if it exists # Safety -... | account_collection | -| `account_collection_get_provider_operator_keys` | Get the provider operator keys account if it exists Note: Returns null if... | account_collection | -| `account_collection_get_provider_owner_keys` | Get the provider owner keys account if it exists # Safety - `collection`... | account_collection | -| `account_collection_get_provider_platform_keys` | Get the provider platform keys account if it exists Note: Returns null if... | account_collection | -| `account_collection_get_provider_voting_keys` | Get the provider voting keys account if it exists # Safety - `collection`... | account_collection | -| `account_collection_has_identity_invitation` | Check if identity invitation account exists # Safety - `collection` must... | account_collection | -| `account_collection_has_identity_registration` | Check if identity registration account exists # Safety - `collection` must... | account_collection | -| `account_collection_has_identity_topup_not_bound` | Check if identity topup not bound account exists # Safety - `collection`... | account_collection | -| `account_collection_has_provider_operator_keys` | Check if provider operator keys account exists # Safety - `collection`... | account_collection | -| `account_collection_has_provider_owner_keys` | Check if provider owner keys account exists # Safety - `collection` must... | account_collection | -| `account_collection_has_provider_platform_keys` | Check if provider platform keys account exists # Safety - `collection`... | account_collection | -| `account_collection_has_provider_voting_keys` | Check if provider voting keys account exists # Safety - `collection` must... | account_collection | -| `account_collection_summary` | Get a human-readable summary of all accounts in the collection Returns a... | account_collection | -| `account_collection_summary_data` | Get structured account collection summary data Returns a struct containing... | account_collection | -| `account_collection_summary_free` | Free an account collection summary and all its allocated memory # Safety -... | account_collection | -| `account_derive_extended_private_key_at` | Derive an extended private key from an account at a given index, using the... | account_derivation | -| `account_derive_extended_private_key_from_mnemonic` | Derive an extended private key from a mnemonic + optional passphrase at the... | account_derivation | -| `account_derive_extended_private_key_from_seed` | Derive an extended private key from a raw seed buffer at the given index | account_derivation | -| `account_derive_private_key_as_wif_at` | Derive a private key from an account at a given chain/index and return as... | account_derivation | -| `account_derive_private_key_at` | Derive a private key (secp256k1) from an account at a given chain/index,... | account_derivation | -| `account_derive_private_key_from_mnemonic` | Derive a private key from a mnemonic + optional passphrase at the given index | account_derivation | -| `account_derive_private_key_from_seed` | Derive a private key from a raw seed buffer at the given index | account_derivation | -| `account_free` | Free an account handle # Safety - `account` must be a valid pointer to an... | account | -| `account_get_account_type` | Get the account type of an account # Safety - `account` must be a valid... | account | -| `account_get_extended_public_key_as_string` | Get the extended public key of an account as a string # Safety - `account`... | account | -| `account_get_is_watch_only` | Check if an account is watch-only # Safety - `account` must be a valid... | account | -| `account_get_network` | Get the network of an account # Safety - `account` must be a valid pointer... | account | -| `bls_account_derive_private_key_from_mnemonic` | No description | account_derivation | -| `bls_account_derive_private_key_from_seed` | No description | account_derivation | -| `bls_account_free` | No description | account | -| `bls_account_get_account_type` | No description | account | -| `bls_account_get_extended_public_key_as_string` | No description | account | -| `bls_account_get_is_watch_only` | No description | account | -| `bls_account_get_network` | No description | account | -| `derivation_bip44_account_path` | Derive a BIP44 account path (m/44'/5'/account') # Safety `path_out` must... | derivation | -| `eddsa_account_derive_private_key_from_mnemonic` | No description | account_derivation | -| `eddsa_account_derive_private_key_from_seed` | No description | account_derivation | -| `eddsa_account_free` | No description | account | -| `eddsa_account_get_account_type` | No description | account | -| `eddsa_account_get_extended_public_key_as_string` | No description | account | -| `eddsa_account_get_is_watch_only` | No description | account | -| `eddsa_account_get_network` | No description | account | -| `managed_account_collection_count` | Get the total number of accounts in the managed collection # Safety -... | managed_account_collection | -| `managed_account_collection_free` | Free a managed account collection handle # Safety - `collection` must be a... | managed_account_collection | -| `managed_account_collection_free_platform_payment_keys` | Free platform payment keys array returned by managed_account_collection_get_p... | managed_account_collection | -| `managed_account_collection_get_bip32_account` | Get a BIP32 account by index from the managed collection # Safety -... | managed_account_collection | -| `managed_account_collection_get_bip32_indices` | Get all BIP32 account indices from managed collection # Safety -... | managed_account_collection | -| `managed_account_collection_get_bip44_account` | Get a BIP44 account by index from the managed collection # Safety -... | managed_account_collection | -| `managed_account_collection_get_bip44_indices` | Get all BIP44 account indices from managed collection # Safety -... | managed_account_collection | -| `managed_account_collection_get_coinjoin_account` | Get a CoinJoin account by index from the managed collection # Safety -... | managed_account_collection | -| `managed_account_collection_get_coinjoin_indices` | Get all CoinJoin account indices from managed collection # Safety -... | managed_account_collection | -| `managed_account_collection_get_identity_invitation` | Get the identity invitation account if it exists in managed collection #... | managed_account_collection | -| `managed_account_collection_get_identity_registration` | Get the identity registration account if it exists in managed collection #... | managed_account_collection | -| `managed_account_collection_get_identity_topup` | Get an identity topup account by registration index from managed collection ... | managed_account_collection | -| `managed_account_collection_get_identity_topup_indices` | Get all identity topup registration indices from managed collection #... | managed_account_collection | -| `managed_account_collection_get_identity_topup_not_bound` | Get the identity topup not bound account if it exists in managed collection ... | managed_account_collection | -| `managed_account_collection_get_platform_payment_account` | Get a Platform Payment account by account index and key class from the... | managed_account_collection | -| `managed_account_collection_get_platform_payment_keys` | Get all Platform Payment account keys from managed collection Returns an... | managed_account_collection | -| `managed_account_collection_get_provider_operator_keys` | Get the provider operator keys account if it exists in managed collection... | managed_account_collection | -| `managed_account_collection_get_provider_owner_keys` | Get the provider owner keys account if it exists in managed collection #... | managed_account_collection | -| `managed_account_collection_get_provider_platform_keys` | Get the provider platform keys account if it exists in managed collection... | managed_account_collection | -| `managed_account_collection_get_provider_voting_keys` | Get the provider voting keys account if it exists in managed collection #... | managed_account_collection | -| `managed_account_collection_has_identity_invitation` | Check if identity invitation account exists in managed collection # Safety ... | managed_account_collection | -| `managed_account_collection_has_identity_registration` | Check if identity registration account exists in managed collection #... | managed_account_collection | -| `managed_account_collection_has_identity_topup_not_bound` | Check if identity topup not bound account exists in managed collection #... | managed_account_collection | -| `managed_account_collection_has_platform_payment_accounts` | Check if there are any Platform Payment accounts in the managed collection ... | managed_account_collection | -| `managed_account_collection_has_provider_operator_keys` | Check if provider operator keys account exists in managed collection #... | managed_account_collection | -| `managed_account_collection_has_provider_owner_keys` | Check if provider owner keys account exists in managed collection # Safety ... | managed_account_collection | -| `managed_account_collection_has_provider_platform_keys` | Check if provider platform keys account exists in managed collection #... | managed_account_collection | -| `managed_account_collection_has_provider_voting_keys` | Check if provider voting keys account exists in managed collection # Safety... | managed_account_collection | -| `managed_account_collection_platform_payment_count` | Get the number of Platform Payment accounts in the managed collection #... | managed_account_collection | -| `managed_account_collection_summary` | Get a human-readable summary of all accounts in the managed collection ... | managed_account_collection | -| `managed_account_collection_summary_data` | Get structured account collection summary data for managed collection ... | managed_account_collection | -| `managed_account_collection_summary_free` | Free a managed account collection summary and all its allocated memory #... | managed_account_collection | -| `managed_core_account_free` | Free a managed account handle # Safety - `account` must be a valid pointer... | managed_account | -| `managed_core_account_free_transactions` | Free transactions array returned by managed_core_account_get_transactions ... | managed_account | -| `managed_core_account_get_account_type` | Get the account type of a managed account # Safety - `account` must be a... | managed_account | -| `managed_core_account_get_address_pool` | Get an address pool from a managed account by type This function returns... | managed_account | -| `managed_core_account_get_balance` | Get the balance of a managed account | managed_account | -| `managed_core_account_get_external_address_pool` | Get the external address pool from a managed account This function returns... | managed_account | -| `managed_core_account_get_index` | Get the account index from a managed account Returns the primary account... | managed_account | -| `managed_core_account_get_internal_address_pool` | Get the internal address pool from a managed account This function returns... | managed_account | -| `managed_core_account_get_network` | Get the network of a managed account # Safety - `account` must be a valid... | managed_account | -| `managed_core_account_get_transaction_count` | Get the number of transactions in a managed account Only available with the... | managed_account | -| `managed_core_account_get_transactions` | Get all transactions from a managed account Returns an array of... | managed_account | -| `managed_core_account_get_utxo_count` | Get the number of UTXOs in a managed account | managed_account | -| `managed_platform_account_free` | Free a managed platform account handle # Safety - `account` must be a... | managed_account | -| `managed_platform_account_get_account_index` | Get the account index of a managed platform account # Safety - `account`... | managed_account | -| `managed_platform_account_get_address_pool` | Get the address pool from a managed platform account Platform accounts only... | managed_account | -| `managed_platform_account_get_credit_balance` | Get the total credit balance of a managed platform account Returns the... | managed_account | -| `managed_platform_account_get_duff_balance` | Get the total balance in duffs of a managed platform account Returns the... | managed_account | -| `managed_platform_account_get_funded_address_count` | Get the number of funded addresses in a managed platform account # Safety ... | managed_account | -| `managed_platform_account_get_is_watch_only` | Check if a managed platform account is watch-only # Safety - `account`... | managed_account | -| `managed_platform_account_get_key_class` | Get the key class of a managed platform account # Safety - `account` must... | managed_account | -| `managed_platform_account_get_network` | Get the network of a managed platform account # Safety - `account` must be... | managed_account | -| `managed_platform_account_get_total_address_count` | Get the total number of addresses in a managed platform account # Safety -... | managed_account | - -### Address Management - -Functions: 10 - -| Function | Description | Module | -|----------|-------------|--------| -| `address_array_free` | Free address array # Safety - `addresses` must be a valid pointer to an... | address | -| `address_free` | Free address string # Safety - `address` must be a valid pointer created... | address | -| `address_get_type` | Get address type Returns: - 0: P2PKH address - 1: P2SH address - 2: Other... | address | -| `address_info_array_free` | Free an array of FFIAddressInfo structures # Safety - `infos` must be a... | address_pool | -| `address_info_free` | Free a single FFIAddressInfo structure # Safety - `info` must be a valid... | address_pool | -| `address_pool_free` | Free an address pool handle # Safety - `pool` must be a valid pointer to... | address_pool | -| `address_pool_get_address_at_index` | Get a single address info at a specific index from the pool Returns... | address_pool | -| `address_pool_get_addresses_in_range` | Get a range of addresses from the pool Returns an array of FFIAddressInfo... | address_pool | -| `address_to_pubkey_hash` | Extract public key hash from P2PKH address # Safety - `address` must be a... | transaction | -| `address_validate` | Validate an address # Safety - `address` must be a valid null-terminated C... | address | - -### Transaction Management - -Functions: 14 - -| Function | Description | Module | -|----------|-------------|--------| -| `transaction_add_input` | Add an input to a transaction # Safety - `tx` must be a valid pointer to an... | transaction | -| `transaction_add_output` | Add an output to a transaction # Safety - `tx` must be a valid pointer to... | transaction | -| `transaction_bytes_free` | Free transaction bytes # Safety - `tx_bytes` must be a valid pointer... | transaction | -| `transaction_check_result_free` | Free a transaction check result # Safety - `result` must be a valid... | transaction_checking | -| `transaction_classify` | Get the transaction classification for routing Returns a string describing... | transaction_checking | -| `transaction_create` | Create a new empty transaction # Returns - Pointer to FFITransaction on... | transaction | -| `transaction_deserialize` | Deserialize a transaction # Safety - `data` must be a valid pointer to... | transaction | -| `transaction_destroy` | Destroy a transaction # Safety - `tx` must be a valid pointer to an... | transaction | -| `transaction_get_txid` | Get the transaction ID # Safety - `tx` must be a valid pointer to an... | transaction | -| `transaction_get_txid_from_bytes` | Get transaction ID from raw transaction bytes # Safety - `tx_bytes` must be... | transaction | -| `transaction_serialize` | Serialize a transaction # Safety - `tx` must be a valid pointer to an... | transaction | -| `transaction_sighash` | Calculate signature hash for an input # Safety - `tx` must be a valid... | transaction | -| `transaction_sign_input` | Sign a transaction input # Safety - `tx` must be a valid pointer to an... | transaction | -| `utxo_array_free` | Free UTXO array # Safety - `utxos` must be a valid pointer to an array of... | utxo | - -### Key Management - -Functions: 14 - -| Function | Description | Module | -|----------|-------------|--------| -| `bip38_decrypt_private_key` | Decrypt a BIP38 encrypted private key # Safety This function is unsafe... | bip38 | -| `bip38_encrypt_private_key` | Encrypt a private key with BIP38 # Safety This function is unsafe because... | bip38 | -| `derivation_derive_private_key_from_seed` | Derive private key for a specific path from seed # Safety - `seed` must be... | derivation | -| `derivation_new_master_key` | Create a new master extended private key from seed # Safety - `seed` must... | derivation | -| `extended_private_key_free` | Free an extended private key # Safety - `key` must be a valid pointer... | keys | -| `extended_private_key_get_private_key` | Get the private key from an extended private key Extracts the non-extended... | keys | -| `extended_private_key_to_string` | Get extended private key as string (xprv format) Returns the extended... | keys | -| `extended_public_key_free` | Free an extended public key # Safety - `key` must be a valid pointer... | keys | -| `extended_public_key_get_public_key` | Get the public key from an extended public key Extracts the non-extended... | keys | -| `extended_public_key_to_string` | Get extended public key as string (xpub format) Returns the extended public... | keys | -| `private_key_free` | Free a private key # Safety - `key` must be a valid pointer created by... | keys | -| `private_key_to_wif` | Get private key as WIF string from FFIPrivateKey # Safety - `key` must be... | keys | -| `public_key_free` | Free a public key # Safety - `key` must be a valid pointer created by... | keys | -| `public_key_to_hex` | Get public key as hex string from FFIPublicKey # Safety - `key` must be a... | keys | - -### Mnemonic Operations - -Functions: 6 - -| Function | Description | Module | -|----------|-------------|--------| -| `mnemonic_free` | Free a mnemonic string # Safety - `mnemonic` must be a valid pointer... | mnemonic | -| `mnemonic_generate` | Generate a new mnemonic with specified word count (12, 15, 18, 21, or 24) #... | mnemonic | -| `mnemonic_generate_with_language` | Generate a new mnemonic with specified language and word count # Safety ... | mnemonic | -| `mnemonic_to_seed` | Convert mnemonic to seed with optional passphrase # Safety - `mnemonic`... | mnemonic | -| `mnemonic_validate` | Validate a mnemonic phrase # Safety - `mnemonic` must be a valid... | mnemonic | -| `mnemonic_word_count` | Get word count from mnemonic # Safety - `mnemonic` must be a valid... | mnemonic | - -### Utility Functions - -Functions: 17 - -| Function | Description | Module | -|----------|-------------|--------| -| `derivation_bip44_payment_path` | Derive a BIP44 payment path (m/44'/5'/account'/change/index) # Safety ... | derivation | -| `derivation_coinjoin_path` | Derive CoinJoin path (m/9'/5'/4'/account') # Safety `path_out` must point... | derivation | -| `derivation_identity_authentication_path` | Derive identity authentication path (m/9'/5'/5'/0'/identity_index'/key_index'... | derivation | -| `derivation_identity_registration_path` | Derive identity registration path (m/9'/5'/5'/1'/index') # Safety ... | derivation | -| `derivation_identity_topup_path` | Derive identity top-up path (m/9'/5'/5'/2'/identity_index'/top_up_index') #... | derivation | -| `derivation_path_free` | Free derivation path arrays Note: This function expects the count to... | keys | -| `derivation_path_parse` | Convert derivation path string to indices # Safety - `path` must be a... | keys | -| `derivation_string_free` | Free derivation path string # Safety - `s` must be a valid pointer to a C... | derivation | -| `derivation_xpriv_free` | Free extended private key # Safety - `xpriv` must be a valid pointer to an... | derivation | -| `derivation_xpriv_to_string` | Get extended private key as string # Safety - `xpriv` must be a valid... | derivation | -| `derivation_xpriv_to_xpub` | Derive public key from extended private key # Safety - `xpriv` must be a... | derivation | -| `derivation_xpub_fingerprint` | Get fingerprint from extended public key (4 bytes) # Safety - `xpub` must... | derivation | -| `derivation_xpub_free` | Free extended public key # Safety - `xpub` must be a valid pointer to an... | derivation | -| `derivation_xpub_to_string` | Get extended public key as string # Safety - `xpub` must be a valid... | derivation | -| `free_u32_array` | Free a u32 array allocated by this library # Safety - `array` must be a... | account_collection | -| `script_p2pkh` | Create a P2PKH script pubkey # Safety - `pubkey_hash` must be a valid... | transaction | -| `string_free` | Free a string # Safety - `s` must be a valid pointer created by C string... | utils | - -## Detailed Function Documentation - -### Initialization - Detailed - -#### `key_wallet_ffi_initialize` - -```c -key_wallet_ffi_initialize() -> bool -``` - -**Description:** -Initialize the library - -**Module:** `lib` - ---- - -#### `key_wallet_ffi_version` - -```c -key_wallet_ffi_version() -> *const c_char -``` - -**Description:** -Get library version Returns a static string that should NOT be freed by the caller - -**Module:** `lib` - ---- - -### Error Handling - Detailed - -#### `account_result_free_error` - -```c -account_result_free_error(result: *mut FFIAccountResult) -> () -``` - -**Description:** -Free an account result's error message (if any) Note: This does NOT free the account handle itself - use account_free for that # Safety - `result` must be a valid pointer to an FFIAccountResult - The error_message field must be either null or a valid CString allocated by this library - The caller must ensure the result pointer remains valid for the duration of this call - -**Safety:** -- `result` must be a valid pointer to an FFIAccountResult - The error_message field must be either null or a valid CString allocated by this library - The caller must ensure the result pointer remains valid for the duration of this call - -**Module:** `account` - ---- - -#### `error_message_free` - -```c -error_message_free(message: *mut c_char) -> () -``` - -**Description:** -Free an error message # Safety - `message` must be a valid pointer to a C string that was allocated by this library - The pointer must not be used after calling this function - This function must only be called once per allocation - -**Safety:** -- `message` must be a valid pointer to a C string that was allocated by this library - The pointer must not be used after calling this function - This function must only be called once per allocation - -**Module:** `error` - ---- - -#### `managed_core_account_result_free_error` - -```c -managed_core_account_result_free_error(result: *mut FFIManagedCoreAccountResult,) -> () -``` - -**Description:** -Free a managed account result's error message (if any) Note: This does NOT free the account handle itself - use managed_core_account_free for that # Safety - `result` must be a valid pointer to an FFIManagedCoreAccountResult - The error_message field must be either null or a valid CString allocated by this library - The caller must ensure the result pointer remains valid for the duration of this call - -**Safety:** -- `result` must be a valid pointer to an FFIManagedCoreAccountResult - The error_message field must be either null or a valid CString allocated by this library - The caller must ensure the result pointer remains valid for the duration of this call - -**Module:** `managed_account` - ---- - -#### `managed_platform_account_result_free_error` - -```c -managed_platform_account_result_free_error(result: *mut FFIManagedPlatformAccountResult,) -> () -``` - -**Description:** -Free a managed platform account result's error message (if any) Note: This does NOT free the account handle itself - use managed_platform_account_free for that # Safety - `result` must be a valid pointer to an FFIManagedPlatformAccountResult - The error_message field must be either null or a valid CString allocated by this library - The caller must ensure the result pointer remains valid for the duration of this call - -**Safety:** -- `result` must be a valid pointer to an FFIManagedPlatformAccountResult - The error_message field must be either null or a valid CString allocated by this library - The caller must ensure the result pointer remains valid for the duration of this call - -**Module:** `managed_account` - ---- - -### Wallet Manager - Detailed - -#### `wallet_manager_add_wallet_from_mnemonic` - -```c -wallet_manager_add_wallet_from_mnemonic(manager: *mut FFIWalletManager, mnemonic: *const c_char, error: *mut FFIError,) -> bool -``` - -**Description:** -Add a wallet from mnemonic to the manager (backward compatibility) # Safety - `manager` must be a valid pointer to an FFIWalletManager instance - `mnemonic` must be a valid pointer to a null-terminated C string - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - -**Safety:** -- `manager` must be a valid pointer to an FFIWalletManager instance - `mnemonic` must be a valid pointer to a null-terminated C string - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - -**Module:** `wallet_manager` - ---- - -#### `wallet_manager_add_wallet_from_mnemonic_return_serialized_bytes` - -```c -wallet_manager_add_wallet_from_mnemonic_return_serialized_bytes(manager: *mut FFIWalletManager, mnemonic: *const c_char, birth_height: c_uint, account_options: *const crate::types::FFIWalletAccountCreationOptions, downgrade_to_pubkey_wallet: bool, allow_external_signing: bool, wallet_bytes_out: *mut *mut u8, wallet_bytes_len_out: *mut usize, wallet_id_out: *mut u8, error: *mut FFIError,) -> bool -``` - -**Module:** `wallet_manager` - ---- - -#### `wallet_manager_add_wallet_from_mnemonic_with_options` - -```c -wallet_manager_add_wallet_from_mnemonic_with_options(manager: *mut FFIWalletManager, mnemonic: *const c_char, account_options: *const crate::types::FFIWalletAccountCreationOptions, error: *mut FFIError,) -> bool -``` - -**Description:** -Add a wallet from mnemonic to the manager with options # Safety - `manager` must be a valid pointer to an FFIWalletManager instance - `mnemonic` must be a valid pointer to a null-terminated C string - `account_options` must be a valid pointer to FFIWalletAccountCreationOptions or null - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - -**Safety:** -- `manager` must be a valid pointer to an FFIWalletManager instance - `mnemonic` must be a valid pointer to a null-terminated C string - `account_options` must be a valid pointer to FFIWalletAccountCreationOptions or null - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - -**Module:** `wallet_manager` - ---- - -#### `wallet_manager_create` - -```c -wallet_manager_create(network: FFINetwork, error: *mut FFIError,) -> *mut FFIWalletManager -``` - -**Description:** -Create a new wallet manager # Safety `error` must be a valid pointer to an `FFIError`. The returned pointer must be freed with `wallet_manager_free`. - -**Safety:** -`error` must be a valid pointer to an `FFIError`. The returned pointer must be freed with `wallet_manager_free`. - -**Module:** `wallet_manager` - ---- - -#### `wallet_manager_current_height` - -```c -wallet_manager_current_height(manager: *const FFIWalletManager, error: *mut FFIError,) -> c_uint -``` - -**Description:** -Get current height for a network # Safety - `manager` must be a valid pointer to an FFIWalletManager - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - -**Safety:** -- `manager` must be a valid pointer to an FFIWalletManager - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - -**Module:** `wallet_manager` - ---- - -#### `wallet_manager_describe` - -```c -wallet_manager_describe(manager: *const FFIWalletManager, error: *mut FFIError,) -> *mut c_char -``` - -**Description:** -Describe the wallet manager for a given network and return a newly allocated C string. # Safety - `manager` must be a valid pointer to an `FFIWalletManager` - Callers must free the returned string with `wallet_manager_free_string` - -**Safety:** -- `manager` must be a valid pointer to an `FFIWalletManager` - Callers must free the returned string with `wallet_manager_free_string` - -**Module:** `wallet_manager` - ---- - -#### `wallet_manager_free` - -```c -wallet_manager_free(manager: *mut FFIWalletManager) -> () -``` - -**Description:** -Free wallet manager # Safety - `manager` must be a valid pointer to an FFIWalletManager that was created by this library - The pointer must not be used after calling this function - This function must only be called once per manager - -**Safety:** -- `manager` must be a valid pointer to an FFIWalletManager that was created by this library - The pointer must not be used after calling this function - This function must only be called once per manager - -**Module:** `wallet_manager` - ---- - -#### `wallet_manager_free_addresses` - -```c -wallet_manager_free_addresses(addresses: *mut *mut c_char, count: usize) -> () -``` - -**Description:** -Free address array # Safety - `addresses` must be a valid pointer to an array of C string pointers allocated by this library - `count` must match the original allocation size - Each address pointer in the array must be either null or a valid C string allocated by this library - The pointers must not be used after calling this function - This function must only be called once per allocation - -**Safety:** -- `addresses` must be a valid pointer to an array of C string pointers allocated by this library - `count` must match the original allocation size - Each address pointer in the array must be either null or a valid C string allocated by this library - The pointers must not be used after calling this function - This function must only be called once per allocation - -**Module:** `wallet_manager` - ---- - -#### `wallet_manager_free_string` - -```c -wallet_manager_free_string(value: *mut c_char) -> () -``` - -**Description:** -Free a string previously returned by wallet manager APIs. # Safety - `value` must be either null or a pointer obtained from `wallet_manager_describe` (or other wallet manager FFI helpers that specify this free function). - The pointer must not be used after this call returns. - -**Safety:** -- `value` must be either null or a pointer obtained from `wallet_manager_describe` (or other wallet manager FFI helpers that specify this free function). - The pointer must not be used after this call returns. - -**Module:** `wallet_manager` - ---- - -#### `wallet_manager_free_wallet_bytes` - -```c -wallet_manager_free_wallet_bytes(wallet_bytes: *mut u8, bytes_len: usize) -> () -``` - -**Module:** `wallet_manager` - ---- - -#### `wallet_manager_free_wallet_ids` - -```c -wallet_manager_free_wallet_ids(wallet_ids: *mut u8, count: usize) -> () -``` - -**Description:** -Free wallet IDs buffer # Safety - `wallet_ids` must be a valid pointer to a buffer allocated by this library - `count` must match the number of wallet IDs in the buffer - The pointer must not be used after calling this function - This function must only be called once per buffer - -**Safety:** -- `wallet_ids` must be a valid pointer to a buffer allocated by this library - `count` must match the number of wallet IDs in the buffer - The pointer must not be used after calling this function - This function must only be called once per buffer - -**Module:** `wallet_manager` - ---- - -#### `wallet_manager_get_managed_wallet_info` - -```c -wallet_manager_get_managed_wallet_info(manager: *const FFIWalletManager, wallet_id: *const u8, error: *mut FFIError,) -> *mut crate::managed_wallet::FFIManagedWalletInfo -``` - -**Description:** -Get managed wallet info from the manager Returns a reference to the managed wallet info if found # Safety - `manager` must be a valid pointer to an FFIWalletManager instance - `wallet_id` must be a valid pointer to a 32-byte wallet ID - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - The returned managed wallet info must be freed with managed_wallet_info_free() - -**Safety:** -- `manager` must be a valid pointer to an FFIWalletManager instance - `wallet_id` must be a valid pointer to a 32-byte wallet ID - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - The returned managed wallet info must be freed with managed_wallet_info_free() - -**Module:** `wallet_manager` - ---- - -#### `wallet_manager_get_wallet` - -```c -wallet_manager_get_wallet(manager: *const FFIWalletManager, wallet_id: *const u8, error: *mut FFIError,) -> *const crate::types::FFIWallet -``` - -**Description:** -Get a wallet from the manager Returns a reference to the wallet if found # Safety - `manager` must be a valid pointer to an FFIWalletManager instance - `wallet_id` must be a valid pointer to a 32-byte wallet ID - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - The returned wallet must be freed with wallet_free_const() - -**Safety:** -- `manager` must be a valid pointer to an FFIWalletManager instance - `wallet_id` must be a valid pointer to a 32-byte wallet ID - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - The returned wallet must be freed with wallet_free_const() - -**Module:** `wallet_manager` - ---- - -#### `wallet_manager_get_wallet_balance` - -```c -wallet_manager_get_wallet_balance(manager: *const FFIWalletManager, wallet_id: *const u8, confirmed_out: *mut u64, unconfirmed_out: *mut u64, error: *mut FFIError,) -> bool -``` - -**Description:** -Get wallet balance Returns the confirmed and unconfirmed balance for a specific wallet # Safety - `manager` must be a valid pointer to an FFIWalletManager instance - `wallet_id` must be a valid pointer to a 32-byte wallet ID - `confirmed_out` must be a valid pointer to a u64 (maps to C uint64_t) - `unconfirmed_out` must be a valid pointer to a u64 (maps to C uint64_t) - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - -**Safety:** -- `manager` must be a valid pointer to an FFIWalletManager instance - `wallet_id` must be a valid pointer to a 32-byte wallet ID - `confirmed_out` must be a valid pointer to a u64 (maps to C uint64_t) - `unconfirmed_out` must be a valid pointer to a u64 (maps to C uint64_t) - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - -**Module:** `wallet_manager` - ---- - -#### `wallet_manager_get_wallet_ids` - -```c -wallet_manager_get_wallet_ids(manager: *const FFIWalletManager, wallet_ids_out: *mut *mut u8, count_out: *mut usize, error: *mut FFIError,) -> bool -``` - -**Description:** -Get wallet IDs # Safety - `manager` must be a valid pointer to an FFIWalletManager - `wallet_ids_out` must be a valid pointer to a pointer that will receive the wallet IDs - `count_out` must be a valid pointer to receive the count - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - -**Safety:** -- `manager` must be a valid pointer to an FFIWalletManager - `wallet_ids_out` must be a valid pointer to a pointer that will receive the wallet IDs - `count_out` must be a valid pointer to receive the count - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - -**Module:** `wallet_manager` - ---- - -#### `wallet_manager_import_wallet_from_bytes` - -```c -wallet_manager_import_wallet_from_bytes(manager: *mut FFIWalletManager, wallet_bytes: *const u8, wallet_bytes_len: usize, wallet_id_out: *mut u8, error: *mut FFIError,) -> bool -``` - -**Module:** `wallet_manager` - ---- - -#### `wallet_manager_network` - -```c -wallet_manager_network(manager: *const FFIWalletManager, error: *mut FFIError,) -> FFINetwork -``` - -**Description:** -Get the network for this wallet manager # Safety - `manager` must be a valid pointer to an FFIWalletManager - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - -**Safety:** -- `manager` must be a valid pointer to an FFIWalletManager - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - -**Module:** `wallet_manager` - ---- - -#### `wallet_manager_process_transaction` - -```c -wallet_manager_process_transaction(manager: *mut FFIWalletManager, tx_bytes: *const u8, tx_len: usize, context: *const crate::types::FFITransactionContext, update_state_if_found: bool, error: *mut FFIError,) -> bool -``` - -**Description:** -Process a transaction through all wallets Checks a transaction against all wallets and updates their states if relevant. Returns true if the transaction was relevant to at least one wallet. # Safety - `manager` must be a valid pointer to an FFIWalletManager instance - `tx_bytes` must be a valid pointer to transaction bytes - `tx_len` must be the length of the transaction bytes - `context` must be a valid pointer to FFITransactionContext - `update_state_if_found` indicates whether to update wallet state when transaction is relevant - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - -**Safety:** -- `manager` must be a valid pointer to an FFIWalletManager instance - `tx_bytes` must be a valid pointer to transaction bytes - `tx_len` must be the length of the transaction bytes - `context` must be a valid pointer to FFITransactionContext - `update_state_if_found` indicates whether to update wallet state when transaction is relevant - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - -**Module:** `wallet_manager` - ---- - -#### `wallet_manager_set_transaction_label` - -```c -wallet_manager_set_transaction_label(manager: *mut FFIWalletManager, wallet_id: *const u8, account_type: FFIAccountKind, account_index: c_uint, txid: *const u8, label: *const c_char, error: *mut FFIError,) -> bool -``` - -**Description:** -Set or clear a label on a transaction record in the shared wallet manager state # Safety - `manager` must be a valid pointer to an FFIWalletManager instance - `wallet_id` must be a valid pointer to a 32-byte wallet ID - `txid` must be a valid pointer to a 32-byte transaction ID - `label` must be a valid null-terminated UTF-8 string, or null to clear the label - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - -**Safety:** -- `manager` must be a valid pointer to an FFIWalletManager instance - `wallet_id` must be a valid pointer to a 32-byte wallet ID - `txid` must be a valid pointer to a 32-byte transaction ID - `label` must be a valid null-terminated UTF-8 string, or null to clear the label - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - -**Module:** `managed_account` - ---- - -#### `wallet_manager_wallet_count` - -```c -wallet_manager_wallet_count(manager: *const FFIWalletManager, error: *mut FFIError,) -> usize -``` - -**Description:** -Get wallet count # Safety - `manager` must be a valid pointer to an FFIWalletManager instance - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - -**Safety:** -- `manager` must be a valid pointer to an FFIWalletManager instance - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - -**Module:** `wallet_manager` - ---- - -### Wallet Operations - Detailed - -#### `account_get_parent_wallet_id` - -```c -account_get_parent_wallet_id(account: *const FFIAccount) -> *const u8 -``` - -**Description:** -Get the parent wallet ID of an account # Safety - `account` must be a valid pointer to an FFIAccount instance - Returns a pointer to the 32-byte wallet ID, or NULL if not set or account is null - The returned pointer is valid only as long as the account exists - The caller should copy the data if needed for longer use - -**Safety:** -- `account` must be a valid pointer to an FFIAccount instance - Returns a pointer to the 32-byte wallet ID, or NULL if not set or account is null - The returned pointer is valid only as long as the account exists - The caller should copy the data if needed for longer use - -**Module:** `account` - ---- - -#### `bls_account_get_parent_wallet_id` - -```c -bls_account_get_parent_wallet_id(account: *const FFIBLSAccount,) -> *const u8 -``` - -**Module:** `account` - ---- - -#### `eddsa_account_get_parent_wallet_id` - -```c -eddsa_account_get_parent_wallet_id(account: *const FFIEdDSAAccount,) -> *const u8 -``` - -**Module:** `account` - ---- - -#### `ffi_managed_wallet_free` - -```c -ffi_managed_wallet_free(managed_wallet: *mut FFIManagedWalletInfo) -> () -``` - -**Description:** -Free a managed wallet (FFIManagedWalletInfo type) # Safety - `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo - This function must only be called once per managed wallet - -**Safety:** -- `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo - This function must only be called once per managed wallet - -**Module:** `transaction_checking` - ---- - -#### `key_wallet_derive_address_from_key` - -```c -key_wallet_derive_address_from_key(private_key: *const u8, network: FFINetwork,) -> *mut c_char -``` - -**Description:** -Derive an address from a private key # Safety - `private_key` must be a valid pointer to 32 bytes - `network` is the network for the address # Returns - Pointer to C string with address (caller must free) - NULL on error - -**Safety:** -- `private_key` must be a valid pointer to 32 bytes - `network` is the network for the address - -**Module:** `derivation` - ---- - -#### `key_wallet_derive_address_from_seed` - -```c -key_wallet_derive_address_from_seed(seed: *const u8, network: FFINetwork, path: *const c_char,) -> *mut c_char -``` - -**Description:** -Derive an address from a seed at a specific derivation path # Safety - `seed` must be a valid pointer to 64 bytes - `network` is the network for the address - `path` must be a valid null-terminated C string (e.g., "m/44'/5'/0'/0/0") # Returns - Pointer to C string with address (caller must free) - NULL on error - -**Safety:** -- `seed` must be a valid pointer to 64 bytes - `network` is the network for the address - `path` must be a valid null-terminated C string (e.g., "m/44'/5'/0'/0/0") - -**Module:** `derivation` - ---- - -#### `key_wallet_derive_private_key_from_seed` - -```c -key_wallet_derive_private_key_from_seed(seed: *const u8, path: *const c_char, key_out: *mut u8,) -> i32 -``` - -**Description:** -Derive a private key from a seed at a specific derivation path # Safety - `seed` must be a valid pointer to 64 bytes - `path` must be a valid null-terminated C string (e.g., "m/44'/5'/0'/0/0") - `key_out` must be a valid pointer to a buffer of at least 32 bytes # Returns - 0 on success - -1 on error - -**Safety:** -- `seed` must be a valid pointer to 64 bytes - `path` must be a valid null-terminated C string (e.g., "m/44'/5'/0'/0/0") - `key_out` must be a valid pointer to a buffer of at least 32 bytes - -**Module:** `derivation` - ---- - -#### `managed_core_account_get_parent_wallet_id` - -```c -managed_core_account_get_parent_wallet_id(wallet_id: *const u8,) -> *const u8 -``` - -**Description:** -Get the parent wallet ID of a managed account Note: ManagedAccount doesn't store the parent wallet ID directly. The wallet ID is typically known from the context (e.g., when getting the account from a managed wallet). # Safety - `wallet_id` must be a valid pointer to a 32-byte wallet ID buffer that was provided by the caller - The returned pointer is the same as the input pointer for convenience - The caller must not free the returned pointer as it's the same as the input - -**Safety:** -- `wallet_id` must be a valid pointer to a 32-byte wallet ID buffer that was provided by the caller - The returned pointer is the same as the input pointer for convenience - The caller must not free the returned pointer as it's the same as the input - -**Module:** `managed_account` - ---- - -#### `managed_wallet_check_transaction` - -```c -managed_wallet_check_transaction(managed_wallet: *mut FFIManagedWalletInfo, wallet: *mut FFIWallet, tx_bytes: *const u8, tx_len: usize, context_type: FFITransactionContextType, block_info: FFIBlockInfo, islock_data: *const u8, islock_len: usize, update_state: bool, result_out: *mut FFITransactionCheckResult, error: *mut FFIError,) -> bool -``` - -**Description:** -Check if a transaction belongs to the wallet This function checks a transaction against all relevant account types in the wallet and returns detailed information about which accounts are affected. # Safety - `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo - `wallet` must be a valid pointer to an FFIWallet (needed for address generation and DashPay queries) - `tx_bytes` must be a valid pointer to transaction bytes with at least `tx_len` bytes - `result_out` must be a valid pointer to store the result - `error` must be a valid pointer to an FFIError - The affected_accounts array in the result must be freed with `transaction_check_result_free` - -**Safety:** -- `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo - `wallet` must be a valid pointer to an FFIWallet (needed for address generation and DashPay queries) - `tx_bytes` must be a valid pointer to transaction bytes with at least `tx_len` bytes - `result_out` must be a valid pointer to store the result - `error` must be a valid pointer to an FFIError - The affected_accounts array in the result must be freed with `transaction_check_result_free` - -**Module:** `transaction_checking` - ---- - -#### `managed_wallet_free` - -```c -managed_wallet_free(managed_wallet: *mut FFIManagedWalletInfo) -> () -``` - -**Description:** -Free managed wallet info # Safety - `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo or null - After calling this function, the pointer becomes invalid and must not be used - -**Safety:** -- `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo or null - After calling this function, the pointer becomes invalid and must not be used - -**Module:** `managed_wallet` - ---- - -#### `managed_wallet_generate_addresses_to_index` - -```c -managed_wallet_generate_addresses_to_index(managed_wallet: *mut FFIManagedWalletInfo, wallet: *const FFIWallet, account_type: FFIAccountKind, account_index: c_uint, pool_type: FFIAddressPoolType, target_index: c_uint, error: *mut FFIError,) -> bool -``` - -**Description:** -Generate addresses up to a specific index in a pool This ensures that addresses up to and including the specified index exist in the pool. This is useful for wallet recovery or when specific indices are needed. # Safety - `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo - `wallet` must be a valid pointer to an FFIWallet (for key derivation) - `error` must be a valid pointer to an FFIError - -**Safety:** -- `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo - `wallet` must be a valid pointer to an FFIWallet (for key derivation) - `error` must be a valid pointer to an FFIError - -**Module:** `address_pool` - ---- - -#### `managed_wallet_get_account` - -```c -managed_wallet_get_account(manager: *const FFIWalletManager, wallet_id: *const u8, account_index: c_uint, account_type: FFIAccountKind,) -> FFIManagedCoreAccountResult -``` - -**Description:** -Get a managed account from a managed wallet This function gets a ManagedAccount from the wallet manager's managed wallet info, returning a managed account handle that wraps the ManagedAccount. # Safety - `manager` must be a valid pointer to an FFIWalletManager instance - `wallet_id` must be a valid pointer to a 32-byte wallet ID - The caller must ensure all pointers remain valid for the duration of this call - The returned account must be freed with `managed_core_account_free` when no longer needed - -**Safety:** -- `manager` must be a valid pointer to an FFIWalletManager instance - `wallet_id` must be a valid pointer to a 32-byte wallet ID - The caller must ensure all pointers remain valid for the duration of this call - The returned account must be freed with `managed_core_account_free` when no longer needed - -**Module:** `managed_account` - ---- - -#### `managed_wallet_get_account_collection` - -```c -managed_wallet_get_account_collection(manager: *const FFIWalletManager, wallet_id: *const u8, error: *mut FFIError,) -> *mut FFIManagedCoreAccountCollection -``` - -**Description:** -Get managed account collection for a specific network from wallet manager # Safety - `manager` must be a valid pointer to an FFIWalletManager instance - `wallet_id` must be a valid pointer to a 32-byte wallet ID - `error` must be a valid pointer to an FFIError structure - The returned pointer must be freed with `managed_account_collection_free` when no longer needed - -**Safety:** -- `manager` must be a valid pointer to an FFIWalletManager instance - `wallet_id` must be a valid pointer to a 32-byte wallet ID - `error` must be a valid pointer to an FFIError structure - The returned pointer must be freed with `managed_account_collection_free` when no longer needed - -**Module:** `managed_account_collection` - ---- - -#### `managed_wallet_get_account_count` - -```c -managed_wallet_get_account_count(manager: *const FFIWalletManager, wallet_id: *const u8, error: *mut FFIError,) -> c_uint -``` - -**Description:** -Get number of accounts in a managed wallet # Safety - `manager` must be a valid pointer to an FFIWalletManager instance - `wallet_id` must be a valid pointer to a 32-byte wallet ID - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - -**Safety:** -- `manager` must be a valid pointer to an FFIWalletManager instance - `wallet_id` must be a valid pointer to a 32-byte wallet ID - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - -**Module:** `managed_account` - ---- - -#### `managed_wallet_get_address_pool_info` - -```c -managed_wallet_get_address_pool_info(managed_wallet: *const FFIManagedWalletInfo, account_type: FFIAccountKind, account_index: c_uint, pool_type: FFIAddressPoolType, info_out: *mut FFIAddressPoolInfo, error: *mut FFIError,) -> bool -``` - -**Description:** -Get address pool information for an account # Safety - `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo - `info_out` must be a valid pointer to store the pool info - `error` must be a valid pointer to an FFIError - -**Safety:** -- `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo - `info_out` must be a valid pointer to store the pool info - `error` must be a valid pointer to an FFIError - -**Module:** `address_pool` - ---- - -#### `managed_wallet_get_balance` - -```c -managed_wallet_get_balance(managed_wallet: *const FFIManagedWalletInfo, confirmed_out: *mut u64, unconfirmed_out: *mut u64, immature_out: *mut u64, locked_out: *mut u64, total_out: *mut u64, error: *mut FFIError,) -> bool -``` - -**Description:** -Get wallet balance from managed wallet info Returns the balance breakdown including confirmed, unconfirmed, immature, locked, and total amounts. # Safety - `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo - `confirmed_out` must be a valid pointer to store the confirmed balance - `unconfirmed_out` must be a valid pointer to store the unconfirmed balance - `immature_out` must be a valid pointer to store the immature balance - `locked_out` must be a valid pointer to store the locked balance - `total_out` must be a valid pointer to store the total balance - `error` must be a valid pointer to an FFIError - -**Safety:** -- `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo - `confirmed_out` must be a valid pointer to store the confirmed balance - `unconfirmed_out` must be a valid pointer to store the unconfirmed balance - `immature_out` must be a valid pointer to store the immature balance - `locked_out` must be a valid pointer to store the locked balance - `total_out` must be a valid pointer to store the total balance - `error` must be a valid pointer to an FFIError - -**Module:** `managed_wallet` - ---- - -#### `managed_wallet_get_bip_44_external_address_range` - -```c -managed_wallet_get_bip_44_external_address_range(managed_wallet: *mut FFIManagedWalletInfo, wallet: *const FFIWallet, account_index: std::os::raw::c_uint, start_index: std::os::raw::c_uint, end_index: std::os::raw::c_uint, addresses_out: *mut *mut *mut c_char, count_out: *mut usize, error: *mut FFIError,) -> bool -``` - -**Description:** -Get BIP44 external (receive) addresses in the specified range Returns external addresses from start_index (inclusive) to end_index (exclusive). If addresses in the range haven't been generated yet, they will be generated. # Safety - `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo - `wallet` must be a valid pointer to an FFIWallet - `addresses_out` must be a valid pointer to store the address array pointer - `count_out` must be a valid pointer to store the count - `error` must be a valid pointer to an FFIError - Free the result with address_array_free(addresses_out, count_out) - -**Safety:** -- `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo - `wallet` must be a valid pointer to an FFIWallet - `addresses_out` must be a valid pointer to store the address array pointer - `count_out` must be a valid pointer to store the count - `error` must be a valid pointer to an FFIError - Free the result with address_array_free(addresses_out, count_out) - -**Module:** `managed_wallet` - ---- - -#### `managed_wallet_get_bip_44_internal_address_range` - -```c -managed_wallet_get_bip_44_internal_address_range(managed_wallet: *mut FFIManagedWalletInfo, wallet: *const FFIWallet, account_index: std::os::raw::c_uint, start_index: std::os::raw::c_uint, end_index: std::os::raw::c_uint, addresses_out: *mut *mut *mut c_char, count_out: *mut usize, error: *mut FFIError,) -> bool -``` - -**Description:** -Get BIP44 internal (change) addresses in the specified range Returns internal addresses from start_index (inclusive) to end_index (exclusive). If addresses in the range haven't been generated yet, they will be generated. # Safety - `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo - `wallet` must be a valid pointer to an FFIWallet - `addresses_out` must be a valid pointer to store the address array pointer - `count_out` must be a valid pointer to store the count - `error` must be a valid pointer to an FFIError - Free the result with address_array_free(addresses_out, count_out) - -**Safety:** -- `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo - `wallet` must be a valid pointer to an FFIWallet - `addresses_out` must be a valid pointer to store the address array pointer - `count_out` must be a valid pointer to store the count - `error` must be a valid pointer to an FFIError - Free the result with address_array_free(addresses_out, count_out) - -**Module:** `managed_wallet` - ---- - -#### `managed_wallet_get_dashpay_external_account` - -```c -managed_wallet_get_dashpay_external_account(manager: *const FFIWalletManager, wallet_id: *const u8, account_index: c_uint, user_identity_id: *const u8, friend_identity_id: *const u8,) -> FFIManagedCoreAccountResult -``` - -**Description:** -Get a managed DashPay external account by composite key # Safety - Pointers must be valid - -**Safety:** -- Pointers must be valid - -**Module:** `managed_account` - ---- - -#### `managed_wallet_get_dashpay_receiving_account` - -```c -managed_wallet_get_dashpay_receiving_account(manager: *const FFIWalletManager, wallet_id: *const u8, account_index: c_uint, user_identity_id: *const u8, friend_identity_id: *const u8,) -> FFIManagedCoreAccountResult -``` - -**Description:** -Get a managed DashPay receiving funds account by composite key # Safety - `manager`, `wallet_id` must be valid - `user_identity_id` and `friend_identity_id` must each point to 32 bytes - -**Safety:** -- `manager`, `wallet_id` must be valid - `user_identity_id` and `friend_identity_id` must each point to 32 bytes - -**Module:** `managed_account` - ---- - -#### `managed_wallet_get_next_bip44_change_address` - -```c -managed_wallet_get_next_bip44_change_address(managed_wallet: *mut FFIManagedWalletInfo, wallet: *const FFIWallet, account_index: std::os::raw::c_uint, error: *mut FFIError,) -> *mut c_char -``` - -**Description:** -Get the next unused change address Generates the next unused change address for the specified account. This properly manages address gaps and updates the managed wallet state. # Safety - `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo - `wallet` must be a valid pointer to an FFIWallet - `error` must be a valid pointer to an FFIError - The returned string must be freed by the caller - -**Safety:** -- `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo - `wallet` must be a valid pointer to an FFIWallet - `error` must be a valid pointer to an FFIError - The returned string must be freed by the caller - -**Module:** `managed_wallet` - ---- - -#### `managed_wallet_get_next_bip44_receive_address` - -```c -managed_wallet_get_next_bip44_receive_address(managed_wallet: *mut FFIManagedWalletInfo, wallet: *const FFIWallet, account_index: std::os::raw::c_uint, error: *mut FFIError,) -> *mut c_char -``` - -**Description:** -Get the next unused receive address Generates the next unused receive address for the specified account. This properly manages address gaps and updates the managed wallet state. # Safety - `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo - `wallet` must be a valid pointer to an FFIWallet - `error` must be a valid pointer to an FFIError - The returned string must be freed by the caller - -**Safety:** -- `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo - `wallet` must be a valid pointer to an FFIWallet - `error` must be a valid pointer to an FFIError - The returned string must be freed by the caller - -**Module:** `managed_wallet` - ---- - -#### `managed_wallet_get_platform_payment_account` - -```c -managed_wallet_get_platform_payment_account(manager: *const FFIWalletManager, wallet_id: *const u8, account_index: c_uint, key_class: c_uint,) -> FFIManagedPlatformAccountResult -``` - -**Description:** -Get a managed platform payment account from a managed wallet Platform Payment accounts (DIP-17) are identified by account index and key_class. Returns a platform account handle that wraps the ManagedPlatformAccount. # Safety - `manager` must be a valid pointer to an FFIWalletManager instance - `wallet_id` must be a valid pointer to a 32-byte wallet ID - The caller must ensure all pointers remain valid for the duration of this call - The returned account must be freed with `managed_platform_account_free` when no longer needed - -**Safety:** -- `manager` must be a valid pointer to an FFIWalletManager instance - `wallet_id` must be a valid pointer to a 32-byte wallet ID - The caller must ensure all pointers remain valid for the duration of this call - The returned account must be freed with `managed_platform_account_free` when no longer needed - -**Module:** `managed_account` - ---- - -#### `managed_wallet_get_top_up_account_with_registration_index` - -```c -managed_wallet_get_top_up_account_with_registration_index(manager: *const FFIWalletManager, wallet_id: *const u8, registration_index: c_uint,) -> FFIManagedCoreAccountResult -``` - -**Description:** -Get a managed IdentityTopUp account with a specific registration index This is used for top-up accounts that are bound to a specific identity. Returns a managed account handle that wraps the ManagedAccount. # Safety - `manager` must be a valid pointer to an FFIWalletManager instance - `wallet_id` must be a valid pointer to a 32-byte wallet ID - The caller must ensure all pointers remain valid for the duration of this call - The returned account must be freed with `managed_core_account_free` when no longer needed - -**Safety:** -- `manager` must be a valid pointer to an FFIWalletManager instance - `wallet_id` must be a valid pointer to a 32-byte wallet ID - The caller must ensure all pointers remain valid for the duration of this call - The returned account must be freed with `managed_core_account_free` when no longer needed - -**Module:** `managed_account` - ---- - -#### `managed_wallet_get_utxos` - -```c -managed_wallet_get_utxos(managed_info: *const FFIManagedWalletInfo, utxos_out: *mut *mut FFIUTXO, count_out: *mut usize, error: *mut FFIError,) -> bool -``` - -**Description:** -Get all UTXOs from managed wallet info # Safety - `managed_info` must be a valid pointer to an FFIManagedWalletInfo instance - `utxos_out` must be a valid pointer to store the UTXO array pointer - `count_out` must be a valid pointer to store the UTXO count - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - The returned UTXO array must be freed with `utxo_array_free` when no longer needed - -**Safety:** -- `managed_info` must be a valid pointer to an FFIManagedWalletInfo instance - `utxos_out` must be a valid pointer to store the UTXO array pointer - `count_out` must be a valid pointer to store the UTXO count - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - The returned UTXO array must be freed with `utxo_array_free` when no longer needed - -**Module:** `utxo` - ---- - -#### `managed_wallet_info_free` - -```c -managed_wallet_info_free(wallet_info: *mut FFIManagedWalletInfo) -> () -``` - -**Description:** -Free managed wallet info returned by wallet_manager_get_managed_wallet_info # Safety - `wallet_info` must be a valid pointer returned by wallet_manager_get_managed_wallet_info or null - After calling this function, the pointer becomes invalid and must not be used - -**Safety:** -- `wallet_info` must be a valid pointer returned by wallet_manager_get_managed_wallet_info or null - After calling this function, the pointer becomes invalid and must not be used - -**Module:** `managed_wallet` - ---- - -#### `managed_wallet_last_processed_height` - -```c -managed_wallet_last_processed_height(managed_wallet: *const FFIManagedWalletInfo, error: *mut FFIError,) -> c_uint -``` - -**Description:** -Get current last processed height from wallet info # Safety - `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - -**Safety:** -- `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - -**Module:** `managed_wallet` - ---- - -#### `managed_wallet_mark_address_used` - -```c -managed_wallet_mark_address_used(managed_wallet: *mut FFIManagedWalletInfo, address: *const c_char, error: *mut FFIError,) -> bool -``` - -**Description:** -Mark an address as used in the pool This updates the pool's tracking of which addresses have been used, which is important for gap limit management and wallet recovery. # Safety - `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo - `address` must be a valid C string - `error` must be a valid pointer to an FFIError - -**Safety:** -- `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo - `address` must be a valid C string - `error` must be a valid pointer to an FFIError - -**Module:** `address_pool` - ---- - -#### `managed_wallet_set_gap_limit` - -```c -managed_wallet_set_gap_limit(managed_wallet: *mut FFIManagedWalletInfo, account_type: FFIAccountKind, account_index: c_uint, pool_type: FFIAddressPoolType, gap_limit: c_uint, error: *mut FFIError,) -> bool -``` - -**Description:** -Set the gap limit for an address pool The gap limit determines how many unused addresses to maintain at the end of the pool. This is important for wallet recovery and address discovery. # Safety - `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo - `error` must be a valid pointer to an FFIError - -**Safety:** -- `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo - `error` must be a valid pointer to an FFIError - -**Module:** `address_pool` - ---- - -#### `wallet_add_account` - -```c -wallet_add_account(wallet: *mut FFIWallet, account_type: crate::types::FFIAccountKind, account_index: c_uint,) -> crate::types::FFIAccountResult -``` - -**Description:** -Add an account to the wallet without xpub # Safety This function dereferences a raw pointer to FFIWallet. The caller must ensure that: - The wallet pointer is either null or points to a valid FFIWallet - The FFIWallet remains valid for the duration of this call # Note This function does NOT support the following account types: - `PlatformPayment`: Use `wallet_add_platform_payment_account()` instead - `DashpayReceivingFunds`: Use `wallet_add_dashpay_receiving_account()` instead - `DashpayExternalAccount`: Use `wallet_add_dashpay_external_account_with_xpub_bytes()` instead - -**Safety:** -This function dereferences a raw pointer to FFIWallet. The caller must ensure that: - The wallet pointer is either null or points to a valid FFIWallet - The FFIWallet remains valid for the duration of this call - -**Module:** `wallet` - ---- - -#### `wallet_add_account_with_string_xpub` - -```c -wallet_add_account_with_string_xpub(wallet: *mut FFIWallet, account_type: crate::types::FFIAccountKind, account_index: c_uint, xpub_string: *const c_char,) -> crate::types::FFIAccountResult -``` - -**Description:** -Add an account to the wallet with xpub as string # Safety This function dereferences raw pointers. The caller must ensure that: - The wallet pointer is either null or points to a valid FFIWallet - The xpub_string pointer is either null or points to a valid null-terminated C string - The FFIWallet remains valid for the duration of this call # Note This function does NOT support the following account types: - `PlatformPayment`: Use `wallet_add_platform_payment_account()` instead - `DashpayReceivingFunds`: Use `wallet_add_dashpay_receiving_account()` instead - `DashpayExternalAccount`: Use `wallet_add_dashpay_external_account_with_xpub_bytes()` instead - -**Safety:** -This function dereferences raw pointers. The caller must ensure that: - The wallet pointer is either null or points to a valid FFIWallet - The xpub_string pointer is either null or points to a valid null-terminated C string - The FFIWallet remains valid for the duration of this call - -**Module:** `wallet` - ---- - -#### `wallet_add_account_with_xpub_bytes` - -```c -wallet_add_account_with_xpub_bytes(wallet: *mut FFIWallet, account_type: crate::types::FFIAccountKind, account_index: c_uint, xpub_bytes: *const u8, xpub_len: usize,) -> crate::types::FFIAccountResult -``` - -**Description:** -Add an account to the wallet with xpub as byte array # Safety This function dereferences raw pointers. The caller must ensure that: - The wallet pointer is either null or points to a valid FFIWallet - The xpub_bytes pointer is either null or points to at least xpub_len bytes - The FFIWallet remains valid for the duration of this call # Note This function does NOT support the following account types: - `PlatformPayment`: Use `wallet_add_platform_payment_account()` instead - `DashpayReceivingFunds`: Use `wallet_add_dashpay_receiving_account()` instead - `DashpayExternalAccount`: Use `wallet_add_dashpay_external_account_with_xpub_bytes()` instead - -**Safety:** -This function dereferences raw pointers. The caller must ensure that: - The wallet pointer is either null or points to a valid FFIWallet - The xpub_bytes pointer is either null or points to at least xpub_len bytes - The FFIWallet remains valid for the duration of this call - -**Module:** `wallet` - ---- - -#### `wallet_add_dashpay_external_account_with_xpub_bytes` - -```c -wallet_add_dashpay_external_account_with_xpub_bytes(wallet: *mut FFIWallet, account_index: c_uint, user_identity_id: *const u8, friend_identity_id: *const u8, xpub_bytes: *const u8, xpub_len: usize,) -> FFIAccountResult -``` - -**Description:** -Add a DashPay external (watch-only) account with xpub bytes # Safety - `wallet` must be valid, `xpub_bytes` must point to `xpub_len` bytes - `user_identity_id` and `friend_identity_id` must each point to 32 bytes - -**Safety:** -- `wallet` must be valid, `xpub_bytes` must point to `xpub_len` bytes - `user_identity_id` and `friend_identity_id` must each point to 32 bytes - -**Module:** `wallet` - ---- - -#### `wallet_add_dashpay_receiving_account` - -```c -wallet_add_dashpay_receiving_account(wallet: *mut FFIWallet, account_index: c_uint, user_identity_id: *const u8, friend_identity_id: *const u8,) -> FFIAccountResult -``` - -**Description:** -Add a DashPay receiving funds account # Safety - `wallet` must be a valid pointer - `user_identity_id` and `friend_identity_id` must each point to 32 bytes - -**Safety:** -- `wallet` must be a valid pointer - `user_identity_id` and `friend_identity_id` must each point to 32 bytes - -**Module:** `wallet` - ---- - -#### `wallet_add_platform_payment_account` - -```c -wallet_add_platform_payment_account(wallet: *mut FFIWallet, account_index: c_uint, key_class: c_uint,) -> crate::types::FFIAccountResult -``` - -**Description:** -Add a Platform Payment account (DIP-17) to the wallet Platform Payment accounts use the derivation path: `m/9'/coin_type'/17'/account'/key_class'/index` # Arguments * `wallet` - Pointer to the wallet * `account_index` - The account index (hardened) in the derivation path * `key_class` - The key class (hardened) - typically 0' for main addresses # Safety This function dereferences a raw pointer to FFIWallet. The caller must ensure that: - The wallet pointer is either null or points to a valid FFIWallet - The FFIWallet remains valid for the duration of this call - -**Safety:** -This function dereferences a raw pointer to FFIWallet. The caller must ensure that: - The wallet pointer is either null or points to a valid FFIWallet - The FFIWallet remains valid for the duration of this call - -**Module:** `wallet` - ---- - -#### `wallet_build_and_sign_asset_lock_transaction` - -```c -wallet_build_and_sign_asset_lock_transaction(manager: *const FFIWalletManager, wallet: *const FFIWallet, account_index: u32, funding_types: *const FFIAssetLockFundingType, identity_indices: *const u32, credit_output_scripts: *const *const u8, credit_output_script_lens: *const usize, credit_output_amounts: *const u64, credit_outputs_count: usize, fee_per_kb: u64, fee_out: *mut u64, tx_bytes_out: *mut *mut u8, tx_len_out: *mut usize, private_keys_out: *mut [u8; 32], error: *mut FFIError,) -> bool -``` - -**Description:** -Build and sign an asset lock transaction for Core to Platform transfers. Creates a special transaction (type 8) with `AssetLockPayload` that locks Dash for Platform credits. Derives one unique private key per credit output from the specified funding account types. # Parameters - `funding_types`: Array of `credit_outputs_count` funding account types, one per credit output (registration, top-up, invitation, etc.) - `identity_indices`: Array of `credit_outputs_count` identity indices. Only used for `IdentityTopUp` entries; ignored for other funding types. - `private_keys_out`: Caller-allocated array of `credit_outputs_count` × 32-byte buffers. On success, each `private_keys_out[i]` receives the one-time private key corresponding to `credit_output_scripts[i]`. # Safety - All pointer parameters must be valid and non-null - All parallel arrays must have at least `credit_outputs_count` elements - `private_keys_out` must point to an array of `credit_outputs_count` × `[u8; 32]` buffers - Caller must free `tx_bytes_out` with `transaction_bytes_free` - -**Safety:** -- All pointer parameters must be valid and non-null - All parallel arrays must have at least `credit_outputs_count` elements - `private_keys_out` must point to an array of `credit_outputs_count` × `[u8; 32]` buffers - Caller must free `tx_bytes_out` with `transaction_bytes_free` - -**Module:** `transaction` - ---- - -#### `wallet_build_and_sign_transaction` - -```c -wallet_build_and_sign_transaction(manager: *const FFIWalletManager, wallet: *const FFIWallet, account_index: u32, outputs: *const FFITxOutput, outputs_count: usize, fee_per_kb: u64, fee_out: *mut u64, tx_bytes_out: *mut *mut u8, tx_len_out: *mut usize, error: *mut FFIError,) -> bool -``` - -**Description:** -Build and sign a transaction using the wallet's managed info This is the recommended way to build transactions. It handles: - UTXO selection using coin selection algorithms - Fee calculation - Change address generation - Transaction signing # Safety - `manager` must be a valid pointer to an FFIWalletManager - `wallet` must be a valid pointer to an FFIWallet - `account_index` must be a valid BIP44 account index present in the wallet - `outputs` must be a valid pointer to an array of FFITxOutput with at least `outputs_count` elements - `fee_rate` must be a valid variant of FFIFeeRate - `fee_out` must be a valid, non-null pointer to a `u64`; on success it receives the calculated transaction fee in duffs - `tx_bytes_out` must be a valid pointer to store the transaction bytes pointer - `tx_len_out` must be a valid pointer to store the transaction length - `error` must be a valid pointer to an FFIError - The returned transaction bytes must be freed with `transaction_bytes_free` - -**Safety:** -- `manager` must be a valid pointer to an FFIWalletManager - `wallet` must be a valid pointer to an FFIWallet - `account_index` must be a valid BIP44 account index present in the wallet - `outputs` must be a valid pointer to an array of FFITxOutput with at least `outputs_count` elements - `fee_rate` must be a valid variant of FFIFeeRate - `fee_out` must be a valid, non-null pointer to a `u64`; on success it receives the calculated transaction fee in duffs - `tx_bytes_out` must be a valid pointer to store the transaction bytes pointer - `tx_len_out` must be a valid pointer to store the transaction length - `error` must be a valid pointer to an FFIError - The returned transaction bytes must be freed with `transaction_bytes_free` - -**Module:** `transaction` - ---- - -#### `wallet_check_transaction` - -```c -wallet_check_transaction(wallet: *mut FFIWallet, tx_bytes: *const u8, tx_len: usize, context_type: FFITransactionContextType, block_info: FFIBlockInfo, islock_data: *const u8, islock_len: usize, update_state: bool, result_out: *mut FFITransactionCheckResult, error: *mut FFIError,) -> bool -``` - -**Description:** -Check if a transaction belongs to the wallet using ManagedWalletInfo # Safety - `wallet` must be a valid mutable pointer to an FFIWallet - `tx_bytes` must be a valid pointer to transaction bytes with at least `tx_len` bytes - `result_out` must be a valid pointer to store the result - `error` must be a valid pointer to an FFIError - -**Safety:** -- `wallet` must be a valid mutable pointer to an FFIWallet - `tx_bytes` must be a valid pointer to transaction bytes with at least `tx_len` bytes - `result_out` must be a valid pointer to store the result - `error` must be a valid pointer to an FFIError - -**Module:** `transaction` - ---- - -#### `wallet_create_from_mnemonic` - -```c -wallet_create_from_mnemonic(mnemonic: *const c_char, network: FFINetwork, error: *mut FFIError,) -> *mut FFIWallet -``` - -**Description:** -Create a new wallet from mnemonic (backward compatibility - single network) # Safety - `mnemonic` must be a valid pointer to a null-terminated C string - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - The returned pointer must be freed with `wallet_free` when no longer needed - -**Safety:** -- `mnemonic` must be a valid pointer to a null-terminated C string - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - The returned pointer must be freed with `wallet_free` when no longer needed - -**Module:** `wallet` - ---- - -#### `wallet_create_from_mnemonic_with_options` - -```c -wallet_create_from_mnemonic_with_options(mnemonic: *const c_char, network: FFINetwork, account_options: *const FFIWalletAccountCreationOptions, error: *mut FFIError,) -> *mut FFIWallet -``` - -**Description:** -Create a new wallet from mnemonic with options # Safety - `mnemonic` must be a valid pointer to a null-terminated C string - `account_options` must be a valid pointer to FFIWalletAccountCreationOptions or null - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - The returned pointer must be freed with `wallet_free` when no longer needed - -**Safety:** -- `mnemonic` must be a valid pointer to a null-terminated C string - `account_options` must be a valid pointer to FFIWalletAccountCreationOptions or null - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - The returned pointer must be freed with `wallet_free` when no longer needed - -**Module:** `wallet` - ---- - -#### `wallet_create_from_seed` - -```c -wallet_create_from_seed(seed: *const u8, seed_len: usize, network: FFINetwork, error: *mut FFIError,) -> *mut FFIWallet -``` - -**Description:** -Create a new wallet from seed (backward compatibility) # Safety - `seed` must be a valid pointer to a byte array of `seed_len` length - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - -**Safety:** -- `seed` must be a valid pointer to a byte array of `seed_len` length - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - -**Module:** `wallet` - ---- - -#### `wallet_create_from_seed_with_options` - -```c -wallet_create_from_seed_with_options(seed: *const u8, seed_len: usize, network: FFINetwork, account_options: *const FFIWalletAccountCreationOptions, error: *mut FFIError,) -> *mut FFIWallet -``` - -**Description:** -Create a new wallet from seed with options # Safety - `seed` must be a valid pointer to a byte array of `seed_len` length - `account_options` must be a valid pointer to FFIWalletAccountCreationOptions or null - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - -**Safety:** -- `seed` must be a valid pointer to a byte array of `seed_len` length - `account_options` must be a valid pointer to FFIWalletAccountCreationOptions or null - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - -**Module:** `wallet` - ---- - -#### `wallet_create_random` - -```c -wallet_create_random(network: FFINetwork, error: *mut FFIError,) -> *mut FFIWallet -``` - -**Description:** -Create a new random wallet (backward compatibility) # Safety - `error` must be a valid pointer to an FFIError structure - The caller must ensure the pointer remains valid for the duration of this call - -**Safety:** -- `error` must be a valid pointer to an FFIError structure - The caller must ensure the pointer remains valid for the duration of this call - -**Module:** `wallet` - ---- - -#### `wallet_create_random_with_options` - -```c -wallet_create_random_with_options(network: FFINetwork, account_options: *const FFIWalletAccountCreationOptions, error: *mut FFIError,) -> *mut FFIWallet -``` - -**Description:** -Create a new random wallet with options # Safety - `account_options` must be a valid pointer to FFIWalletAccountCreationOptions or null - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - -**Safety:** -- `account_options` must be a valid pointer to FFIWalletAccountCreationOptions or null - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - -**Module:** `wallet` - ---- - -#### `wallet_derive_extended_private_key` - -```c -wallet_derive_extended_private_key(wallet: *const FFIWallet, derivation_path: *const c_char, error: *mut FFIError,) -> *mut FFIExtendedPrivKey -``` - -**Description:** -Derive extended private key at a specific path Returns an opaque FFIExtendedPrivKey pointer that must be freed with extended_private_key_free # Safety - `wallet` must be a valid pointer to an FFIWallet - `derivation_path` must be a valid null-terminated C string - `error` must be a valid pointer to an FFIError - The returned pointer must be freed with `extended_private_key_free` - -**Safety:** -- `wallet` must be a valid pointer to an FFIWallet - `derivation_path` must be a valid null-terminated C string - `error` must be a valid pointer to an FFIError - The returned pointer must be freed with `extended_private_key_free` - -**Module:** `keys` - ---- - -#### `wallet_derive_extended_public_key` - -```c -wallet_derive_extended_public_key(wallet: *const FFIWallet, derivation_path: *const c_char, error: *mut FFIError,) -> *mut FFIExtendedPubKey -``` - -**Description:** -Derive extended public key at a specific path Returns an opaque FFIExtendedPubKey pointer that must be freed with extended_public_key_free # Safety - `wallet` must be a valid pointer to an FFIWallet - `derivation_path` must be a valid null-terminated C string - `error` must be a valid pointer to an FFIError - The returned pointer must be freed with `extended_public_key_free` - -**Safety:** -- `wallet` must be a valid pointer to an FFIWallet - `derivation_path` must be a valid null-terminated C string - `error` must be a valid pointer to an FFIError - The returned pointer must be freed with `extended_public_key_free` - -**Module:** `keys` - ---- - -#### `wallet_derive_private_key` - -```c -wallet_derive_private_key(wallet: *const FFIWallet, derivation_path: *const c_char, error: *mut FFIError,) -> *mut FFIPrivateKey -``` - -**Description:** -Derive private key at a specific path Returns an opaque FFIPrivateKey pointer that must be freed with private_key_free # Safety - `wallet` must be a valid pointer to an FFIWallet - `derivation_path` must be a valid null-terminated C string - `error` must be a valid pointer to an FFIError - The returned pointer must be freed with `private_key_free` - -**Safety:** -- `wallet` must be a valid pointer to an FFIWallet - `derivation_path` must be a valid null-terminated C string - `error` must be a valid pointer to an FFIError - The returned pointer must be freed with `private_key_free` - -**Module:** `keys` - ---- - -#### `wallet_derive_private_key_as_wif` - -```c -wallet_derive_private_key_as_wif(wallet: *const FFIWallet, derivation_path: *const c_char, error: *mut FFIError,) -> *mut c_char -``` - -**Description:** -Derive private key at a specific path and return as WIF string # Safety - `wallet` must be a valid pointer to an FFIWallet - `derivation_path` must be a valid null-terminated C string - `error` must be a valid pointer to an FFIError - The returned string must be freed with `string_free` - -**Safety:** -- `wallet` must be a valid pointer to an FFIWallet - `derivation_path` must be a valid null-terminated C string - `error` must be a valid pointer to an FFIError - The returned string must be freed with `string_free` - -**Module:** `keys` - ---- - -#### `wallet_derive_public_key` - -```c -wallet_derive_public_key(wallet: *const FFIWallet, derivation_path: *const c_char, error: *mut FFIError,) -> *mut FFIPublicKey -``` - -**Description:** -Derive public key at a specific path Returns an opaque FFIPublicKey pointer that must be freed with public_key_free # Safety - `wallet` must be a valid pointer to an FFIWallet - `derivation_path` must be a valid null-terminated C string - `error` must be a valid pointer to an FFIError - The returned pointer must be freed with `public_key_free` - -**Safety:** -- `wallet` must be a valid pointer to an FFIWallet - `derivation_path` must be a valid null-terminated C string - `error` must be a valid pointer to an FFIError - The returned pointer must be freed with `public_key_free` - -**Module:** `keys` - ---- - -#### `wallet_derive_public_key_as_hex` - -```c -wallet_derive_public_key_as_hex(wallet: *const FFIWallet, derivation_path: *const c_char, error: *mut FFIError,) -> *mut c_char -``` - -**Description:** -Derive public key at a specific path and return as hex string # Safety - `wallet` must be a valid pointer to an FFIWallet - `derivation_path` must be a valid null-terminated C string - `error` must be a valid pointer to an FFIError - The returned string must be freed with `string_free` - -**Safety:** -- `wallet` must be a valid pointer to an FFIWallet - `derivation_path` must be a valid null-terminated C string - `error` must be a valid pointer to an FFIError - The returned string must be freed with `string_free` - -**Module:** `keys` - ---- - -#### `wallet_free` - -```c -wallet_free(wallet: *mut FFIWallet) -> () -``` - -**Description:** -Free a wallet # Safety - `wallet` must be a valid pointer to an FFIWallet that was created by this library - The pointer must not be used after calling this function - This function must only be called once per wallet - -**Safety:** -- `wallet` must be a valid pointer to an FFIWallet that was created by this library - The pointer must not be used after calling this function - This function must only be called once per wallet - -**Module:** `wallet` - ---- - -#### `wallet_free_const` - -```c -wallet_free_const(wallet: *const FFIWallet) -> () -``` - -**Description:** -Free a const wallet handle This is a const-safe wrapper for wallet_free() that accepts a const pointer. Use this function when you have a *const FFIWallet that needs to be freed, such as wallets returned from wallet_manager_get_wallet(). # Safety - `wallet` must be a valid pointer created by wallet creation functions or null - After calling this function, the pointer becomes invalid - This function must only be called once per wallet - The wallet must have been allocated by this library (not stack or static memory) - -**Safety:** -- `wallet` must be a valid pointer created by wallet creation functions or null - After calling this function, the pointer becomes invalid - This function must only be called once per wallet - The wallet must have been allocated by this library (not stack or static memory) - -**Module:** `wallet` - ---- - -#### `wallet_get_account` - -```c -wallet_get_account(wallet: *const FFIWallet, account_index: c_uint, account_type: FFIAccountKind,) -> FFIAccountResult -``` - -**Description:** -Get an account handle for a specific account type Returns a result containing either the account handle or an error # Safety - `wallet` must be a valid pointer to an FFIWallet instance - The caller must ensure the wallet pointer remains valid for the duration of this call - -**Safety:** -- `wallet` must be a valid pointer to an FFIWallet instance - The caller must ensure the wallet pointer remains valid for the duration of this call - -**Module:** `account` - ---- - -#### `wallet_get_account_collection` - -```c -wallet_get_account_collection(wallet: *const FFIWallet, error: *mut FFIError,) -> *mut FFIAccountCollection -``` - -**Description:** -Get account collection for a specific network from wallet # Safety - `wallet` must be a valid pointer to an FFIWallet instance - `error` must be a valid pointer to an FFIError structure - The returned pointer must be freed with `account_collection_free` when no longer needed - -**Safety:** -- `wallet` must be a valid pointer to an FFIWallet instance - `error` must be a valid pointer to an FFIError structure - The returned pointer must be freed with `account_collection_free` when no longer needed - -**Module:** `account_collection` - ---- - -#### `wallet_get_account_count` - -```c -wallet_get_account_count(wallet: *const FFIWallet, error: *mut FFIError,) -> c_uint -``` - -**Description:** -Get number of accounts # Safety - `wallet` must be a valid pointer to an FFIWallet instance - `error` must be a valid pointer to an FFIError structure or null - The caller must ensure both pointers remain valid for the duration of this call - -**Safety:** -- `wallet` must be a valid pointer to an FFIWallet instance - `error` must be a valid pointer to an FFIError structure or null - The caller must ensure both pointers remain valid for the duration of this call - -**Module:** `account` - ---- - -#### `wallet_get_account_xpriv` - -```c -wallet_get_account_xpriv(wallet: *const FFIWallet, account_index: c_uint, error: *mut FFIError,) -> *mut c_char -``` - -**Description:** -Get extended private key for account # Safety - `wallet` must be a valid pointer to an FFIWallet - `error` must be a valid pointer to an FFIError - The returned string must be freed with `string_free` - -**Safety:** -- `wallet` must be a valid pointer to an FFIWallet - `error` must be a valid pointer to an FFIError - The returned string must be freed with `string_free` - -**Module:** `keys` - ---- - -#### `wallet_get_account_xpub` - -```c -wallet_get_account_xpub(wallet: *const FFIWallet, account_index: c_uint, error: *mut FFIError,) -> *mut c_char -``` - -**Description:** -Get extended public key for account # Safety - `wallet` must be a valid pointer to an FFIWallet - `error` must be a valid pointer to an FFIError - The returned string must be freed with `string_free` - -**Safety:** -- `wallet` must be a valid pointer to an FFIWallet - `error` must be a valid pointer to an FFIError - The returned string must be freed with `string_free` - -**Module:** `keys` - ---- - -#### `wallet_get_id` - -```c -wallet_get_id(wallet: *const FFIWallet, id_out: *mut u8, error: *mut FFIError,) -> bool -``` - -**Description:** -Get wallet ID (32-byte hash) # Safety - `wallet` must be a valid pointer to an FFIWallet - `id_out` must be a valid pointer to a 32-byte buffer - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - -**Safety:** -- `wallet` must be a valid pointer to an FFIWallet - `id_out` must be a valid pointer to a 32-byte buffer - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - -**Module:** `wallet` - ---- - -#### `wallet_get_top_up_account_with_registration_index` - -```c -wallet_get_top_up_account_with_registration_index(wallet: *const FFIWallet, registration_index: c_uint,) -> FFIAccountResult -``` - -**Description:** -Get an IdentityTopUp account handle with a specific registration index This is used for top-up accounts that are bound to a specific identity Returns a result containing either the account handle or an error # Safety - `wallet` must be a valid pointer to an FFIWallet instance - The caller must ensure the wallet pointer remains valid for the duration of this call - -**Safety:** -- `wallet` must be a valid pointer to an FFIWallet instance - The caller must ensure the wallet pointer remains valid for the duration of this call - -**Module:** `account` - ---- - -#### `wallet_get_utxos` - -```c -wallet_get_utxos(_wallet: *const crate::types::FFIWallet, utxos_out: *mut *mut FFIUTXO, count_out: *mut usize, error: *mut FFIError,) -> bool -``` - -**Description:** -Get all UTXOs (deprecated - use managed_wallet_get_utxos instead) # Safety This function is deprecated and returns an empty list. Use `managed_wallet_get_utxos` with a ManagedWalletInfo instead. - -**Safety:** -This function is deprecated and returns an empty list. Use `managed_wallet_get_utxos` with a ManagedWalletInfo instead. - -**Module:** `utxo` - ---- - -#### `wallet_get_xpub` - -```c -wallet_get_xpub(wallet: *const FFIWallet, account_index: c_uint, error: *mut FFIError,) -> *mut c_char -``` - -**Description:** -Get extended public key for account # Safety - `wallet` must be a valid pointer to an FFIWallet instance - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - The returned C string must be freed by the caller when no longer needed - -**Safety:** -- `wallet` must be a valid pointer to an FFIWallet instance - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - The returned C string must be freed by the caller when no longer needed - -**Module:** `wallet` - ---- - -#### `wallet_has_mnemonic` - -```c -wallet_has_mnemonic(wallet: *const FFIWallet, error: *mut FFIError,) -> bool -``` - -**Description:** -Check if wallet has mnemonic # Safety - `wallet` must be a valid pointer to an FFIWallet instance - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - -**Safety:** -- `wallet` must be a valid pointer to an FFIWallet instance - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - -**Module:** `wallet` - ---- - -#### `wallet_is_watch_only` - -```c -wallet_is_watch_only(wallet: *const FFIWallet, error: *mut FFIError,) -> bool -``` - -**Description:** -Check if wallet is watch-only # Safety - `wallet` must be a valid pointer to an FFIWallet instance - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - -**Safety:** -- `wallet` must be a valid pointer to an FFIWallet instance - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - -**Module:** `wallet` - ---- - -### Account Management - Detailed - -#### `account_collection_count` - -```c -account_collection_count(collection: *const FFIAccountCollection,) -> c_uint -``` - -**Description:** -Get the total number of accounts in the collection # Safety - `collection` must be a valid pointer to an FFIAccountCollection - -**Safety:** -- `collection` must be a valid pointer to an FFIAccountCollection - -**Module:** `account_collection` - ---- - -#### `account_collection_free` - -```c -account_collection_free(collection: *mut FFIAccountCollection) -> () -``` - -**Description:** -Free an account collection handle # Safety - `collection` must be a valid pointer to an FFIAccountCollection created by this library - `collection` must not be used after calling this function - -**Safety:** -- `collection` must be a valid pointer to an FFIAccountCollection created by this library - `collection` must not be used after calling this function - -**Module:** `account_collection` - ---- - -#### `account_collection_get_bip32_account` - -```c -account_collection_get_bip32_account(collection: *const FFIAccountCollection, index: c_uint,) -> *mut FFIAccount -``` - -**Description:** -Get a BIP32 account by index from the collection # Safety - `collection` must be a valid pointer to an FFIAccountCollection - The returned pointer must be freed with `account_free` when no longer needed - -**Safety:** -- `collection` must be a valid pointer to an FFIAccountCollection - The returned pointer must be freed with `account_free` when no longer needed - -**Module:** `account_collection` - ---- - -#### `account_collection_get_bip32_indices` - -```c -account_collection_get_bip32_indices(collection: *const FFIAccountCollection, out_indices: *mut *mut c_uint, out_count: *mut usize,) -> bool -``` - -**Description:** -Get all BIP32 account indices # Safety - `collection` must be a valid pointer to an FFIAccountCollection - `out_indices` must be a valid pointer to store the indices array - `out_count` must be a valid pointer to store the count - The returned array must be freed with `free_u32_array` when no longer needed - -**Safety:** -- `collection` must be a valid pointer to an FFIAccountCollection - `out_indices` must be a valid pointer to store the indices array - `out_count` must be a valid pointer to store the count - The returned array must be freed with `free_u32_array` when no longer needed - -**Module:** `account_collection` - ---- - -#### `account_collection_get_bip44_account` - -```c -account_collection_get_bip44_account(collection: *const FFIAccountCollection, index: c_uint,) -> *mut FFIAccount -``` - -**Description:** -Get a BIP44 account by index from the collection # Safety - `collection` must be a valid pointer to an FFIAccountCollection - The returned pointer must be freed with `account_free` when no longer needed - -**Safety:** -- `collection` must be a valid pointer to an FFIAccountCollection - The returned pointer must be freed with `account_free` when no longer needed - -**Module:** `account_collection` - ---- - -#### `account_collection_get_bip44_indices` - -```c -account_collection_get_bip44_indices(collection: *const FFIAccountCollection, out_indices: *mut *mut c_uint, out_count: *mut usize,) -> bool -``` - -**Description:** -Get all BIP44 account indices # Safety - `collection` must be a valid pointer to an FFIAccountCollection - `out_indices` must be a valid pointer to store the indices array - `out_count` must be a valid pointer to store the count - The returned array must be freed with `free_u32_array` when no longer needed - -**Safety:** -- `collection` must be a valid pointer to an FFIAccountCollection - `out_indices` must be a valid pointer to store the indices array - `out_count` must be a valid pointer to store the count - The returned array must be freed with `free_u32_array` when no longer needed - -**Module:** `account_collection` - ---- - -#### `account_collection_get_coinjoin_account` - -```c -account_collection_get_coinjoin_account(collection: *const FFIAccountCollection, index: c_uint,) -> *mut FFIAccount -``` - -**Description:** -Get a CoinJoin account by index from the collection # Safety - `collection` must be a valid pointer to an FFIAccountCollection - The returned pointer must be freed with `account_free` when no longer needed - -**Safety:** -- `collection` must be a valid pointer to an FFIAccountCollection - The returned pointer must be freed with `account_free` when no longer needed - -**Module:** `account_collection` - ---- - -#### `account_collection_get_coinjoin_indices` - -```c -account_collection_get_coinjoin_indices(collection: *const FFIAccountCollection, out_indices: *mut *mut c_uint, out_count: *mut usize,) -> bool -``` - -**Description:** -Get all CoinJoin account indices # Safety - `collection` must be a valid pointer to an FFIAccountCollection - `out_indices` must be a valid pointer to store the indices array - `out_count` must be a valid pointer to store the count - The returned array must be freed with `free_u32_array` when no longer needed - -**Safety:** -- `collection` must be a valid pointer to an FFIAccountCollection - `out_indices` must be a valid pointer to store the indices array - `out_count` must be a valid pointer to store the count - The returned array must be freed with `free_u32_array` when no longer needed - -**Module:** `account_collection` - ---- - -#### `account_collection_get_identity_invitation` - -```c -account_collection_get_identity_invitation(collection: *const FFIAccountCollection,) -> *mut FFIAccount -``` - -**Description:** -Get the identity invitation account if it exists # Safety - `collection` must be a valid pointer to an FFIAccountCollection - The returned pointer must be freed with `account_free` when no longer needed - -**Safety:** -- `collection` must be a valid pointer to an FFIAccountCollection - The returned pointer must be freed with `account_free` when no longer needed - -**Module:** `account_collection` - ---- - -#### `account_collection_get_identity_registration` - -```c -account_collection_get_identity_registration(collection: *const FFIAccountCollection,) -> *mut FFIAccount -``` - -**Description:** -Get the identity registration account if it exists # Safety - `collection` must be a valid pointer to an FFIAccountCollection - The returned pointer must be freed with `account_free` when no longer needed - -**Safety:** -- `collection` must be a valid pointer to an FFIAccountCollection - The returned pointer must be freed with `account_free` when no longer needed - -**Module:** `account_collection` - ---- - -#### `account_collection_get_identity_topup` - -```c -account_collection_get_identity_topup(collection: *const FFIAccountCollection, registration_index: c_uint,) -> *mut FFIAccount -``` - -**Description:** -Get an identity topup account by registration index # Safety - `collection` must be a valid pointer to an FFIAccountCollection - The returned pointer must be freed with `account_free` when no longer needed - -**Safety:** -- `collection` must be a valid pointer to an FFIAccountCollection - The returned pointer must be freed with `account_free` when no longer needed - -**Module:** `account_collection` - ---- - -#### `account_collection_get_identity_topup_indices` - -```c -account_collection_get_identity_topup_indices(collection: *const FFIAccountCollection, out_indices: *mut *mut c_uint, out_count: *mut usize,) -> bool -``` - -**Description:** -Get all identity topup registration indices # Safety - `collection` must be a valid pointer to an FFIAccountCollection - `out_indices` must be a valid pointer to store the indices array - `out_count` must be a valid pointer to store the count - The returned array must be freed with `free_u32_array` when no longer needed - -**Safety:** -- `collection` must be a valid pointer to an FFIAccountCollection - `out_indices` must be a valid pointer to store the indices array - `out_count` must be a valid pointer to store the count - The returned array must be freed with `free_u32_array` when no longer needed - -**Module:** `account_collection` - ---- - -#### `account_collection_get_identity_topup_not_bound` - -```c -account_collection_get_identity_topup_not_bound(collection: *const FFIAccountCollection,) -> *mut FFIAccount -``` - -**Description:** -Get the identity topup not bound account if it exists # Safety - `collection` must be a valid pointer to an FFIAccountCollection - The returned pointer must be freed with `account_free` when no longer needed - -**Safety:** -- `collection` must be a valid pointer to an FFIAccountCollection - The returned pointer must be freed with `account_free` when no longer needed - -**Module:** `account_collection` - ---- - -#### `account_collection_get_provider_operator_keys` - -```c -account_collection_get_provider_operator_keys(collection: *const FFIAccountCollection,) -> *mut std::os::raw::c_void -``` - -**Description:** -Get the provider operator keys account if it exists Note: Returns null if the `bls` feature is not enabled # Safety - `collection` must be a valid pointer to an FFIAccountCollection - The returned pointer must be freed with `bls_account_free` when no longer needed (when BLS is enabled) - -**Safety:** -- `collection` must be a valid pointer to an FFIAccountCollection - The returned pointer must be freed with `bls_account_free` when no longer needed (when BLS is enabled) - -**Module:** `account_collection` - ---- - -#### `account_collection_get_provider_owner_keys` - -```c -account_collection_get_provider_owner_keys(collection: *const FFIAccountCollection,) -> *mut FFIAccount -``` - -**Description:** -Get the provider owner keys account if it exists # Safety - `collection` must be a valid pointer to an FFIAccountCollection - The returned pointer must be freed with `account_free` when no longer needed - -**Safety:** -- `collection` must be a valid pointer to an FFIAccountCollection - The returned pointer must be freed with `account_free` when no longer needed - -**Module:** `account_collection` - ---- - -#### `account_collection_get_provider_platform_keys` - -```c -account_collection_get_provider_platform_keys(collection: *const FFIAccountCollection,) -> *mut std::os::raw::c_void -``` - -**Description:** -Get the provider platform keys account if it exists Note: Returns null if the `eddsa` feature is not enabled # Safety - `collection` must be a valid pointer to an FFIAccountCollection - The returned pointer must be freed with `eddsa_account_free` when no longer needed (when EdDSA is enabled) - -**Safety:** -- `collection` must be a valid pointer to an FFIAccountCollection - The returned pointer must be freed with `eddsa_account_free` when no longer needed (when EdDSA is enabled) - -**Module:** `account_collection` - ---- - -#### `account_collection_get_provider_voting_keys` - -```c -account_collection_get_provider_voting_keys(collection: *const FFIAccountCollection,) -> *mut FFIAccount -``` - -**Description:** -Get the provider voting keys account if it exists # Safety - `collection` must be a valid pointer to an FFIAccountCollection - The returned pointer must be freed with `account_free` when no longer needed - -**Safety:** -- `collection` must be a valid pointer to an FFIAccountCollection - The returned pointer must be freed with `account_free` when no longer needed - -**Module:** `account_collection` - ---- - -#### `account_collection_has_identity_invitation` - -```c -account_collection_has_identity_invitation(collection: *const FFIAccountCollection,) -> bool -``` - -**Description:** -Check if identity invitation account exists # Safety - `collection` must be a valid pointer to an FFIAccountCollection - -**Safety:** -- `collection` must be a valid pointer to an FFIAccountCollection - -**Module:** `account_collection` - ---- - -#### `account_collection_has_identity_registration` - -```c -account_collection_has_identity_registration(collection: *const FFIAccountCollection,) -> bool -``` - -**Description:** -Check if identity registration account exists # Safety - `collection` must be a valid pointer to an FFIAccountCollection - -**Safety:** -- `collection` must be a valid pointer to an FFIAccountCollection - -**Module:** `account_collection` - ---- - -#### `account_collection_has_identity_topup_not_bound` - -```c -account_collection_has_identity_topup_not_bound(collection: *const FFIAccountCollection,) -> bool -``` - -**Description:** -Check if identity topup not bound account exists # Safety - `collection` must be a valid pointer to an FFIAccountCollection - -**Safety:** -- `collection` must be a valid pointer to an FFIAccountCollection - -**Module:** `account_collection` - ---- - -#### `account_collection_has_provider_operator_keys` - -```c -account_collection_has_provider_operator_keys(collection: *const FFIAccountCollection,) -> bool -``` - -**Description:** -Check if provider operator keys account exists # Safety - `collection` must be a valid pointer to an FFIAccountCollection - -**Safety:** -- `collection` must be a valid pointer to an FFIAccountCollection - -**Module:** `account_collection` - ---- - -#### `account_collection_has_provider_owner_keys` - -```c -account_collection_has_provider_owner_keys(collection: *const FFIAccountCollection,) -> bool -``` - -**Description:** -Check if provider owner keys account exists # Safety - `collection` must be a valid pointer to an FFIAccountCollection - -**Safety:** -- `collection` must be a valid pointer to an FFIAccountCollection - -**Module:** `account_collection` - ---- - -#### `account_collection_has_provider_platform_keys` - -```c -account_collection_has_provider_platform_keys(collection: *const FFIAccountCollection,) -> bool -``` - -**Description:** -Check if provider platform keys account exists # Safety - `collection` must be a valid pointer to an FFIAccountCollection - -**Safety:** -- `collection` must be a valid pointer to an FFIAccountCollection - -**Module:** `account_collection` - ---- - -#### `account_collection_has_provider_voting_keys` - -```c -account_collection_has_provider_voting_keys(collection: *const FFIAccountCollection,) -> bool -``` - -**Description:** -Check if provider voting keys account exists # Safety - `collection` must be a valid pointer to an FFIAccountCollection - -**Safety:** -- `collection` must be a valid pointer to an FFIAccountCollection - -**Module:** `account_collection` - ---- - -#### `account_collection_summary` - -```c -account_collection_summary(collection: *const FFIAccountCollection,) -> *mut c_char -``` - -**Description:** -Get a human-readable summary of all accounts in the collection Returns a formatted string showing all account types and their indices. The format is designed to be clear and readable for end users. # Safety - `collection` must be a valid pointer to an FFIAccountCollection - The returned string must be freed with `string_free` when no longer needed - Returns null if the collection pointer is null - -**Safety:** -- `collection` must be a valid pointer to an FFIAccountCollection - The returned string must be freed with `string_free` when no longer needed - Returns null if the collection pointer is null - -**Module:** `account_collection` - ---- - -#### `account_collection_summary_data` - -```c -account_collection_summary_data(collection: *const FFIAccountCollection,) -> *mut FFIAccountCollectionSummary -``` - -**Description:** -Get structured account collection summary data Returns a struct containing arrays of indices for each account type and boolean flags for special accounts. This provides Swift with programmatic access to account information. # Safety - `collection` must be a valid pointer to an FFIAccountCollection - The returned pointer must be freed with `account_collection_summary_free` when no longer needed - Returns null if the collection pointer is null - -**Safety:** -- `collection` must be a valid pointer to an FFIAccountCollection - The returned pointer must be freed with `account_collection_summary_free` when no longer needed - Returns null if the collection pointer is null - -**Module:** `account_collection` - ---- - -#### `account_collection_summary_free` - -```c -account_collection_summary_free(summary: *mut FFIAccountCollectionSummary,) -> () -``` - -**Description:** -Free an account collection summary and all its allocated memory # Safety - `summary` must be a valid pointer to an FFIAccountCollectionSummary created by `account_collection_summary_data` - `summary` must not be used after calling this function - -**Safety:** -- `summary` must be a valid pointer to an FFIAccountCollectionSummary created by `account_collection_summary_data` - `summary` must not be used after calling this function - -**Module:** `account_collection` - ---- - -#### `account_derive_extended_private_key_at` - -```c -account_derive_extended_private_key_at(account: *const FFIAccount, master_xpriv: *const FFIExtendedPrivKey, index: c_uint, error: *mut FFIError,) -> *mut FFIExtendedPrivKey -``` - -**Description:** -Derive an extended private key from an account at a given index, using the provided master xpriv. Returns an opaque FFIExtendedPrivKey pointer that must be freed with `extended_private_key_free`. Notes: - This is chain-agnostic. For accounts with internal/external chains, this returns an error. - For hardened-only account types (e.g., EdDSA), a hardened index is used. # Safety - `account` and `master_xpriv` must be valid, non-null pointers allocated by this library. - `error` must be a valid pointer to an FFIError. - The caller must free the returned pointer with `extended_private_key_free`. - -**Safety:** -- `account` and `master_xpriv` must be valid, non-null pointers allocated by this library. - `error` must be a valid pointer to an FFIError. - The caller must free the returned pointer with `extended_private_key_free`. - -**Module:** `account_derivation` - ---- - -#### `account_derive_extended_private_key_from_mnemonic` - -```c -account_derive_extended_private_key_from_mnemonic(account: *const FFIAccount, mnemonic: *const c_char, passphrase: *const c_char, index: c_uint, error: *mut FFIError,) -> *mut FFIExtendedPrivKey -``` - -**Description:** -Derive an extended private key from a mnemonic + optional passphrase at the given index. Returns an opaque FFIExtendedPrivKey pointer that must be freed with `extended_private_key_free`. # Safety - `account` must be a valid pointer to an FFIAccount - `mnemonic` must be a valid, null-terminated C string - `passphrase` may be null; if not null, must be a valid C string - `error` must be a valid pointer to an FFIError - -**Safety:** -- `account` must be a valid pointer to an FFIAccount - `mnemonic` must be a valid, null-terminated C string - `passphrase` may be null; if not null, must be a valid C string - `error` must be a valid pointer to an FFIError - -**Module:** `account_derivation` - ---- - -#### `account_derive_extended_private_key_from_seed` - -```c -account_derive_extended_private_key_from_seed(account: *const FFIAccount, seed: *const u8, seed_len: usize, index: c_uint, error: *mut FFIError,) -> *mut FFIExtendedPrivKey -``` - -**Description:** -Derive an extended private key from a raw seed buffer at the given index. Returns an opaque FFIExtendedPrivKey pointer that must be freed with `extended_private_key_free`. # Safety - `account` must be a valid pointer to an FFIAccount - `seed` must point to a valid buffer of length `seed_len` - `error` must be a valid pointer to an FFIError - -**Safety:** -- `account` must be a valid pointer to an FFIAccount - `seed` must point to a valid buffer of length `seed_len` - `error` must be a valid pointer to an FFIError - -**Module:** `account_derivation` - ---- - -#### `account_derive_private_key_as_wif_at` - -```c -account_derive_private_key_as_wif_at(account: *const FFIAccount, master_xpriv: *const FFIExtendedPrivKey, index: c_uint, error: *mut FFIError,) -> *mut c_char -``` - -**Description:** -Derive a private key from an account at a given chain/index and return as WIF string. Caller must free the returned string with `string_free`. # Safety - `account` and `master_xpriv` must be valid pointers allocated by this library - `error` must be a valid pointer to an FFIError - -**Safety:** -- `account` and `master_xpriv` must be valid pointers allocated by this library - `error` must be a valid pointer to an FFIError - -**Module:** `account_derivation` - ---- - -#### `account_derive_private_key_at` - -```c -account_derive_private_key_at(account: *const FFIAccount, master_xpriv: *const FFIExtendedPrivKey, index: c_uint, error: *mut FFIError,) -> *mut FFIPrivateKey -``` - -**Description:** -Derive a private key (secp256k1) from an account at a given chain/index, using the provided master xpriv. Returns an opaque FFIPrivateKey pointer that must be freed with `private_key_free`. # Safety - `account` and `master_xpriv` must be valid pointers allocated by this library - `error` must be a valid pointer to an FFIError - -**Safety:** -- `account` and `master_xpriv` must be valid pointers allocated by this library - `error` must be a valid pointer to an FFIError - -**Module:** `account_derivation` - ---- - -#### `account_derive_private_key_from_mnemonic` - -```c -account_derive_private_key_from_mnemonic(account: *const FFIAccount, mnemonic: *const c_char, passphrase: *const c_char, index: c_uint, error: *mut FFIError,) -> *mut FFIPrivateKey -``` - -**Description:** -Derive a private key from a mnemonic + optional passphrase at the given index. Returns an opaque FFIPrivateKey pointer that must be freed with `private_key_free`. # Safety - `account` must be a valid pointer to an FFIAccount - `mnemonic` must be a valid, null-terminated C string - `passphrase` may be null; if not null, must be a valid C string - `error` must be a valid pointer to an FFIError - -**Safety:** -- `account` must be a valid pointer to an FFIAccount - `mnemonic` must be a valid, null-terminated C string - `passphrase` may be null; if not null, must be a valid C string - `error` must be a valid pointer to an FFIError - -**Module:** `account_derivation` - ---- - -#### `account_derive_private_key_from_seed` - -```c -account_derive_private_key_from_seed(account: *const FFIAccount, seed: *const u8, seed_len: usize, index: c_uint, error: *mut FFIError,) -> *mut FFIPrivateKey -``` - -**Description:** -Derive a private key from a raw seed buffer at the given index. Returns an opaque FFIPrivateKey pointer that must be freed with `private_key_free`. # Safety - `account` must be a valid pointer to an FFIAccount - `seed` must point to a valid buffer of length `seed_len` - `error` must be a valid pointer to an FFIError - -**Safety:** -- `account` must be a valid pointer to an FFIAccount - `seed` must point to a valid buffer of length `seed_len` - `error` must be a valid pointer to an FFIError - -**Module:** `account_derivation` - ---- - -#### `account_free` - -```c -account_free(account: *mut FFIAccount) -> () -``` - -**Description:** -Free an account handle # Safety - `account` must be a valid pointer to an FFIAccount that was allocated by this library - The pointer must not be used after calling this function - This function must only be called once per allocation - -**Safety:** -- `account` must be a valid pointer to an FFIAccount that was allocated by this library - The pointer must not be used after calling this function - This function must only be called once per allocation - -**Module:** `account` - ---- - -#### `account_get_account_type` - -```c -account_get_account_type(account: *const FFIAccount, out_index: *mut c_uint,) -> FFIAccountKind -``` - -**Description:** -Get the account type of an account # Safety - `account` must be a valid pointer to an FFIAccount instance - `out_index` must be a valid pointer to a c_uint where the index will be stored - Returns FFIAccountKind::StandardBIP44 with index 0 if the account is null - -**Safety:** -- `account` must be a valid pointer to an FFIAccount instance - `out_index` must be a valid pointer to a c_uint where the index will be stored - Returns FFIAccountKind::StandardBIP44 with index 0 if the account is null - -**Module:** `account` - ---- - -#### `account_get_extended_public_key_as_string` - -```c -account_get_extended_public_key_as_string(account: *const FFIAccount,) -> *mut std::os::raw::c_char -``` - -**Description:** -Get the extended public key of an account as a string # Safety - `account` must be a valid pointer to an FFIAccount instance - The returned string must be freed by the caller using `string_free` - Returns NULL if the account is null - -**Safety:** -- `account` must be a valid pointer to an FFIAccount instance - The returned string must be freed by the caller using `string_free` - Returns NULL if the account is null - -**Module:** `account` - ---- - -#### `account_get_is_watch_only` - -```c -account_get_is_watch_only(account: *const FFIAccount) -> bool -``` - -**Description:** -Check if an account is watch-only # Safety - `account` must be a valid pointer to an FFIAccount instance - Returns false if the account is null - -**Safety:** -- `account` must be a valid pointer to an FFIAccount instance - Returns false if the account is null - -**Module:** `account` - ---- - -#### `account_get_network` - -```c -account_get_network(account: *const FFIAccount) -> FFINetwork -``` - -**Description:** -Get the network of an account # Safety - `account` must be a valid pointer to an FFIAccount instance - Returns `FFINetwork::Mainnet` if the account is null - -**Safety:** -- `account` must be a valid pointer to an FFIAccount instance - Returns `FFINetwork::Mainnet` if the account is null - -**Module:** `account` - ---- - -#### `bls_account_derive_private_key_from_mnemonic` - -```c -bls_account_derive_private_key_from_mnemonic(account: *const FFIBLSAccount, mnemonic: *const c_char, passphrase: *const c_char, index: c_uint, error: *mut FFIError,) -> *mut c_char -``` - -**Module:** `account_derivation` - ---- - -#### `bls_account_derive_private_key_from_seed` - -```c -bls_account_derive_private_key_from_seed(account: *const FFIBLSAccount, seed: *const u8, seed_len: usize, index: c_uint, error: *mut FFIError,) -> *mut c_char -``` - -**Module:** `account_derivation` - ---- - -#### `bls_account_free` - -```c -bls_account_free(account: *mut FFIBLSAccount) -> () -``` - -**Module:** `account` - ---- - -#### `bls_account_get_account_type` - -```c -bls_account_get_account_type(account: *const FFIBLSAccount, out_index: *mut c_uint,) -> FFIAccountKind -``` - -**Module:** `account` - ---- - -#### `bls_account_get_extended_public_key_as_string` - -```c -bls_account_get_extended_public_key_as_string(account: *const FFIBLSAccount,) -> *mut std::os::raw::c_char -``` - -**Module:** `account` - ---- - -#### `bls_account_get_is_watch_only` - -```c -bls_account_get_is_watch_only(account: *const FFIBLSAccount) -> bool -``` - -**Module:** `account` - ---- - -#### `bls_account_get_network` - -```c -bls_account_get_network(account: *const FFIBLSAccount) -> FFINetwork -``` - -**Module:** `account` - ---- - -#### `derivation_bip44_account_path` - -```c -derivation_bip44_account_path(network: FFINetwork, account_index: c_uint, path_out: *mut c_char, path_max_len: usize, error: *mut FFIError,) -> bool -``` - -**Description:** -Derive a BIP44 account path (m/44'/5'/account') # Safety `path_out` must point to a writable buffer of at least `path_max_len` bytes and `error` must be a valid pointer to an `FFIError`. - -**Safety:** -`path_out` must point to a writable buffer of at least `path_max_len` bytes and `error` must be a valid pointer to an `FFIError`. - -**Module:** `derivation` - ---- - -#### `eddsa_account_derive_private_key_from_mnemonic` - -```c -eddsa_account_derive_private_key_from_mnemonic(account: *const FFIEdDSAAccount, mnemonic: *const c_char, passphrase: *const c_char, index: c_uint, error: *mut FFIError,) -> *mut c_char -``` - -**Module:** `account_derivation` - ---- - -#### `eddsa_account_derive_private_key_from_seed` - -```c -eddsa_account_derive_private_key_from_seed(account: *const FFIEdDSAAccount, seed: *const u8, seed_len: usize, index: c_uint, error: *mut FFIError,) -> *mut c_char -``` - -**Module:** `account_derivation` - ---- - -#### `eddsa_account_free` - -```c -eddsa_account_free(account: *mut FFIEdDSAAccount) -> () -``` - -**Module:** `account` - ---- - -#### `eddsa_account_get_account_type` - -```c -eddsa_account_get_account_type(account: *const FFIEdDSAAccount, out_index: *mut c_uint,) -> FFIAccountKind -``` - -**Module:** `account` - ---- - -#### `eddsa_account_get_extended_public_key_as_string` - -```c -eddsa_account_get_extended_public_key_as_string(account: *const FFIEdDSAAccount,) -> *mut std::os::raw::c_char -``` - -**Module:** `account` - ---- - -#### `eddsa_account_get_is_watch_only` - -```c -eddsa_account_get_is_watch_only(account: *const FFIEdDSAAccount) -> bool -``` - -**Module:** `account` - ---- - -#### `eddsa_account_get_network` - -```c -eddsa_account_get_network(account: *const FFIEdDSAAccount) -> FFINetwork -``` - -**Module:** `account` - ---- - -#### `managed_account_collection_count` - -```c -managed_account_collection_count(collection: *const FFIManagedCoreAccountCollection,) -> c_uint -``` - -**Description:** -Get the total number of accounts in the managed collection # Safety - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - -**Safety:** -- `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - -**Module:** `managed_account_collection` - ---- - -#### `managed_account_collection_free` - -```c -managed_account_collection_free(collection: *mut FFIManagedCoreAccountCollection,) -> () -``` - -**Description:** -Free a managed account collection handle # Safety - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection created by this library - `collection` must not be used after calling this function - -**Safety:** -- `collection` must be a valid pointer to an FFIManagedCoreAccountCollection created by this library - `collection` must not be used after calling this function - -**Module:** `managed_account_collection` - ---- - -#### `managed_account_collection_free_platform_payment_keys` - -```c -managed_account_collection_free_platform_payment_keys(keys: *mut crate::managed_account::FFIPlatformPaymentAccountKey, count: usize,) -> () -``` - -**Description:** -Free platform payment keys array returned by managed_account_collection_get_platform_payment_keys # Safety - `keys` must be a pointer returned by `managed_account_collection_get_platform_payment_keys` - `count` must be the count returned by `managed_account_collection_get_platform_payment_keys` - This function must only be called once per allocation - -**Safety:** -- `keys` must be a pointer returned by `managed_account_collection_get_platform_payment_keys` - `count` must be the count returned by `managed_account_collection_get_platform_payment_keys` - This function must only be called once per allocation - -**Module:** `managed_account_collection` - ---- - -#### `managed_account_collection_get_bip32_account` - -```c -managed_account_collection_get_bip32_account(collection: *const FFIManagedCoreAccountCollection, index: c_uint,) -> *mut FFIManagedCoreAccount -``` - -**Description:** -Get a BIP32 account by index from the managed collection # Safety - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - The returned pointer must be freed with `managed_core_account_free` when no longer needed - -**Safety:** -- `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - The returned pointer must be freed with `managed_core_account_free` when no longer needed - -**Module:** `managed_account_collection` - ---- - -#### `managed_account_collection_get_bip32_indices` - -```c -managed_account_collection_get_bip32_indices(collection: *const FFIManagedCoreAccountCollection, out_indices: *mut *mut c_uint, out_count: *mut usize,) -> bool -``` - -**Description:** -Get all BIP32 account indices from managed collection # Safety - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - `out_indices` must be a valid pointer to store the indices array - `out_count` must be a valid pointer to store the count - The returned array must be freed with `free_u32_array` when no longer needed - -**Safety:** -- `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - `out_indices` must be a valid pointer to store the indices array - `out_count` must be a valid pointer to store the count - The returned array must be freed with `free_u32_array` when no longer needed - -**Module:** `managed_account_collection` - ---- - -#### `managed_account_collection_get_bip44_account` - -```c -managed_account_collection_get_bip44_account(collection: *const FFIManagedCoreAccountCollection, index: c_uint,) -> *mut FFIManagedCoreAccount -``` - -**Description:** -Get a BIP44 account by index from the managed collection # Safety - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - The returned pointer must be freed with `managed_core_account_free` when no longer needed - -**Safety:** -- `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - The returned pointer must be freed with `managed_core_account_free` when no longer needed - -**Module:** `managed_account_collection` - ---- - -#### `managed_account_collection_get_bip44_indices` - -```c -managed_account_collection_get_bip44_indices(collection: *const FFIManagedCoreAccountCollection, out_indices: *mut *mut c_uint, out_count: *mut usize,) -> bool -``` - -**Description:** -Get all BIP44 account indices from managed collection # Safety - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - `out_indices` must be a valid pointer to store the indices array - `out_count` must be a valid pointer to store the count - The returned array must be freed with `free_u32_array` when no longer needed - -**Safety:** -- `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - `out_indices` must be a valid pointer to store the indices array - `out_count` must be a valid pointer to store the count - The returned array must be freed with `free_u32_array` when no longer needed - -**Module:** `managed_account_collection` - ---- - -#### `managed_account_collection_get_coinjoin_account` - -```c -managed_account_collection_get_coinjoin_account(collection: *const FFIManagedCoreAccountCollection, index: c_uint,) -> *mut FFIManagedCoreAccount -``` - -**Description:** -Get a CoinJoin account by index from the managed collection # Safety - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - The returned pointer must be freed with `managed_core_account_free` when no longer needed - -**Safety:** -- `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - The returned pointer must be freed with `managed_core_account_free` when no longer needed - -**Module:** `managed_account_collection` - ---- - -#### `managed_account_collection_get_coinjoin_indices` - -```c -managed_account_collection_get_coinjoin_indices(collection: *const FFIManagedCoreAccountCollection, out_indices: *mut *mut c_uint, out_count: *mut usize,) -> bool -``` - -**Description:** -Get all CoinJoin account indices from managed collection # Safety - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - `out_indices` must be a valid pointer to store the indices array - `out_count` must be a valid pointer to store the count - The returned array must be freed with `free_u32_array` when no longer needed - -**Safety:** -- `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - `out_indices` must be a valid pointer to store the indices array - `out_count` must be a valid pointer to store the count - The returned array must be freed with `free_u32_array` when no longer needed - -**Module:** `managed_account_collection` - ---- - -#### `managed_account_collection_get_identity_invitation` - -```c -managed_account_collection_get_identity_invitation(collection: *const FFIManagedCoreAccountCollection,) -> *mut FFIManagedCoreAccount -``` - -**Description:** -Get the identity invitation account if it exists in managed collection # Safety - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - The returned pointer must be freed with `managed_core_account_free` when no longer needed - -**Safety:** -- `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - The returned pointer must be freed with `managed_core_account_free` when no longer needed - -**Module:** `managed_account_collection` - ---- - -#### `managed_account_collection_get_identity_registration` - -```c -managed_account_collection_get_identity_registration(collection: *const FFIManagedCoreAccountCollection,) -> *mut FFIManagedCoreAccount -``` - -**Description:** -Get the identity registration account if it exists in managed collection # Safety - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - The returned pointer must be freed with `managed_core_account_free` when no longer needed - -**Safety:** -- `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - The returned pointer must be freed with `managed_core_account_free` when no longer needed - -**Module:** `managed_account_collection` - ---- - -#### `managed_account_collection_get_identity_topup` - -```c -managed_account_collection_get_identity_topup(collection: *const FFIManagedCoreAccountCollection, registration_index: c_uint,) -> *mut FFIManagedCoreAccount -``` - -**Description:** -Get an identity topup account by registration index from managed collection # Safety - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - The returned pointer must be freed with `managed_core_account_free` when no longer needed - -**Safety:** -- `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - The returned pointer must be freed with `managed_core_account_free` when no longer needed - -**Module:** `managed_account_collection` - ---- - -#### `managed_account_collection_get_identity_topup_indices` - -```c -managed_account_collection_get_identity_topup_indices(collection: *const FFIManagedCoreAccountCollection, out_indices: *mut *mut c_uint, out_count: *mut usize,) -> bool -``` - -**Description:** -Get all identity topup registration indices from managed collection # Safety - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - `out_indices` must be a valid pointer to store the indices array - `out_count` must be a valid pointer to store the count - The returned array must be freed with `free_u32_array` when no longer needed - -**Safety:** -- `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - `out_indices` must be a valid pointer to store the indices array - `out_count` must be a valid pointer to store the count - The returned array must be freed with `free_u32_array` when no longer needed - -**Module:** `managed_account_collection` - ---- - -#### `managed_account_collection_get_identity_topup_not_bound` - -```c -managed_account_collection_get_identity_topup_not_bound(collection: *const FFIManagedCoreAccountCollection,) -> *mut FFIManagedCoreAccount -``` - -**Description:** -Get the identity topup not bound account if it exists in managed collection # Safety - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - `manager` must be a valid pointer to an FFIWalletManager - The returned pointer must be freed with `managed_core_account_free` when no longer needed - -**Safety:** -- `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - `manager` must be a valid pointer to an FFIWalletManager - The returned pointer must be freed with `managed_core_account_free` when no longer needed - -**Module:** `managed_account_collection` - ---- - -#### `managed_account_collection_get_platform_payment_account` - -```c -managed_account_collection_get_platform_payment_account(collection: *const FFIManagedCoreAccountCollection, account_index: c_uint, key_class: c_uint,) -> *mut crate::managed_account::FFIManagedPlatformAccount -``` - -**Description:** -Get a Platform Payment account by account index and key class from the managed collection Platform Payment accounts (DIP-17) are identified by two indices: - account_index: The account' level in the derivation path - key_class: The key_class' level in the derivation path (typically 0) # Safety - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - The returned pointer must be freed with `managed_platform_account_free` when no longer needed - -**Safety:** -- `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - The returned pointer must be freed with `managed_platform_account_free` when no longer needed - -**Module:** `managed_account_collection` - ---- - -#### `managed_account_collection_get_platform_payment_keys` - -```c -managed_account_collection_get_platform_payment_keys(collection: *const FFIManagedCoreAccountCollection, out_keys: *mut *mut crate::managed_account::FFIPlatformPaymentAccountKey, out_count: *mut usize,) -> bool -``` - -**Description:** -Get all Platform Payment account keys from managed collection Returns an array of FFIPlatformPaymentAccountKey structures. # Safety - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - `out_keys` must be a valid pointer to store the keys array - `out_count` must be a valid pointer to store the count - The returned array must be freed with `managed_account_collection_free_platform_payment_keys` when no longer needed - -**Safety:** -- `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - `out_keys` must be a valid pointer to store the keys array - `out_count` must be a valid pointer to store the count - The returned array must be freed with `managed_account_collection_free_platform_payment_keys` when no longer needed - -**Module:** `managed_account_collection` - ---- - -#### `managed_account_collection_get_provider_operator_keys` - -```c -managed_account_collection_get_provider_operator_keys(collection: *const FFIManagedCoreAccountCollection,) -> *mut std::os::raw::c_void -``` - -**Description:** -Get the provider operator keys account if it exists in managed collection Note: Returns null if the `bls` feature is not enabled # Safety - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - The returned pointer must be freed with `managed_core_account_free` when no longer needed (when BLS is enabled) - -**Safety:** -- `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - The returned pointer must be freed with `managed_core_account_free` when no longer needed (when BLS is enabled) - -**Module:** `managed_account_collection` - ---- - -#### `managed_account_collection_get_provider_owner_keys` - -```c -managed_account_collection_get_provider_owner_keys(collection: *const FFIManagedCoreAccountCollection,) -> *mut FFIManagedCoreAccount -``` - -**Description:** -Get the provider owner keys account if it exists in managed collection # Safety - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - The returned pointer must be freed with `managed_core_account_free` when no longer needed - -**Safety:** -- `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - The returned pointer must be freed with `managed_core_account_free` when no longer needed - -**Module:** `managed_account_collection` - ---- - -#### `managed_account_collection_get_provider_platform_keys` - -```c -managed_account_collection_get_provider_platform_keys(collection: *const FFIManagedCoreAccountCollection,) -> *mut std::os::raw::c_void -``` - -**Description:** -Get the provider platform keys account if it exists in managed collection Note: Returns null if the `eddsa` feature is not enabled # Safety - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - The returned pointer must be freed with `managed_core_account_free` when no longer needed (when EdDSA is enabled) - -**Safety:** -- `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - The returned pointer must be freed with `managed_core_account_free` when no longer needed (when EdDSA is enabled) - -**Module:** `managed_account_collection` - ---- - -#### `managed_account_collection_get_provider_voting_keys` - -```c -managed_account_collection_get_provider_voting_keys(collection: *const FFIManagedCoreAccountCollection,) -> *mut FFIManagedCoreAccount -``` - -**Description:** -Get the provider voting keys account if it exists in managed collection # Safety - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - The returned pointer must be freed with `managed_core_account_free` when no longer needed - -**Safety:** -- `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - The returned pointer must be freed with `managed_core_account_free` when no longer needed - -**Module:** `managed_account_collection` - ---- - -#### `managed_account_collection_has_identity_invitation` - -```c -managed_account_collection_has_identity_invitation(collection: *const FFIManagedCoreAccountCollection,) -> bool -``` - -**Description:** -Check if identity invitation account exists in managed collection # Safety - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - -**Safety:** -- `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - -**Module:** `managed_account_collection` - ---- - -#### `managed_account_collection_has_identity_registration` - -```c -managed_account_collection_has_identity_registration(collection: *const FFIManagedCoreAccountCollection,) -> bool -``` - -**Description:** -Check if identity registration account exists in managed collection # Safety - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - -**Safety:** -- `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - -**Module:** `managed_account_collection` - ---- - -#### `managed_account_collection_has_identity_topup_not_bound` - -```c -managed_account_collection_has_identity_topup_not_bound(collection: *const FFIManagedCoreAccountCollection,) -> bool -``` - -**Description:** -Check if identity topup not bound account exists in managed collection # Safety - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - -**Safety:** -- `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - -**Module:** `managed_account_collection` - ---- - -#### `managed_account_collection_has_platform_payment_accounts` - -```c -managed_account_collection_has_platform_payment_accounts(collection: *const FFIManagedCoreAccountCollection,) -> bool -``` - -**Description:** -Check if there are any Platform Payment accounts in the managed collection # Safety - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - -**Safety:** -- `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - -**Module:** `managed_account_collection` - ---- - -#### `managed_account_collection_has_provider_operator_keys` - -```c -managed_account_collection_has_provider_operator_keys(collection: *const FFIManagedCoreAccountCollection,) -> bool -``` - -**Description:** -Check if provider operator keys account exists in managed collection # Safety - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - -**Safety:** -- `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - -**Module:** `managed_account_collection` - ---- - -#### `managed_account_collection_has_provider_owner_keys` - -```c -managed_account_collection_has_provider_owner_keys(collection: *const FFIManagedCoreAccountCollection,) -> bool -``` - -**Description:** -Check if provider owner keys account exists in managed collection # Safety - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - -**Safety:** -- `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - -**Module:** `managed_account_collection` - ---- - -#### `managed_account_collection_has_provider_platform_keys` - -```c -managed_account_collection_has_provider_platform_keys(collection: *const FFIManagedCoreAccountCollection,) -> bool -``` - -**Description:** -Check if provider platform keys account exists in managed collection # Safety - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - -**Safety:** -- `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - -**Module:** `managed_account_collection` - ---- - -#### `managed_account_collection_has_provider_voting_keys` - -```c -managed_account_collection_has_provider_voting_keys(collection: *const FFIManagedCoreAccountCollection,) -> bool -``` - -**Description:** -Check if provider voting keys account exists in managed collection # Safety - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - -**Safety:** -- `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - -**Module:** `managed_account_collection` - ---- - -#### `managed_account_collection_platform_payment_count` - -```c -managed_account_collection_platform_payment_count(collection: *const FFIManagedCoreAccountCollection,) -> c_uint -``` - -**Description:** -Get the number of Platform Payment accounts in the managed collection # Safety - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - -**Safety:** -- `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - -**Module:** `managed_account_collection` - ---- - -#### `managed_account_collection_summary` - -```c -managed_account_collection_summary(collection: *const FFIManagedCoreAccountCollection,) -> *mut c_char -``` - -**Description:** -Get a human-readable summary of all accounts in the managed collection Returns a formatted string showing all account types and their indices. The format is designed to be clear and readable for end users. # Safety - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - The returned string must be freed with `string_free` when no longer needed - Returns null if the collection pointer is null - -**Safety:** -- `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - The returned string must be freed with `string_free` when no longer needed - Returns null if the collection pointer is null - -**Module:** `managed_account_collection` - ---- - -#### `managed_account_collection_summary_data` - -```c -managed_account_collection_summary_data(collection: *const FFIManagedCoreAccountCollection,) -> *mut FFIManagedCoreAccountCollectionSummary -``` - -**Description:** -Get structured account collection summary data for managed collection Returns a struct containing arrays of indices for each account type and boolean flags for special accounts. This provides Swift with programmatic access to account information. # Safety - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - The returned pointer must be freed with `managed_account_collection_summary_free` when no longer needed - Returns null if the collection pointer is null - -**Safety:** -- `collection` must be a valid pointer to an FFIManagedCoreAccountCollection - The returned pointer must be freed with `managed_account_collection_summary_free` when no longer needed - Returns null if the collection pointer is null - -**Module:** `managed_account_collection` - ---- - -#### `managed_account_collection_summary_free` - -```c -managed_account_collection_summary_free(summary: *mut FFIManagedCoreAccountCollectionSummary,) -> () -``` - -**Description:** -Free a managed account collection summary and all its allocated memory # Safety - `summary` must be a valid pointer to an FFIManagedCoreAccountCollectionSummary created by `managed_account_collection_summary_data` - `summary` must not be used after calling this function - -**Safety:** -- `summary` must be a valid pointer to an FFIManagedCoreAccountCollectionSummary created by `managed_account_collection_summary_data` - `summary` must not be used after calling this function - -**Module:** `managed_account_collection` - ---- - -#### `managed_core_account_free` - -```c -managed_core_account_free(account: *mut FFIManagedCoreAccount) -> () -``` - -**Description:** -Free a managed account handle # Safety - `account` must be a valid pointer to an FFIManagedCoreAccount that was allocated by this library - The pointer must not be used after calling this function - This function must only be called once per allocation - -**Safety:** -- `account` must be a valid pointer to an FFIManagedCoreAccount that was allocated by this library - The pointer must not be used after calling this function - This function must only be called once per allocation - -**Module:** `managed_account` - ---- - -#### `managed_core_account_free_transactions` - -```c -managed_core_account_free_transactions(transactions: *mut FFITransactionRecord, count: usize,) -> () -``` - -**Description:** -Free transactions array returned by managed_core_account_get_transactions Only available with the `keep-finalized-transactions` Cargo feature, in which configuration `managed_core_account_get_transactions` is also available — the two functions are paired. # Safety - `transactions` must be a pointer returned by `managed_core_account_get_transactions` - `count` must be the count returned by `managed_core_account_get_transactions` - This function must only be called once per allocation - -**Safety:** -- `transactions` must be a pointer returned by `managed_core_account_get_transactions` - `count` must be the count returned by `managed_core_account_get_transactions` - This function must only be called once per allocation - -**Module:** `managed_account` - ---- - -#### `managed_core_account_get_account_type` - -```c -managed_core_account_get_account_type(account: *const FFIManagedCoreAccount, index_out: *mut c_uint,) -> FFIAccountKind -``` - -**Description:** -Get the account type of a managed account # Safety - `account` must be a valid pointer to an FFIManagedCoreAccount instance - `index_out` must be a valid pointer to receive the account index (or null) - -**Safety:** -- `account` must be a valid pointer to an FFIManagedCoreAccount instance - `index_out` must be a valid pointer to receive the account index (or null) - -**Module:** `managed_account` - ---- - -#### `managed_core_account_get_address_pool` - -```c -managed_core_account_get_address_pool(account: *const FFIManagedCoreAccount, pool_type: FFIAddressPoolType,) -> *mut FFIAddressPool -``` - -**Description:** -Get an address pool from a managed account by type This function returns the appropriate address pool based on the pool type parameter. For Standard accounts with External/Internal pool types, returns the corresponding pool. For non-standard accounts with Single pool type, returns their single address pool. # Safety - `manager` must be a valid pointer to an FFIWalletManager instance - `account` must be a valid pointer to an FFIManagedCoreAccount instance - `wallet_id` must be a valid pointer to a 32-byte wallet ID - The returned pool must be freed with `address_pool_free` when no longer needed - -**Safety:** -- `manager` must be a valid pointer to an FFIWalletManager instance - `account` must be a valid pointer to an FFIManagedCoreAccount instance - `wallet_id` must be a valid pointer to a 32-byte wallet ID - The returned pool must be freed with `address_pool_free` when no longer needed - -**Module:** `managed_account` - ---- - -#### `managed_core_account_get_balance` - -```c -managed_core_account_get_balance(account: *const FFIManagedCoreAccount, balance_out: *mut crate::types::FFIBalance,) -> bool -``` - -**Description:** -Get the balance of a managed account. Returns `false` (and leaves `balance_out` untouched) when the handle wraps a keys-only account (identity / asset-lock / provider) — those don't track per-account balances. Use [`managed_core_account_get_account_type`] to disambiguate, or only call this for funds-bearing accounts. # Safety - `account` must be a valid pointer to an FFIManagedCoreAccount instance - `balance_out` must be a valid pointer to an FFIBalance structure - -**Safety:** -- `account` must be a valid pointer to an FFIManagedCoreAccount instance - `balance_out` must be a valid pointer to an FFIBalance structure - -**Module:** `managed_account` - ---- - -#### `managed_core_account_get_external_address_pool` - -```c -managed_core_account_get_external_address_pool(account: *const FFIManagedCoreAccount,) -> *mut FFIAddressPool -``` - -**Description:** -Get the external address pool from a managed account This function returns the external (receive) address pool for Standard accounts. Returns NULL for account types that don't have separate external/internal pools. # Safety - `account` must be a valid pointer to an FFIManagedCoreAccount instance - The returned pool must be freed with `address_pool_free` when no longer needed - -**Safety:** -- `account` must be a valid pointer to an FFIManagedCoreAccount instance - The returned pool must be freed with `address_pool_free` when no longer needed - -**Module:** `managed_account` - ---- - -#### `managed_core_account_get_index` - -```c -managed_core_account_get_index(account: *const FFIManagedCoreAccount,) -> c_uint -``` - -**Description:** -Get the account index from a managed account Returns the primary account index for Standard and CoinJoin accounts. Returns 0 for account types that don't have an index (like Identity or Provider accounts). # Safety - `account` must be a valid pointer to an FFIManagedCoreAccount instance - -**Safety:** -- `account` must be a valid pointer to an FFIManagedCoreAccount instance - -**Module:** `managed_account` - ---- - -#### `managed_core_account_get_internal_address_pool` - -```c -managed_core_account_get_internal_address_pool(account: *const FFIManagedCoreAccount,) -> *mut FFIAddressPool -``` - -**Description:** -Get the internal address pool from a managed account This function returns the internal (change) address pool for Standard accounts. Returns NULL for account types that don't have separate external/internal pools. # Safety - `account` must be a valid pointer to an FFIManagedCoreAccount instance - The returned pool must be freed with `address_pool_free` when no longer needed - -**Safety:** -- `account` must be a valid pointer to an FFIManagedCoreAccount instance - The returned pool must be freed with `address_pool_free` when no longer needed - -**Module:** `managed_account` - ---- - -#### `managed_core_account_get_network` - -```c -managed_core_account_get_network(account: *const FFIManagedCoreAccount,) -> FFINetwork -``` - -**Description:** -Get the network of a managed account # Safety - `account` must be a valid pointer to an FFIManagedCoreAccount instance - Returns `FFINetwork::Mainnet` if the account is null - -**Safety:** -- `account` must be a valid pointer to an FFIManagedCoreAccount instance - Returns `FFINetwork::Mainnet` if the account is null - -**Module:** `managed_account` - ---- - -#### `managed_core_account_get_transaction_count` - -```c -managed_core_account_get_transaction_count(account: *const FFIManagedCoreAccount,) -> c_uint -``` - -**Description:** -Get the number of transactions in a managed account Only available with the `keep-finalized-transactions` Cargo feature. With the feature off (the default), records of chainlocked transactions are dropped from the in-memory map, so the count would not reflect the full history — the function is intentionally not exposed. # Safety - `account` must be a valid pointer to an FFIManagedCoreAccount instance - -**Safety:** -- `account` must be a valid pointer to an FFIManagedCoreAccount instance - -**Module:** `managed_account` - ---- - -#### `managed_core_account_get_transactions` - -```c -managed_core_account_get_transactions(account: *const FFIManagedCoreAccount, transactions_out: *mut *mut FFITransactionRecord, count_out: *mut usize,) -> bool -``` - -**Description:** -Get all transactions from a managed account Returns an array of FFITransactionRecord structures. Only available with the `keep-finalized-transactions` Cargo feature. With the feature off (the default), records of chainlocked transactions are dropped from the in-memory map, so this would only return a partial history — the function is intentionally not exposed. # Safety - `account` must be a valid pointer to an FFIManagedCoreAccount instance - `transactions_out` must be a valid pointer to receive the transactions array pointer - `count_out` must be a valid pointer to receive the count - The caller must free the returned array using `managed_core_account_free_transactions` - -**Safety:** -- `account` must be a valid pointer to an FFIManagedCoreAccount instance - `transactions_out` must be a valid pointer to receive the transactions array pointer - `count_out` must be a valid pointer to receive the count - The caller must free the returned array using `managed_core_account_free_transactions` - -**Module:** `managed_account` - ---- - -#### `managed_core_account_get_utxo_count` - -```c -managed_core_account_get_utxo_count(account: *const FFIManagedCoreAccount,) -> c_uint -``` - -**Description:** -Get the number of UTXOs in a managed account. Always returns 0 for keys-only accounts (identity / asset-lock / provider), which do not track per-account UTXOs. # Safety - `account` must be a valid pointer to an FFIManagedCoreAccount instance - -**Safety:** -- `account` must be a valid pointer to an FFIManagedCoreAccount instance - -**Module:** `managed_account` - ---- - -#### `managed_platform_account_free` - -```c -managed_platform_account_free(account: *mut FFIManagedPlatformAccount) -> () -``` - -**Description:** -Free a managed platform account handle # Safety - `account` must be a valid pointer to an FFIManagedPlatformAccount that was allocated by this library - The pointer must not be used after calling this function - This function must only be called once per allocation - -**Safety:** -- `account` must be a valid pointer to an FFIManagedPlatformAccount that was allocated by this library - The pointer must not be used after calling this function - This function must only be called once per allocation - -**Module:** `managed_account` - ---- - -#### `managed_platform_account_get_account_index` - -```c -managed_platform_account_get_account_index(account: *const FFIManagedPlatformAccount,) -> c_uint -``` - -**Description:** -Get the account index of a managed platform account # Safety - `account` must be a valid pointer to an FFIManagedPlatformAccount instance - -**Safety:** -- `account` must be a valid pointer to an FFIManagedPlatformAccount instance - -**Module:** `managed_account` - ---- - -#### `managed_platform_account_get_address_pool` - -```c -managed_platform_account_get_address_pool(account: *const FFIManagedPlatformAccount,) -> *mut FFIAddressPool -``` - -**Description:** -Get the address pool from a managed platform account Platform accounts only have a single address pool. # Safety - `account` must be a valid pointer to an FFIManagedPlatformAccount instance - The returned pool must be freed with `address_pool_free` when no longer needed - -**Safety:** -- `account` must be a valid pointer to an FFIManagedPlatformAccount instance - The returned pool must be freed with `address_pool_free` when no longer needed - -**Module:** `managed_account` - ---- - -#### `managed_platform_account_get_credit_balance` - -```c -managed_platform_account_get_credit_balance(account: *const FFIManagedPlatformAccount,) -> u64 -``` - -**Description:** -Get the total credit balance of a managed platform account Returns the balance in credits (1000 credits = 1 duff) # Safety - `account` must be a valid pointer to an FFIManagedPlatformAccount instance - -**Safety:** -- `account` must be a valid pointer to an FFIManagedPlatformAccount instance - -**Module:** `managed_account` - ---- - -#### `managed_platform_account_get_duff_balance` - -```c -managed_platform_account_get_duff_balance(account: *const FFIManagedPlatformAccount,) -> u64 -``` - -**Description:** -Get the total balance in duffs of a managed platform account Returns the balance in duffs (credit_balance / 1000) # Safety - `account` must be a valid pointer to an FFIManagedPlatformAccount instance - -**Safety:** -- `account` must be a valid pointer to an FFIManagedPlatformAccount instance - -**Module:** `managed_account` - ---- - -#### `managed_platform_account_get_funded_address_count` - -```c -managed_platform_account_get_funded_address_count(account: *const FFIManagedPlatformAccount,) -> c_uint -``` - -**Description:** -Get the number of funded addresses in a managed platform account # Safety - `account` must be a valid pointer to an FFIManagedPlatformAccount instance - -**Safety:** -- `account` must be a valid pointer to an FFIManagedPlatformAccount instance - -**Module:** `managed_account` - ---- - -#### `managed_platform_account_get_is_watch_only` - -```c -managed_platform_account_get_is_watch_only(account: *const FFIManagedPlatformAccount,) -> bool -``` - -**Description:** -Check if a managed platform account is watch-only # Safety - `account` must be a valid pointer to an FFIManagedPlatformAccount instance - -**Safety:** -- `account` must be a valid pointer to an FFIManagedPlatformAccount instance - -**Module:** `managed_account` - ---- - -#### `managed_platform_account_get_key_class` - -```c -managed_platform_account_get_key_class(account: *const FFIManagedPlatformAccount,) -> c_uint -``` - -**Description:** -Get the key class of a managed platform account # Safety - `account` must be a valid pointer to an FFIManagedPlatformAccount instance - -**Safety:** -- `account` must be a valid pointer to an FFIManagedPlatformAccount instance - -**Module:** `managed_account` - ---- - -#### `managed_platform_account_get_network` - -```c -managed_platform_account_get_network(account: *const FFIManagedPlatformAccount,) -> FFINetwork -``` - -**Description:** -Get the network of a managed platform account # Safety - `account` must be a valid pointer to an FFIManagedPlatformAccount instance - Returns `FFINetwork::Mainnet` if the account is null - -**Safety:** -- `account` must be a valid pointer to an FFIManagedPlatformAccount instance - Returns `FFINetwork::Mainnet` if the account is null - -**Module:** `managed_account` - ---- - -#### `managed_platform_account_get_total_address_count` - -```c -managed_platform_account_get_total_address_count(account: *const FFIManagedPlatformAccount,) -> c_uint -``` - -**Description:** -Get the total number of addresses in a managed platform account # Safety - `account` must be a valid pointer to an FFIManagedPlatformAccount instance - -**Safety:** -- `account` must be a valid pointer to an FFIManagedPlatformAccount instance - -**Module:** `managed_account` - ---- - -### Address Management - Detailed - -#### `address_array_free` - -```c -address_array_free(addresses: *mut *mut c_char, count: usize) -> () -``` - -**Description:** -Free address array # Safety - `addresses` must be a valid pointer to an array of address strings or null - Each address in the array must be a valid C string pointer - `count` must be the correct number of addresses in the array - After calling this function, all pointers become invalid - -**Safety:** -- `addresses` must be a valid pointer to an array of address strings or null - Each address in the array must be a valid C string pointer - `count` must be the correct number of addresses in the array - After calling this function, all pointers become invalid - -**Module:** `address` - ---- - -#### `address_free` - -```c -address_free(address: *mut c_char) -> () -``` - -**Description:** -Free address string # Safety - `address` must be a valid pointer created by address functions or null - After calling this function, the pointer becomes invalid - -**Safety:** -- `address` must be a valid pointer created by address functions or null - After calling this function, the pointer becomes invalid - -**Module:** `address` - ---- - -#### `address_get_type` - -```c -address_get_type(address: *const c_char, network: FFINetwork, error: *mut FFIError,) -> c_uchar -``` - -**Description:** -Get address type Returns: - 0: P2PKH address - 1: P2SH address - 2: Other address type - u8::MAX (255): Error occurred # Safety - `address` must be a valid null-terminated C string - `error` must be a valid pointer to an FFIError - -**Safety:** -- `address` must be a valid null-terminated C string - `error` must be a valid pointer to an FFIError - -**Module:** `address` - ---- - -#### `address_info_array_free` - -```c -address_info_array_free(infos: *mut *mut FFIAddressInfo, count: usize) -> () -``` - -**Description:** -Free an array of FFIAddressInfo structures # Safety - `infos` must be a valid pointer to an array of FFIAddressInfo pointers allocated by this library or null - `count` must be the exact number of elements in the array - The pointers must not be used after calling this function - -**Safety:** -- `infos` must be a valid pointer to an array of FFIAddressInfo pointers allocated by this library or null - `count` must be the exact number of elements in the array - The pointers must not be used after calling this function - -**Module:** `address_pool` - ---- - -#### `address_info_free` - -```c -address_info_free(info: *mut FFIAddressInfo) -> () -``` - -**Description:** -Free a single FFIAddressInfo structure # Safety - `info` must be a valid pointer to an FFIAddressInfo allocated by this library or null - The pointer must not be used after calling this function - -**Safety:** -- `info` must be a valid pointer to an FFIAddressInfo allocated by this library or null - The pointer must not be used after calling this function - -**Module:** `address_pool` - ---- - -#### `address_pool_free` - -```c -address_pool_free(pool: *mut FFIAddressPool) -> () -``` - -**Description:** -Free an address pool handle # Safety - `pool` must be a valid pointer to an FFIAddressPool that was allocated by this library - The pointer must not be used after calling this function - This function must only be called once per allocation - -**Safety:** -- `pool` must be a valid pointer to an FFIAddressPool that was allocated by this library - The pointer must not be used after calling this function - This function must only be called once per allocation - -**Module:** `address_pool` - ---- - -#### `address_pool_get_address_at_index` - -```c -address_pool_get_address_at_index(pool: *const FFIAddressPool, index: u32, error: *mut FFIError,) -> *mut FFIAddressInfo -``` - -**Description:** -Get a single address info at a specific index from the pool Returns detailed information about the address at the given index, or NULL if the index is out of bounds or not generated yet. # Safety - `pool` must be a valid pointer to an FFIAddressPool - `error` must be a valid pointer to an FFIError - The returned FFIAddressInfo must be freed using `address_info_free` - -**Safety:** -- `pool` must be a valid pointer to an FFIAddressPool - `error` must be a valid pointer to an FFIError - The returned FFIAddressInfo must be freed using `address_info_free` - -**Module:** `address_pool` - ---- - -#### `address_pool_get_addresses_in_range` - -```c -address_pool_get_addresses_in_range(pool: *const FFIAddressPool, start_index: u32, end_index: u32, count_out: *mut usize, error: *mut FFIError,) -> *mut *mut FFIAddressInfo -``` - -**Description:** -Get a range of addresses from the pool Returns an array of FFIAddressInfo structures for addresses in the range [start_index, end_index). The count_out parameter will be set to the actual number of addresses returned. Note: This function only reads existing addresses from the pool. It does not generate new addresses. Use managed_wallet_generate_addresses_to_index if you need to generate addresses first. # Safety - `pool` must be a valid pointer to an FFIAddressPool - `count_out` must be a valid pointer to store the count - `error` must be a valid pointer to an FFIError - The returned array must be freed using `address_info_array_free` - -**Safety:** -- `pool` must be a valid pointer to an FFIAddressPool - `count_out` must be a valid pointer to store the count - `error` must be a valid pointer to an FFIError - The returned array must be freed using `address_info_array_free` - -**Module:** `address_pool` - ---- - -#### `address_to_pubkey_hash` - -```c -address_to_pubkey_hash(address: *const c_char, network: FFINetwork, hash_out: *mut u8,) -> i32 -``` - -**Description:** -Extract public key hash from P2PKH address # Safety - `address` must be a valid pointer to a null-terminated C string - `hash_out` must be a valid pointer to a buffer of at least 20 bytes # Returns - 0 on success - -1 on error - -**Safety:** -- `address` must be a valid pointer to a null-terminated C string - `hash_out` must be a valid pointer to a buffer of at least 20 bytes - -**Module:** `transaction` - ---- - -#### `address_validate` - -```c -address_validate(address: *const c_char, network: FFINetwork, error: *mut FFIError,) -> bool -``` - -**Description:** -Validate an address # Safety - `address` must be a valid null-terminated C string - `error` must be a valid pointer to an FFIError - -**Safety:** -- `address` must be a valid null-terminated C string - `error` must be a valid pointer to an FFIError - -**Module:** `address` - ---- - -### Transaction Management - Detailed - -#### `transaction_add_input` - -```c -transaction_add_input(tx: *mut FFITransaction, input: *const FFITxIn,) -> i32 -``` - -**Description:** -Add an input to a transaction # Safety - `tx` must be a valid pointer to an FFITransaction - `input` must be a valid pointer to an FFITxIn # Returns - 0 on success - -1 on error - -**Safety:** -- `tx` must be a valid pointer to an FFITransaction - `input` must be a valid pointer to an FFITxIn - -**Module:** `transaction` - ---- - -#### `transaction_add_output` - -```c -transaction_add_output(tx: *mut FFITransaction, output: *const FFITxOut,) -> i32 -``` - -**Description:** -Add an output to a transaction # Safety - `tx` must be a valid pointer to an FFITransaction - `output` must be a valid pointer to an FFITxOut # Returns - 0 on success - -1 on error - -**Safety:** -- `tx` must be a valid pointer to an FFITransaction - `output` must be a valid pointer to an FFITxOut - -**Module:** `transaction` - ---- - -#### `transaction_bytes_free` - -```c -transaction_bytes_free(tx_bytes: *mut u8) -> () -``` - -**Description:** -Free transaction bytes # Safety - `tx_bytes` must be a valid pointer created by transaction functions or null - After calling this function, the pointer becomes invalid - -**Safety:** -- `tx_bytes` must be a valid pointer created by transaction functions or null - After calling this function, the pointer becomes invalid - -**Module:** `transaction` - ---- - -#### `transaction_check_result_free` - -```c -transaction_check_result_free(result: *mut FFITransactionCheckResult) -> () -``` - -**Description:** -Free a transaction check result # Safety - `result` must be a valid pointer to an FFITransactionCheckResult - This function must only be called once per result - -**Safety:** -- `result` must be a valid pointer to an FFITransactionCheckResult - This function must only be called once per result - -**Module:** `transaction_checking` - ---- - -#### `transaction_classify` - -```c -transaction_classify(tx_bytes: *const u8, tx_len: usize, error: *mut FFIError,) -> *mut c_char -``` - -**Description:** -Get the transaction classification for routing Returns a string describing the transaction type (e.g., "Standard", "CoinJoin", "AssetLock", "AssetUnlock", "ProviderRegistration", etc.) # Safety - `tx_bytes` must be a valid pointer to transaction bytes with at least `tx_len` bytes - `error` must be a valid pointer to an FFIError - The returned string must be freed by the caller - -**Safety:** -- `tx_bytes` must be a valid pointer to transaction bytes with at least `tx_len` bytes - `error` must be a valid pointer to an FFIError - The returned string must be freed by the caller - -**Module:** `transaction_checking` - ---- - -#### `transaction_create` - -```c -transaction_create() -> *mut FFITransaction -``` - -**Description:** -Create a new empty transaction # Returns - Pointer to FFITransaction on success - NULL on error - -**Module:** `transaction` - ---- - -#### `transaction_deserialize` - -```c -transaction_deserialize(data: *const u8, len: u32) -> *mut FFITransaction -``` - -**Description:** -Deserialize a transaction # Safety - `data` must be a valid pointer to serialized transaction data - `len` must be the correct length of the data # Returns - Pointer to FFITransaction on success - NULL on error - -**Safety:** -- `data` must be a valid pointer to serialized transaction data - `len` must be the correct length of the data - -**Module:** `transaction` - ---- - -#### `transaction_destroy` - -```c -transaction_destroy(tx: *mut FFITransaction) -> () -``` - -**Description:** -Destroy a transaction # Safety - `tx` must be a valid pointer to an FFITransaction created by transaction functions or null - After calling this function, the pointer becomes invalid - -**Safety:** -- `tx` must be a valid pointer to an FFITransaction created by transaction functions or null - After calling this function, the pointer becomes invalid - -**Module:** `transaction` - ---- - -#### `transaction_get_txid` - -```c -transaction_get_txid(tx: *const FFITransaction, txid_out: *mut u8) -> i32 -``` - -**Description:** -Get the transaction ID # Safety - `tx` must be a valid pointer to an FFITransaction - `txid_out` must be a valid pointer to a buffer of at least 32 bytes # Returns - 0 on success - -1 on error - -**Safety:** -- `tx` must be a valid pointer to an FFITransaction - `txid_out` must be a valid pointer to a buffer of at least 32 bytes - -**Module:** `transaction` - ---- - -#### `transaction_get_txid_from_bytes` - -```c -transaction_get_txid_from_bytes(tx_bytes: *const u8, tx_len: usize, error: *mut FFIError,) -> *mut c_char -``` - -**Description:** -Get transaction ID from raw transaction bytes # Safety - `tx_bytes` must be a valid pointer to transaction bytes - `tx_len` must be the correct length of the transaction - `error` must be a valid pointer to an FFIError # Returns - Pointer to null-terminated hex string of TXID (must be freed with string_free) - NULL on error - -**Safety:** -- `tx_bytes` must be a valid pointer to transaction bytes - `tx_len` must be the correct length of the transaction - `error` must be a valid pointer to an FFIError - -**Module:** `transaction` - ---- - -#### `transaction_serialize` - -```c -transaction_serialize(tx: *const FFITransaction, out_buf: *mut u8, out_len: *mut u32,) -> i32 -``` - -**Description:** -Serialize a transaction # Safety - `tx` must be a valid pointer to an FFITransaction - `out_buf` can be NULL to get size only - `out_len` must be a valid pointer to store the size # Returns - 0 on success - -1 on error - -**Safety:** -- `tx` must be a valid pointer to an FFITransaction - `out_buf` can be NULL to get size only - `out_len` must be a valid pointer to store the size - -**Module:** `transaction` - ---- - -#### `transaction_sighash` - -```c -transaction_sighash(tx: *const FFITransaction, input_index: u32, script_pubkey: *const u8, script_pubkey_len: u32, sighash_type: u32, hash_out: *mut u8,) -> i32 -``` - -**Description:** -Calculate signature hash for an input # Safety - `tx` must be a valid pointer to an FFITransaction - `script_pubkey` must be a valid pointer to the script pubkey - `hash_out` must be a valid pointer to a buffer of at least 32 bytes # Returns - 0 on success - -1 on error - -**Safety:** -- `tx` must be a valid pointer to an FFITransaction - `script_pubkey` must be a valid pointer to the script pubkey - `hash_out` must be a valid pointer to a buffer of at least 32 bytes - -**Module:** `transaction` - ---- - -#### `transaction_sign_input` - -```c -transaction_sign_input(tx: *mut FFITransaction, input_index: u32, private_key: *const u8, script_pubkey: *const u8, script_pubkey_len: u32, sighash_type: u32,) -> i32 -``` - -**Description:** -Sign a transaction input # Safety - `tx` must be a valid pointer to an FFITransaction - `private_key` must be a valid pointer to a 32-byte private key - `script_pubkey` must be a valid pointer to the script pubkey # Returns - 0 on success - -1 on error - -**Safety:** -- `tx` must be a valid pointer to an FFITransaction - `private_key` must be a valid pointer to a 32-byte private key - `script_pubkey` must be a valid pointer to the script pubkey - -**Module:** `transaction` - ---- - -#### `utxo_array_free` - -```c -utxo_array_free(utxos: *mut FFIUTXO, count: usize) -> () -``` - -**Description:** -Free UTXO array # Safety - `utxos` must be a valid pointer to an array of FFIUTXO structs allocated by this library - `count` must match the number of UTXOs in the array - The pointer must not be used after calling this function - This function must only be called once per array - -**Safety:** -- `utxos` must be a valid pointer to an array of FFIUTXO structs allocated by this library - `count` must match the number of UTXOs in the array - The pointer must not be used after calling this function - This function must only be called once per array - -**Module:** `utxo` - ---- - -### Key Management - Detailed - -#### `bip38_decrypt_private_key` - -```c -bip38_decrypt_private_key(_encrypted_key: *const c_char, _passphrase: *const c_char, error: *mut FFIError,) -> *mut c_char -``` - -**Description:** -Decrypt a BIP38 encrypted private key # Safety This function is unsafe because it dereferences raw pointers: - `encrypted_key` must be a valid, null-terminated C string - `passphrase` must be a valid, null-terminated C string - `error` must be a valid pointer to an FFIError - -**Safety:** -This function is unsafe because it dereferences raw pointers: - `encrypted_key` must be a valid, null-terminated C string - `passphrase` must be a valid, null-terminated C string - `error` must be a valid pointer to an FFIError - -**Module:** `bip38` - ---- - -#### `bip38_encrypt_private_key` - -```c -bip38_encrypt_private_key(_private_key: *const c_char, _passphrase: *const c_char, error: *mut FFIError,) -> *mut c_char -``` - -**Description:** -Encrypt a private key with BIP38 # Safety This function is unsafe because it dereferences raw pointers: - `private_key` must be a valid, null-terminated C string - `passphrase` must be a valid, null-terminated C string - `error` must be a valid pointer to an FFIError - -**Safety:** -This function is unsafe because it dereferences raw pointers: - `private_key` must be a valid, null-terminated C string - `passphrase` must be a valid, null-terminated C string - `error` must be a valid pointer to an FFIError - -**Module:** `bip38` - ---- - -#### `derivation_derive_private_key_from_seed` - -```c -derivation_derive_private_key_from_seed(seed: *const u8, seed_len: usize, path: *const c_char, network: FFINetwork, error: *mut FFIError,) -> *mut FFIExtendedPrivKey -``` - -**Description:** -Derive private key for a specific path from seed # Safety - `seed` must be a valid pointer to a byte array of `seed_len` length - `path` must be a valid pointer to a null-terminated C string - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - -**Safety:** -- `seed` must be a valid pointer to a byte array of `seed_len` length - `path` must be a valid pointer to a null-terminated C string - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - -**Module:** `derivation` - ---- - -#### `derivation_new_master_key` - -```c -derivation_new_master_key(seed: *const u8, seed_len: usize, network: FFINetwork, error: *mut FFIError,) -> *mut FFIExtendedPrivKey -``` - -**Description:** -Create a new master extended private key from seed # Safety - `seed` must be a valid pointer to a byte array of `seed_len` length - `error` must be a valid pointer to an FFIError structure - The caller must ensure the seed pointer remains valid for the duration of this call - -**Safety:** -- `seed` must be a valid pointer to a byte array of `seed_len` length - `error` must be a valid pointer to an FFIError structure - The caller must ensure the seed pointer remains valid for the duration of this call - -**Module:** `derivation` - ---- - -#### `extended_private_key_free` - -```c -extended_private_key_free(key: *mut FFIExtendedPrivKey) -> () -``` - -**Description:** -Free an extended private key # Safety - `key` must be a valid pointer created by extended private key functions or null - After calling this function, the pointer becomes invalid - -**Safety:** -- `key` must be a valid pointer created by extended private key functions or null - After calling this function, the pointer becomes invalid - -**Module:** `keys` - ---- - -#### `extended_private_key_get_private_key` - -```c -extended_private_key_get_private_key(extended_key: *const FFIExtendedPrivKey, error: *mut FFIError,) -> *mut FFIPrivateKey -``` - -**Description:** -Get the private key from an extended private key Extracts the non-extended private key from an extended private key. # Safety - `extended_key` must be a valid pointer to an FFIExtendedPrivKey - `error` must be a valid pointer to an FFIError - The returned FFIPrivateKey must be freed with `private_key_free` - -**Safety:** -- `extended_key` must be a valid pointer to an FFIExtendedPrivKey - `error` must be a valid pointer to an FFIError - The returned FFIPrivateKey must be freed with `private_key_free` - -**Module:** `keys` - ---- - -#### `extended_private_key_to_string` - -```c -extended_private_key_to_string(key: *const FFIExtendedPrivKey, _network: FFINetwork, error: *mut FFIError,) -> *mut c_char -``` - -**Description:** -Get extended private key as string (xprv format) Returns the extended private key in base58 format (xprv... for mainnet, tprv... for testnet) # Safety - `key` must be a valid pointer to an FFIExtendedPrivKey - `network` is ignored; the network is encoded in the extended key - `error` must be a valid pointer to an FFIError - The returned string must be freed with `string_free` - -**Safety:** -- `key` must be a valid pointer to an FFIExtendedPrivKey - `network` is ignored; the network is encoded in the extended key - `error` must be a valid pointer to an FFIError - The returned string must be freed with `string_free` - -**Module:** `keys` - ---- - -#### `extended_public_key_free` - -```c -extended_public_key_free(key: *mut FFIExtendedPubKey) -> () -``` - -**Description:** -Free an extended public key # Safety - `key` must be a valid pointer created by extended public key functions or null - After calling this function, the pointer becomes invalid - -**Safety:** -- `key` must be a valid pointer created by extended public key functions or null - After calling this function, the pointer becomes invalid - -**Module:** `keys` - ---- - -#### `extended_public_key_get_public_key` - -```c -extended_public_key_get_public_key(extended_key: *const FFIExtendedPubKey, error: *mut FFIError,) -> *mut FFIPublicKey -``` - -**Description:** -Get the public key from an extended public key Extracts the non-extended public key from an extended public key. # Safety - `extended_key` must be a valid pointer to an FFIExtendedPubKey - `error` must be a valid pointer to an FFIError - The returned FFIPublicKey must be freed with `public_key_free` - -**Safety:** -- `extended_key` must be a valid pointer to an FFIExtendedPubKey - `error` must be a valid pointer to an FFIError - The returned FFIPublicKey must be freed with `public_key_free` - -**Module:** `keys` - ---- - -#### `extended_public_key_to_string` - -```c -extended_public_key_to_string(key: *const FFIExtendedPubKey, _network: FFINetwork, error: *mut FFIError,) -> *mut c_char -``` - -**Description:** -Get extended public key as string (xpub format) Returns the extended public key in base58 format (xpub... for mainnet, tpub... for testnet) # Safety - `key` must be a valid pointer to an FFIExtendedPubKey - `network` is ignored; the network is encoded in the extended key - `error` must be a valid pointer to an FFIError - The returned string must be freed with `string_free` - -**Safety:** -- `key` must be a valid pointer to an FFIExtendedPubKey - `network` is ignored; the network is encoded in the extended key - `error` must be a valid pointer to an FFIError - The returned string must be freed with `string_free` - -**Module:** `keys` - ---- - -#### `private_key_free` - -```c -private_key_free(key: *mut FFIPrivateKey) -> () -``` - -**Description:** -Free a private key # Safety - `key` must be a valid pointer created by private key functions or null - After calling this function, the pointer becomes invalid - -**Safety:** -- `key` must be a valid pointer created by private key functions or null - After calling this function, the pointer becomes invalid - -**Module:** `keys` - ---- - -#### `private_key_to_wif` - -```c -private_key_to_wif(key: *const FFIPrivateKey, network: FFINetwork, error: *mut FFIError,) -> *mut c_char -``` - -**Description:** -Get private key as WIF string from FFIPrivateKey # Safety - `key` must be a valid pointer to an FFIPrivateKey - `error` must be a valid pointer to an FFIError - The returned string must be freed with `string_free` - -**Safety:** -- `key` must be a valid pointer to an FFIPrivateKey - `error` must be a valid pointer to an FFIError - The returned string must be freed with `string_free` - -**Module:** `keys` - ---- - -#### `public_key_free` - -```c -public_key_free(key: *mut FFIPublicKey) -> () -``` - -**Description:** -Free a public key # Safety - `key` must be a valid pointer created by public key functions or null - After calling this function, the pointer becomes invalid - -**Safety:** -- `key` must be a valid pointer created by public key functions or null - After calling this function, the pointer becomes invalid - -**Module:** `keys` - ---- - -#### `public_key_to_hex` - -```c -public_key_to_hex(key: *const FFIPublicKey, error: *mut FFIError,) -> *mut c_char -``` - -**Description:** -Get public key as hex string from FFIPublicKey # Safety - `key` must be a valid pointer to an FFIPublicKey - `error` must be a valid pointer to an FFIError - The returned string must be freed with `string_free` - -**Safety:** -- `key` must be a valid pointer to an FFIPublicKey - `error` must be a valid pointer to an FFIError - The returned string must be freed with `string_free` - -**Module:** `keys` - ---- - -### Mnemonic Operations - Detailed - -#### `mnemonic_free` - -```c -mnemonic_free(mnemonic: *mut c_char) -> () -``` - -**Description:** -Free a mnemonic string # Safety - `mnemonic` must be a valid pointer created by mnemonic generation functions or null - After calling this function, the pointer becomes invalid - -**Safety:** -- `mnemonic` must be a valid pointer created by mnemonic generation functions or null - After calling this function, the pointer becomes invalid - -**Module:** `mnemonic` - ---- - -#### `mnemonic_generate` - -```c -mnemonic_generate(word_count: c_uint, error: *mut FFIError,) -> *mut c_char -``` - -**Description:** -Generate a new mnemonic with specified word count (12, 15, 18, 21, or 24) # Safety `error` must be a valid pointer to an `FFIError`. The returned string must be freed with `mnemonic_free`. - -**Safety:** -`error` must be a valid pointer to an `FFIError`. The returned string must be freed with `mnemonic_free`. - -**Module:** `mnemonic` - ---- - -#### `mnemonic_generate_with_language` - -```c -mnemonic_generate_with_language(word_count: c_uint, language: FFILanguage, error: *mut FFIError,) -> *mut c_char -``` - -**Description:** -Generate a new mnemonic with specified language and word count # Safety `error` must be a valid pointer to an `FFIError`. The returned string must be freed with `mnemonic_free`. - -**Safety:** -`error` must be a valid pointer to an `FFIError`. The returned string must be freed with `mnemonic_free`. - -**Module:** `mnemonic` - ---- - -#### `mnemonic_to_seed` - -```c -mnemonic_to_seed(mnemonic: *const c_char, passphrase: *const c_char, seed_out: *mut u8, seed_len: *mut usize, error: *mut FFIError,) -> bool -``` - -**Description:** -Convert mnemonic to seed with optional passphrase # Safety - `mnemonic` must be a valid null-terminated C string - `passphrase` must be a valid null-terminated C string or null - `seed_out` must be a valid pointer to a buffer of at least 64 bytes - `seed_len` must be a valid pointer to store the seed length - `error` must be a valid pointer to an FFIError - -**Safety:** -- `mnemonic` must be a valid null-terminated C string - `passphrase` must be a valid null-terminated C string or null - `seed_out` must be a valid pointer to a buffer of at least 64 bytes - `seed_len` must be a valid pointer to store the seed length - `error` must be a valid pointer to an FFIError - -**Module:** `mnemonic` - ---- - -#### `mnemonic_validate` - -```c -mnemonic_validate(mnemonic: *const c_char, error: *mut FFIError) -> bool -``` - -**Description:** -Validate a mnemonic phrase # Safety - `mnemonic` must be a valid null-terminated C string or null - `error` must be a valid pointer to an FFIError - -**Safety:** -- `mnemonic` must be a valid null-terminated C string or null - `error` must be a valid pointer to an FFIError - -**Module:** `mnemonic` - ---- - -#### `mnemonic_word_count` - -```c -mnemonic_word_count(mnemonic: *const c_char, error: *mut FFIError,) -> c_uint -``` - -**Description:** -Get word count from mnemonic # Safety - `mnemonic` must be a valid null-terminated C string or null - `error` must be a valid pointer to an FFIError - -**Safety:** -- `mnemonic` must be a valid null-terminated C string or null - `error` must be a valid pointer to an FFIError - -**Module:** `mnemonic` - ---- - -### Utility Functions - Detailed - -#### `derivation_bip44_payment_path` - -```c -derivation_bip44_payment_path(network: FFINetwork, account_index: c_uint, is_change: bool, address_index: c_uint, path_out: *mut c_char, path_max_len: usize, error: *mut FFIError,) -> bool -``` - -**Description:** -Derive a BIP44 payment path (m/44'/5'/account'/change/index) # Safety `path_out` must point to a writable buffer of at least `path_max_len` bytes and `error` must be a valid pointer to an `FFIError`. - -**Safety:** -`path_out` must point to a writable buffer of at least `path_max_len` bytes and `error` must be a valid pointer to an `FFIError`. - -**Module:** `derivation` - ---- - -#### `derivation_coinjoin_path` - -```c -derivation_coinjoin_path(network: FFINetwork, account_index: c_uint, path_out: *mut c_char, path_max_len: usize, error: *mut FFIError,) -> bool -``` - -**Description:** -Derive CoinJoin path (m/9'/5'/4'/account') # Safety `path_out` must point to a writable buffer of at least `path_max_len` bytes and `error` must be a valid pointer to an `FFIError`. - -**Safety:** -`path_out` must point to a writable buffer of at least `path_max_len` bytes and `error` must be a valid pointer to an `FFIError`. - -**Module:** `derivation` - ---- - -#### `derivation_identity_authentication_path` - -```c -derivation_identity_authentication_path(network: FFINetwork, identity_index: c_uint, key_index: c_uint, path_out: *mut c_char, path_max_len: usize, error: *mut FFIError,) -> bool -``` - -**Description:** -Derive identity authentication path (m/9'/5'/5'/0'/identity_index'/key_index') # Safety `path_out` must point to a writable buffer of at least `path_max_len` bytes and `error` must be a valid pointer to an `FFIError`. - -**Safety:** -`path_out` must point to a writable buffer of at least `path_max_len` bytes and `error` must be a valid pointer to an `FFIError`. - -**Module:** `derivation` - ---- - -#### `derivation_identity_registration_path` - -```c -derivation_identity_registration_path(network: FFINetwork, identity_index: c_uint, path_out: *mut c_char, path_max_len: usize, error: *mut FFIError,) -> bool -``` - -**Description:** -Derive identity registration path (m/9'/5'/5'/1'/index') # Safety `path_out` must point to a writable buffer of at least `path_max_len` bytes and `error` must be a valid pointer to an `FFIError`. - -**Safety:** -`path_out` must point to a writable buffer of at least `path_max_len` bytes and `error` must be a valid pointer to an `FFIError`. - -**Module:** `derivation` - ---- - -#### `derivation_identity_topup_path` - -```c -derivation_identity_topup_path(network: FFINetwork, identity_index: c_uint, topup_index: c_uint, path_out: *mut c_char, path_max_len: usize, error: *mut FFIError,) -> bool -``` - -**Description:** -Derive identity top-up path (m/9'/5'/5'/2'/identity_index'/top_up_index') # Safety `path_out` must point to a writable buffer of at least `path_max_len` bytes and `error` must be a valid pointer to an `FFIError`. - -**Safety:** -`path_out` must point to a writable buffer of at least `path_max_len` bytes and `error` must be a valid pointer to an `FFIError`. - -**Module:** `derivation` - ---- - -#### `derivation_path_free` - -```c -derivation_path_free(indices: *mut u32, hardened: *mut bool, count: usize,) -> () -``` - -**Description:** -Free derivation path arrays Note: This function expects the count to properly free the slices # Safety - `indices` must be a valid pointer created by `derivation_path_parse` or null - `hardened` must be a valid pointer created by `derivation_path_parse` or null - `count` must match the count from `derivation_path_parse` - After calling this function, the pointers become invalid - -**Safety:** -- `indices` must be a valid pointer created by `derivation_path_parse` or null - `hardened` must be a valid pointer created by `derivation_path_parse` or null - `count` must match the count from `derivation_path_parse` - After calling this function, the pointers become invalid - -**Module:** `keys` - ---- - -#### `derivation_path_parse` - -```c -derivation_path_parse(path: *const c_char, indices_out: *mut *mut u32, hardened_out: *mut *mut bool, count_out: *mut usize, error: *mut FFIError,) -> bool -``` - -**Description:** -Convert derivation path string to indices # Safety - `path` must be a valid null-terminated C string or null - `indices_out` must be a valid pointer to store the indices array pointer - `hardened_out` must be a valid pointer to store the hardened flags array pointer - `count_out` must be a valid pointer to store the count - `error` must be a valid pointer to an FFIError - The returned arrays must be freed with `derivation_path_free` - -**Safety:** -- `path` must be a valid null-terminated C string or null - `indices_out` must be a valid pointer to store the indices array pointer - `hardened_out` must be a valid pointer to store the hardened flags array pointer - `count_out` must be a valid pointer to store the count - `error` must be a valid pointer to an FFIError - The returned arrays must be freed with `derivation_path_free` - -**Module:** `keys` - ---- - -#### `derivation_string_free` - -```c -derivation_string_free(s: *mut c_char) -> () -``` - -**Description:** -Free derivation path string # Safety - `s` must be a valid pointer to a C string that was allocated by this library - The pointer must not be used after calling this function - This function must only be called once per allocation - -**Safety:** -- `s` must be a valid pointer to a C string that was allocated by this library - The pointer must not be used after calling this function - This function must only be called once per allocation - -**Module:** `derivation` - ---- - -#### `derivation_xpriv_free` - -```c -derivation_xpriv_free(xpriv: *mut FFIExtendedPrivKey) -> () -``` - -**Description:** -Free extended private key # Safety - `xpriv` must be a valid pointer to an FFIExtendedPrivKey that was allocated by this library - The pointer must not be used after calling this function - This function must only be called once per allocation - -**Safety:** -- `xpriv` must be a valid pointer to an FFIExtendedPrivKey that was allocated by this library - The pointer must not be used after calling this function - This function must only be called once per allocation - -**Module:** `derivation` - ---- - -#### `derivation_xpriv_to_string` - -```c -derivation_xpriv_to_string(xpriv: *const FFIExtendedPrivKey, error: *mut FFIError,) -> *mut c_char -``` - -**Description:** -Get extended private key as string # Safety - `xpriv` must be a valid pointer to an FFIExtendedPrivKey - `error` must be a valid pointer to an FFIError - The returned string must be freed with `string_free` - -**Safety:** -- `xpriv` must be a valid pointer to an FFIExtendedPrivKey - `error` must be a valid pointer to an FFIError - The returned string must be freed with `string_free` - -**Module:** `derivation` - ---- - -#### `derivation_xpriv_to_xpub` - -```c -derivation_xpriv_to_xpub(xpriv: *const FFIExtendedPrivKey, error: *mut FFIError,) -> *mut FFIExtendedPubKey -``` - -**Description:** -Derive public key from extended private key # Safety - `xpriv` must be a valid pointer to an FFIExtendedPrivKey - `error` must be a valid pointer to an FFIError - The returned pointer must be freed with `extended_public_key_free` - -**Safety:** -- `xpriv` must be a valid pointer to an FFIExtendedPrivKey - `error` must be a valid pointer to an FFIError - The returned pointer must be freed with `extended_public_key_free` - -**Module:** `derivation` - ---- - -#### `derivation_xpub_fingerprint` - -```c -derivation_xpub_fingerprint(xpub: *const FFIExtendedPubKey, fingerprint_out: *mut u8, error: *mut FFIError,) -> bool -``` - -**Description:** -Get fingerprint from extended public key (4 bytes) # Safety - `xpub` must be a valid pointer to an FFIExtendedPubKey - `fingerprint_out` must be a valid pointer to a buffer of at least 4 bytes - `error` must be a valid pointer to an FFIError - -**Safety:** -- `xpub` must be a valid pointer to an FFIExtendedPubKey - `fingerprint_out` must be a valid pointer to a buffer of at least 4 bytes - `error` must be a valid pointer to an FFIError - -**Module:** `derivation` - ---- - -#### `derivation_xpub_free` - -```c -derivation_xpub_free(xpub: *mut FFIExtendedPubKey) -> () -``` - -**Description:** -Free extended public key # Safety - `xpub` must be a valid pointer to an FFIExtendedPubKey that was allocated by this library - The pointer must not be used after calling this function - This function must only be called once per allocation - -**Safety:** -- `xpub` must be a valid pointer to an FFIExtendedPubKey that was allocated by this library - The pointer must not be used after calling this function - This function must only be called once per allocation - -**Module:** `derivation` - ---- - -#### `derivation_xpub_to_string` - -```c -derivation_xpub_to_string(xpub: *const FFIExtendedPubKey, error: *mut FFIError,) -> *mut c_char -``` - -**Description:** -Get extended public key as string # Safety - `xpub` must be a valid pointer to an FFIExtendedPubKey - `error` must be a valid pointer to an FFIError - The returned string must be freed with `string_free` - -**Safety:** -- `xpub` must be a valid pointer to an FFIExtendedPubKey - `error` must be a valid pointer to an FFIError - The returned string must be freed with `string_free` - -**Module:** `derivation` - ---- - -#### `free_u32_array` - -```c -free_u32_array(array: *mut c_uint, count: usize) -> () -``` - -**Description:** -Free a u32 array allocated by this library # Safety - `array` must be a valid pointer to an array allocated by this library - `array` must not be used after calling this function - -**Safety:** -- `array` must be a valid pointer to an array allocated by this library - `array` must not be used after calling this function - -**Module:** `account_collection` - ---- - -#### `script_p2pkh` - -```c -script_p2pkh(pubkey_hash: *const u8, out_buf: *mut u8, out_len: *mut u32,) -> i32 -``` - -**Description:** -Create a P2PKH script pubkey # Safety - `pubkey_hash` must be a valid pointer to a 20-byte public key hash - `out_buf` can be NULL to get size only - `out_len` must be a valid pointer to store the size # Returns - 0 on success - -1 on error - -**Safety:** -- `pubkey_hash` must be a valid pointer to a 20-byte public key hash - `out_buf` can be NULL to get size only - `out_len` must be a valid pointer to store the size - -**Module:** `transaction` - ---- - -#### `string_free` - -```c -string_free(s: *mut c_char) -> () -``` - -**Description:** -Free a string # Safety - `s` must be a valid pointer created by C string creation functions or null - After calling this function, the pointer becomes invalid - -**Safety:** -- `s` must be a valid pointer created by C string creation functions or null - After calling this function, the pointer becomes invalid - -**Module:** `utils` - ---- - -## Type Definitions - -### Core Types - -- `FFIError` - Error handling structure -- `FFIWallet` - Wallet handle -- `FFIWalletManager` - Wallet manager handle -- `FFIBalance` - Balance information -- `FFIUTXO` - Unspent transaction output -- `FFINetwork` - Network enumeration - -## Memory Management - -### Important Rules - -1. **Ownership Transfer**: Functions returning pointers transfer ownership to the caller -2. **Cleanup Required**: All returned pointers must be freed using the appropriate `_free` or `_destroy` function -3. **Thread Safety**: Most functions are thread-safe, but check individual function documentation -4. **Error Handling**: Always check the `FFIError` parameter after function calls - -## Usage Examples - -### Basic Wallet Manager Usage - -```c -// Create wallet manager -FFIError error = {0}; -FFIWalletManager* manager = wallet_manager_create(&error); -if (error.code != 0) { - // Handle error -} - -// Get wallet count -size_t count = wallet_manager_wallet_count(manager, &error); - -// Clean up -wallet_manager_free(manager); -``` diff --git a/key-wallet-ffi/FFI_DOCS_README.md b/key-wallet-ffi/FFI_DOCS_README.md deleted file mode 100644 index 00257e0e6..000000000 --- a/key-wallet-ffi/FFI_DOCS_README.md +++ /dev/null @@ -1,111 +0,0 @@ -# FFI Documentation Guide - -## Overview - -The `FFI_API.md` file contains comprehensive documentation for all FFI functions in the key-wallet-ffi library. This documentation is automatically generated from the source code to ensure it stays up-to-date. - -## Keeping Documentation Updated - -### Automatic Verification - -A GitHub Action automatically verifies that the FFI documentation is up-to-date on every push and pull request. If the documentation is out of sync, the CI will fail and provide instructions on how to update it. - -### Manual Updates - -To update the FFI documentation after making changes to FFI functions: - -```bash -# Using Make -make update-docs - -# Or directly with Python -cd key-wallet-ffi -python3 scripts/generate_ffi_docs.py -``` - -### Checking Documentation - -To verify the documentation is current without updating: - -```bash -# Using Make -make check-docs - -# Or directly with the script -bash scripts/check_ffi_docs.sh -``` - -## Documentation Structure - -The `FFI_API.md` file includes: - -1. **Table of Contents** - Quick navigation to different sections -2. **Function Reference** - Categorized list of all functions -3. **Detailed Documentation** - Full signatures and descriptions -4. **Type Definitions** - Core FFI types used -5. **Memory Management** - Important rules for FFI usage -6. **Usage Examples** - Sample code for common operations - -## Categories - -Functions are automatically categorized into: - -- Initialization -- Error Handling -- Wallet Manager -- Wallet Operations -- Account Management -- Address Management -- Transaction Management -- Key Management -- BIP38 Encryption -- UTXO Management -- Mnemonic Operations -- Utility Functions - -## Adding New FFI Functions - -When adding new FFI functions: - -1. Add the function with `#[no_mangle]` and `extern "C"` attributes -2. Include doc comments with `///` -3. Add safety documentation if the function is `unsafe` -4. Run `make update-docs` to regenerate documentation -5. Commit both the code changes and updated `FFI_API.md` - -## Example FFI Function - -```rust -/// Get wallet balance for a specific network -/// -/// # Safety -/// -/// - `manager` must be a valid pointer to an FFIWalletManager -/// - `wallet_id` must point to exactly 32 bytes -/// - `error` must be a valid pointer to an FFIError -#[no_mangle] -pub unsafe extern "C" fn wallet_manager_get_wallet_balance( - manager: *const FFIWalletManager, - wallet_id: *const u8, - wallet_id_len: usize, - network: FFINetwork, - error: *mut FFIError, -) -> FFIBalance { - // Implementation -} -``` - -## CI/CD Integration - -The documentation verification is integrated into the CI pipeline: - -1. **On Push/PR**: Verifies documentation is up-to-date -2. **On Failure**: Comments on PR with update instructions -3. **Required Check**: Must pass before merging - -## Tools - -- `scripts/generate_ffi_docs.py` - Python script that parses Rust files and generates documentation -- `scripts/check_ffi_docs.sh` - Bash script to verify documentation is current -- `.github/workflows/verify-ffi-docs.yml` - GitHub Action for CI verification -- `Makefile` - Convenient commands for documentation tasks diff --git a/key-wallet-ffi/IMPORT_WALLET_FFI.md b/key-wallet-ffi/IMPORT_WALLET_FFI.md deleted file mode 100644 index 47bdd61e9..000000000 --- a/key-wallet-ffi/IMPORT_WALLET_FFI.md +++ /dev/null @@ -1,104 +0,0 @@ -# Wallet Import FFI Binding - -## Overview - -The `wallet_manager_import_wallet_from_bytes` FFI function allows importing a previously serialized wallet from bincode bytes into a wallet manager instance. - -## Function Signature - -```c -bool wallet_manager_import_wallet_from_bytes( - FFIWalletManager *manager, - const uint8_t *wallet_bytes, - size_t wallet_bytes_len, - uint8_t *wallet_id_out, - FFIError *error -); -``` - -## Parameters - -- `manager`: Pointer to an FFIWalletManager instance -- `wallet_bytes`: Pointer to bincode-serialized wallet bytes -- `wallet_bytes_len`: Length of the wallet bytes -- `wallet_id_out`: Pointer to a 32-byte buffer that will receive the wallet ID -- `error`: Pointer to an FFIError structure for error reporting (can be NULL) - -## Return Value - -- `true`: Wallet imported successfully -- `false`: Import failed (check error for details) - -## Error Codes - -The function may set the following error codes: - -- `InvalidInput` (1): Null pointer or invalid length provided -- `SerializationError` (9): Failed to deserialize wallet from bincode -- `InvalidState` (11): Wallet already exists in the manager -- `WalletError` (8): Other wallet-related errors - -## Usage Example - -```c -#include "key-wallet-ffi.h" - -// Load wallet bytes from file or network -uint8_t *wallet_bytes = load_wallet_bytes(); -size_t bytes_len = get_wallet_bytes_length(); - -// Prepare output buffer for wallet ID -uint8_t wallet_id[32]; - -// Import the wallet -FFIError error = {0}; -bool success = wallet_manager_import_wallet_from_bytes( - manager, - wallet_bytes, - bytes_len, - wallet_id, - &error -); - -if (success) { - printf("Wallet imported with ID: "); - for (int i = 0; i < 32; i++) { - printf("%02x", wallet_id[i]); - } - printf("\n"); -} else { - printf("Import failed: %s\n", error.message); - if (error.message) { - error_message_free(error.message); - } -} -``` - -## Building with Bincode Support - -To use this function, the FFI library must be built with the `bincode` feature enabled: - -```bash -cargo build --features bincode -``` - -## Serialization Format - -The wallet bytes must be in bincode format (version 2.0). The serialization includes: -- Wallet seed and key material -- Account information -- Address pools and indices -- Transaction history -- Other wallet metadata - -## Safety Considerations - -1. The `wallet_bytes` pointer must remain valid for the duration of the function call -2. The `wallet_id_out` buffer must be at least 32 bytes -3. Do not use the wallet_id_out buffer if the function returns false -4. Always free error messages using `error_message_free()` when done -5. The imported wallet must not already exist in the manager (will fail with InvalidState) - -## Thread Safety - -The wallet manager uses internal locking, so this function is thread-safe with respect to other wallet manager operations on the same instance. diff --git a/key-wallet-ffi/README.md b/key-wallet-ffi/README.md deleted file mode 100644 index e5f186f6a..000000000 --- a/key-wallet-ffi/README.md +++ /dev/null @@ -1,133 +0,0 @@ -# Key Wallet FFI - -FFI bindings for the key-wallet library, providing a C-compatible interface for use in other languages like Swift, Kotlin, Python, etc. - -## Features - -- **C-compatible FFI**: Direct C-style FFI bindings without code generation -- **Memory-safe**: Rust's ownership model ensures memory safety across FFI boundary -- **Thread-safe**: All exposed types are thread-safe -- **Error handling**: Proper error propagation across language boundaries - -## Supported Languages - -This library provides C-compatible FFI that can be used by: -- Swift (iOS/macOS) -- Kotlin (Android) via JNI -- Python via ctypes/cffi -- Any language that can interface with C libraries - -## Building - -### Prerequisites - -- Rust 1.70+ -- For iOS: Xcode and cargo-lipo -- For Android: Android NDK - -### Build libraries - -#### Standalone Build - -```bash -# Build for current platform -cargo build --release - -# Build for iOS (requires cargo-lipo) -cargo lipo --release - -# Build for Android (requires cargo-ndk) -cargo ndk -t arm64-v8a -t armeabi-v7a -t x86_64 -t x86 -o ./jniLibs build --release -``` - -## Usage Examples - -### Swift - -```swift -import KeyWalletFFI - -// Create mnemonic -let mnemonic = try Mnemonic(wordCount: 12, language: .english) - -// Create wallet -let wallet = try HDWallet.fromMnemonic( - mnemonic: mnemonic, - passphrase: "", - network: .dash -) - -// Derive address -let account = try wallet.getBip44Account(account: 0) -let firstAddress = try wallet.derivePub(path: "m/44'/5'/0'/0/0") -``` - -### Kotlin - -```kotlin -import com.dash.keywallet.* - -// Create mnemonic -val mnemonic = Mnemonic.fromPhrase( - "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", - Language.ENGLISH -) - -// Create wallet -val wallet = HDWallet.fromMnemonic(mnemonic, "", Network.DASH) - -// Generate addresses -val generator = AddressGenerator(Network.DASH) -val addresses = generator.generateRange(accountXpub, true, 0u, 10u) -``` - -### Python - -```python -from key_wallet_ffi import * - -# Create mnemonic -mnemonic = Mnemonic.from_phrase( - "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", - Language.ENGLISH -) - -# Create wallet -wallet = HDWallet.from_mnemonic(mnemonic, "", Network.DASH) - -# Get first address -first_addr = wallet.derive_pub("m/44'/5'/0'/0/0") -``` - -## API Reference - -### Core Types - -- `Mnemonic`: BIP39 mnemonic phrase handling -- `HDWallet`: Hierarchical deterministic wallet -- `ExtendedKey`: Extended public/private keys -- `Address`: Dash address encoding/decoding -- `AddressGenerator`: Bulk address generation - -### Enums - -- `Network`: Dash, Testnet, Regtest, Devnet -- `Language`: Supported mnemonic languages -- `AddressType`: P2PKH, P2SH - -### Error Handling - -All methods that can fail return a `Result` type with specific error variants: -- `InvalidMnemonic` -- `InvalidDerivationPath` -- `InvalidAddress` -- `Bip32Error` -- `KeyError` - -## Thread Safety - -All exposed types are `Send + Sync` and wrapped in `Arc` for thread-safe reference counting. - -## License - -This project is licensed under the CC0 1.0 Universal license. diff --git a/key-wallet-ffi/build-ios.sh b/key-wallet-ffi/build-ios.sh deleted file mode 100755 index c296a5c75..000000000 --- a/key-wallet-ffi/build-ios.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/bash - -# Build script for key-wallet-ffi iOS targets - -set -e - -echo "Building key-wallet-ffi for iOS..." - -# Ensure we have the required iOS targets -rustup target add aarch64-apple-ios aarch64-apple-ios-sim - -# Build for iOS devices (arm64) -echo "Building for iOS devices (arm64)..." -cargo build --release --target aarch64-apple-ios - -# Build for iOS simulator (arm64 - Apple Silicon Macs) -echo "Building for iOS simulator (arm64)..." -cargo build --release --target aarch64-apple-ios-sim - -# Create output directory -echo "Creating output directory..." -mkdir -p target/universal/release - -# Copy simulator library (no need for lipo since we only have one architecture) -cp target/aarch64-apple-ios-sim/release/libkey_wallet_ffi.a target/universal/release/libkey_wallet_ffi_sim.a - -# Copy device library -cp target/aarch64-apple-ios/release/libkey_wallet_ffi.a target/universal/release/libkey_wallet_ffi_device.a - -echo "Build complete!" -echo "Libraries available at:" -echo " - Device: target/universal/release/libkey_wallet_ffi_device.a" -echo " - Simulator: target/universal/release/libkey_wallet_ffi_sim.a" diff --git a/key-wallet-ffi/build.rs b/key-wallet-ffi/build.rs deleted file mode 100644 index 29549edbb..000000000 --- a/key-wallet-ffi/build.rs +++ /dev/null @@ -1,32 +0,0 @@ -use std::path::Path; -use std::{env, fs}; - -fn main() { - let crate_name = env::var("CARGO_PKG_NAME").unwrap(); - let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); - let out_dir = env::var("OUT_DIR").unwrap(); - - println!("cargo:rerun-if-changed=cbindgen.toml"); - println!("cargo:rerun-if-changed=src/"); - - let target_dir = Path::new(&out_dir) - .ancestors() - .nth(3) // This line moves up to the target/ directory - .expect("Failed to find target dir"); - - let include_dir = target_dir.join("include").join(&crate_name); - - fs::create_dir_all(&include_dir).unwrap(); - - let output_path = include_dir.join(format!("{}.h", &crate_name)); - - let config_path = Path::new(&crate_dir).join("cbindgen.toml"); - let config = cbindgen::Config::from_file(&config_path).expect("Failed to read cbindgen.toml"); - - cbindgen::Builder::new() - .with_crate(&crate_dir) - .with_config(config) - .generate() - .expect("Unable to generate bindings") - .write_to_file(&output_path); -} diff --git a/key-wallet-ffi/cbindgen.toml b/key-wallet-ffi/cbindgen.toml deleted file mode 100644 index c9377f364..000000000 --- a/key-wallet-ffi/cbindgen.toml +++ /dev/null @@ -1,66 +0,0 @@ -language = "C" -header = """/** - * Key Wallet FFI - C Header File - * - * This header provides C-compatible function declarations for the key-wallet - * Rust library FFI bindings. - * - * AUTO-GENERATED FILE - DO NOT EDIT - * Generated using cbindgen - */""" - -include_guard = "KEY_WALLET_FFI_H" -autogen_warning = "/* Warning: This file is auto-generated by cbindgen. Do not modify manually. */" -include_version = true -usize_is_size_t = true -no_includes = false -includes = ["../dash-network/dash-network.h"] -sys_includes = ["stdint.h", "stddef.h", "stdbool.h"] - -# Style options -style = "type" -tab_width = 4 -line_length = 100 -documentation = true -documentation_style = "c" - -[enum] -# Configure enum generation -prefix_with_name = true -rename_variants = "ScreamingSnakeCase" -add_sentinel = false -derive_helper_methods = false - -[struct] -# Configure struct generation -rename_fields = "None" -derive_constructor = false -derive_eq = false -derive_neq = false -derive_lt = false -derive_lte = false -derive_gt = false -derive_gte = false - -[fn] -# Configure function generation -rename_args = "None" -must_use = "MUST_USE_FUNC" -prefix = "" -postfix = "" - -[parse] -# This prevents cbindgen from walking into dependencies (e.g., secp256k1) and -# "learning" enough to confidently define your struct bodies. -parse_deps = false -include = [] -exclude = [] -clean = false -extra_bindings = [] - -[parse.expand] -# Which crates to expand (parse dependencies) -crates = [] -all_features = false -default_features = true -features = [] diff --git a/key-wallet-ffi/examples/check_transaction.c b/key-wallet-ffi/examples/check_transaction.c deleted file mode 100644 index 0426d50cf..000000000 --- a/key-wallet-ffi/examples/check_transaction.c +++ /dev/null @@ -1,131 +0,0 @@ -// Example of using wallet_check_transaction FFI function - -#include -#include -#include -#include - -// FFI type definitions (normally these would be in a header file) -typedef enum { - Dash = 0, - Testnet = 1, - Regtest = 2, - Devnet = 3 -} FFINetwork; - -typedef enum { - Mempool = 0, - InstantSend = 1, - InBlock = 2, - InChainLockedBlock = 3 -} FFITransactionContextType; - -typedef struct { - bool is_relevant; - uint64_t total_received; - uint64_t total_sent; - uint32_t affected_accounts_count; -} FFITransactionCheckResult; - -typedef struct { - int32_t code; - char* message; -} FFIError; - -// External function declarations -extern void* wallet_create_from_mnemonic( - const char* mnemonic, - FFINetwork network, - FFIError* error -); - -extern bool wallet_check_transaction( - void* wallet, - FFINetwork network, - const uint8_t* tx_bytes, - size_t tx_len, - FFITransactionContextType context_type, - uint32_t block_height, - const uint8_t* block_hash, // 32 bytes if not null - uint64_t timestamp, - bool update_state, - FFITransactionCheckResult* result_out, - FFIError* error -); - -extern void wallet_free(void* wallet); - -int main() { - // Example mnemonic (DO NOT USE IN PRODUCTION) - const char* mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; - - FFIError error = {0}; - FFINetwork network = Testnet; - - // Create wallet - void* wallet = wallet_create_from_mnemonic(mnemonic, network, &error); - if (!wallet) { - printf("Failed to create wallet: %s\n", error.message); - return 1; - } - - printf("Wallet created successfully\n"); - - // Example transaction bytes (this would be a real transaction in practice) - uint8_t tx_bytes[] = { /* ... transaction data ... */ }; - size_t tx_len = sizeof(tx_bytes); - - // Check if transaction belongs to wallet - FFITransactionCheckResult result = {0}; - bool success = wallet_check_transaction( - wallet, - network, - tx_bytes, - tx_len, - Mempool, // Transaction is in mempool - 0, // No block height for mempool tx - NULL, // No block hash for mempool tx - 0, // No timestamp - false, // Don't update wallet state - &result, - &error - ); - - if (success) { - if (result.is_relevant) { - printf("Transaction belongs to wallet!\n"); - printf(" Total received: %llu\n", (unsigned long long)result.total_received); - printf(" Total sent: %llu\n", (unsigned long long)result.total_sent); - printf(" Affected accounts: %u\n", result.affected_accounts_count); - } else { - printf("Transaction does not belong to wallet\n"); - } - } else { - printf("Failed to check transaction: %s\n", error.message); - } - - // Check a confirmed transaction - uint8_t block_hash[32] = { /* ... block hash ... */ }; - success = wallet_check_transaction( - wallet, - network, - tx_bytes, - tx_len, - InBlock, // Transaction is in a block - 650000, // Block height - block_hash, // Block hash - 1234567890, // Unix timestamp - true, // Update wallet state - &result, - &error - ); - - if (success && result.is_relevant) { - printf("Confirmed transaction processed and wallet state updated\n"); - } - - // Clean up - wallet_free(wallet); - - return 0; -} diff --git a/key-wallet-ffi/scripts/generate_ffi_docs.py b/key-wallet-ffi/scripts/generate_ffi_docs.py deleted file mode 100755 index 57f27b16a..000000000 --- a/key-wallet-ffi/scripts/generate_ffi_docs.py +++ /dev/null @@ -1,321 +0,0 @@ -#!/usr/bin/env python3 -""" -Generate FFI API documentation for key-wallet-ffi -""" - -import os -import re -import sys -from pathlib import Path -from dataclasses import dataclass -from typing import List, Optional, Dict -import subprocess - -@dataclass -class FFIFunction: - name: str - signature: str - module: str - doc_comment: Optional[str] = None - safety_comment: Optional[str] = None - params: List[str] = None - return_type: str = None - -def extract_ffi_functions(file_path: Path) -> List[FFIFunction]: - """Extract all #[no_mangle] extern "C" functions from a Rust file. - - Handles nested parentheses in parameter types (e.g., function pointers). - """ - functions: List[FFIFunction] = [] - - with open(file_path, 'r', encoding='utf-8') as f: - content = f.read() - - for m in re.finditer(r'(?m)^\s*#\[no_mangle\]\s*$', content): - idx = m.end() - fn_match = re.search(r'\bextern\s+"C"\s+fn\s+([A-Za-z0-9_]+)\s*\(', content[idx:], re.S) - if not fn_match: - continue - name = fn_match.group(1) - abs_start = idx + fn_match.start() - paren_start = content.find('(', abs_start) - if paren_start == -1: - continue - depth = 0 - i = paren_start - while i < len(content): - ch = content[i] - if ch == '(': - depth += 1 - elif ch == ')': - depth -= 1 - if depth == 0: - break - i += 1 - if depth != 0: - continue - paren_end = i - - params_raw = content[paren_start + 1:paren_end] - brace_idx = content.find('{', paren_end) - header_tail = content[paren_end:brace_idx if brace_idx != -1 else len(content)] - ret_match = re.search(r'->\s*([^\n{]+)', header_tail) - return_type = ret_match.group(1).strip() if ret_match else '()' - - # Collect contiguous doc comments above #[no_mangle] - doc_lines_rev: List[str] = [] - line_start = content.rfind('\n', 0, m.start()) + 1 - j = line_start - 1 - while j > 0: - prev_nl = content.rfind('\n', 0, j) - line = content[prev_nl + 1:j] - if line.strip().startswith('///'): - doc_lines_rev.append(line.strip()[3:].strip()) - j = prev_nl - continue - if line.strip() == '' and doc_lines_rev: - j = prev_nl - continue - break - doc_lines = list(reversed(doc_lines_rev)) if doc_lines_rev else [] - - safety_comment = None - if doc_lines: - joined = '\n'.join(doc_lines) - if '# Safety' in joined: - safety_lines: List[str] = [] - in_safety = False - for dl in doc_lines: - if dl.strip().startswith('# Safety'): - in_safety = True - continue - if in_safety and dl.strip().startswith('#'): - break - if in_safety: - safety_lines.append(dl) - safety_comment = ' '.join(safety_lines).strip() if safety_lines else None - - params_clean = re.sub(r'\s+', ' ', params_raw.strip()) - module_name = file_path.stem - - functions.append(FFIFunction( - name=name, - signature=f"{name}({params_clean}) -> {return_type}", - module=module_name, - doc_comment=' '.join(doc_lines) if doc_lines else None, - safety_comment=safety_comment, - params=params_clean, - return_type=return_type, - )) - - return functions - -def categorize_functions(functions: List[FFIFunction]) -> Dict[str, List[FFIFunction]]: - """Categorize functions by their module/purpose.""" - categories = { - 'Initialization': [], - 'Error Handling': [], - 'Wallet Manager': [], - 'Wallet Operations': [], - 'Account Management': [], - 'Address Management': [], - 'Transaction Management': [], - 'Key Management': [], - 'BIP38 Encryption': [], - 'UTXO Management': [], - 'Mnemonic Operations': [], - 'Utility Functions': [], - } - - for func in functions: - name = func.name.lower() - - if 'initialize' in name or 'version' in name: - categories['Initialization'].append(func) - elif 'error' in name: - categories['Error Handling'].append(func) - elif 'wallet_manager' in name: - categories['Wallet Manager'].append(func) - elif 'wallet' in name and 'manager' not in name: - categories['Wallet Operations'].append(func) - elif 'account' in name: - categories['Account Management'].append(func) - elif 'address' in name: - categories['Address Management'].append(func) - elif 'transaction' in name or 'tx' in name: - categories['Transaction Management'].append(func) - elif 'key' in name or 'derive' in name: - categories['Key Management'].append(func) - elif 'bip38' in name: - categories['BIP38 Encryption'].append(func) - elif 'utxo' in name: - categories['UTXO Management'].append(func) - elif 'mnemonic' in name: - categories['Mnemonic Operations'].append(func) - else: - categories['Utility Functions'].append(func) - - # Remove empty categories - return {k: v for k, v in categories.items() if v} - -def generate_markdown(functions: List[FFIFunction]) -> str: - """Generate markdown documentation from FFI functions.""" - - categories = categorize_functions(functions) - - md = [] - md.append("# Key-Wallet FFI API Documentation") - md.append("") - md.append("This document provides a comprehensive reference for all FFI (Foreign Function Interface) functions available in the key-wallet-ffi library.") - md.append("") - md.append("**Auto-generated**: This documentation is automatically generated from the source code. Do not edit manually.") - md.append("") - md.append(f"**Total Functions**: {len(functions)}") - md.append("") - - # Table of Contents - md.append("## Table of Contents") - md.append("") - for category in categories.keys(): - anchor = category.lower().replace(' ', '-') - md.append(f"- [{category}](#{anchor})") - md.append("") - - # Function Reference - md.append("## Function Reference") - md.append("") - - for category, funcs in categories.items(): - if not funcs: - continue - - anchor = category.lower().replace(' ', '-') - md.append(f"### {category}") - md.append("") - md.append(f"Functions: {len(funcs)}") - md.append("") - - # Create a table for each category - md.append("| Function | Description | Module |") - md.append("|----------|-------------|--------|") - - for func in sorted(funcs, key=lambda f: f.name): - desc = func.doc_comment.split('.')[0] if func.doc_comment else "No description" - desc = desc.replace('|', '\\|') # Escape pipes in description - if len(desc) > 80: - # Truncate at last complete word before 77 chars to avoid mid-word breaks - truncate_pos = desc.rfind(' ', 0, 77) - if truncate_pos > 60: # Only if we find a space reasonably close - desc = desc[:truncate_pos] + "..." - else: - desc = desc[:77] + "..." - md.append(f"| `{func.name}` | {desc} | {func.module} |") - - md.append("") - - # Detailed Function Documentation - md.append("## Detailed Function Documentation") - md.append("") - - for category, funcs in categories.items(): - if not funcs: - continue - - md.append(f"### {category} - Detailed") - md.append("") - - for func in sorted(funcs, key=lambda f: f.name): - md.append(f"#### `{func.name}`") - md.append("") - md.append("```c") - md.append(func.signature) - md.append("```") - md.append("") - - if func.doc_comment: - md.append("**Description:**") - md.append(func.doc_comment) - md.append("") - - if func.safety_comment: - md.append("**Safety:**") - md.append(func.safety_comment) - md.append("") - - md.append(f"**Module:** `{func.module}`") - md.append("") - md.append("---") - md.append("") - - # Type Definitions - md.append("## Type Definitions") - md.append("") - md.append("### Core Types") - md.append("") - md.append("- `FFIError` - Error handling structure") - md.append("- `FFIWallet` - Wallet handle") - md.append("- `FFIWalletManager` - Wallet manager handle") - md.append("- `FFIBalance` - Balance information") - md.append("- `FFIUTXO` - Unspent transaction output") - md.append("- `FFINetwork` - Network enumeration") - md.append("") - - # Memory Management - md.append("## Memory Management") - md.append("") - md.append("### Important Rules") - md.append("") - md.append("1. **Ownership Transfer**: Functions returning pointers transfer ownership to the caller") - md.append("2. **Cleanup Required**: All returned pointers must be freed using the appropriate `_free` or `_destroy` function") - md.append("3. **Thread Safety**: Most functions are thread-safe, but check individual function documentation") - md.append("4. **Error Handling**: Always check the `FFIError` parameter after function calls") - md.append("") - - # Usage Examples - md.append("## Usage Examples") - md.append("") - md.append("### Basic Wallet Manager Usage") - md.append("") - md.append("```c") - md.append("// Create wallet manager") - md.append("FFIError error = {0};") - md.append("FFIWalletManager* manager = wallet_manager_create(&error);") - md.append("if (error.code != 0) {") - md.append(" // Handle error") - md.append("}") - md.append("") - md.append("// Get wallet count") - md.append("size_t count = wallet_manager_wallet_count(manager, &error);") - md.append("") - md.append("// Clean up") - md.append("wallet_manager_free(manager);") - md.append("```") - md.append("") - - return '\n'.join(md) - -def main(): - # Find all Rust source files - src_dir = Path(__file__).parent.parent / "src" - - all_functions = [] - - for rust_file in src_dir.rglob("*.rs"): - functions = extract_ffi_functions(rust_file) - all_functions.extend(functions) - - # Generate markdown - markdown = generate_markdown(all_functions) - - # Write to file - output_file = Path(__file__).parent.parent / "FFI_API.md" - with open(output_file, 'w', encoding='utf-8') as f: - f.write(markdown) - - print(f"Generated FFI documentation with {len(all_functions)} functions") - print(f"Output: {output_file}") - - return 0 - -if __name__ == "__main__": - sys.exit(main()) diff --git a/key-wallet-ffi/src/account.rs b/key-wallet-ffi/src/account.rs deleted file mode 100644 index 76b00a1f3..000000000 --- a/key-wallet-ffi/src/account.rs +++ /dev/null @@ -1,570 +0,0 @@ -//! Account management functions - -use crate::deref_ptr; -use crate::error::{FFIError, FFIErrorCode}; -use crate::types::{FFIAccountKind, FFIAccountResult, FFIWallet}; -use dash_network::ffi::FFINetwork; -#[cfg(feature = "bls")] -use key_wallet::account::BLSAccount; -#[cfg(feature = "eddsa")] -use key_wallet::account::EdDSAAccount; -use std::os::raw::c_uint; -use std::sync::Arc; - -/// Opaque account handle -pub struct FFIAccount { - pub(crate) account: Arc, -} - -impl FFIAccount { - /// Create a new FFI account handle - pub fn new(account: &key_wallet::Account) -> Self { - FFIAccount { - account: Arc::new(account.clone()), - } - } - - /// Get a reference to the inner account - pub fn inner(&self) -> &key_wallet::Account { - self.account.as_ref() - } -} - -/// Opaque BLS account handle -#[cfg(feature = "bls")] -pub struct FFIBLSAccount { - pub(crate) account: Arc, -} - -#[cfg(feature = "bls")] -impl FFIBLSAccount { - /// Create a new FFI BLS account handle - pub fn new(account: &BLSAccount) -> Self { - FFIBLSAccount { - account: Arc::new(account.clone()), - } - } - - /// Get a reference to the inner BLS account - pub fn inner(&self) -> &BLSAccount { - self.account.as_ref() - } -} - -/// Opaque EdDSA account handle -#[cfg(feature = "eddsa")] -pub struct FFIEdDSAAccount { - pub(crate) account: Arc, -} - -#[cfg(feature = "eddsa")] -impl FFIEdDSAAccount { - /// Create a new FFI EdDSA account handle - pub fn new(account: &EdDSAAccount) -> Self { - FFIEdDSAAccount { - account: Arc::new(account.clone()), - } - } - - /// Get a reference to the inner EdDSA account - pub fn inner(&self) -> &EdDSAAccount { - self.account.as_ref() - } -} - -/// Get an account handle for a specific account type -/// Returns a result containing either the account handle or an error -/// -/// # Safety -/// -/// - `wallet` must be a valid pointer to an FFIWallet instance -/// - The caller must ensure the wallet pointer remains valid for the duration of this call -#[no_mangle] -pub unsafe extern "C" fn wallet_get_account( - wallet: *const FFIWallet, - account_index: c_uint, - account_type: FFIAccountKind, -) -> FFIAccountResult { - if wallet.is_null() { - return FFIAccountResult::error(FFIErrorCode::InvalidInput, "Wallet is null".to_string()); - } - - let wallet = &*wallet; - let account_type_rust = account_type.to_account_type(account_index); - - match wallet.inner().accounts.account_of_type(account_type_rust) { - Some(account) => { - let ffi_account = FFIAccount::new(account); - FFIAccountResult::success(Box::into_raw(Box::new(ffi_account))) - } - None => FFIAccountResult::error(FFIErrorCode::NotFound, "Account not found".to_string()), - } -} - -/// Get an IdentityTopUp account handle with a specific registration index -/// This is used for top-up accounts that are bound to a specific identity -/// Returns a result containing either the account handle or an error -/// -/// # Safety -/// -/// - `wallet` must be a valid pointer to an FFIWallet instance -/// - The caller must ensure the wallet pointer remains valid for the duration of this call -#[no_mangle] -pub unsafe extern "C" fn wallet_get_top_up_account_with_registration_index( - wallet: *const FFIWallet, - registration_index: c_uint, -) -> FFIAccountResult { - if wallet.is_null() { - return FFIAccountResult::error(FFIErrorCode::InvalidInput, "Wallet is null".to_string()); - } - - let wallet = &*wallet; - - // This function is specifically for IdentityTopUp accounts - let account_type = key_wallet::AccountType::IdentityTopUp { - registration_index, - }; - - match wallet.inner().accounts.account_of_type(account_type) { - Some(account) => { - let ffi_account = FFIAccount::new(account); - FFIAccountResult::success(Box::into_raw(Box::new(ffi_account))) - } - None => FFIAccountResult::error( - FFIErrorCode::NotFound, - format!( - "IdentityTopUp account for registration index {} not found", - registration_index - ), - ), - } -} - -/// Free an account handle -/// -/// # Safety -/// -/// - `account` must be a valid pointer to an FFIAccount that was allocated by this library -/// - The pointer must not be used after calling this function -/// - This function must only be called once per allocation -#[no_mangle] -pub unsafe extern "C" fn account_free(account: *mut FFIAccount) { - if !account.is_null() { - let _ = Box::from_raw(account); - } -} - -/// Free a BLS account handle -/// -/// # Safety -/// -/// - `account` must be a valid pointer to an FFIBLSAccount -/// - The pointer must not be used after calling this function -/// - This function must only be called once per allocation -#[cfg(feature = "bls")] -#[no_mangle] -pub unsafe extern "C" fn bls_account_free(account: *mut FFIBLSAccount) { - if !account.is_null() { - let _ = Box::from_raw(account); - } -} - -/// Free an EdDSA account handle -/// -/// # Safety -/// -/// - `account` must be a valid pointer to an FFIEdDSAAccount -/// - The pointer must not be used after calling this function -/// - This function must only be called once per allocation -#[cfg(feature = "eddsa")] -#[no_mangle] -pub unsafe extern "C" fn eddsa_account_free(account: *mut FFIEdDSAAccount) { - if !account.is_null() { - let _ = Box::from_raw(account); - } -} - -/// Free an account result's error message (if any) -/// Note: This does NOT free the account handle itself - use account_free for that -/// -/// # Safety -/// -/// - `result` must be a valid pointer to an FFIAccountResult -/// - The error_message field must be either null or a valid CString allocated by this library -/// - The caller must ensure the result pointer remains valid for the duration of this call -#[no_mangle] -pub unsafe extern "C" fn account_result_free_error(result: *mut FFIAccountResult) { - if !result.is_null() { - let result = &mut *result; - if !result.error_message.is_null() { - let _ = std::ffi::CString::from_raw(result.error_message); - result.error_message = std::ptr::null_mut(); - } - } -} - -/// Get the extended public key of an account as a string -/// -/// # Safety -/// -/// - `account` must be a valid pointer to an FFIAccount instance -/// - The returned string must be freed by the caller using `string_free` -/// - Returns NULL if the account is null -#[no_mangle] -pub unsafe extern "C" fn account_get_extended_public_key_as_string( - account: *const FFIAccount, -) -> *mut std::os::raw::c_char { - if account.is_null() { - return std::ptr::null_mut(); - } - - let account = &*account; - let xpub = account.inner().extended_public_key(); - - match std::ffi::CString::new(xpub.to_string()) { - Ok(c_str) => c_str.into_raw(), - Err(_) => std::ptr::null_mut(), - } -} - -/// Get the network of an account -/// -/// # Safety -/// -/// - `account` must be a valid pointer to an FFIAccount instance -/// - Returns `FFINetwork::Mainnet` if the account is null -#[no_mangle] -pub unsafe extern "C" fn account_get_network(account: *const FFIAccount) -> FFINetwork { - if account.is_null() { - return FFINetwork::Mainnet; - } - - let account = &*account; - account.inner().network.into() -} - -/// Get the parent wallet ID of an account -/// -/// # Safety -/// -/// - `account` must be a valid pointer to an FFIAccount instance -/// - Returns a pointer to the 32-byte wallet ID, or NULL if not set or account is null -/// - The returned pointer is valid only as long as the account exists -/// - The caller should copy the data if needed for longer use -#[no_mangle] -pub unsafe extern "C" fn account_get_parent_wallet_id(account: *const FFIAccount) -> *const u8 { - if account.is_null() { - return std::ptr::null(); - } - - let account = &*account; - match account.inner().parent_wallet_id { - Some(ref id) => id.as_ptr(), - None => std::ptr::null(), - } -} - -/// Get the account type of an account -/// -/// # Safety -/// -/// - `account` must be a valid pointer to an FFIAccount instance -/// - `out_index` must be a valid pointer to a c_uint where the index will be stored -/// - Returns FFIAccountKind::StandardBIP44 with index 0 if the account is null -#[no_mangle] -pub unsafe extern "C" fn account_get_account_type( - account: *const FFIAccount, - out_index: *mut c_uint, -) -> FFIAccountKind { - if account.is_null() || out_index.is_null() { - if !out_index.is_null() { - *out_index = 0; - } - return FFIAccountKind::StandardBIP44; - } - - let account = &*account; - let (account_type, index, registration_index) = - FFIAccountKind::from_account_type(&account.inner().account_type); - - // For IdentityTopUp, the registration_index is the relevant index - *out_index = registration_index.unwrap_or(index); - - account_type -} - -/// Check if an account is watch-only -/// -/// # Safety -/// -/// - `account` must be a valid pointer to an FFIAccount instance -/// - Returns false if the account is null -#[no_mangle] -pub unsafe extern "C" fn account_get_is_watch_only(account: *const FFIAccount) -> bool { - if account.is_null() { - return false; - } - - let account = &*account; - account.inner().is_watch_only -} - -// BLS account getter functions -/// Get the extended public key of a BLS account as a string -/// -/// # Safety -/// -/// - `account` must be a valid pointer to an FFIBLSAccount instance -/// - The returned string must be freed by the caller using `string_free` -/// - Returns NULL if the account is null -#[cfg(feature = "bls")] -#[no_mangle] -pub unsafe extern "C" fn bls_account_get_extended_public_key_as_string( - account: *const FFIBLSAccount, -) -> *mut std::os::raw::c_char { - if account.is_null() { - return std::ptr::null_mut(); - } - - let account = &*account; - // For BLS accounts, we need to encode the extended public key bytes - // There's no standard string representation for BLS extended keys - let bytes = account.inner().bls_public_key.to_bytes(); - let hex_string = hex::encode(bytes); - - match std::ffi::CString::new(hex_string) { - Ok(c_str) => c_str.into_raw(), - Err(_) => std::ptr::null_mut(), - } -} - -/// Get the network of a BLS account -/// -/// # Safety -/// -/// - `account` must be a valid pointer to an FFIBLSAccount instance -/// - Returns `FFINetwork::Mainnet` if the account is null -#[cfg(feature = "bls")] -#[no_mangle] -pub unsafe extern "C" fn bls_account_get_network(account: *const FFIBLSAccount) -> FFINetwork { - if account.is_null() { - return FFINetwork::Mainnet; - } - - let account = &*account; - account.inner().network.into() -} - -/// Get the parent wallet ID of a BLS account -/// -/// # Safety -/// -/// - `account` must be a valid pointer to an FFIBLSAccount instance -/// - Returns a pointer to the 32-byte wallet ID, or NULL if not set or account is null -/// - The returned pointer is valid only as long as the account exists -/// - The caller should copy the data if needed for longer use -#[cfg(feature = "bls")] -#[no_mangle] -pub unsafe extern "C" fn bls_account_get_parent_wallet_id( - account: *const FFIBLSAccount, -) -> *const u8 { - if account.is_null() { - return std::ptr::null(); - } - - let account = &*account; - match &account.inner().parent_wallet_id { - Some(id) => id.as_ptr(), - None => std::ptr::null(), - } -} - -/// Get the account type of a BLS account -/// -/// # Safety -/// -/// - `account` must be a valid pointer to an FFIBLSAccount instance -/// - `out_index` must be a valid pointer to a c_uint where the index will be stored -/// - Returns FFIAccountKind::StandardBIP44 with index 0 if the account is null -#[cfg(feature = "bls")] -#[no_mangle] -pub unsafe extern "C" fn bls_account_get_account_type( - account: *const FFIBLSAccount, - out_index: *mut c_uint, -) -> FFIAccountKind { - if account.is_null() || out_index.is_null() { - if !out_index.is_null() { - *out_index = 0; - } - return FFIAccountKind::StandardBIP44; - } - - let account = &*account; - let (account_type, index, registration_index) = - FFIAccountKind::from_account_type(&account.inner().account_type); - - // For IdentityTopUp, the registration_index is the relevant index - *out_index = registration_index.unwrap_or(index); - - account_type -} - -/// Check if a BLS account is watch-only -/// -/// # Safety -/// -/// - `account` must be a valid pointer to an FFIBLSAccount instance -/// - Returns false if the account is null -#[cfg(feature = "bls")] -#[no_mangle] -pub unsafe extern "C" fn bls_account_get_is_watch_only(account: *const FFIBLSAccount) -> bool { - if account.is_null() { - return false; - } - - let account = &*account; - account.inner().is_watch_only -} - -// EdDSA account getter functions -/// Get the extended public key of an EdDSA account as a string -/// -/// # Safety -/// -/// - `account` must be a valid pointer to an FFIEdDSAAccount instance -/// - The returned string must be freed by the caller using `string_free` -/// - Returns NULL if the account is null -#[cfg(feature = "eddsa")] -#[no_mangle] -pub unsafe extern "C" fn eddsa_account_get_extended_public_key_as_string( - account: *const FFIEdDSAAccount, -) -> *mut std::os::raw::c_char { - if account.is_null() { - return std::ptr::null_mut(); - } - - let account = &*account; - // For EdDSA accounts, we need to encode the extended public key - // There's no standard string representation for Ed25519 extended keys - let bytes = account.inner().ed25519_public_key.encode(); - let hex_string = hex::encode(bytes); - - match std::ffi::CString::new(hex_string) { - Ok(c_str) => c_str.into_raw(), - Err(_) => std::ptr::null_mut(), - } -} - -/// Get the network of an EdDSA account -/// -/// # Safety -/// -/// - `account` must be a valid pointer to an FFIEdDSAAccount instance -/// - Returns `FFINetwork::Mainnet` if the account is null -#[cfg(feature = "eddsa")] -#[no_mangle] -pub unsafe extern "C" fn eddsa_account_get_network(account: *const FFIEdDSAAccount) -> FFINetwork { - if account.is_null() { - return FFINetwork::Mainnet; - } - - let account = &*account; - account.inner().network.into() -} - -/// Get the parent wallet ID of an EdDSA account -/// -/// # Safety -/// -/// - `account` must be a valid pointer to an FFIEdDSAAccount instance -/// - Returns a pointer to the 32-byte wallet ID, or NULL if not set or account is null -/// - The returned pointer is valid only as long as the account exists -/// - The caller should copy the data if needed for longer use -#[cfg(feature = "eddsa")] -#[no_mangle] -pub unsafe extern "C" fn eddsa_account_get_parent_wallet_id( - account: *const FFIEdDSAAccount, -) -> *const u8 { - if account.is_null() { - return std::ptr::null(); - } - - let account = &*account; - match &account.inner().parent_wallet_id { - Some(id) => id.as_ptr(), - None => std::ptr::null(), - } -} - -/// Get the account type of an EdDSA account -/// -/// # Safety -/// -/// - `account` must be a valid pointer to an FFIEdDSAAccount instance -/// - `out_index` must be a valid pointer to a c_uint where the index will be stored -/// - Returns FFIAccountKind::StandardBIP44 with index 0 if the account is null -#[cfg(feature = "eddsa")] -#[no_mangle] -pub unsafe extern "C" fn eddsa_account_get_account_type( - account: *const FFIEdDSAAccount, - out_index: *mut c_uint, -) -> FFIAccountKind { - if account.is_null() || out_index.is_null() { - if !out_index.is_null() { - *out_index = 0; - } - return FFIAccountKind::StandardBIP44; - } - - let account = &*account; - let (account_type, index, registration_index) = - FFIAccountKind::from_account_type(&account.inner().account_type); - - // For IdentityTopUp, the registration_index is the relevant index - *out_index = registration_index.unwrap_or(index); - - account_type -} - -/// Check if an EdDSA account is watch-only -/// -/// # Safety -/// -/// - `account` must be a valid pointer to an FFIEdDSAAccount instance -/// - Returns false if the account is null -#[cfg(feature = "eddsa")] -#[no_mangle] -pub unsafe extern "C" fn eddsa_account_get_is_watch_only(account: *const FFIEdDSAAccount) -> bool { - if account.is_null() { - return false; - } - - let account = &*account; - account.inner().is_watch_only -} - -/// Get number of accounts -/// -/// # Safety -/// -/// - `wallet` must be a valid pointer to an FFIWallet instance -/// - `error` must be a valid pointer to an FFIError structure or null -/// - The caller must ensure both pointers remain valid for the duration of this call -#[no_mangle] -pub unsafe extern "C" fn wallet_get_account_count( - wallet: *const FFIWallet, - error: *mut FFIError, -) -> c_uint { - let wallet = deref_ptr!(wallet, error); - let accounts = &wallet.inner().accounts; - let count = accounts.standard_bip44_accounts.len() - + accounts.standard_bip32_accounts.len() - + accounts.coinjoin_accounts.len() - + accounts.identity_registration.is_some() as usize - + accounts.identity_topup.len(); - count as c_uint -} - -#[cfg(test)] -#[path = "account_tests.rs"] -mod tests; diff --git a/key-wallet-ffi/src/account_collection.rs b/key-wallet-ffi/src/account_collection.rs deleted file mode 100644 index 912cc4660..000000000 --- a/key-wallet-ffi/src/account_collection.rs +++ /dev/null @@ -1,1573 +0,0 @@ -//! FFI bindings for account collections -//! -//! This module provides FFI-compatible account collection functionality that mirrors -//! the AccountCollection structure from key-wallet but uses FFI-safe types. - -use std::ffi::CString; -use std::os::raw::{c_char, c_uint}; -use std::ptr; - -use crate::account::FFIAccount; -use crate::deref_ptr; -use crate::error::FFIError; -use crate::types::FFIWallet; - -/// Opaque handle to an account collection -pub struct FFIAccountCollection { - /// The underlying account collection reference - collection: key_wallet::AccountCollection, -} - -impl FFIAccountCollection { - /// Create a new FFI account collection from a key_wallet AccountCollection - pub fn new(collection: &key_wallet::AccountCollection) -> Self { - FFIAccountCollection { - collection: collection.clone(), - } - } -} - -/// C-compatible summary of all accounts in a collection -/// -/// This struct provides Swift with structured data about all accounts -/// that exist in the collection, allowing programmatic access to account -/// indices and presence information. -#[repr(C)] -pub struct FFIAccountCollectionSummary { - /// Array of BIP44 account indices - pub bip44_indices: *mut c_uint, - /// Number of BIP44 accounts - pub bip44_count: usize, - - /// Array of BIP32 account indices - pub bip32_indices: *mut c_uint, - /// Number of BIP32 accounts - pub bip32_count: usize, - - /// Array of CoinJoin account indices - pub coinjoin_indices: *mut c_uint, - /// Number of CoinJoin accounts - pub coinjoin_count: usize, - - /// Array of identity top-up registration indices - pub identity_topup_indices: *mut c_uint, - /// Number of identity top-up accounts - pub identity_topup_count: usize, - - /// Whether identity registration account exists - pub has_identity_registration: bool, - /// Whether identity invitation account exists - pub has_identity_invitation: bool, - /// Whether identity top-up not bound account exists - pub has_identity_topup_not_bound: bool, - /// Whether provider voting keys account exists - pub has_provider_voting_keys: bool, - /// Whether provider owner keys account exists - pub has_provider_owner_keys: bool, - - #[cfg(feature = "bls")] - /// Whether provider operator keys account exists - pub has_provider_operator_keys: bool, - - #[cfg(feature = "eddsa")] - /// Whether provider platform keys account exists - pub has_provider_platform_keys: bool, -} - -/// Get account collection for a specific network from wallet -/// -/// # Safety -/// -/// - `wallet` must be a valid pointer to an FFIWallet instance -/// - `error` must be a valid pointer to an FFIError structure -/// - The returned pointer must be freed with `account_collection_free` when no longer needed -#[no_mangle] -pub unsafe extern "C" fn wallet_get_account_collection( - wallet: *const FFIWallet, - error: *mut FFIError, -) -> *mut FFIAccountCollection { - let wallet = deref_ptr!(wallet, error); - let ffi_collection = FFIAccountCollection::new(&wallet.inner().accounts); - Box::into_raw(Box::new(ffi_collection)) -} - -/// Free an account collection handle -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIAccountCollection created by this library -/// - `collection` must not be used after calling this function -#[no_mangle] -pub unsafe extern "C" fn account_collection_free(collection: *mut FFIAccountCollection) { - if !collection.is_null() { - let _ = Box::from_raw(collection); - } -} - -// Standard BIP44 accounts functions - -/// Get a BIP44 account by index from the collection -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIAccountCollection -/// - The returned pointer must be freed with `account_free` when no longer needed -#[no_mangle] -pub unsafe extern "C" fn account_collection_get_bip44_account( - collection: *const FFIAccountCollection, - index: c_uint, -) -> *mut FFIAccount { - if collection.is_null() { - return ptr::null_mut(); - } - - let collection = &*collection; - match collection.collection.standard_bip44_accounts.get(&index) { - Some(account) => { - let ffi_account = FFIAccount::new(account); - Box::into_raw(Box::new(ffi_account)) - } - None => ptr::null_mut(), - } -} - -/// Get all BIP44 account indices -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIAccountCollection -/// - `out_indices` must be a valid pointer to store the indices array -/// - `out_count` must be a valid pointer to store the count -/// - The returned array must be freed with `free_u32_array` when no longer needed -#[no_mangle] -pub unsafe extern "C" fn account_collection_get_bip44_indices( - collection: *const FFIAccountCollection, - out_indices: *mut *mut c_uint, - out_count: *mut usize, -) -> bool { - if collection.is_null() || out_indices.is_null() || out_count.is_null() { - return false; - } - - let collection = &*collection; - let mut indices: Vec = - collection.collection.standard_bip44_accounts.keys().copied().collect(); - - if indices.is_empty() { - *out_indices = ptr::null_mut(); - *out_count = 0; - return true; - } - - indices.sort(); - - let mut boxed_slice = indices.into_boxed_slice(); - let ptr = boxed_slice.as_mut_ptr(); - let len = boxed_slice.len(); - std::mem::forget(boxed_slice); - - *out_indices = ptr; - *out_count = len; - true -} - -// Standard BIP32 accounts functions - -/// Get a BIP32 account by index from the collection -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIAccountCollection -/// - The returned pointer must be freed with `account_free` when no longer needed -#[no_mangle] -pub unsafe extern "C" fn account_collection_get_bip32_account( - collection: *const FFIAccountCollection, - index: c_uint, -) -> *mut FFIAccount { - if collection.is_null() { - return ptr::null_mut(); - } - - let collection = &*collection; - match collection.collection.standard_bip32_accounts.get(&index) { - Some(account) => { - let ffi_account = FFIAccount::new(account); - Box::into_raw(Box::new(ffi_account)) - } - None => ptr::null_mut(), - } -} - -/// Get all BIP32 account indices -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIAccountCollection -/// - `out_indices` must be a valid pointer to store the indices array -/// - `out_count` must be a valid pointer to store the count -/// - The returned array must be freed with `free_u32_array` when no longer needed -#[no_mangle] -pub unsafe extern "C" fn account_collection_get_bip32_indices( - collection: *const FFIAccountCollection, - out_indices: *mut *mut c_uint, - out_count: *mut usize, -) -> bool { - if collection.is_null() || out_indices.is_null() || out_count.is_null() { - return false; - } - - let collection = &*collection; - let indices: Vec = - collection.collection.standard_bip32_accounts.keys().copied().collect(); - - if indices.is_empty() { - *out_indices = ptr::null_mut(); - *out_count = 0; - return true; - } - - let mut boxed_slice = indices.into_boxed_slice(); - let ptr = boxed_slice.as_mut_ptr(); - let len = boxed_slice.len(); - std::mem::forget(boxed_slice); - - *out_indices = ptr; - *out_count = len; - true -} - -// CoinJoin accounts functions - -/// Get a CoinJoin account by index from the collection -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIAccountCollection -/// - The returned pointer must be freed with `account_free` when no longer needed -#[no_mangle] -pub unsafe extern "C" fn account_collection_get_coinjoin_account( - collection: *const FFIAccountCollection, - index: c_uint, -) -> *mut FFIAccount { - if collection.is_null() { - return ptr::null_mut(); - } - - let collection = &*collection; - match collection.collection.coinjoin_accounts.get(&index) { - Some(account) => { - let ffi_account = FFIAccount::new(account); - Box::into_raw(Box::new(ffi_account)) - } - None => ptr::null_mut(), - } -} - -/// Get all CoinJoin account indices -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIAccountCollection -/// - `out_indices` must be a valid pointer to store the indices array -/// - `out_count` must be a valid pointer to store the count -/// - The returned array must be freed with `free_u32_array` when no longer needed -#[no_mangle] -pub unsafe extern "C" fn account_collection_get_coinjoin_indices( - collection: *const FFIAccountCollection, - out_indices: *mut *mut c_uint, - out_count: *mut usize, -) -> bool { - if collection.is_null() || out_indices.is_null() || out_count.is_null() { - return false; - } - - let collection = &*collection; - let mut indices: Vec = - collection.collection.coinjoin_accounts.keys().copied().collect(); - - if indices.is_empty() { - *out_indices = ptr::null_mut(); - *out_count = 0; - return true; - } - - indices.sort(); - - let mut boxed_slice = indices.into_boxed_slice(); - let ptr = boxed_slice.as_mut_ptr(); - let len = boxed_slice.len(); - std::mem::forget(boxed_slice); - - *out_indices = ptr; - *out_count = len; - true -} - -// Identity accounts functions - -/// Get the identity registration account if it exists -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIAccountCollection -/// - The returned pointer must be freed with `account_free` when no longer needed -#[no_mangle] -pub unsafe extern "C" fn account_collection_get_identity_registration( - collection: *const FFIAccountCollection, -) -> *mut FFIAccount { - if collection.is_null() { - return ptr::null_mut(); - } - - let collection = &*collection; - match &collection.collection.identity_registration { - Some(account) => { - let ffi_account = FFIAccount::new(account); - Box::into_raw(Box::new(ffi_account)) - } - None => ptr::null_mut(), - } -} - -/// Check if identity registration account exists -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIAccountCollection -#[no_mangle] -pub unsafe extern "C" fn account_collection_has_identity_registration( - collection: *const FFIAccountCollection, -) -> bool { - if collection.is_null() { - return false; - } - - let collection = &*collection; - collection.collection.identity_registration.is_some() -} - -/// Get an identity topup account by registration index -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIAccountCollection -/// - The returned pointer must be freed with `account_free` when no longer needed -#[no_mangle] -pub unsafe extern "C" fn account_collection_get_identity_topup( - collection: *const FFIAccountCollection, - registration_index: c_uint, -) -> *mut FFIAccount { - if collection.is_null() { - return ptr::null_mut(); - } - - let collection = &*collection; - match collection.collection.identity_topup.get(®istration_index) { - Some(account) => { - let ffi_account = FFIAccount::new(account); - Box::into_raw(Box::new(ffi_account)) - } - None => ptr::null_mut(), - } -} - -/// Get all identity topup registration indices -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIAccountCollection -/// - `out_indices` must be a valid pointer to store the indices array -/// - `out_count` must be a valid pointer to store the count -/// - The returned array must be freed with `free_u32_array` when no longer needed -#[no_mangle] -pub unsafe extern "C" fn account_collection_get_identity_topup_indices( - collection: *const FFIAccountCollection, - out_indices: *mut *mut c_uint, - out_count: *mut usize, -) -> bool { - if collection.is_null() || out_indices.is_null() || out_count.is_null() { - return false; - } - - let collection = &*collection; - let mut indices: Vec = collection.collection.identity_topup.keys().copied().collect(); - - if indices.is_empty() { - *out_indices = ptr::null_mut(); - *out_count = 0; - return true; - } - - indices.sort(); - - let mut boxed_slice = indices.into_boxed_slice(); - let ptr = boxed_slice.as_mut_ptr(); - let len = boxed_slice.len(); - std::mem::forget(boxed_slice); - - *out_indices = ptr; - *out_count = len; - true -} - -/// Get the identity topup not bound account if it exists -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIAccountCollection -/// - The returned pointer must be freed with `account_free` when no longer needed -#[no_mangle] -pub unsafe extern "C" fn account_collection_get_identity_topup_not_bound( - collection: *const FFIAccountCollection, -) -> *mut FFIAccount { - if collection.is_null() { - return ptr::null_mut(); - } - - let collection = &*collection; - match &collection.collection.identity_topup_not_bound { - Some(account) => { - let ffi_account = FFIAccount::new(account); - Box::into_raw(Box::new(ffi_account)) - } - None => ptr::null_mut(), - } -} - -/// Check if identity topup not bound account exists -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIAccountCollection -#[no_mangle] -pub unsafe extern "C" fn account_collection_has_identity_topup_not_bound( - collection: *const FFIAccountCollection, -) -> bool { - if collection.is_null() { - return false; - } - - let collection = &*collection; - collection.collection.identity_topup_not_bound.is_some() -} - -/// Get the identity invitation account if it exists -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIAccountCollection -/// - The returned pointer must be freed with `account_free` when no longer needed -#[no_mangle] -pub unsafe extern "C" fn account_collection_get_identity_invitation( - collection: *const FFIAccountCollection, -) -> *mut FFIAccount { - if collection.is_null() { - return ptr::null_mut(); - } - - let collection = &*collection; - match &collection.collection.identity_invitation { - Some(account) => { - let ffi_account = FFIAccount::new(account); - Box::into_raw(Box::new(ffi_account)) - } - None => ptr::null_mut(), - } -} - -/// Check if identity invitation account exists -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIAccountCollection -#[no_mangle] -pub unsafe extern "C" fn account_collection_has_identity_invitation( - collection: *const FFIAccountCollection, -) -> bool { - if collection.is_null() { - return false; - } - - let collection = &*collection; - collection.collection.identity_invitation.is_some() -} - -// Provider accounts functions - -/// Get the provider voting keys account if it exists -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIAccountCollection -/// - The returned pointer must be freed with `account_free` when no longer needed -#[no_mangle] -pub unsafe extern "C" fn account_collection_get_provider_voting_keys( - collection: *const FFIAccountCollection, -) -> *mut FFIAccount { - if collection.is_null() { - return ptr::null_mut(); - } - - let collection = &*collection; - match &collection.collection.provider_voting_keys { - Some(account) => { - let ffi_account = FFIAccount::new(account); - Box::into_raw(Box::new(ffi_account)) - } - None => ptr::null_mut(), - } -} - -/// Check if provider voting keys account exists -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIAccountCollection -#[no_mangle] -pub unsafe extern "C" fn account_collection_has_provider_voting_keys( - collection: *const FFIAccountCollection, -) -> bool { - if collection.is_null() { - return false; - } - - let collection = &*collection; - collection.collection.provider_voting_keys.is_some() -} - -/// Get the provider owner keys account if it exists -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIAccountCollection -/// - The returned pointer must be freed with `account_free` when no longer needed -#[no_mangle] -pub unsafe extern "C" fn account_collection_get_provider_owner_keys( - collection: *const FFIAccountCollection, -) -> *mut FFIAccount { - if collection.is_null() { - return ptr::null_mut(); - } - - let collection = &*collection; - match &collection.collection.provider_owner_keys { - Some(account) => { - let ffi_account = FFIAccount::new(account); - Box::into_raw(Box::new(ffi_account)) - } - None => ptr::null_mut(), - } -} - -/// Check if provider owner keys account exists -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIAccountCollection -#[no_mangle] -pub unsafe extern "C" fn account_collection_has_provider_owner_keys( - collection: *const FFIAccountCollection, -) -> bool { - if collection.is_null() { - return false; - } - - let collection = &*collection; - collection.collection.provider_owner_keys.is_some() -} - -/// Get the provider operator keys account if it exists -/// Note: Returns null if the `bls` feature is not enabled -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIAccountCollection -/// - The returned pointer must be freed with `bls_account_free` when no longer needed (when BLS is enabled) -#[no_mangle] -pub unsafe extern "C" fn account_collection_get_provider_operator_keys( - collection: *const FFIAccountCollection, -) -> *mut std::os::raw::c_void { - #[cfg(feature = "bls")] - { - if collection.is_null() { - return ptr::null_mut(); - } - - let collection = &*collection; - match &collection.collection.provider_operator_keys { - Some(account) => { - let ffi_account = crate::account::FFIBLSAccount::new(account); - Box::into_raw(Box::new(ffi_account)) as *mut std::os::raw::c_void - } - None => ptr::null_mut(), - } - } - - #[cfg(not(feature = "bls"))] - { - // BLS feature not enabled, always return null - let _ = collection; // Avoid unused parameter warning - ptr::null_mut() - } -} - -/// Check if provider operator keys account exists -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIAccountCollection -#[no_mangle] -pub unsafe extern "C" fn account_collection_has_provider_operator_keys( - collection: *const FFIAccountCollection, -) -> bool { - if collection.is_null() { - return false; - } - - #[cfg(feature = "bls")] - { - let collection = &*collection; - collection.collection.provider_operator_keys.is_some() - } - - #[cfg(not(feature = "bls"))] - { - false - } -} - -/// Get the provider platform keys account if it exists -/// Note: Returns null if the `eddsa` feature is not enabled -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIAccountCollection -/// - The returned pointer must be freed with `eddsa_account_free` when no longer needed (when EdDSA is enabled) -#[no_mangle] -pub unsafe extern "C" fn account_collection_get_provider_platform_keys( - collection: *const FFIAccountCollection, -) -> *mut std::os::raw::c_void { - #[cfg(feature = "eddsa")] - { - if collection.is_null() { - return ptr::null_mut(); - } - - let collection = &*collection; - match &collection.collection.provider_platform_keys { - Some(account) => { - let ffi_account = crate::account::FFIEdDSAAccount::new(account); - Box::into_raw(Box::new(ffi_account)) as *mut std::os::raw::c_void - } - None => ptr::null_mut(), - } - } - - #[cfg(not(feature = "eddsa"))] - { - // EdDSA feature not enabled, always return null - let _ = collection; // Avoid unused parameter warning - ptr::null_mut() - } -} - -/// Check if provider platform keys account exists -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIAccountCollection -#[no_mangle] -pub unsafe extern "C" fn account_collection_has_provider_platform_keys( - collection: *const FFIAccountCollection, -) -> bool { - if collection.is_null() { - return false; - } - - #[cfg(feature = "eddsa")] - { - let collection = &*collection; - collection.collection.provider_platform_keys.is_some() - } - - #[cfg(not(feature = "eddsa"))] - { - false - } -} - -// Utility functions - -/// Free a u32 array allocated by this library -/// -/// # Safety -/// -/// - `array` must be a valid pointer to an array allocated by this library -/// - `array` must not be used after calling this function -#[no_mangle] -pub unsafe extern "C" fn free_u32_array(array: *mut c_uint, count: usize) { - if !array.is_null() && count > 0 { - let _ = Vec::from_raw_parts(array, count, count); - } -} - -/// Get the total number of accounts in the collection -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIAccountCollection -#[no_mangle] -pub unsafe extern "C" fn account_collection_count( - collection: *const FFIAccountCollection, -) -> c_uint { - if collection.is_null() { - return 0; - } - - let collection = &*collection; - let mut count = 0u32; - - count += collection.collection.standard_bip44_accounts.len() as u32; - count += collection.collection.standard_bip32_accounts.len() as u32; - count += collection.collection.coinjoin_accounts.len() as u32; - count += collection.collection.identity_topup.len() as u32; - - if collection.collection.identity_registration.is_some() { - count += 1; - } - if collection.collection.identity_topup_not_bound.is_some() { - count += 1; - } - if collection.collection.identity_invitation.is_some() { - count += 1; - } - if collection.collection.provider_voting_keys.is_some() { - count += 1; - } - if collection.collection.provider_owner_keys.is_some() { - count += 1; - } - - #[cfg(feature = "bls")] - if collection.collection.provider_operator_keys.is_some() { - count += 1; - } - - #[cfg(feature = "eddsa")] - if collection.collection.provider_platform_keys.is_some() { - count += 1; - } - - count -} - -/// Get a human-readable summary of all accounts in the collection -/// -/// Returns a formatted string showing all account types and their indices. -/// The format is designed to be clear and readable for end users. -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIAccountCollection -/// - The returned string must be freed with `string_free` when no longer needed -/// - Returns null if the collection pointer is null -#[no_mangle] -pub unsafe extern "C" fn account_collection_summary( - collection: *const FFIAccountCollection, -) -> *mut c_char { - if collection.is_null() { - return ptr::null_mut(); - } - - let collection = &*collection; - let mut summary_parts = Vec::new(); - - summary_parts.push("Account Summary:".to_string()); - - // BIP44 Accounts - if !collection.collection.standard_bip44_accounts.is_empty() { - let mut indices: Vec = - collection.collection.standard_bip44_accounts.keys().copied().collect(); - indices.sort(); - let count = indices.len(); - let indices_str = format!("{:?}", indices); - summary_parts.push(format!( - "• BIP44 Accounts: {} {} at indices {}", - count, - if count == 1 { - "account" - } else { - "accounts" - }, - indices_str - )); - } - - // BIP32 Accounts - if !collection.collection.standard_bip32_accounts.is_empty() { - let mut indices: Vec = - collection.collection.standard_bip32_accounts.keys().copied().collect(); - indices.sort(); - let count = indices.len(); - let indices_str = format!("{:?}", indices); - summary_parts.push(format!( - "• BIP32 Accounts: {} {} at indices {}", - count, - if count == 1 { - "account" - } else { - "accounts" - }, - indices_str - )); - } - - // CoinJoin Accounts - if !collection.collection.coinjoin_accounts.is_empty() { - let mut indices: Vec = - collection.collection.coinjoin_accounts.keys().copied().collect(); - indices.sort(); - let count = indices.len(); - let indices_str = format!("{:?}", indices); - summary_parts.push(format!( - "• CoinJoin Accounts: {} {} at indices {}", - count, - if count == 1 { - "account" - } else { - "accounts" - }, - indices_str - )); - } - - // Identity TopUp Accounts - if !collection.collection.identity_topup.is_empty() { - let mut indices: Vec = collection.collection.identity_topup.keys().copied().collect(); - indices.sort(); - let count = indices.len(); - let indices_str = format!("{:?}", indices); - summary_parts.push(format!( - "• Identity TopUp: {} {} at indices {}", - count, - if count == 1 { - "account" - } else { - "accounts" - }, - indices_str - )); - } - - // Special accounts (single instances) - if collection.collection.identity_registration.is_some() { - summary_parts.push("• Identity Registration Account".to_string()); - } - - if collection.collection.identity_topup_not_bound.is_some() { - summary_parts.push("• Identity TopUp Not Bound Account".to_string()); - } - - if collection.collection.identity_invitation.is_some() { - summary_parts.push("• Identity Invitation Account".to_string()); - } - - if collection.collection.provider_voting_keys.is_some() { - summary_parts.push("• Provider Voting Keys Account".to_string()); - } - - if collection.collection.provider_owner_keys.is_some() { - summary_parts.push("• Provider Owner Keys Account".to_string()); - } - - #[cfg(feature = "bls")] - if collection.collection.provider_operator_keys.is_some() { - summary_parts.push("• Provider Operator Keys Account (BLS)".to_string()); - } - - #[cfg(feature = "eddsa")] - if collection.collection.provider_platform_keys.is_some() { - summary_parts.push("• Provider Platform Keys Account (EdDSA)".to_string()); - } - - // If there are no accounts at all - if summary_parts.len() == 1 { - summary_parts.push("No accounts configured".to_string()); - } - - let summary = summary_parts.join("\n"); - - match CString::new(summary) { - Ok(c_str) => c_str.into_raw(), - Err(_) => ptr::null_mut(), - } -} - -/// Get structured account collection summary data -/// -/// Returns a struct containing arrays of indices for each account type and boolean -/// flags for special accounts. This provides Swift with programmatic access to -/// account information. -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIAccountCollection -/// - The returned pointer must be freed with `account_collection_summary_free` when no longer needed -/// - Returns null if the collection pointer is null -#[no_mangle] -pub unsafe extern "C" fn account_collection_summary_data( - collection: *const FFIAccountCollection, -) -> *mut FFIAccountCollectionSummary { - if collection.is_null() { - return ptr::null_mut(); - } - - let collection = &*collection; - - // Collect BIP44 indices - let mut bip44_indices: Vec = - collection.collection.standard_bip44_accounts.keys().copied().collect(); - bip44_indices.sort(); - let (bip44_ptr, bip44_count) = if bip44_indices.is_empty() { - (ptr::null_mut(), 0) - } else { - let count = bip44_indices.len(); - let mut boxed_slice = bip44_indices.into_boxed_slice(); - let ptr = boxed_slice.as_mut_ptr(); - std::mem::forget(boxed_slice); - (ptr, count) - }; - - // Collect BIP32 indices - let mut bip32_indices: Vec = - collection.collection.standard_bip32_accounts.keys().copied().collect(); - bip32_indices.sort(); - let (bip32_ptr, bip32_count) = if bip32_indices.is_empty() { - (ptr::null_mut(), 0) - } else { - let count = bip32_indices.len(); - let mut boxed_slice = bip32_indices.into_boxed_slice(); - let ptr = boxed_slice.as_mut_ptr(); - std::mem::forget(boxed_slice); - (ptr, count) - }; - - // Collect CoinJoin indices - let mut coinjoin_indices: Vec = - collection.collection.coinjoin_accounts.keys().copied().collect(); - coinjoin_indices.sort(); - let (coinjoin_ptr, coinjoin_count) = if coinjoin_indices.is_empty() { - (ptr::null_mut(), 0) - } else { - let count = coinjoin_indices.len(); - let mut boxed_slice = coinjoin_indices.into_boxed_slice(); - let ptr = boxed_slice.as_mut_ptr(); - std::mem::forget(boxed_slice); - (ptr, count) - }; - - // Collect identity topup indices - let mut topup_indices: Vec = - collection.collection.identity_topup.keys().copied().collect(); - topup_indices.sort(); - let (topup_ptr, topup_count) = if topup_indices.is_empty() { - (ptr::null_mut(), 0) - } else { - let count = topup_indices.len(); - let mut boxed_slice = topup_indices.into_boxed_slice(); - let ptr = boxed_slice.as_mut_ptr(); - std::mem::forget(boxed_slice); - (ptr, count) - }; - - // Create the summary struct - let summary = FFIAccountCollectionSummary { - bip44_indices: bip44_ptr, - bip44_count, - bip32_indices: bip32_ptr, - bip32_count, - coinjoin_indices: coinjoin_ptr, - coinjoin_count, - identity_topup_indices: topup_ptr, - identity_topup_count: topup_count, - has_identity_registration: collection.collection.identity_registration.is_some(), - has_identity_invitation: collection.collection.identity_invitation.is_some(), - has_identity_topup_not_bound: collection.collection.identity_topup_not_bound.is_some(), - has_provider_voting_keys: collection.collection.provider_voting_keys.is_some(), - has_provider_owner_keys: collection.collection.provider_owner_keys.is_some(), - #[cfg(feature = "bls")] - has_provider_operator_keys: collection.collection.provider_operator_keys.is_some(), - #[cfg(feature = "eddsa")] - has_provider_platform_keys: collection.collection.provider_platform_keys.is_some(), - }; - - Box::into_raw(Box::new(summary)) -} - -/// Free an account collection summary and all its allocated memory -/// -/// # Safety -/// -/// - `summary` must be a valid pointer to an FFIAccountCollectionSummary created by `account_collection_summary_data` -/// - `summary` must not be used after calling this function -#[no_mangle] -pub unsafe extern "C" fn account_collection_summary_free( - summary: *mut FFIAccountCollectionSummary, -) { - if !summary.is_null() { - let summary = Box::from_raw(summary); - - // Free all the allocated arrays - if !summary.bip44_indices.is_null() && summary.bip44_count > 0 { - let _ = Vec::from_raw_parts( - summary.bip44_indices, - summary.bip44_count, - summary.bip44_count, - ); - } - - if !summary.bip32_indices.is_null() && summary.bip32_count > 0 { - let _ = Vec::from_raw_parts( - summary.bip32_indices, - summary.bip32_count, - summary.bip32_count, - ); - } - - if !summary.coinjoin_indices.is_null() && summary.coinjoin_count > 0 { - let _ = Vec::from_raw_parts( - summary.coinjoin_indices, - summary.coinjoin_count, - summary.coinjoin_count, - ); - } - - if !summary.identity_topup_indices.is_null() && summary.identity_topup_count > 0 { - let _ = Vec::from_raw_parts( - summary.identity_topup_indices, - summary.identity_topup_count, - summary.identity_topup_count, - ); - } - - // The summary struct itself is dropped automatically when the Box is dropped - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::wallet::wallet_create_from_mnemonic_with_options; - use dash_network::ffi::FFINetwork; - use std::ffi::CString; - - #[test] - fn test_account_collection_basic() { - unsafe { - let mnemonic = CString::new("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").unwrap(); - let error = &mut FFIError::default(); - - // Create wallet with default accounts - let wallet = wallet_create_from_mnemonic_with_options( - mnemonic.as_ptr(), - FFINetwork::Testnet, - ptr::null(), - error, - ); - assert!(!wallet.is_null()); - - // Get account collection - let collection = wallet_get_account_collection(wallet, error); - assert!(!collection.is_null()); - - // Check that we have some accounts - let count = account_collection_count(collection); - assert!(count > 0); - - // Check BIP44 accounts - let mut indices: *mut c_uint = ptr::null_mut(); - let mut indices_count: usize = 0; - let success = - account_collection_get_bip44_indices(collection, &mut indices, &mut indices_count); - assert!(success); - assert!(indices_count > 0); - - // Get first BIP44 account - let account = account_collection_get_bip44_account(collection, 0); - assert!(!account.is_null()); - - // Clean up - crate::account::account_free(account); - if !indices.is_null() { - free_u32_array(indices, indices_count); - } - account_collection_free(collection); - crate::wallet::wallet_free(wallet); - } - } - - #[test] - #[cfg(feature = "bls")] - fn test_bls_account() { - unsafe { - let mnemonic = CString::new("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").unwrap(); - let error = &mut FFIError::default(); - - // Create wallet with provider accounts - let mut options = crate::types::FFIWalletAccountCreationOptions::default_options(); - options.option_type = crate::types::FFIAccountCreationOptionType::AllAccounts; - - // Add provider operator keys account type - let special_types = [crate::types::FFIAccountKind::ProviderOperatorKeys]; - options.special_account_types = special_types.as_ptr(); - options.special_account_types_count = special_types.len(); - - let wallet = wallet_create_from_mnemonic_with_options( - mnemonic.as_ptr(), - FFINetwork::Testnet, - &options, - error, - ); - assert!(!wallet.is_null()); - - // Get account collection - let collection = wallet_get_account_collection(wallet, error); - assert!(!collection.is_null()); - - // Check for provider operator keys account (BLS) - let has_operator = account_collection_has_provider_operator_keys(collection); - if has_operator { - let operator_account = account_collection_get_provider_operator_keys(collection); - assert!(!operator_account.is_null()); - - // Free the BLS account - crate::account::bls_account_free( - operator_account as *mut crate::account::FFIBLSAccount, - ); - } - - // Clean up - account_collection_free(collection); - crate::wallet::wallet_free(wallet); - } - } - - #[test] - #[cfg(feature = "eddsa")] - fn test_eddsa_account() { - unsafe { - let mnemonic = CString::new("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").unwrap(); - let error = &mut FFIError::default(); - - // Create wallet with provider accounts - let mut options = crate::types::FFIWalletAccountCreationOptions::default_options(); - options.option_type = crate::types::FFIAccountCreationOptionType::AllAccounts; - - // Add provider platform keys account type - let special_types = [crate::types::FFIAccountKind::ProviderPlatformKeys]; - options.special_account_types = special_types.as_ptr(); - options.special_account_types_count = special_types.len(); - - let wallet = wallet_create_from_mnemonic_with_options( - mnemonic.as_ptr(), - FFINetwork::Testnet, - &options, - error, - ); - assert!(!wallet.is_null()); - - // Get account collection - let collection = wallet_get_account_collection(wallet, error); - assert!(!collection.is_null()); - - // Check for provider platform keys account (EdDSA) - let has_platform = account_collection_has_provider_platform_keys(collection); - if has_platform { - let platform_account = account_collection_get_provider_platform_keys(collection); - assert!(!platform_account.is_null()); - - // Free the EdDSA account - crate::account::eddsa_account_free( - platform_account as *mut crate::account::FFIEdDSAAccount, - ); - } - - // Clean up - account_collection_free(collection); - crate::wallet::wallet_free(wallet); - } - } - - #[test] - fn test_account_collection_summary() { - unsafe { - use std::ffi::CStr; - - let mnemonic = CString::new("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").unwrap(); - let error = &mut FFIError::default(); - - // Create wallet with multiple account types - let mut options = crate::types::FFIWalletAccountCreationOptions::default_options(); - options.option_type = crate::types::FFIAccountCreationOptionType::AllAccounts; - - // Add various special accounts - let special_types = [ - crate::types::FFIAccountKind::ProviderVotingKeys, - crate::types::FFIAccountKind::ProviderOwnerKeys, - crate::types::FFIAccountKind::IdentityRegistration, - crate::types::FFIAccountKind::IdentityInvitation, - ]; - options.special_account_types = special_types.as_ptr(); - options.special_account_types_count = special_types.len(); - - // Configure standard accounts - store vectors in variables to keep them alive - let bip44_indices = [0, 4, 5, 8]; - let bip32_indices = [0]; - let coinjoin_indices = [0, 1]; - let topup_indices = [0, 1, 2]; - - options.bip44_indices = bip44_indices.as_ptr(); - options.bip44_count = bip44_indices.len(); - - options.bip32_indices = bip32_indices.as_ptr(); - options.bip32_count = bip32_indices.len(); - - options.coinjoin_indices = coinjoin_indices.as_ptr(); - options.coinjoin_count = coinjoin_indices.len(); - - options.topup_indices = topup_indices.as_ptr(); - options.topup_count = topup_indices.len(); - - let wallet = wallet_create_from_mnemonic_with_options( - mnemonic.as_ptr(), - FFINetwork::Testnet, - &options, - error, - ); - assert!(!wallet.is_null()); - - // Get account collection - let collection = wallet_get_account_collection(wallet, error); - assert!(!collection.is_null()); - - // Get the summary - let summary_ptr = account_collection_summary(collection); - assert!(!summary_ptr.is_null()); - - // Convert to Rust string to verify content - let summary_cstr = CStr::from_ptr(summary_ptr); - let summary = summary_cstr.to_str().unwrap(); - - // Verify the summary contains expected content - assert!(summary.contains("Account Summary:")); - // The indices might not be in that exact format, so check more flexibly - assert!(summary.contains("BIP44 Accounts")); - assert!(summary.contains("BIP32 Accounts")); - assert!(summary.contains("CoinJoin Accounts")); - assert!(summary.contains("Identity TopUp")); - assert!(summary.contains("Identity Registration Account")); - assert!(summary.contains("Identity Invitation Account")); - assert!(summary.contains("Provider Voting Keys Account")); - assert!(summary.contains("Provider Owner Keys Account")); - - // Clean up - crate::utils::string_free(summary_ptr); - account_collection_free(collection); - crate::wallet::wallet_free(wallet); - } - } - - #[test] - fn test_account_collection_summary_empty() { - unsafe { - use std::ffi::CStr; - - let mnemonic = CString::new("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").unwrap(); - let error = &mut FFIError::default(); - - // Create wallet with no accounts using SpecificAccounts with empty lists - let mut options = crate::types::FFIWalletAccountCreationOptions::default_options(); - options.option_type = crate::types::FFIAccountCreationOptionType::SpecificAccounts; - // All arrays are already null/0 from default_options() - - let wallet = wallet_create_from_mnemonic_with_options( - mnemonic.as_ptr(), - FFINetwork::Testnet, - &options, - error, - ); - assert!(!wallet.is_null()); - - // Get account collection - let collection = wallet_get_account_collection(wallet, error); - - // With SpecificAccounts and empty lists, collection might be null or empty - if collection.is_null() { - // If the collection doesn't exist, that's OK for this test - just clean up and return - crate::wallet::wallet_free(wallet); - return; - } - - // Get the summary - let summary_ptr = account_collection_summary(collection); - assert!(!summary_ptr.is_null()); - - // Convert to Rust string to verify content - let summary_cstr = CStr::from_ptr(summary_ptr); - let summary = summary_cstr.to_str().unwrap(); - - // Verify the summary shows no accounts - assert!(summary.contains("Account Summary:")); - assert!(summary.contains("No accounts configured")); - - // Clean up - crate::utils::string_free(summary_ptr); - account_collection_free(collection); - crate::wallet::wallet_free(wallet); - } - } - - #[test] - fn test_account_collection_summary_null_safety() { - unsafe { - // Test with null collection - let summary_ptr = account_collection_summary(ptr::null()); - assert!(summary_ptr.is_null()); - } - } - - #[test] - fn test_account_collection_summary_data() { - unsafe { - let mnemonic = CString::new("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").unwrap(); - let error = &mut FFIError::default(); - - // Create wallet with various account types - let mut options = crate::types::FFIWalletAccountCreationOptions::default_options(); - options.option_type = crate::types::FFIAccountCreationOptionType::AllAccounts; - - // Add various special accounts - let special_types = [ - crate::types::FFIAccountKind::ProviderVotingKeys, - crate::types::FFIAccountKind::ProviderOwnerKeys, - crate::types::FFIAccountKind::IdentityRegistration, - crate::types::FFIAccountKind::IdentityInvitation, - ]; - options.special_account_types = special_types.as_ptr(); - options.special_account_types_count = special_types.len(); - - // Configure standard accounts - let bip44_indices = [0, 4, 5, 8]; - let bip32_indices = [0]; - let coinjoin_indices = [0, 1]; - let topup_indices = [0, 1, 2]; - - options.bip44_indices = bip44_indices.as_ptr(); - options.bip44_count = bip44_indices.len(); - - options.bip32_indices = bip32_indices.as_ptr(); - options.bip32_count = bip32_indices.len(); - - options.coinjoin_indices = coinjoin_indices.as_ptr(); - options.coinjoin_count = coinjoin_indices.len(); - - options.topup_indices = topup_indices.as_ptr(); - options.topup_count = topup_indices.len(); - - let wallet = wallet_create_from_mnemonic_with_options( - mnemonic.as_ptr(), - FFINetwork::Testnet, - &options, - error, - ); - assert!(!wallet.is_null()); - - // Get account collection - let collection = wallet_get_account_collection(wallet, error); - assert!(!collection.is_null()); - - // Get the summary data - let summary = account_collection_summary_data(collection); - assert!(!summary.is_null()); - - let summary_ref = &*summary; - - // Verify BIP44 indices - assert_eq!(summary_ref.bip44_count, 4); - assert!(!summary_ref.bip44_indices.is_null()); - let bip44_slice = - std::slice::from_raw_parts(summary_ref.bip44_indices, summary_ref.bip44_count); - assert_eq!(bip44_slice, &[0, 4, 5, 8]); - - // Verify BIP32 indices - assert_eq!(summary_ref.bip32_count, 1); - assert!(!summary_ref.bip32_indices.is_null()); - let bip32_slice = - std::slice::from_raw_parts(summary_ref.bip32_indices, summary_ref.bip32_count); - assert_eq!(bip32_slice, &[0]); - - // Verify CoinJoin indices - assert_eq!(summary_ref.coinjoin_count, 2); - assert!(!summary_ref.coinjoin_indices.is_null()); - let coinjoin_slice = std::slice::from_raw_parts( - summary_ref.coinjoin_indices, - summary_ref.coinjoin_count, - ); - assert_eq!(coinjoin_slice, &[0, 1]); - - // Verify identity topup indices - assert_eq!(summary_ref.identity_topup_count, 3); - assert!(!summary_ref.identity_topup_indices.is_null()); - let topup_slice = std::slice::from_raw_parts( - summary_ref.identity_topup_indices, - summary_ref.identity_topup_count, - ); - assert_eq!(topup_slice, &[0, 1, 2]); - - // Verify boolean flags - assert!(summary_ref.has_identity_registration); - assert!(summary_ref.has_identity_invitation); - assert!(summary_ref.has_provider_voting_keys); - assert!(summary_ref.has_provider_owner_keys); - - // Clean up - account_collection_summary_free(summary); - account_collection_free(collection); - crate::wallet::wallet_free(wallet); - } - } - - #[test] - fn test_account_collection_summary_data_empty() { - unsafe { - let mnemonic = CString::new("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").unwrap(); - let error = &mut FFIError::default(); - - // Create wallet with no accounts - but still create a collection on the network - // Use SpecificAccounts with empty lists to get truly empty collections - let mut options = crate::types::FFIWalletAccountCreationOptions::default_options(); - options.option_type = crate::types::FFIAccountCreationOptionType::SpecificAccounts; - - // Set empty arrays for all account types - options.bip44_indices = ptr::null(); - options.bip44_count = 0; - options.bip32_indices = ptr::null(); - options.bip32_count = 0; - options.coinjoin_indices = ptr::null(); - options.coinjoin_count = 0; - options.topup_indices = ptr::null(); - options.topup_count = 0; - options.special_account_types = ptr::null(); - options.special_account_types_count = 0; - - let wallet = wallet_create_from_mnemonic_with_options( - mnemonic.as_ptr(), - FFINetwork::Testnet, - &options, - error, - ); - assert!(!wallet.is_null()); - - // Get account collection - let collection = wallet_get_account_collection(wallet, error); - - // With AllAccounts but empty lists, collection should still exist - if collection.is_null() { - // If the collection doesn't exist, that's OK for this test - just clean up and return - crate::wallet::wallet_free(wallet); - return; - } - - // Get the summary data - let summary = account_collection_summary_data(collection); - assert!(!summary.is_null()); - - let summary_ref = &*summary; - - // Verify all arrays are empty - assert_eq!(summary_ref.bip44_count, 0); - assert!(summary_ref.bip44_indices.is_null()); - - assert_eq!(summary_ref.bip32_count, 0); - assert!(summary_ref.bip32_indices.is_null()); - - assert_eq!(summary_ref.coinjoin_count, 0); - assert!(summary_ref.coinjoin_indices.is_null()); - - assert_eq!(summary_ref.identity_topup_count, 0); - assert!(summary_ref.identity_topup_indices.is_null()); - - // Verify all boolean flags are false - assert!(!summary_ref.has_identity_registration); - assert!(!summary_ref.has_identity_invitation); - assert!(!summary_ref.has_identity_topup_not_bound); - assert!(!summary_ref.has_provider_voting_keys); - assert!(!summary_ref.has_provider_owner_keys); - - // Clean up - account_collection_summary_free(summary); - account_collection_free(collection); - crate::wallet::wallet_free(wallet); - } - } - - #[test] - fn test_account_collection_summary_data_null_safety() { - unsafe { - // Test with null collection - let summary = account_collection_summary_data(ptr::null()); - assert!(summary.is_null()); - - // Test freeing null summary (should not crash) - account_collection_summary_free(ptr::null_mut()); - } - } - - #[test] - fn test_account_collection_summary_memory_management() { - unsafe { - let mnemonic = CString::new("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").unwrap(); - let error = &mut FFIError::default(); - - // Create wallet with default accounts (which should have at least BIP44 account 0) - let wallet = wallet_create_from_mnemonic_with_options( - mnemonic.as_ptr(), - FFINetwork::Testnet, - ptr::null(), - error, - ); - assert!(!wallet.is_null()); - - // Get account collection - let collection = wallet_get_account_collection(wallet, error); - assert!(!collection.is_null()); - - // Get multiple summaries to test memory management - let summary1 = account_collection_summary_data(collection); - assert!(!summary1.is_null()); - - let summary2 = account_collection_summary_data(collection); - assert!(!summary2.is_null()); - - // The two summaries should be different pointers - assert_ne!(summary1, summary2); - - // But they should contain the same data - let summary1_ref = &*summary1; - let summary2_ref = &*summary2; - assert_eq!(summary1_ref.bip44_count, summary2_ref.bip44_count); - assert_eq!( - summary1_ref.has_identity_registration, - summary2_ref.has_identity_registration - ); - - // Clean up both summaries - account_collection_summary_free(summary1); - account_collection_summary_free(summary2); - - // Clean up - account_collection_free(collection); - crate::wallet::wallet_free(wallet); - } - } -} diff --git a/key-wallet-ffi/src/account_derivation.rs b/key-wallet-ffi/src/account_derivation.rs deleted file mode 100644 index cabc9d35a..000000000 --- a/key-wallet-ffi/src/account_derivation.rs +++ /dev/null @@ -1,406 +0,0 @@ -//! Account-level derivation functions exposed over FFI - -use crate::account::FFIAccount; -#[cfg(feature = "bls")] -use crate::account::FFIBLSAccount; -#[cfg(feature = "eddsa")] -use crate::account::FFIEdDSAAccount; -use crate::error::{FFIError, FFIErrorCode}; -use crate::keys::{FFIExtendedPrivKey, FFIPrivateKey}; -use crate::{check_ptr, deref_ptr, unwrap_or_return}; -use key_wallet::account::derivation::AccountDerivation; -use key_wallet::account::AccountTrait; -use std::ffi::CString; -use std::os::raw::{c_char, c_uint}; -use std::ptr; - -// No extra FFI enum for chain selection; account semantics decide path. - -/// Derive an extended private key from an account at a given index, using the provided master xpriv. -/// -/// Returns an opaque FFIExtendedPrivKey pointer that must be freed with `extended_private_key_free`. -/// -/// Notes: -/// - This is chain-agnostic. For accounts with internal/external chains, this returns an error. -/// - For hardened-only account types (e.g., EdDSA), a hardened index is used. -/// -/// # Safety -/// - `account` and `master_xpriv` must be valid, non-null pointers allocated by this library. -/// - `error` must be a valid pointer to an FFIError. -/// - The caller must free the returned pointer with `extended_private_key_free`. -#[no_mangle] -pub unsafe extern "C" fn account_derive_extended_private_key_at( - account: *const FFIAccount, - master_xpriv: *const FFIExtendedPrivKey, - index: c_uint, - error: *mut FFIError, -) -> *mut FFIExtendedPrivKey { - let account = deref_ptr!(account, error); - let master_xpriv = deref_ptr!(master_xpriv, error); - - if account.inner().is_watch_only() { - (*error).set( - FFIErrorCode::WalletError, - "Account is watch-only; private derivation not allowed", - ); - return ptr::null_mut(); - } - - let derived = unwrap_or_return!( - account.inner().derive_from_master_xpriv_extended_xpriv_at(master_xpriv.inner(), index), - error - ); - Box::into_raw(Box::new(FFIExtendedPrivKey::from_inner(derived))) -} - -// ========================= BLS (feature = "bls") ========================= -/// Derive a BLS private key from a raw seed buffer at the given index. -/// -/// Returns a newly allocated hex string of the 32-byte private key. The caller must free -/// it with `string_free`. -/// -/// Notes: -/// - Uses the account's network for master key creation. -/// - Chain-agnostic; may return an error for accounts with internal/external chains. -/// -/// # Safety -/// - `account` must be a valid, non-null pointer to an `FFIBLSAccount` (only when `bls` feature is enabled). -/// - `seed` must point to a readable buffer of length `seed_len` (1..=64 bytes expected). -/// - `error` must be a valid pointer to an FFIError. -/// - Returned string must be freed with `string_free`. -#[cfg(feature = "bls")] -#[no_mangle] -pub unsafe extern "C" fn bls_account_derive_private_key_from_seed( - account: *const FFIBLSAccount, - seed: *const u8, - seed_len: usize, - index: c_uint, - error: *mut FFIError, -) -> *mut c_char { - let account = deref_ptr!(account, error); - check_ptr!(seed, error); - if seed_len == 0 || seed_len > 64 { - (*error).set(FFIErrorCode::InvalidInput, "Seed length must be between 1 and 64 bytes"); - return ptr::null_mut(); - } - let seed_slice = std::slice::from_raw_parts(seed, seed_len); - let sk = unwrap_or_return!( - account.inner().derive_from_seed_private_key_at(seed_slice, index), - error - ); - unwrap_or_return!(CString::new(hex::encode(sk.to_be_bytes())), error).into_raw() -} - -/// Derive a BLS private key from a mnemonic + optional passphrase at the given index. -/// -/// Returns a newly allocated hex string of the 32-byte private key. The caller must free -/// it with `string_free`. -/// -/// Notes: -/// - Uses the English wordlist for parsing the mnemonic. -/// - Chain-agnostic; may return an error for accounts with internal/external chains. -/// -/// # Safety -/// - `account` must be a valid, non-null pointer to an `FFIBLSAccount` (only when `bls` feature is enabled). -/// - `mnemonic` must be a valid, null-terminated UTF-8 C string. -/// - `passphrase` may be null; if not null, must be a valid UTF-8 C string. -/// - `error` must be a valid pointer to an FFIError. -/// - Returned string must be freed with `string_free`. -#[cfg(feature = "bls")] -#[no_mangle] -pub unsafe extern "C" fn bls_account_derive_private_key_from_mnemonic( - account: *const FFIBLSAccount, - mnemonic: *const c_char, - passphrase: *const c_char, - index: c_uint, - error: *mut FFIError, -) -> *mut c_char { - let account = deref_ptr!(account, error); - let mnemonic = deref_ptr!(mnemonic, error); - let mnemonic_str = unwrap_or_return!(std::ffi::CStr::from_ptr(mnemonic).to_str(), error); - let passphrase_str = if passphrase.is_null() { - None - } else { - Some(unwrap_or_return!(std::ffi::CStr::from_ptr(passphrase).to_str(), error)) - }; - let sk = unwrap_or_return!( - account.inner().derive_from_mnemonic_private_key_at( - mnemonic_str, - passphrase_str, - key_wallet::mnemonic::Language::English, - index, - ), - error - ); - unwrap_or_return!(CString::new(hex::encode(sk.to_be_bytes())), error).into_raw() -} - -// ========================= EdDSA (feature = "eddsa") ========================= -/// Derive an EdDSA (ed25519) private key from a raw seed buffer at the given index. -/// -/// Returns a newly allocated hex string of the 32-byte private key. The caller must free -/// it with `string_free`. -/// -/// Notes: -/// - EdDSA only supports hardened derivation; the index will be used accordingly. -/// - Chain-agnostic; EdDSA accounts typically do not have internal/external split. -/// -/// # Safety -/// - `account` must be a valid, non-null pointer to an `FFIEdDSAAccount` (only when `eddsa` feature is enabled). -/// - `seed` must point to a readable buffer of length `seed_len` (1..=64 bytes expected). -/// - `error` must be a valid pointer to an FFIError. -/// - Returned string must be freed with `string_free`. -#[cfg(feature = "eddsa")] -#[no_mangle] -pub unsafe extern "C" fn eddsa_account_derive_private_key_from_seed( - account: *const FFIEdDSAAccount, - seed: *const u8, - seed_len: usize, - index: c_uint, - error: *mut FFIError, -) -> *mut c_char { - let account = deref_ptr!(account, error); - check_ptr!(seed, error); - let seed_slice = std::slice::from_raw_parts(seed, seed_len); - let sk = unwrap_or_return!( - account.inner().derive_from_seed_private_key_at(seed_slice, index), - error - ); - unwrap_or_return!(CString::new(hex::encode(sk.to_bytes())), error).into_raw() -} - -/// Derive an EdDSA (ed25519) private key from a mnemonic + optional passphrase at the given index. -/// -/// Returns a newly allocated hex string of the 32-byte private key. The caller must free -/// it with `string_free`. -/// -/// Notes: -/// - Uses the English wordlist for parsing the mnemonic. -/// -/// # Safety -/// - `account` must be a valid, non-null pointer to an `FFIEdDSAAccount` (only when `eddsa` feature is enabled). -/// - `mnemonic` must be a valid, null-terminated UTF-8 C string. -/// - `passphrase` may be null; if not null, must be a valid UTF-8 C string. -/// - `error` must be a valid pointer to an FFIError. -/// - Returned string must be freed with `string_free`. -#[cfg(feature = "eddsa")] -#[no_mangle] -pub unsafe extern "C" fn eddsa_account_derive_private_key_from_mnemonic( - account: *const FFIEdDSAAccount, - mnemonic: *const c_char, - passphrase: *const c_char, - index: c_uint, - error: *mut FFIError, -) -> *mut c_char { - let account = deref_ptr!(account, error); - let mnemonic = deref_ptr!(mnemonic, error); - let mnemonic_str = unwrap_or_return!(std::ffi::CStr::from_ptr(mnemonic).to_str(), error); - let passphrase_str = if passphrase.is_null() { - None - } else { - Some(unwrap_or_return!(std::ffi::CStr::from_ptr(passphrase).to_str(), error)) - }; - let sk = unwrap_or_return!( - account.inner().derive_from_mnemonic_private_key_at( - mnemonic_str, - passphrase_str, - key_wallet::mnemonic::Language::English, - index, - ), - error - ); - unwrap_or_return!(CString::new(hex::encode(sk.to_bytes())), error).into_raw() -} - -/// Derive a private key (secp256k1) from an account at a given chain/index, using the provided master xpriv. -/// Returns an opaque FFIPrivateKey pointer that must be freed with `private_key_free`. -/// -/// # Safety -/// - `account` and `master_xpriv` must be valid pointers allocated by this library -/// - `error` must be a valid pointer to an FFIError -#[no_mangle] -pub unsafe extern "C" fn account_derive_private_key_at( - account: *const FFIAccount, - master_xpriv: *const FFIExtendedPrivKey, - index: c_uint, - error: *mut FFIError, -) -> *mut FFIPrivateKey { - let account = deref_ptr!(account, error); - let master_xpriv = deref_ptr!(master_xpriv, error); - - if account.inner().is_watch_only() { - (*error).set( - FFIErrorCode::WalletError, - "Account is watch-only; private derivation not allowed", - ); - return ptr::null_mut(); - } - - let derived = unwrap_or_return!( - account.inner().derive_from_master_xpriv_extended_xpriv_at(master_xpriv.inner(), index), - error - ); - Box::into_raw(Box::new(FFIPrivateKey::from_secret(derived.private_key))) -} - -/// Derive a private key from an account at a given chain/index and return as WIF string. -/// Caller must free the returned string with `string_free`. -/// -/// # Safety -/// - `account` and `master_xpriv` must be valid pointers allocated by this library -/// - `error` must be a valid pointer to an FFIError -#[no_mangle] -pub unsafe extern "C" fn account_derive_private_key_as_wif_at( - account: *const FFIAccount, - master_xpriv: *const FFIExtendedPrivKey, - index: c_uint, - error: *mut FFIError, -) -> *mut c_char { - let account = deref_ptr!(account, error); - let master_xpriv = deref_ptr!(master_xpriv, error); - - if account.inner().is_watch_only() { - (*error).set( - FFIErrorCode::WalletError, - "Account is watch-only; private derivation not allowed", - ); - return ptr::null_mut(); - } - - let derived = unwrap_or_return!( - account.inner().derive_from_master_xpriv_extended_xpriv_at(master_xpriv.inner(), index), - error - ); - let dash_priv = dashcore::PrivateKey { - compressed: true, - network: account.inner().network(), - inner: derived.private_key, - }; - unwrap_or_return!(CString::new(dash_priv.to_wif()), error).into_raw() -} - -/// Derive an extended private key from a raw seed buffer at the given index. -/// Returns an opaque FFIExtendedPrivKey pointer that must be freed with `extended_private_key_free`. -/// -/// # Safety -/// - `account` must be a valid pointer to an FFIAccount -/// - `seed` must point to a valid buffer of length `seed_len` -/// - `error` must be a valid pointer to an FFIError -#[no_mangle] -pub unsafe extern "C" fn account_derive_extended_private_key_from_seed( - account: *const FFIAccount, - seed: *const u8, - seed_len: usize, - index: c_uint, - error: *mut FFIError, -) -> *mut FFIExtendedPrivKey { - let account = deref_ptr!(account, error); - check_ptr!(seed, error); - let seed_slice = std::slice::from_raw_parts(seed, seed_len); - let derived = unwrap_or_return!( - account.inner().derive_from_seed_extended_xpriv_at(seed_slice, index), - error - ); - Box::into_raw(Box::new(FFIExtendedPrivKey::from_inner(derived))) -} - -/// Derive a private key from a raw seed buffer at the given index. -/// Returns an opaque FFIPrivateKey pointer that must be freed with `private_key_free`. -/// -/// # Safety -/// - `account` must be a valid pointer to an FFIAccount -/// - `seed` must point to a valid buffer of length `seed_len` -/// - `error` must be a valid pointer to an FFIError -#[no_mangle] -pub unsafe extern "C" fn account_derive_private_key_from_seed( - account: *const FFIAccount, - seed: *const u8, - seed_len: usize, - index: c_uint, - error: *mut FFIError, -) -> *mut FFIPrivateKey { - let account = deref_ptr!(account, error); - check_ptr!(seed, error); - let seed_slice = std::slice::from_raw_parts(seed, seed_len); - let derived = unwrap_or_return!( - account.inner().derive_from_seed_extended_xpriv_at(seed_slice, index), - error - ); - Box::into_raw(Box::new(FFIPrivateKey::from_secret(derived.private_key))) -} - -/// Derive an extended private key from a mnemonic + optional passphrase at the given index. -/// Returns an opaque FFIExtendedPrivKey pointer that must be freed with `extended_private_key_free`. -/// -/// # Safety -/// - `account` must be a valid pointer to an FFIAccount -/// - `mnemonic` must be a valid, null-terminated C string -/// - `passphrase` may be null; if not null, must be a valid C string -/// - `error` must be a valid pointer to an FFIError -#[no_mangle] -pub unsafe extern "C" fn account_derive_extended_private_key_from_mnemonic( - account: *const FFIAccount, - mnemonic: *const c_char, - passphrase: *const c_char, - index: c_uint, - error: *mut FFIError, -) -> *mut FFIExtendedPrivKey { - let account = deref_ptr!(account, error); - let mnemonic = deref_ptr!(mnemonic, error); - let mnemonic_str = unwrap_or_return!(std::ffi::CStr::from_ptr(mnemonic).to_str(), error); - let passphrase_str = if passphrase.is_null() { - None - } else { - Some(unwrap_or_return!(std::ffi::CStr::from_ptr(passphrase).to_str(), error)) - }; - let derived = unwrap_or_return!( - account.inner().derive_from_mnemonic_extended_xpriv_at( - mnemonic_str, - passphrase_str, - key_wallet::mnemonic::Language::English, - index, - ), - error - ); - Box::into_raw(Box::new(FFIExtendedPrivKey::from_inner(derived))) -} - -/// Derive a private key from a mnemonic + optional passphrase at the given index. -/// Returns an opaque FFIPrivateKey pointer that must be freed with `private_key_free`. -/// -/// # Safety -/// - `account` must be a valid pointer to an FFIAccount -/// - `mnemonic` must be a valid, null-terminated C string -/// - `passphrase` may be null; if not null, must be a valid C string -/// - `error` must be a valid pointer to an FFIError -#[no_mangle] -pub unsafe extern "C" fn account_derive_private_key_from_mnemonic( - account: *const FFIAccount, - mnemonic: *const c_char, - passphrase: *const c_char, - index: c_uint, - error: *mut FFIError, -) -> *mut FFIPrivateKey { - let account = deref_ptr!(account, error); - let mnemonic = deref_ptr!(mnemonic, error); - let mnemonic_str = unwrap_or_return!(std::ffi::CStr::from_ptr(mnemonic).to_str(), error); - let passphrase_str = if passphrase.is_null() { - None - } else { - Some(unwrap_or_return!(std::ffi::CStr::from_ptr(passphrase).to_str(), error)) - }; - let derived = unwrap_or_return!( - account.inner().derive_from_mnemonic_extended_xpriv_at( - mnemonic_str, - passphrase_str, - key_wallet::mnemonic::Language::English, - index, - ), - error - ); - Box::into_raw(Box::new(FFIPrivateKey::from_secret(derived.private_key))) -} - -#[cfg(test)] -#[path = "account_derivation_tests.rs"] -mod account_derivation_tests; diff --git a/key-wallet-ffi/src/account_derivation_tests.rs b/key-wallet-ffi/src/account_derivation_tests.rs deleted file mode 100644 index 688560d3a..000000000 --- a/key-wallet-ffi/src/account_derivation_tests.rs +++ /dev/null @@ -1,249 +0,0 @@ -//! Tests for account-level derivation FFI - -#[cfg(test)] -mod tests { - use crate::account::account_free; - use crate::account_derivation::*; - use crate::derivation::*; - use crate::error::{FFIError, FFIErrorCode}; - use crate::keys::{extended_private_key_free, private_key_free}; - use crate::types::FFIAccountKind; - use crate::wallet; - use dash_network::ffi::FFINetwork; - - const MNEMONIC: &str = - "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; - - #[test] - fn test_account_derive_private_key_at_receive_index() { - let mut error = FFIError::default(); - - let mnemonic = std::ffi::CString::new(MNEMONIC).unwrap(); - let passphrase = std::ffi::CString::new("").unwrap(); - - // Create wallet on testnet with default accounts - let wallet = unsafe { - wallet::wallet_create_from_mnemonic(mnemonic.as_ptr(), FFINetwork::Testnet, &mut error) - }; - assert!(!wallet.is_null()); - assert_eq!(error.code, FFIErrorCode::Success); - - // Get account 0 (BIP44) - let account = unsafe { - crate::account::wallet_get_account(wallet, 0, FFIAccountKind::StandardBIP44).account - }; - assert!(!account.is_null()); - - // Build a master xpriv from the same mnemonic seed - let mut seed = [0u8; 64]; - // Deterministic seed from mnemonic helper - let ok = unsafe { - crate::mnemonic::mnemonic_to_seed( - mnemonic.as_ptr(), - passphrase.as_ptr(), - seed.as_mut_ptr(), - &mut (seed.len()), - &mut error, - ) - }; - assert!(ok); - - let master_xpriv = unsafe { - derivation_new_master_key(seed.as_ptr(), seed.len(), FFINetwork::Testnet, &mut error) - }; - assert!(!master_xpriv.is_null()); - - // For standard accounts with internal/external, this helper should fail - let priv_key = - unsafe { account_derive_private_key_at(account, master_xpriv, 0, &mut error) }; - assert!(priv_key.is_null()); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - - // Derive WIF should also fail for such accounts - let wif = - unsafe { account_derive_private_key_as_wif_at(account, master_xpriv, 0, &mut error) }; - assert!(wif.is_null()); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - - // Cleanup - unsafe { - crate::utils::string_free(wif); - private_key_free(priv_key); - extended_private_key_free(master_xpriv); - account_free(account); - wallet::wallet_free(wallet); - } - } - - #[test] - fn test_bls_and_eddsa_from_seed_and_mnemonic_null_safety() { - let mut error = FFIError::default(); - - // BLS nulls - #[cfg(feature = "bls")] - unsafe { - assert!(super::super::bls_account_derive_private_key_from_seed( - std::ptr::null(), - std::ptr::null(), - 0, - 0, - &mut error, - ) - .is_null()); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - } - - // EdDSA nulls - #[cfg(feature = "eddsa")] - unsafe { - assert!(super::super::eddsa_account_derive_private_key_from_seed( - std::ptr::null(), - std::ptr::null(), - 0, - 0, - &mut error, - ) - .is_null()); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - } - } - - #[test] - fn test_account_derive_extended_private_key_at_change_index() { - let mut error = FFIError::default(); - - let mnemonic = std::ffi::CString::new(MNEMONIC).unwrap(); - let passphrase = std::ffi::CString::new("").unwrap(); - - // Create wallet on testnet with default accounts - let wallet = unsafe { - wallet::wallet_create_from_mnemonic(mnemonic.as_ptr(), FFINetwork::Testnet, &mut error) - }; - assert!(!wallet.is_null()); - - // Get account 0 (BIP44) - let account = unsafe { - crate::account::wallet_get_account(wallet, 0, FFIAccountKind::StandardBIP44).account - }; - assert!(!account.is_null()); - - // Seed and master xpriv - let mut seed = [0u8; 64]; - let ok = unsafe { - crate::mnemonic::mnemonic_to_seed( - mnemonic.as_ptr(), - passphrase.as_ptr(), - seed.as_mut_ptr(), - &mut (seed.len()), - &mut error, - ) - }; - assert!(ok); - let master_xpriv = unsafe { - derivation_new_master_key(seed.as_ptr(), seed.len(), FFINetwork::Testnet, &mut error) - }; - assert!(!master_xpriv.is_null()); - - // Extended xpriv helper should also fail for standard accounts - let xpriv = - unsafe { account_derive_extended_private_key_at(account, master_xpriv, 5, &mut error) }; - assert!(xpriv.is_null()); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - - // Cleanup - unsafe { - extended_private_key_free(master_xpriv); - account_free(account); - wallet::wallet_free(wallet); - } - } - - #[test] - fn test_account_derive_from_seed_and_mnemonic_helpers_fail_for_standard() { - let mut error = FFIError::default(); - - let mnemonic = std::ffi::CString::new(MNEMONIC).unwrap(); - let passphrase = std::ffi::CString::new("").unwrap(); - - // Create wallet and get account 0 - let wallet = unsafe { - wallet::wallet_create_from_mnemonic(mnemonic.as_ptr(), FFINetwork::Testnet, &mut error) - }; - assert!(!wallet.is_null()); - let account = unsafe { - crate::account::wallet_get_account(wallet, 0, FFIAccountKind::StandardBIP44).account - }; - assert!(!account.is_null()); - - // Prepare seed - let mut seed = [0u8; 64]; - let mut seed_len = seed.len(); - let ok = unsafe { - crate::mnemonic::mnemonic_to_seed( - mnemonic.as_ptr(), - passphrase.as_ptr(), - seed.as_mut_ptr(), - &mut seed_len, - &mut error, - ) - }; - assert!(ok); - - // account_derive_extended_private_key_from_seed should fail for standard accounts - let xpriv_seed = unsafe { - super::super::account_derive_extended_private_key_from_seed( - account, - seed.as_ptr(), - seed_len, - 0, - &mut error, - ) - }; - assert!(xpriv_seed.is_null()); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - - // account_derive_private_key_from_seed should fail - let priv_seed = unsafe { - super::super::account_derive_private_key_from_seed( - account, - seed.as_ptr(), - seed_len, - 0, - &mut error, - ) - }; - assert!(priv_seed.is_null()); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - - // account_derive_extended_private_key_from_mnemonic should fail - let xpriv_mn = unsafe { - super::super::account_derive_extended_private_key_from_mnemonic( - account, - mnemonic.as_ptr(), - passphrase.as_ptr(), - 0, - &mut error, - ) - }; - assert!(xpriv_mn.is_null()); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - - // account_derive_private_key_from_mnemonic should fail - let priv_mn = unsafe { - super::super::account_derive_private_key_from_mnemonic( - account, - mnemonic.as_ptr(), - passphrase.as_ptr(), - 0, - &mut error, - ) - }; - assert!(priv_mn.is_null()); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - - unsafe { - account_free(account); - wallet::wallet_free(wallet); - } - } -} diff --git a/key-wallet-ffi/src/account_tests.rs b/key-wallet-ffi/src/account_tests.rs deleted file mode 100644 index 5e90bb34b..000000000 --- a/key-wallet-ffi/src/account_tests.rs +++ /dev/null @@ -1,201 +0,0 @@ -#[cfg(test)] -#[allow(clippy::module_inception)] -mod tests { - use super::super::*; - use crate::error::{FFIError, FFIErrorCode}; - use crate::types::FFIAccountKind; - use crate::wallet; - use std::ffi::CString; - use std::ptr; - - #[test] - fn test_wallet_get_account_null_wallet() { - let result = unsafe { wallet_get_account(ptr::null(), 0, FFIAccountKind::StandardBIP44) }; - - assert!(result.account.is_null()); - assert_ne!(result.error_code, 0); - assert_eq!(result.error_code, FFIErrorCode::InvalidInput as i32); - - // Clean up error message if present - if !result.error_message.is_null() { - unsafe { - let _ = CString::from_raw(result.error_message); - } - } - } - - #[test] - fn test_wallet_get_account_existing() { - let mut error = FFIError::default(); - - // Create a wallet with default accounts - let mnemonic = CString::new("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").unwrap(); - - let wallet = unsafe { - wallet::wallet_create_from_mnemonic(mnemonic.as_ptr(), FFINetwork::Testnet, &mut error) - }; - - // Try to get the default account (should exist) - let result = unsafe { wallet_get_account(wallet, 0, FFIAccountKind::StandardBIP44) }; - - // Note: Since the account may not exist yet (depends on wallet creation logic), - // we just check that the call doesn't return an error for invalid parameters - // The actual account existence check would depend on the wallet implementation - - // Clean up the account if it was returned - if !result.account.is_null() { - unsafe { - account_free(result.account); - } - } - - // Clean up error message if present - if !result.error_message.is_null() { - unsafe { - let _ = CString::from_raw(result.error_message); - } - } - - // Clean up - unsafe { - wallet::wallet_free(wallet); - } - } - - #[test] - fn test_wallet_get_account_count_null_wallet() { - let mut error = FFIError::default(); - - let count = unsafe { wallet_get_account_count(ptr::null(), &mut error) }; - - assert_eq!(count, 0); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - } - - #[test] - fn test_wallet_get_account_count() { - let mut error = FFIError::default(); - - // Create a wallet - let mnemonic = CString::new("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").unwrap(); - - let wallet = unsafe { - wallet::wallet_create_from_mnemonic(mnemonic.as_ptr(), FFINetwork::Testnet, &mut error) - }; - - let count = unsafe { wallet_get_account_count(wallet, &mut error) }; - - // Should have at least one default account - assert!(count >= 1); - assert_eq!(error.code, FFIErrorCode::Success); - - // Clean up - unsafe { - wallet::wallet_free(wallet); - } - } - - #[test] - fn test_account_type_values() { - // Test FFIAccountKind enum values - assert_eq!(FFIAccountKind::StandardBIP44 as u32, 0); - assert_eq!(FFIAccountKind::StandardBIP32 as u32, 1); - assert_eq!(FFIAccountKind::CoinJoin as u32, 2); - assert_eq!(FFIAccountKind::IdentityRegistration as u32, 3); - assert_eq!(FFIAccountKind::IdentityTopUp as u32, 4); - assert_eq!(FFIAccountKind::IdentityTopUpNotBoundToIdentity as u32, 5); - assert_eq!(FFIAccountKind::IdentityInvitation as u32, 6); - assert_eq!(FFIAccountKind::ProviderVotingKeys as u32, 7); - assert_eq!(FFIAccountKind::ProviderOwnerKeys as u32, 8); - assert_eq!(FFIAccountKind::ProviderOperatorKeys as u32, 9); - assert_eq!(FFIAccountKind::ProviderPlatformKeys as u32, 10); - } - - #[test] - fn test_account_getters() { - let mut error = FFIError::default(); - - // Create a wallet - let mnemonic = CString::new("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").unwrap(); - - let wallet = unsafe { - wallet::wallet_create_from_mnemonic(mnemonic.as_ptr(), FFINetwork::Testnet, &mut error) - }; - - assert!(!wallet.is_null()); - assert_eq!(error.code, FFIErrorCode::Success); - - // Get an account - let result = unsafe { wallet_get_account(wallet, 0, FFIAccountKind::StandardBIP44) }; - - if !result.account.is_null() { - // Test all the getter functions - unsafe { - // Test get xpub - let xpub_str = account_get_extended_public_key_as_string(result.account); - assert!(!xpub_str.is_null()); - let xpub = CString::from_raw(xpub_str); - let xpub_string = xpub.to_string_lossy(); - assert!(xpub_string.starts_with("tpub")); // Testnet xpub should start with tpub - - // Test get network - let network = account_get_network(result.account); - assert_eq!(network, FFINetwork::Testnet); - - // Test get parent wallet id (may be null) - let _wallet_id = account_get_parent_wallet_id(result.account); - // Just check it doesn't crash - may be null - - // Test get account type - let mut index = 999u32; - let account_type = account_get_account_type(result.account, &mut index); - assert_eq!(account_type as u32, FFIAccountKind::StandardBIP44 as u32); - assert_eq!(index, 0); // Account index should be 0 - - // Test is watch only - should be false for a wallet created from mnemonic - let is_watch_only = account_get_is_watch_only(result.account); - assert!(!is_watch_only); - - // Clean up - account_free(result.account); - } - } - - // Clean up error message if present - if !result.error_message.is_null() { - unsafe { - let _ = CString::from_raw(result.error_message); - } - } - - // Clean up - unsafe { - wallet::wallet_free(wallet); - } - } - - #[test] - fn test_account_getters_null_safety() { - // Test all getter functions with null pointers - let xpub = unsafe { account_get_extended_public_key_as_string(ptr::null()) }; - assert!(xpub.is_null()); - - let network = unsafe { account_get_network(ptr::null()) }; - assert_eq!(network, FFINetwork::Mainnet); - - let wallet_id = unsafe { account_get_parent_wallet_id(ptr::null()) }; - assert!(wallet_id.is_null()); - - let mut index = 0u32; - let account_type = unsafe { account_get_account_type(ptr::null(), &mut index) }; - assert_eq!(account_type as u32, FFIAccountKind::StandardBIP44 as u32); - assert_eq!(index, 0); - - // Test with null out_index - let account_type = unsafe { account_get_account_type(ptr::null(), ptr::null_mut()) }; - assert_eq!(account_type as u32, FFIAccountKind::StandardBIP44 as u32); - - let is_watch_only = unsafe { account_get_is_watch_only(ptr::null()) }; - assert!(!is_watch_only); - } -} diff --git a/key-wallet-ffi/src/address.rs b/key-wallet-ffi/src/address.rs deleted file mode 100644 index ce9800161..000000000 --- a/key-wallet-ffi/src/address.rs +++ /dev/null @@ -1,106 +0,0 @@ -//! Address derivation and management - -#[cfg(test)] -#[path = "address_tests.rs"] -mod tests; - -use crate::error::FFIError; -use crate::{deref_ptr, unwrap_or_return}; -use dash_network::ffi::FFINetwork; -use std::ffi::{CStr, CString}; -use std::os::raw::{c_char, c_uchar}; - -/// Free address string -/// -/// # Safety -/// -/// - `address` must be a valid pointer created by address functions or null -/// - After calling this function, the pointer becomes invalid -#[no_mangle] -pub unsafe extern "C" fn address_free(address: *mut c_char) { - if !address.is_null() { - unsafe { - let _ = CString::from_raw(address); - } - } -} - -/// Free address array -/// -/// # Safety -/// -/// - `addresses` must be a valid pointer to an array of address strings or null -/// - Each address in the array must be a valid C string pointer -/// - `count` must be the correct number of addresses in the array -/// - After calling this function, all pointers become invalid -#[no_mangle] -pub unsafe extern "C" fn address_array_free(addresses: *mut *mut c_char, count: usize) { - if !addresses.is_null() { - unsafe { - let slice = std::slice::from_raw_parts_mut(addresses, count); - for addr in slice { - if !addr.is_null() { - let _ = CString::from_raw(*addr); - } - } - // Free the array itself - let _ = Box::from_raw(std::ptr::slice_from_raw_parts_mut(addresses, count)); - } - } -} - -/// Validate an address -/// -/// # Safety -/// -/// - `address` must be a valid null-terminated C string -/// - `error` must be a valid pointer to an FFIError -#[no_mangle] -pub unsafe extern "C" fn address_validate( - address: *const c_char, - network: FFINetwork, - error: *mut FFIError, -) -> bool { - use std::str::FromStr; - - let address = deref_ptr!(address, error); - let address_str = unwrap_or_return!(CStr::from_ptr(address).to_str(), error); - let network_rust: key_wallet::Network = network.into(); - - let addr = unwrap_or_return!(key_wallet::Address::from_str(address_str), error); - let _ = unwrap_or_return!(addr.require_network(network_rust), error); - true -} - -/// Get address type -/// -/// Returns: -/// - 0: P2PKH address -/// - 1: P2SH address -/// - 2: Other address type -/// - u8::MAX (255): Error occurred -/// -/// # Safety -/// -/// - `address` must be a valid null-terminated C string -/// - `error` must be a valid pointer to an FFIError -#[no_mangle] -pub unsafe extern "C" fn address_get_type( - address: *const c_char, - network: FFINetwork, - error: *mut FFIError, -) -> c_uchar { - use std::str::FromStr; - - let address = deref_ptr!(address, error, u8::MAX); - let address_str = unwrap_or_return!(CStr::from_ptr(address).to_str(), error, u8::MAX); - let network_rust: key_wallet::Network = network.into(); - let addr = unwrap_or_return!(key_wallet::Address::from_str(address_str), error, u8::MAX); - let checked = unwrap_or_return!(addr.require_network(network_rust), error, u8::MAX); - - match checked.address_type() { - Some(key_wallet::AddressType::P2pkh) => 0, - Some(key_wallet::AddressType::P2sh) => 1, - Some(_) | None => 2, - } -} diff --git a/key-wallet-ffi/src/address_pool.rs b/key-wallet-ffi/src/address_pool.rs deleted file mode 100644 index 886dfb0ee..000000000 --- a/key-wallet-ffi/src/address_pool.rs +++ /dev/null @@ -1,1178 +0,0 @@ -//! Address pool management FFI bindings -//! -//! This module provides FFI bindings for managing address pools within -//! managed accounts, including gap limit management and address generation. - -use std::ffi::CString; -use std::os::raw::{c_char, c_uint}; - -use crate::error::{FFIError, FFIErrorCode}; -use crate::managed_wallet::FFIManagedWalletInfo; -use crate::types::{FFIAccountKind, FFIWallet}; -use crate::utils::rust_string_to_c; -use crate::{check_ptr, deref_ptr, deref_ptr_mut, unwrap_or_return}; -use key_wallet::account::ManagedAccountCollection; -use key_wallet::managed_account::address_pool::{ - AddressInfo, AddressPool, KeySource, PublicKeyType, -}; -use key_wallet::managed_account::managed_account_trait::ManagedAccountTrait; -use key_wallet::managed_account::{ManagedAccountRef, ManagedAccountRefMut}; -use key_wallet::AccountType; - -// Helper functions to get managed accounts by type. Identity / asset-lock / -// provider variants are stored as keys-only accounts; Standard / CoinJoin -// stay funds-bearing. The returned [`ManagedAccountRef`] enum exposes the -// shared trait surface (address pools, managed type, network) used here -// without forcing callers to dispatch on the variant. -pub(crate) fn get_managed_account_by_type<'a>( - collection: &'a ManagedAccountCollection, - account_type: &AccountType, -) -> Option> { - match account_type { - AccountType::Standard { - index, - standard_account_type, - } => match standard_account_type { - key_wallet::account::StandardAccountType::BIP44Account => { - collection.standard_bip44_accounts.get(index).map(ManagedAccountRef::Funds) - } - key_wallet::account::StandardAccountType::BIP32Account => { - collection.standard_bip32_accounts.get(index).map(ManagedAccountRef::Funds) - } - }, - AccountType::CoinJoin { - index, - } => collection.coinjoin_accounts.get(index).map(ManagedAccountRef::Funds), - AccountType::IdentityRegistration => { - collection.identity_registration.as_ref().map(ManagedAccountRef::Keys) - } - AccountType::IdentityTopUp { - registration_index, - } => collection.identity_topup.get(registration_index).map(ManagedAccountRef::Keys), - AccountType::IdentityTopUpNotBoundToIdentity => { - collection.identity_topup_not_bound.as_ref().map(ManagedAccountRef::Keys) - } - AccountType::IdentityInvitation => { - collection.identity_invitation.as_ref().map(ManagedAccountRef::Keys) - } - AccountType::AssetLockAddressTopUp => { - collection.asset_lock_address_topup.as_ref().map(ManagedAccountRef::Keys) - } - AccountType::AssetLockShieldedAddressTopUp => { - collection.asset_lock_shielded_address_topup.as_ref().map(ManagedAccountRef::Keys) - } - AccountType::ProviderVotingKeys => { - collection.provider_voting_keys.as_ref().map(ManagedAccountRef::Keys) - } - AccountType::ProviderOwnerKeys => { - collection.provider_owner_keys.as_ref().map(ManagedAccountRef::Keys) - } - AccountType::ProviderOperatorKeys => { - collection.provider_operator_keys.as_ref().map(ManagedAccountRef::Keys) - } - AccountType::ProviderPlatformKeys => { - collection.provider_platform_keys.as_ref().map(ManagedAccountRef::Keys) - } - AccountType::DashpayReceivingFunds { - .. - } - | AccountType::DashpayExternalAccount { - .. - } - | AccountType::PlatformPayment { - .. - } => { - // DashPay and Platform Payment accounts are not reachable through - // this address-pool helper. - None - } - } -} - -pub(crate) fn get_managed_account_by_type_mut<'a>( - collection: &'a mut ManagedAccountCollection, - account_type: &AccountType, -) -> Option> { - match account_type { - AccountType::Standard { - index, - standard_account_type, - } => match standard_account_type { - key_wallet::account::StandardAccountType::BIP44Account => { - collection.standard_bip44_accounts.get_mut(index).map(ManagedAccountRefMut::Funds) - } - key_wallet::account::StandardAccountType::BIP32Account => { - collection.standard_bip32_accounts.get_mut(index).map(ManagedAccountRefMut::Funds) - } - }, - AccountType::CoinJoin { - index, - } => collection.coinjoin_accounts.get_mut(index).map(ManagedAccountRefMut::Funds), - AccountType::IdentityRegistration => { - collection.identity_registration.as_mut().map(ManagedAccountRefMut::Keys) - } - AccountType::IdentityTopUp { - registration_index, - } => collection.identity_topup.get_mut(registration_index).map(ManagedAccountRefMut::Keys), - AccountType::IdentityTopUpNotBoundToIdentity => { - collection.identity_topup_not_bound.as_mut().map(ManagedAccountRefMut::Keys) - } - AccountType::IdentityInvitation => { - collection.identity_invitation.as_mut().map(ManagedAccountRefMut::Keys) - } - AccountType::AssetLockAddressTopUp => { - collection.asset_lock_address_topup.as_mut().map(ManagedAccountRefMut::Keys) - } - AccountType::AssetLockShieldedAddressTopUp => { - collection.asset_lock_shielded_address_topup.as_mut().map(ManagedAccountRefMut::Keys) - } - AccountType::ProviderVotingKeys => { - collection.provider_voting_keys.as_mut().map(ManagedAccountRefMut::Keys) - } - AccountType::ProviderOwnerKeys => { - collection.provider_owner_keys.as_mut().map(ManagedAccountRefMut::Keys) - } - AccountType::ProviderOperatorKeys => { - collection.provider_operator_keys.as_mut().map(ManagedAccountRefMut::Keys) - } - AccountType::ProviderPlatformKeys => { - collection.provider_platform_keys.as_mut().map(ManagedAccountRefMut::Keys) - } - AccountType::DashpayReceivingFunds { - .. - } - | AccountType::DashpayExternalAccount { - .. - } => { - // DashPay managed accounts are not currently persisted in ManagedAccountCollection - None - } - AccountType::PlatformPayment { - .. - } => { - // Platform Payment accounts are not currently persisted in ManagedAccountCollection - None - } - } -} - -/// Address pool type -#[repr(C)] -pub enum FFIAddressPoolType { - /// External (receive) addresses - External = 0, - /// Internal (change) addresses - Internal = 1, - /// Single pool (for non-standard accounts) - Single = 2, -} - -/// FFI wrapper for an AddressPool from a ManagedAccount -/// -/// This is a lightweight wrapper that holds a reference to an AddressPool -/// from within a ManagedAccount. It allows querying addresses and pool information. -pub struct FFIAddressPool { - /// Reference to the address pool (mutable for internal consistency even if not modified) - pub(crate) pool: *mut AddressPool, - /// Pool type to track what kind of pool this is - #[allow(dead_code)] - pub(crate) pool_type: FFIAddressPoolType, -} - -/// FFI-compatible version of AddressInfo -#[repr(C)] -pub struct FFIAddressInfo { - /// Address as string - pub address: *mut c_char, - /// Script pubkey bytes - pub script_pubkey: *mut u8, - /// Length of script pubkey - pub script_pubkey_len: usize, - /// Public key bytes (nullable) - pub public_key: *mut u8, - /// Length of public key - pub public_key_len: usize, - /// Derivation index - pub index: u32, - /// Derivation path as string - pub path: *mut c_char, - /// Whether address has been used - pub used: bool, - /// When generated (timestamp) - pub generated_at: u64, - /// When first used (0 if never) - pub used_at: u64, - /// Transaction count - pub tx_count: u32, - /// Total received - pub total_received: u64, - /// Total sent - pub total_sent: u64, - /// Current balance - pub balance: u64, - /// Custom label (nullable) - pub label: *mut c_char, -} - -/// Convert from AddressInfo to FFIAddressInfo -fn address_info_to_ffi(info: &AddressInfo) -> FFIAddressInfo { - // Convert address to string - let address_str = rust_string_to_c(info.address.to_string()); - - // Convert script pubkey to bytes - let script_bytes = info.script_pubkey.as_bytes(); - let script_pubkey_len = script_bytes.len(); - let script_pubkey = if script_pubkey_len > 0 { - let mut bytes = Vec::with_capacity(script_pubkey_len); - bytes.extend_from_slice(script_bytes); - Box::into_raw(bytes.into_boxed_slice()) as *mut u8 - } else { - std::ptr::null_mut() - }; - - // Convert public key to bytes if present - let (public_key, public_key_len) = match &info.public_key { - Some(pk) => match pk { - PublicKeyType::ECDSA(bytes) - | PublicKeyType::EdDSA(bytes) - | PublicKeyType::BLS(bytes) => { - let len = bytes.len(); - if len > 0 { - let mut key_bytes = Vec::with_capacity(len); - key_bytes.extend_from_slice(bytes); - (Box::into_raw(key_bytes.into_boxed_slice()) as *mut u8, len) - } else { - (std::ptr::null_mut(), 0) - } - } - }, - None => (std::ptr::null_mut(), 0), - }; - - // Convert derivation path to string - let path_str = rust_string_to_c(info.path.to_string()); - - // Convert label if present - let label = - info.label.as_ref().map(|l| rust_string_to_c(l.clone())).unwrap_or(std::ptr::null_mut()); - - FFIAddressInfo { - address: address_str, - script_pubkey, - script_pubkey_len, - public_key, - public_key_len, - index: info.index, - path: path_str, - used: info.used, - generated_at: info.generated_at, - used_at: info.used_at.unwrap_or(0), - tx_count: info.tx_count, - total_received: info.total_received, - total_sent: info.total_sent, - balance: info.balance, - label, - } -} - -/// Free an address pool handle -/// -/// # Safety -/// -/// - `pool` must be a valid pointer to an FFIAddressPool that was allocated by this library -/// - The pointer must not be used after calling this function -/// - This function must only be called once per allocation -#[no_mangle] -pub unsafe extern "C" fn address_pool_free(pool: *mut FFIAddressPool) { - if !pool.is_null() { - let _ = Box::from_raw(pool); - } -} - -/// Address pool info -#[repr(C)] -pub struct FFIAddressPoolInfo { - /// Pool type - pub pool_type: FFIAddressPoolType, - /// Number of generated addresses - pub generated_count: c_uint, - /// Number of used addresses - pub used_count: c_uint, - /// Current gap (unused addresses at the end) - pub current_gap: c_uint, - /// Gap limit setting - pub gap_limit: c_uint, - /// Highest used index (-1 if none used) - pub highest_used_index: i32, -} - -/// Get address pool information for an account -/// -/// # Safety -/// -/// - `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo -/// - `info_out` must be a valid pointer to store the pool info -/// - `error` must be a valid pointer to an FFIError -#[no_mangle] -pub unsafe extern "C" fn managed_wallet_get_address_pool_info( - managed_wallet: *const FFIManagedWalletInfo, - account_type: FFIAccountKind, - account_index: c_uint, - pool_type: FFIAddressPoolType, - info_out: *mut FFIAddressPoolInfo, - error: *mut FFIError, -) -> bool { - let wrapper = deref_ptr!(managed_wallet, error); - check_ptr!(info_out, error); - let managed_wallet = wrapper.inner(); - - let account_type_rust = account_type.to_account_type(account_index); - - // Get the specific managed account - let managed_account = unwrap_or_return!( - get_managed_account_by_type(&managed_wallet.accounts, &account_type_rust), - error - ); - - // Get the appropriate address pool - let pool = match pool_type { - FFIAddressPoolType::External => { - // Only standard accounts have external/internal pools - if let key_wallet::managed_account::managed_account_type::ManagedAccountType::Standard { - external_addresses, - .. - } = managed_account.managed_account_type() { - external_addresses - } else { - (*error).set(FFIErrorCode::InvalidInput, "Account type does not have external address pool"); - return false; - } - } - FFIAddressPoolType::Internal => { - // Only standard accounts have external/internal pools - if let key_wallet::managed_account::managed_account_type::ManagedAccountType::Standard { - internal_addresses, - .. - } = managed_account.managed_account_type() { - internal_addresses - } else { - (*error).set(FFIErrorCode::InvalidInput, "Account type does not have internal address pool"); - return false; - } - } - FFIAddressPoolType::Single => { - // Get the first (and only) address pool for non-standard accounts - let pools = managed_account.managed_account_type().address_pools(); - if pools.is_empty() { - (*error).set(FFIErrorCode::InvalidInput, "Account has no address pools"); - return false; - } - pools[0] - } - }; - - // Fill the info structure - let generated_count = pool.addresses.len(); - let used_count = pool.used_indices.len(); - let highest_used = pool.highest_used.unwrap_or(0); - let highest_generated = pool.highest_generated.unwrap_or(0); - let current_gap = highest_generated.saturating_sub(highest_used); - - *info_out = FFIAddressPoolInfo { - pool_type, - generated_count: generated_count as c_uint, - used_count: used_count as c_uint, - current_gap: current_gap as c_uint, - gap_limit: pool.gap_limit as c_uint, - highest_used_index: pool.highest_used.map(|i| i as i32).unwrap_or(-1), - }; - - (*error).clean(); - true -} - -/// Set the gap limit for an address pool -/// -/// The gap limit determines how many unused addresses to maintain at the end -/// of the pool. This is important for wallet recovery and address discovery. -/// -/// # Safety -/// -/// - `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo -/// - `error` must be a valid pointer to an FFIError -#[no_mangle] -pub unsafe extern "C" fn managed_wallet_set_gap_limit( - managed_wallet: *mut FFIManagedWalletInfo, - account_type: FFIAccountKind, - account_index: c_uint, - pool_type: FFIAddressPoolType, - gap_limit: c_uint, - error: *mut FFIError, -) -> bool { - let managed_wallet = deref_ptr_mut!(managed_wallet, error).inner_mut(); - - let account_type_rust = account_type.to_account_type(account_index); - - // Get the specific managed account - let mut managed_account = unwrap_or_return!( - get_managed_account_by_type_mut(&mut managed_wallet.accounts, &account_type_rust), - error - ); - - // Get the appropriate address pool - let pool = match pool_type { - FFIAddressPoolType::External => { - // Only standard accounts have external/internal pools - if let key_wallet::managed_account::managed_account_type::ManagedAccountType::Standard { - external_addresses, - .. - } = managed_account.managed_account_type_mut() { - external_addresses - } else { - (*error).set(FFIErrorCode::InvalidInput, "Account type does not have external address pool"); - return false; - } - } - FFIAddressPoolType::Internal => { - // Only standard accounts have external/internal pools - if let key_wallet::managed_account::managed_account_type::ManagedAccountType::Standard { - internal_addresses, - .. - } = managed_account.managed_account_type_mut() { - internal_addresses - } else { - (*error).set(FFIErrorCode::InvalidInput, "Account type does not have internal address pool"); - return false; - } - } - FFIAddressPoolType::Single => { - // Get the first (and only) address pool for non-standard accounts - let pools = managed_account.managed_account_type_mut().address_pools_mut(); - if pools.is_empty() { - (*error).set(FFIErrorCode::InvalidInput, "Account has no address pools"); - return false; - } - pools.into_iter().next().unwrap() - } - }; - - // Set the gap limit - pool.gap_limit = gap_limit; - - (*error).clean(); - true -} - -/// Generate addresses up to a specific index in a pool -/// -/// This ensures that addresses up to and including the specified index exist -/// in the pool. This is useful for wallet recovery or when specific indices -/// are needed. -/// -/// # Safety -/// -/// - `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo -/// - `wallet` must be a valid pointer to an FFIWallet (for key derivation) -/// - `error` must be a valid pointer to an FFIError -#[no_mangle] -pub unsafe extern "C" fn managed_wallet_generate_addresses_to_index( - managed_wallet: *mut FFIManagedWalletInfo, - wallet: *const FFIWallet, - account_type: FFIAccountKind, - account_index: c_uint, - pool_type: FFIAddressPoolType, - target_index: c_uint, - error: *mut FFIError, -) -> bool { - let managed_wallet = deref_ptr_mut!(managed_wallet, error).inner_mut(); - let wallet = deref_ptr!(wallet, error); - - let account_type_rust = account_type.to_account_type(account_index); - - let account_type_to_check = unwrap_or_return!(account_type_rust.try_into(), error); - - let xpub = unwrap_or_return!( - wallet - .inner() - .extended_public_key_for_account_type(&account_type_to_check, Some(account_index)), - error - ); - - let key_source = KeySource::Public(xpub); - - // Get the specific managed account - let mut managed_account = unwrap_or_return!( - get_managed_account_by_type_mut(&mut managed_wallet.accounts, &account_type_rust), - error - ); - - // Get the appropriate address pool and generate addresses - let result = match pool_type { - FFIAddressPoolType::External => { - // Only standard accounts have external/internal pools - if let key_wallet::managed_account::managed_account_type::ManagedAccountType::Standard { - external_addresses, - .. - } = managed_account.managed_account_type_mut() { - { - let current = external_addresses.highest_generated.unwrap_or(0); - if target_index > current { - let needed = target_index - current; - external_addresses.generate_addresses(needed, &key_source, true) - } else { - Ok(Vec::new()) - } - } - } else { - (*error).set(FFIErrorCode::InvalidInput, "Account type does not have external address pool"); - return false; - } - } - FFIAddressPoolType::Internal => { - // Only standard accounts have external/internal pools - if let key_wallet::managed_account::managed_account_type::ManagedAccountType::Standard { - internal_addresses, - .. - } = managed_account.managed_account_type_mut() { - { - let current = internal_addresses.highest_generated.unwrap_or(0); - if target_index > current { - let needed = target_index - current; - internal_addresses.generate_addresses(needed, &key_source, true) - } else { - Ok(Vec::new()) - } - } - } else { - (*error).set(FFIErrorCode::InvalidInput, "Account type does not have internal address pool"); - return false; - } - } - FFIAddressPoolType::Single => { - // Get the first (and only) address pool for non-standard accounts - let mut pools = managed_account.managed_account_type_mut().address_pools_mut(); - if pools.is_empty() { - (*error).set(FFIErrorCode::InvalidInput, "Account has no address pools"); - return false; - } - { - let pool = &mut pools[0]; - let current = pool.highest_generated.unwrap_or(0); - if target_index > current { - let needed = target_index - current; - pool.generate_addresses(needed, &key_source, true) - } else { - Ok(Vec::new()) - } - } - } - }; - - let _ = unwrap_or_return!(result, error); - true -} - -/// Mark an address as used in the pool -/// -/// This updates the pool's tracking of which addresses have been used, -/// which is important for gap limit management and wallet recovery. -/// -/// # Safety -/// -/// - `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo -/// - `address` must be a valid C string -/// - `error` must be a valid pointer to an FFIError -#[no_mangle] -pub unsafe extern "C" fn managed_wallet_mark_address_used( - managed_wallet: *mut FFIManagedWalletInfo, - address: *const c_char, - error: *mut FFIError, -) -> bool { - let managed_wallet = deref_ptr_mut!(managed_wallet, error).inner_mut(); - let address = deref_ptr!(address, error); - - let address_str = unwrap_or_return!(std::ffi::CStr::from_ptr(address).to_str(), error); - - // Parse address as unchecked first, then convert to the correct network - use core::str::FromStr; - use dashcore::address::{Address, NetworkUnchecked}; - - let unchecked_addr = - unwrap_or_return!(Address::::from_str(address_str), error); - - // Assume the address uses the same network we're working with - let address = unchecked_addr.assume_checked(); - - // Get the account collection - let collection = &mut managed_wallet.accounts; - - // Try to mark the address as used in any account that contains it - let marked = { - let mut found = false; - // Check all accounts for the address - for account in collection.standard_bip44_accounts.values_mut() { - if account.mark_address_used(&address) { - found = true; - break; - } - } - if !found { - for account in collection.standard_bip32_accounts.values_mut() { - if account.mark_address_used(&address) { - found = true; - break; - } - } - } - if !found { - for account in collection.coinjoin_accounts.values_mut() { - if account.mark_address_used(&address) { - found = true; - break; - } - } - } - if !found { - if let Some(account) = &mut collection.identity_registration { - if account.mark_address_used(&address) { - found = true; - } - } - } - if !found { - for account in collection.identity_topup.values_mut() { - if account.mark_address_used(&address) { - found = true; - break; - } - } - } - if !found { - if let Some(account) = &mut collection.identity_topup_not_bound { - if account.mark_address_used(&address) { - found = true; - } - } - } - if !found { - if let Some(account) = &mut collection.identity_invitation { - if account.mark_address_used(&address) { - found = true; - } - } - } - if !found { - if let Some(account) = &mut collection.asset_lock_address_topup { - if account.mark_address_used(&address) { - found = true; - } - } - } - if !found { - if let Some(account) = &mut collection.asset_lock_shielded_address_topup { - if account.mark_address_used(&address) { - found = true; - } - } - } - if !found { - if let Some(account) = &mut collection.provider_voting_keys { - if account.mark_address_used(&address) { - found = true; - } - } - } - if !found { - if let Some(account) = &mut collection.provider_owner_keys { - if account.mark_address_used(&address) { - found = true; - } - } - } - if !found { - if let Some(account) = &mut collection.provider_operator_keys { - if account.mark_address_used(&address) { - found = true; - } - } - } - if !found { - if let Some(account) = &mut collection.provider_platform_keys { - if account.mark_address_used(&address) { - found = true; - } - } - } - if !found { - for account in collection.dashpay_receival_accounts.values_mut() { - if account.mark_address_used(&address) { - found = true; - break; - } - } - } - if !found { - for account in collection.dashpay_external_accounts.values_mut() { - if account.mark_address_used(&address) { - found = true; - break; - } - } - } - found - }; - - if marked { - (*error).clean(); - true - } else { - (*error).set(FFIErrorCode::NotFound, "Address not found in any account"); - false - } -} - -/// Get a single address info at a specific index from the pool -/// -/// Returns detailed information about the address at the given index, or NULL -/// if the index is out of bounds or not generated yet. -/// -/// # Safety -/// -/// - `pool` must be a valid pointer to an FFIAddressPool -/// - `error` must be a valid pointer to an FFIError -/// - The returned FFIAddressInfo must be freed using `address_info_free` -#[no_mangle] -pub unsafe extern "C" fn address_pool_get_address_at_index( - pool: *const FFIAddressPool, - index: u32, - error: *mut FFIError, -) -> *mut FFIAddressInfo { - let pool = deref_ptr!(pool, error); - let address_pool = &*pool.pool; - - // Get the address info at the specified index - let info = unwrap_or_return!(address_pool.info_at_index(index), error); - let ffi_info = address_info_to_ffi(info); - Box::into_raw(Box::new(ffi_info)) -} - -/// Get a range of addresses from the pool -/// -/// Returns an array of FFIAddressInfo structures for addresses in the range [start_index, end_index). -/// The count_out parameter will be set to the actual number of addresses returned. -/// -/// Note: This function only reads existing addresses from the pool. It does not generate new addresses. -/// Use managed_wallet_generate_addresses_to_index if you need to generate addresses first. -/// -/// # Safety -/// -/// - `pool` must be a valid pointer to an FFIAddressPool -/// - `count_out` must be a valid pointer to store the count -/// - `error` must be a valid pointer to an FFIError -/// - The returned array must be freed using `address_info_array_free` -#[no_mangle] -pub unsafe extern "C" fn address_pool_get_addresses_in_range( - pool: *const FFIAddressPool, - start_index: u32, - end_index: u32, - count_out: *mut usize, - error: *mut FFIError, -) -> *mut *mut FFIAddressInfo { - let pool = deref_ptr!(pool, error); - check_ptr!(count_out, error); - - *count_out = 0; - let address_pool = &*pool.pool; - - // Collect address infos in the range - let mut infos = Vec::new(); - - // Special case: if start_index == 0 and end_index == 0, return all addresses - if start_index == 0 && end_index == 0 { - for idx in 0..=address_pool.highest_generated.unwrap_or(0) { - if let Some(info) = address_pool.info_at_index(idx) { - infos.push(Box::into_raw(Box::new(address_info_to_ffi(info)))); - } - } - } else { - // Normal range query - if end_index <= start_index { - (*error).set(FFIErrorCode::InvalidInput, "End index must be greater than start index"); - return std::ptr::null_mut(); - } - - for idx in start_index..end_index { - if let Some(info) = address_pool.info_at_index(idx) { - infos.push(Box::into_raw(Box::new(address_info_to_ffi(info)))); - } - } - } - - if infos.is_empty() { - (*error).set(FFIErrorCode::NotFound, "No addresses found in the specified range"); - return std::ptr::null_mut(); - } - - *count_out = infos.len(); - let array_ptr = Box::into_raw(infos.into_boxed_slice()) as *mut *mut FFIAddressInfo; - - (*error).clean(); - array_ptr -} - -/// Free a single FFIAddressInfo structure -/// -/// # Safety -/// -/// - `info` must be a valid pointer to an FFIAddressInfo allocated by this library or null -/// - The pointer must not be used after calling this function -#[no_mangle] -pub unsafe extern "C" fn address_info_free(info: *mut FFIAddressInfo) { - if !info.is_null() { - let info = Box::from_raw(info); - - // Free the C strings - if !info.address.is_null() { - let _ = CString::from_raw(info.address); - } - if !info.path.is_null() { - let _ = CString::from_raw(info.path); - } - if !info.label.is_null() { - let _ = CString::from_raw(info.label); - } - - // Free the byte arrays - if !info.script_pubkey.is_null() && info.script_pubkey_len > 0 { - let _ = Box::from_raw(std::ptr::slice_from_raw_parts_mut( - info.script_pubkey, - info.script_pubkey_len, - )); - } - if !info.public_key.is_null() && info.public_key_len > 0 { - let _ = Box::from_raw(std::ptr::slice_from_raw_parts_mut( - info.public_key, - info.public_key_len, - )); - } - } -} - -/// Free an array of FFIAddressInfo structures -/// -/// # Safety -/// -/// - `infos` must be a valid pointer to an array of FFIAddressInfo pointers allocated by this library or null -/// - `count` must be the exact number of elements in the array -/// - The pointers must not be used after calling this function -#[no_mangle] -pub unsafe extern "C" fn address_info_array_free(infos: *mut *mut FFIAddressInfo, count: usize) { - if !infos.is_null() && count > 0 { - let array = Box::from_raw(std::ptr::slice_from_raw_parts_mut(infos, count)); - for info_ptr in array.iter() { - address_info_free(*info_ptr); - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use dash_network::ffi::FFINetwork; - - #[test] - fn test_address_pool_type_values() { - assert_eq!(FFIAddressPoolType::External as u32, 0); - assert_eq!(FFIAddressPoolType::Internal as u32, 1); - assert_eq!(FFIAddressPoolType::Single as u32, 2); - } - - #[test] - fn test_address_info_conversion() { - // Test the FFI conversion function with a mock AddressInfo - use key_wallet::bip32::DerivationPath; - use std::str::FromStr; - - // Create a test address programmatically - use dashcore::PublicKey; - - // Use a valid compressed public key (this is a well-known test key) - let pubkey_bytes = [ - 0x02, // Compressed pubkey prefix - 0x50, 0x86, 0x3a, 0xd6, 0x4a, 0x87, 0xae, 0x8a, 0x2f, 0xe8, 0x3c, 0x1a, 0xf1, 0xa8, - 0x40, 0x3c, 0xb5, 0x3f, 0x53, 0xe4, 0x86, 0xd8, 0x51, 0x1d, 0xad, 0x8a, 0x04, 0x88, - 0x7e, 0x5b, 0x23, 0x52, - ]; - let pubkey = PublicKey::from_slice(&pubkey_bytes).unwrap(); - let test_address = dashcore::Address::p2pkh(&pubkey, key_wallet::Network::Testnet); - - let test_path = DerivationPath::from_str("m/44'/5'/0'/0/0").unwrap(); - - let info = AddressInfo { - address: test_address.clone(), - script_pubkey: test_address.script_pubkey(), - public_key: Some(PublicKeyType::ECDSA(vec![0x02, 0x03, 0x04])), - index: 0, - path: test_path, - used: false, - generated_at: 1234567890, - used_at: None, - tx_count: 0, - total_received: 0, - total_sent: 0, - balance: 0, - label: Some("Test Label".to_string()), - metadata: std::collections::BTreeMap::new(), - }; - - // Convert to FFI - let ffi_info = address_info_to_ffi(&info); - - // Verify basic fields - assert_eq!(ffi_info.index, 0); - assert!(!ffi_info.used); - assert_eq!(ffi_info.generated_at, 1234567890); - assert_eq!(ffi_info.used_at, 0); - assert_eq!(ffi_info.public_key_len, 3); - assert!(ffi_info.script_pubkey_len > 0); - - // Clean up the FFI structure - unsafe { - let boxed = Box::new(ffi_info); - address_info_free(Box::into_raw(boxed)); - } - } - - #[test] - fn test_address_info_free() { - // Test that free functions handle NULL gracefully - unsafe { - address_info_free(std::ptr::null_mut()); - address_info_array_free(std::ptr::null_mut(), 0); - address_info_array_free(std::ptr::null_mut(), 10); - } - } - - #[test] - fn test_address_pool_get_address_at_index() { - // Test the simplified address_pool_get_address_at_index function - unsafe { - use crate::managed_account::{ - managed_core_account_free, managed_core_account_get_external_address_pool, - }; - use crate::wallet_manager::{ - wallet_manager_add_wallet_from_mnemonic_with_options, wallet_manager_create, - wallet_manager_free, wallet_manager_free_wallet_ids, wallet_manager_get_wallet_ids, - }; - use std::ffi::CString; - use std::ptr; - - let test_mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; - let mut error = FFIError::default(); - - // Create wallet manager - let manager = wallet_manager_create(FFINetwork::Testnet, &mut error); - assert!(!manager.is_null()); - assert_eq!(error.code, FFIErrorCode::Success); - - // Add a wallet with default accounts - let mnemonic = CString::new(test_mnemonic).unwrap(); - - let success = wallet_manager_add_wallet_from_mnemonic_with_options( - manager, - mnemonic.as_ptr(), - ptr::null(), - &mut error, - ); - assert!(success); - assert_eq!(error.code, FFIErrorCode::Success); - - // Get wallet IDs - let mut wallet_ids_out: *mut u8 = ptr::null_mut(); - let mut count_out: usize = 0; - - let success = wallet_manager_get_wallet_ids( - manager, - &mut wallet_ids_out, - &mut count_out, - &mut error, - ); - assert!(success); - assert_eq!(count_out, 1); - assert!(!wallet_ids_out.is_null()); - - // Get a standard BIP44 managed account - let result = crate::managed_account::managed_wallet_get_account( - manager, - wallet_ids_out, - 0, - FFIAccountKind::StandardBIP44, - ); - - assert!(!result.account.is_null()); - assert_eq!(result.error_code, 0); - - let account = result.account; - - // Get external address pool - let external_pool = managed_core_account_get_external_address_pool(account); - assert!(!external_pool.is_null()); - - // Test getting address at index 0 (should exist by default) - let address_info = address_pool_get_address_at_index(external_pool, 0, &mut error); - - if !address_info.is_null() { - // Verify the address info - let info = &*address_info; - assert_eq!(info.index, 0); - assert!(!info.address.is_null()); - assert!(!info.path.is_null()); - - // Clean up address info - address_info_free(address_info); - } - - // Test getting address at an out-of-bounds index - let invalid_info = address_pool_get_address_at_index(external_pool, 10000, &mut error); - assert!(invalid_info.is_null()); - assert_eq!(error.code, FFIErrorCode::NotFound); - - // Test null pool - let null_info = address_pool_get_address_at_index(ptr::null(), 0, &mut error); - assert!(null_info.is_null()); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - - // Clean up - address_pool_free(external_pool); - managed_core_account_free(account); - wallet_manager_free_wallet_ids(wallet_ids_out, count_out); - wallet_manager_free(manager); - } - } - - #[test] - fn test_address_pool_get_addresses_in_range() { - // Test the simplified address_pool_get_addresses_in_range function - unsafe { - use crate::managed_account::{ - managed_core_account_free, managed_core_account_get_external_address_pool, - }; - use crate::wallet_manager::{ - wallet_manager_add_wallet_from_mnemonic_with_options, wallet_manager_create, - wallet_manager_free, wallet_manager_free_wallet_ids, wallet_manager_get_wallet_ids, - }; - use std::ffi::CString; - use std::ptr; - - let test_mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; - let mut error = FFIError::default(); - - // Create wallet manager - let manager = wallet_manager_create(FFINetwork::Testnet, &mut error); - assert!(!manager.is_null()); - assert_eq!(error.code, FFIErrorCode::Success); - - // Add a wallet with default accounts - let mnemonic = CString::new(test_mnemonic).unwrap(); - - let success = wallet_manager_add_wallet_from_mnemonic_with_options( - manager, - mnemonic.as_ptr(), - ptr::null(), - &mut error, - ); - assert!(success); - assert_eq!(error.code, FFIErrorCode::Success); - - // Get wallet IDs - let mut wallet_ids_out: *mut u8 = ptr::null_mut(); - let mut count_out: usize = 0; - - let success = wallet_manager_get_wallet_ids( - manager, - &mut wallet_ids_out, - &mut count_out, - &mut error, - ); - assert!(success); - assert_eq!(count_out, 1); - assert!(!wallet_ids_out.is_null()); - - // Get a standard BIP44 managed account - let result = crate::managed_account::managed_wallet_get_account( - manager, - wallet_ids_out, - 0, - FFIAccountKind::StandardBIP44, - ); - - assert!(!result.account.is_null()); - assert_eq!(result.error_code, 0); - - let account = result.account; - - // Get external address pool - let external_pool = managed_core_account_get_external_address_pool(account); - assert!(!external_pool.is_null()); - - // Test getting a range of addresses - let mut addresses_count: usize = 0; - let addresses = address_pool_get_addresses_in_range( - external_pool, - 0, - 5, - &mut addresses_count, - &mut error, - ); - - // The pool might not have 5 addresses generated yet, but should have at least 1 - if !addresses.is_null() && addresses_count > 0 { - // Verify we got some addresses - assert!(addresses_count <= 5); - assert_eq!(error.code, FFIErrorCode::Success); - - // Clean up addresses - address_info_array_free(addresses, addresses_count); - } - - // Test invalid range (end <= start) - let invalid_addresses = address_pool_get_addresses_in_range( - external_pool, - 5, - 5, - &mut addresses_count, - &mut error, - ); - assert!(invalid_addresses.is_null()); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - - // Test null pool - let null_addresses = address_pool_get_addresses_in_range( - ptr::null(), - 0, - 5, - &mut addresses_count, - &mut error, - ); - assert!(null_addresses.is_null()); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - - // Test null count_out - let null_count_addresses = address_pool_get_addresses_in_range( - external_pool, - 0, - 5, - ptr::null_mut(), - &mut error, - ); - assert!(null_count_addresses.is_null()); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - - // Clean up - address_pool_free(external_pool); - managed_core_account_free(account); - wallet_manager_free_wallet_ids(wallet_ids_out, count_out); - wallet_manager_free(manager); - } - } -} diff --git a/key-wallet-ffi/src/address_tests.rs b/key-wallet-ffi/src/address_tests.rs deleted file mode 100644 index 1840e962a..000000000 --- a/key-wallet-ffi/src/address_tests.rs +++ /dev/null @@ -1,205 +0,0 @@ -//! Unit tests for address FFI module - -#[cfg(test)] -mod address_tests { - use crate::address::{address_array_free, address_free, address_get_type, address_validate}; - use crate::error::{FFIError, FFIErrorCode}; - use dash_network::ffi::FFINetwork; - use std::ffi::CString; - use std::ptr; - - #[test] - fn test_address_validation() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - // Test valid testnet address (generated from test mnemonic) - let valid_addr = CString::new("yRd4FhXfVGHXpsuZXPNkMrfD9GVj46pnjt").unwrap(); - let is_valid = unsafe { address_validate(valid_addr.as_ptr(), FFINetwork::Testnet, error) }; - assert!(is_valid); - - // Test invalid address - let invalid_addr = CString::new("invalid_address").unwrap(); - let is_valid = - unsafe { address_validate(invalid_addr.as_ptr(), FFINetwork::Testnet, error) }; - assert!(!is_valid); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidAddress); - - // Test null address - let is_valid = unsafe { address_validate(ptr::null(), FFINetwork::Testnet, error) }; - assert!(!is_valid); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); - } - - #[test] - fn test_address_get_type() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - // Test P2PKH address (generated from test mnemonic) - let p2pkh_addr = CString::new("yRd4FhXfVGHXpsuZXPNkMrfD9GVj46pnjt").unwrap(); - let addr_type = - unsafe { address_get_type(p2pkh_addr.as_ptr(), FFINetwork::Testnet, error) }; - assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); - // Returns 0 for P2PKH - assert_eq!(addr_type, 0); - } - - #[test] - fn test_address_validate_valid() { - let mut error = FFIError::default(); - - // Test with valid testnet address - may fail due to library version differences - let addr_str = CString::new("yeRZBWYfeNE4yVUHV4ZLs83Ppn9aMRH57A").unwrap(); - let is_valid = - unsafe { address_validate(addr_str.as_ptr(), FFINetwork::Testnet, &mut error) }; - - assert!(is_valid); - } - - #[test] - fn test_address_validate_invalid() { - let mut error = FFIError::default(); - - // Test with invalid address - let addr_str = CString::new("invalid_address").unwrap(); - let is_valid = - unsafe { address_validate(addr_str.as_ptr(), FFINetwork::Testnet, &mut error) }; - - assert!(!is_valid); - assert_eq!(error.code, FFIErrorCode::InvalidAddress); - } - - #[test] - fn test_address_validate_null() { - let mut error = FFIError::default(); - - let is_valid = unsafe { address_validate(ptr::null(), FFINetwork::Testnet, &mut error) }; - - assert!(!is_valid); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - } - - #[test] - fn test_address_get_type_valid() { - let mut error = FFIError::default(); - - // Test P2PKH address type (use same known-valid address from other tests) - let addr_str = CString::new("yRd4FhXfVGHXpsuZXPNkMrfD9GVj46pnjt").unwrap(); - let addr_type = - unsafe { address_get_type(addr_str.as_ptr(), FFINetwork::Testnet, &mut error) }; - - // Type should be 0, 1, or 2 for valid addresses - // If it's invalid (255), the address might not be valid for testnet - if addr_type == 255 { - assert_eq!(error.code, FFIErrorCode::InvalidAddress); - } else { - assert!(addr_type <= 2); - assert_eq!(error.code, FFIErrorCode::Success); - } - } - - #[test] - fn test_address_get_type_invalid() { - let mut error = FFIError::default(); - - let addr_str = CString::new("invalid_address").unwrap(); - let addr_type = - unsafe { address_get_type(addr_str.as_ptr(), FFINetwork::Testnet, &mut error) }; - - // Should return 255 (u8::MAX) for invalid - assert_eq!(addr_type, 255); - assert_eq!(error.code, FFIErrorCode::InvalidAddress); - } - - #[test] - fn test_address_get_type_null() { - let mut error = FFIError::default(); - - let addr_type = unsafe { address_get_type(ptr::null(), FFINetwork::Testnet, &mut error) }; - - // Should return 255 (u8::MAX) for null input - assert_eq!(addr_type, 255); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - } - - #[test] - fn test_address_free_null() { - // Should handle null gracefully - unsafe { - address_free(ptr::null_mut()); - } - } - - #[test] - fn test_address_array_free() { - // Create some test addresses - let mut addresses = Vec::new(); - for i in 0..3 { - let addr = CString::new(format!("yAddress{}", i)).unwrap(); - addresses.push(addr.into_raw()); - } - - let addrs_ptr = addresses.as_mut_ptr(); - let count = addresses.len(); - std::mem::forget(addresses); - - // Free the addresses - unsafe { - address_array_free(addrs_ptr, count); - } - } - - #[test] - fn test_address_array_free_null() { - // Should handle null gracefully - unsafe { - address_array_free(ptr::null_mut(), 0); - } - } - - #[test] - fn test_address_validation_comprehensive() { - let mut error = FFIError::default(); - - // Test various invalid address formats - let invalid_addresses = [ - "invalid", - "", - "1234567890", - "yXdxAYfK7KGx7gNpVHUfRsQMNpMj5cAadGtoolong", - "zXdxAYfK7KGx7gNpVHUfRsQMNpMj5cAadG", // wrong network prefix - ]; - - unsafe { - for invalid_addr in invalid_addresses.iter() { - let addr_str = CString::new(*invalid_addr).unwrap(); - let is_valid = address_validate(addr_str.as_ptr(), FFINetwork::Testnet, &mut error); - assert!(!is_valid); - } - } - } - - #[test] - fn test_address_get_type_comprehensive() { - let mut error = FFIError::default(); - - // Test various address formats - let test_addresses = [ - "yXdxAYfK7KGx7gNpVHUfRsQMNpMj5cAadG", // potential P2PKH - "8oAH2jGDaJVFBJNUj3QHYNLGgtNfaXcNP7", // potential P2SH - "invalid_address", - ]; - - unsafe { - for addr in test_addresses.iter() { - let addr_str = CString::new(*addr).unwrap(); - let addr_type = - address_get_type(addr_str.as_ptr(), FFINetwork::Testnet, &mut error); - - // Should return a valid type (0, 1, 2) or 255 for error - assert!(addr_type <= 2 || addr_type == 255); - } - } - } -} diff --git a/key-wallet-ffi/src/bip38.rs b/key-wallet-ffi/src/bip38.rs deleted file mode 100644 index 3f95c88cd..000000000 --- a/key-wallet-ffi/src/bip38.rs +++ /dev/null @@ -1,42 +0,0 @@ -//! BIP38 encryption support - -use std::os::raw::c_char; -use std::ptr; - -use crate::error::{FFIError, FFIErrorCode}; - -/// Encrypt a private key with BIP38 -/// -/// # Safety -/// -/// This function is unsafe because it dereferences raw pointers: -/// - `private_key` must be a valid, null-terminated C string -/// - `passphrase` must be a valid, null-terminated C string -/// - `error` must be a valid pointer to an FFIError -#[no_mangle] -pub unsafe extern "C" fn bip38_encrypt_private_key( - _private_key: *const c_char, - _passphrase: *const c_char, - error: *mut FFIError, -) -> *mut c_char { - (*error).set(FFIErrorCode::InternalError, "BIP38 encryption not yet implemented"); - ptr::null_mut() -} - -/// Decrypt a BIP38 encrypted private key -/// -/// # Safety -/// -/// This function is unsafe because it dereferences raw pointers: -/// - `encrypted_key` must be a valid, null-terminated C string -/// - `passphrase` must be a valid, null-terminated C string -/// - `error` must be a valid pointer to an FFIError -#[no_mangle] -pub unsafe extern "C" fn bip38_decrypt_private_key( - _encrypted_key: *const c_char, - _passphrase: *const c_char, - error: *mut FFIError, -) -> *mut c_char { - (*error).set(FFIErrorCode::InternalError, "BIP38 decryption not yet implemented"); - ptr::null_mut() -} diff --git a/key-wallet-ffi/src/derivation.rs b/key-wallet-ffi/src/derivation.rs deleted file mode 100644 index db0dd8b86..000000000 --- a/key-wallet-ffi/src/derivation.rs +++ /dev/null @@ -1,625 +0,0 @@ -//! BIP32 and DIP9 derivation path functions - -use crate::error::{FFIError, FFIErrorCode}; -use crate::keys::FFIExtendedPrivKey; -use crate::keys::FFIExtendedPubKey; -use crate::{check_ptr, deref_ptr, unwrap_or_return}; -use dash_network::ffi::FFINetwork; -use dashcore::Network; -use key_wallet::{ExtendedPrivKey, ExtendedPubKey}; -use secp256k1::Secp256k1; -use std::ffi::{CStr, CString}; -use std::os::raw::{c_char, c_uint}; -use std::ptr; -use std::slice; - -/// Derivation path type for DIP9 -#[repr(C)] -#[derive(Clone, Copy)] -pub enum FFIDerivationPathType { - PathUnknown = 0, - PathBIP32 = 1, - PathBIP44 = 2, - PathBlockchainIdentities = 3, - PathProviderFunds = 4, - PathProviderVotingKeys = 5, - PathProviderOperatorKeys = 6, - PathProviderOwnerKeys = 7, - PathContactBasedFunds = 8, - PathContactBasedFundsRoot = 9, - PathContactBasedFundsExternal = 10, - PathBlockchainIdentityCreditRegistrationFunding = 11, - PathBlockchainIdentityCreditTopupFunding = 12, - PathBlockchainIdentityCreditInvitationFunding = 13, - PathProviderPlatformNodeKeys = 14, - PathCoinJoin = 15, - PathRoot = 255, -} - -/// Create a new master extended private key from seed -/// -/// # Safety -/// -/// - `seed` must be a valid pointer to a byte array of `seed_len` length -/// - `error` must be a valid pointer to an FFIError structure -/// - The caller must ensure the seed pointer remains valid for the duration of this call -#[no_mangle] -pub unsafe extern "C" fn derivation_new_master_key( - seed: *const u8, - seed_len: usize, - network: FFINetwork, - error: *mut FFIError, -) -> *mut FFIExtendedPrivKey { - check_ptr!(seed, error); - let seed_slice = slice::from_raw_parts(seed, seed_len); - let network_rust: key_wallet::Network = network.into(); - let xpriv = unwrap_or_return!( - key_wallet::bip32::ExtendedPrivKey::new_master(network_rust, seed_slice), - error - ); - Box::into_raw(Box::new(FFIExtendedPrivKey::from_inner(xpriv))) -} - -/// Derive a BIP44 account path (m/44'/5'/account') -/// -/// # Safety -/// -/// `path_out` must point to a writable buffer of at least `path_max_len` bytes -/// and `error` must be a valid pointer to an `FFIError`. -#[no_mangle] -pub unsafe extern "C" fn derivation_bip44_account_path( - network: FFINetwork, - account_index: c_uint, - path_out: *mut c_char, - path_max_len: usize, - error: *mut FFIError, -) -> bool { - check_ptr!(path_out, error); - - let network_rust: key_wallet::Network = network.into(); - - use key_wallet::bip32::DerivationPath; - let derivation = DerivationPath::bip_44_account(network_rust, account_index); - - let path_str = format!("{}", derivation); - - let c_string = unwrap_or_return!(CString::new(path_str), error); - - let bytes = c_string.as_bytes_with_nul(); - if bytes.len() > path_max_len { - (*error).set( - FFIErrorCode::InvalidInput, - &format!("Path too long: {} > {}", bytes.len(), path_max_len), - ); - return false; - } - - unsafe { - ptr::copy_nonoverlapping(bytes.as_ptr(), path_out.cast::(), bytes.len()); - } - true -} - -/// Derive a BIP44 payment path (m/44'/5'/account'/change/index) -/// -/// # Safety -/// -/// `path_out` must point to a writable buffer of at least `path_max_len` bytes -/// and `error` must be a valid pointer to an `FFIError`. -#[no_mangle] -pub unsafe extern "C" fn derivation_bip44_payment_path( - network: FFINetwork, - account_index: c_uint, - is_change: bool, - address_index: c_uint, - path_out: *mut c_char, - path_max_len: usize, - error: *mut FFIError, -) -> bool { - check_ptr!(path_out, error); - - let network_rust: key_wallet::Network = network.into(); - - use key_wallet::bip32::DerivationPath; - let derivation = - DerivationPath::bip_44_payment_path(network_rust, account_index, is_change, address_index); - - let path_str = format!("{}", derivation); - - let c_string = unwrap_or_return!(CString::new(path_str), error); - - let bytes = c_string.as_bytes_with_nul(); - if bytes.len() > path_max_len { - (*error).set( - FFIErrorCode::InvalidInput, - &format!("Path too long: {} > {}", bytes.len(), path_max_len), - ); - return false; - } - - unsafe { - ptr::copy_nonoverlapping(bytes.as_ptr(), path_out.cast::(), bytes.len()); - } - true -} - -/// Derive CoinJoin path (m/9'/5'/4'/account') -/// -/// # Safety -/// -/// `path_out` must point to a writable buffer of at least `path_max_len` bytes -/// and `error` must be a valid pointer to an `FFIError`. -#[no_mangle] -pub unsafe extern "C" fn derivation_coinjoin_path( - network: FFINetwork, - account_index: c_uint, - path_out: *mut c_char, - path_max_len: usize, - error: *mut FFIError, -) -> bool { - check_ptr!(path_out, error); - - let network_rust: key_wallet::Network = network.into(); - - use key_wallet::bip32::DerivationPath; - let derivation = DerivationPath::coinjoin_path(network_rust, account_index); - - let path_str = format!("{}", derivation); - - let c_string = unwrap_or_return!(CString::new(path_str), error); - - let bytes = c_string.as_bytes_with_nul(); - if bytes.len() > path_max_len { - (*error).set( - FFIErrorCode::InvalidInput, - &format!("Path too long: {} > {}", bytes.len(), path_max_len), - ); - return false; - } - - unsafe { - ptr::copy_nonoverlapping(bytes.as_ptr(), path_out.cast::(), bytes.len()); - } - true -} - -/// Derive identity registration path (m/9'/5'/5'/1'/index') -/// -/// # Safety -/// -/// `path_out` must point to a writable buffer of at least `path_max_len` bytes -/// and `error` must be a valid pointer to an `FFIError`. -#[no_mangle] -pub unsafe extern "C" fn derivation_identity_registration_path( - network: FFINetwork, - identity_index: c_uint, - path_out: *mut c_char, - path_max_len: usize, - error: *mut FFIError, -) -> bool { - check_ptr!(path_out, error); - - let network_rust: key_wallet::Network = network.into(); - - use key_wallet::bip32::DerivationPath; - let derivation = DerivationPath::identity_registration_path(network_rust, identity_index); - - let path_str = format!("{}", derivation); - - let c_string = unwrap_or_return!(CString::new(path_str), error); - - let bytes = c_string.as_bytes_with_nul(); - if bytes.len() > path_max_len { - (*error).set( - FFIErrorCode::InvalidInput, - &format!("Path too long: {} > {}", bytes.len(), path_max_len), - ); - return false; - } - - unsafe { - ptr::copy_nonoverlapping(bytes.as_ptr(), path_out.cast::(), bytes.len()); - } - true -} - -/// Derive identity top-up path (m/9'/5'/5'/2'/identity_index'/top_up_index') -/// -/// # Safety -/// -/// `path_out` must point to a writable buffer of at least `path_max_len` bytes -/// and `error` must be a valid pointer to an `FFIError`. -#[no_mangle] -pub unsafe extern "C" fn derivation_identity_topup_path( - network: FFINetwork, - identity_index: c_uint, - topup_index: c_uint, - path_out: *mut c_char, - path_max_len: usize, - error: *mut FFIError, -) -> bool { - check_ptr!(path_out, error); - - let network_rust: key_wallet::Network = network.into(); - - use key_wallet::bip32::DerivationPath; - let derivation = - DerivationPath::identity_top_up_path(network_rust, identity_index, topup_index); - - let path_str = format!("{}", derivation); - - let c_string = unwrap_or_return!(CString::new(path_str), error); - - let bytes = c_string.as_bytes_with_nul(); - if bytes.len() > path_max_len { - (*error).set( - FFIErrorCode::InvalidInput, - &format!("Path too long: {} > {}", bytes.len(), path_max_len), - ); - return false; - } - - unsafe { - ptr::copy_nonoverlapping(bytes.as_ptr(), path_out.cast::(), bytes.len()); - } - true -} - -/// Derive identity authentication path (m/9'/5'/5'/0'/identity_index'/key_index') -/// -/// # Safety -/// -/// `path_out` must point to a writable buffer of at least `path_max_len` bytes -/// and `error` must be a valid pointer to an `FFIError`. -#[no_mangle] -pub unsafe extern "C" fn derivation_identity_authentication_path( - network: FFINetwork, - identity_index: c_uint, - key_index: c_uint, - path_out: *mut c_char, - path_max_len: usize, - error: *mut FFIError, -) -> bool { - check_ptr!(path_out, error); - - let network_rust: key_wallet::Network = network.into(); - - use key_wallet::bip32::{DerivationPath, KeyDerivationType}; - let derivation = DerivationPath::identity_authentication_path( - network_rust, - KeyDerivationType::ECDSA, // Using ECDSA for authentication keys - identity_index, - key_index, - ); - - let path_str = format!("{}", derivation); - - let c_string = unwrap_or_return!(CString::new(path_str), error); - - let bytes = c_string.as_bytes_with_nul(); - if bytes.len() > path_max_len { - (*error).set( - FFIErrorCode::InvalidInput, - &format!("Path too long: {} > {}", bytes.len(), path_max_len), - ); - return false; - } - - unsafe { - ptr::copy_nonoverlapping(bytes.as_ptr(), path_out.cast::(), bytes.len()); - } - true -} - -/// Derive private key for a specific path from seed -/// -/// # Safety -/// -/// - `seed` must be a valid pointer to a byte array of `seed_len` length -/// - `path` must be a valid pointer to a null-terminated C string -/// - `error` must be a valid pointer to an FFIError structure -/// - The caller must ensure all pointers remain valid for the duration of this call -#[no_mangle] -pub unsafe extern "C" fn derivation_derive_private_key_from_seed( - seed: *const u8, - seed_len: usize, - path: *const c_char, - network: FFINetwork, - error: *mut FFIError, -) -> *mut FFIExtendedPrivKey { - use key_wallet::bip32::{DerivationPath, ExtendedPrivKey}; - use std::str::FromStr; - - check_ptr!(seed, error); - let path = deref_ptr!(path, error); - - let seed_slice = slice::from_raw_parts(seed, seed_len); - let network_rust: Network = network.into(); - let path_str = unwrap_or_return!(CStr::from_ptr(path).to_str(), error); - let derivation_path = unwrap_or_return!(DerivationPath::from_str(path_str), error); - - let secp = Secp256k1::new(); - let master = unwrap_or_return!(ExtendedPrivKey::new_master(network_rust, seed_slice), error); - let xpriv = unwrap_or_return!(master.derive_priv(&secp, &derivation_path), error); - Box::into_raw(Box::new(FFIExtendedPrivKey::from_inner(xpriv))) -} - -/// Derive public key from extended private key -/// -/// # Safety -/// -/// - `xpriv` must be a valid pointer to an FFIExtendedPrivKey -/// - `error` must be a valid pointer to an FFIError -/// - The returned pointer must be freed with `extended_public_key_free` -#[no_mangle] -pub unsafe extern "C" fn derivation_xpriv_to_xpub( - xpriv: *const FFIExtendedPrivKey, - error: *mut FFIError, -) -> *mut FFIExtendedPubKey { - use key_wallet::bip32::ExtendedPubKey; - let xpriv = deref_ptr!(xpriv, error); - let secp = Secp256k1::new(); - let xpub = ExtendedPubKey::from_priv(&secp, xpriv.inner()); - Box::into_raw(Box::new(FFIExtendedPubKey::from_inner(xpub))) -} - -/// Get extended private key as string -/// -/// # Safety -/// -/// - `xpriv` must be a valid pointer to an FFIExtendedPrivKey -/// - `error` must be a valid pointer to an FFIError -/// - The returned string must be freed with `string_free` -#[no_mangle] -pub unsafe extern "C" fn derivation_xpriv_to_string( - xpriv: *const FFIExtendedPrivKey, - error: *mut FFIError, -) -> *mut c_char { - let xpriv = deref_ptr!(xpriv, error); - unwrap_or_return!(CString::new(xpriv.inner().to_string()), error).into_raw() -} - -/// Get extended public key as string -/// -/// # Safety -/// -/// - `xpub` must be a valid pointer to an FFIExtendedPubKey -/// - `error` must be a valid pointer to an FFIError -/// - The returned string must be freed with `string_free` -#[no_mangle] -pub unsafe extern "C" fn derivation_xpub_to_string( - xpub: *const FFIExtendedPubKey, - error: *mut FFIError, -) -> *mut c_char { - let xpub = deref_ptr!(xpub, error); - unwrap_or_return!(CString::new(xpub.inner().to_string()), error).into_raw() -} - -/// Get fingerprint from extended public key (4 bytes) -/// -/// # Safety -/// -/// - `xpub` must be a valid pointer to an FFIExtendedPubKey -/// - `fingerprint_out` must be a valid pointer to a buffer of at least 4 bytes -/// - `error` must be a valid pointer to an FFIError -#[no_mangle] -pub unsafe extern "C" fn derivation_xpub_fingerprint( - xpub: *const FFIExtendedPubKey, - fingerprint_out: *mut u8, - error: *mut FFIError, -) -> bool { - let xpub = deref_ptr!(xpub, error); - check_ptr!(fingerprint_out, error); - let fingerprint = xpub.inner().fingerprint(); - let bytes = fingerprint.to_bytes(); - ptr::copy_nonoverlapping(bytes.as_ptr(), fingerprint_out, 4); - true -} - -/// Free extended private key -/// -/// # Safety -/// -/// - `xpriv` must be a valid pointer to an FFIExtendedPrivKey that was allocated by this library -/// - The pointer must not be used after calling this function -/// - This function must only be called once per allocation -#[no_mangle] -pub unsafe extern "C" fn derivation_xpriv_free(xpriv: *mut FFIExtendedPrivKey) { - if !xpriv.is_null() { - let _ = Box::from_raw(xpriv); - } -} - -/// Free extended public key -/// -/// # Safety -/// -/// - `xpub` must be a valid pointer to an FFIExtendedPubKey that was allocated by this library -/// - The pointer must not be used after calling this function -/// - This function must only be called once per allocation -#[no_mangle] -pub unsafe extern "C" fn derivation_xpub_free(xpub: *mut FFIExtendedPubKey) { - if !xpub.is_null() { - let _ = Box::from_raw(xpub); - } -} - -/// Free derivation path string -/// -/// # Safety -/// -/// - `s` must be a valid pointer to a C string that was allocated by this library -/// - The pointer must not be used after calling this function -/// - This function must only be called once per allocation -#[no_mangle] -pub unsafe extern "C" fn derivation_string_free(s: *mut c_char) { - if !s.is_null() { - let _ = CString::from_raw(s); - } -} - -// MARK: - Simplified Derivation Functions - -/// Derive an address from a private key -/// -/// # Safety -/// - `private_key` must be a valid pointer to 32 bytes -/// - `network` is the network for the address -/// -/// # Returns -/// - Pointer to C string with address (caller must free) -/// - NULL on error -#[no_mangle] -pub unsafe extern "C" fn key_wallet_derive_address_from_key( - private_key: *const u8, - network: FFINetwork, -) -> *mut c_char { - if private_key.is_null() { - return ptr::null_mut(); - } - - let key_slice = slice::from_raw_parts(private_key, 32); - - // Create a secp256k1 private key - let secp = Secp256k1::new(); - let secret_key = match secp256k1::SecretKey::from_slice(key_slice) { - Ok(sk) => sk, - Err(_) => return ptr::null_mut(), - }; - - // Get public key - let public_key = secp256k1::PublicKey::from_secret_key(&secp, &secret_key); - - // Convert to dashcore PublicKey - let dash_pubkey = dashcore::PublicKey::new(public_key); - - // Convert to Dash address - let dash_network: key_wallet::Network = network.into(); - let address = key_wallet::Address::p2pkh(&dash_pubkey, dash_network); - - match CString::new(address.to_string()) { - Ok(c_str) => c_str.into_raw(), - Err(_) => ptr::null_mut(), - } -} - -/// Derive an address from a seed at a specific derivation path -/// -/// # Safety -/// - `seed` must be a valid pointer to 64 bytes -/// - `network` is the network for the address -/// - `path` must be a valid null-terminated C string (e.g., "m/44'/5'/0'/0/0") -/// -/// # Returns -/// - Pointer to C string with address (caller must free) -/// - NULL on error -#[no_mangle] -pub unsafe extern "C" fn key_wallet_derive_address_from_seed( - seed: *const u8, - network: FFINetwork, - path: *const c_char, -) -> *mut c_char { - if seed.is_null() || path.is_null() { - return ptr::null_mut(); - } - - let seed_slice = slice::from_raw_parts(seed, 64); - let dash_network: key_wallet::Network = network.into(); - - // Parse derivation path - let path_str = match CStr::from_ptr(path).to_str() { - Ok(s) => s, - Err(_) => return ptr::null_mut(), - }; - - use std::str::FromStr; - let derivation_path = match key_wallet::DerivationPath::from_str(path_str) { - Ok(dp) => dp, - Err(_) => return ptr::null_mut(), - }; - - // Create master key from seed - let master_key = match ExtendedPrivKey::new_master(dash_network, seed_slice) { - Ok(xprv) => xprv, - Err(_) => return ptr::null_mut(), - }; - - // Derive at path - let secp = Secp256k1::new(); - let derived_key = match master_key.derive_priv(&secp, &derivation_path) { - Ok(xprv) => xprv, - Err(_) => return ptr::null_mut(), - }; - - // Get public key - let extended_pubkey = ExtendedPubKey::from_priv(&secp, &derived_key); - - // Convert secp256k1::PublicKey to dashcore::PublicKey - let dash_pubkey = dashcore::PublicKey::new(extended_pubkey.public_key); - - // Convert to address - let address = key_wallet::Address::p2pkh(&dash_pubkey, dash_network); - - match CString::new(address.to_string()) { - Ok(c_str) => c_str.into_raw(), - Err(_) => ptr::null_mut(), - } -} - -/// Derive a private key from a seed at a specific derivation path -/// -/// # Safety -/// - `seed` must be a valid pointer to 64 bytes -/// - `path` must be a valid null-terminated C string (e.g., "m/44'/5'/0'/0/0") -/// - `key_out` must be a valid pointer to a buffer of at least 32 bytes -/// -/// # Returns -/// - 0 on success -/// - -1 on error -#[no_mangle] -pub unsafe extern "C" fn key_wallet_derive_private_key_from_seed( - seed: *const u8, - path: *const c_char, - key_out: *mut u8, -) -> i32 { - if seed.is_null() || path.is_null() || key_out.is_null() { - return -1; - } - - let seed_slice = slice::from_raw_parts(seed, 64); - - // Parse derivation path - let path_str = match CStr::from_ptr(path).to_str() { - Ok(s) => s, - Err(_) => return -1, - }; - - use std::str::FromStr; - let derivation_path = match key_wallet::DerivationPath::from_str(path_str) { - Ok(dp) => dp, - Err(_) => return -1, - }; - - // Create master key from seed (use testnet as default, doesn't affect key derivation) - let master_key = match ExtendedPrivKey::new_master(key_wallet::Network::Testnet, seed_slice) { - Ok(xprv) => xprv, - Err(_) => return -1, - }; - - // Derive at path - let secp = Secp256k1::new(); - let derived_key = match master_key.derive_priv(&secp, &derivation_path) { - Ok(xprv) => xprv, - Err(_) => return -1, - }; - - // Copy private key bytes - let key_bytes = derived_key.private_key.secret_bytes(); - ptr::copy_nonoverlapping(key_bytes.as_ptr(), key_out, 32); - - 0 -} - -#[cfg(test)] -#[path = "derivation_tests.rs"] -mod tests; diff --git a/key-wallet-ffi/src/derivation_tests.rs b/key-wallet-ffi/src/derivation_tests.rs deleted file mode 100644 index c7e43a633..000000000 --- a/key-wallet-ffi/src/derivation_tests.rs +++ /dev/null @@ -1,976 +0,0 @@ -//! Tests for derivation path FFI functions - -#[cfg(test)] -#[allow(clippy::module_inception)] -mod tests { - use crate::derivation::*; - use crate::error::{FFIError, FFIErrorCode}; - use crate::mnemonic; - use std::ffi::{CStr, CString}; - use std::os::raw::c_char; - use std::ptr; - - #[test] - fn test_master_key_from_seed() { - let mut error = FFIError::default(); - - // Generate a seed from mnemonic - let mnemonic = CString::new("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").unwrap(); - let passphrase = CString::new("").unwrap(); - let mut seed = [0u8; 64]; - let mut seed_len = seed.len(); - - let success = unsafe { - mnemonic::mnemonic_to_seed( - mnemonic.as_ptr(), - passphrase.as_ptr(), - seed.as_mut_ptr(), - &mut seed_len, - &mut error, - ) - }; - assert!(success); - assert_eq!(seed_len, 64); - - // Create master key from seed - let xprv = unsafe { - derivation_new_master_key(seed.as_ptr(), seed.len(), FFINetwork::Testnet, &mut error) - }; - - assert!(!xprv.is_null()); - - // Clean up - unsafe { - derivation_xpriv_free(xprv); - } - } - - #[test] - fn test_xpriv_to_xpub() { - let mut error = FFIError::default(); - - // Create master key - let mut seed = [0u8; 64]; - for (i, byte) in seed.iter_mut().enumerate() { - *byte = (i % 256) as u8; - } - - let xprv = unsafe { - derivation_new_master_key(seed.as_ptr(), seed.len(), FFINetwork::Testnet, &mut error) - }; - - // Get public key - let xpub = unsafe { derivation_xpriv_to_xpub(xprv, &mut error) }; - - assert!(!xpub.is_null()); - - // Clean up - unsafe { - derivation_xpub_free(xpub); - derivation_xpriv_free(xprv); - } - } - - #[test] - fn test_xpriv_to_string() { - let mut error = FFIError::default(); - - // Create master key - let mut seed = [0u8; 64]; - for (i, byte) in seed.iter_mut().enumerate() { - *byte = (i % 256) as u8; - } - - let xprv = unsafe { - derivation_new_master_key(seed.as_ptr(), seed.len(), FFINetwork::Testnet, &mut error) - }; - - // Convert to string - let xprv_str = unsafe { derivation_xpriv_to_string(xprv, &mut error) }; - assert!(!xprv_str.is_null()); - - let str_val = unsafe { CStr::from_ptr(xprv_str).to_str().unwrap() }; - assert!(str_val.starts_with("tprv")); // Testnet private key - - // Clean up - unsafe { - derivation_string_free(xprv_str); - derivation_xpriv_free(xprv); - } - } - - #[test] - fn test_xpub_to_string() { - let mut error = FFIError::default(); - - // Create master key and get public key - let mut seed = [0u8; 64]; - for (i, byte) in seed.iter_mut().enumerate() { - *byte = (i % 256) as u8; - } - - let xprv = unsafe { - derivation_new_master_key(seed.as_ptr(), seed.len(), FFINetwork::Testnet, &mut error) - }; - - let xpub = unsafe { derivation_xpriv_to_xpub(xprv, &mut error) }; - - // Convert to string - let xpub_str = unsafe { derivation_xpub_to_string(xpub, &mut error) }; - assert!(!xpub_str.is_null()); - - let str_val = unsafe { CStr::from_ptr(xpub_str).to_str().unwrap() }; - assert!(str_val.starts_with("tpub")); // Testnet public key - - // Clean up - unsafe { - derivation_string_free(xpub_str); - derivation_xpub_free(xpub); - derivation_xpriv_free(xprv); - } - } - - #[test] - fn test_xpub_fingerprint() { - let mut error = FFIError::default(); - - // Create master key - let mut seed = [0u8; 64]; - for (i, byte) in seed.iter_mut().enumerate() { - *byte = (i % 256) as u8; - } - - let xprv = unsafe { - derivation_new_master_key(seed.as_ptr(), seed.len(), FFINetwork::Testnet, &mut error) - }; - - let xpub = unsafe { derivation_xpriv_to_xpub(xprv, &mut error) }; - - // Get fingerprint - let mut fingerprint = [0u8; 4]; - let success = - unsafe { derivation_xpub_fingerprint(xpub, fingerprint.as_mut_ptr(), &mut error) }; - - assert!(success); - // Fingerprint should not be all zeros - assert!(fingerprint.iter().any(|&b| b != 0)); - - // Clean up - unsafe { - derivation_xpub_free(xpub); - derivation_xpriv_free(xprv); - } - } - - #[test] - fn test_bip44_paths() { - let mut error = FFIError::default(); - - // Test BIP44 account path - let mut account_path = vec![0u8; 256]; - let success = unsafe { - derivation_bip44_account_path( - FFINetwork::Testnet, - 0, - account_path.as_mut_ptr() as *mut c_char, - account_path.len(), - &mut error, - ) - }; - assert!(success); - - let path_str = - unsafe { CStr::from_ptr(account_path.as_ptr() as *const c_char) }.to_str().unwrap(); - assert_eq!(path_str, "m/44'/1'/0'"); - - // Test BIP44 payment path - let mut payment_path = vec![0u8; 256]; - let success = unsafe { - derivation_bip44_payment_path( - FFINetwork::Testnet, - 0, // account_index - false, // is_change - 0, // address_index - payment_path.as_mut_ptr() as *mut c_char, - payment_path.len(), - &mut error, - ) - }; - assert!(success); - - let path_str = - unsafe { CStr::from_ptr(payment_path.as_ptr() as *const c_char) }.to_str().unwrap(); - assert_eq!(path_str, "m/44'/1'/0'/0/0"); - } - - #[test] - fn test_special_paths() { - let mut error = FFIError::default(); - - // Test CoinJoin path - let mut coinjoin_path = vec![0u8; 256]; - let success = unsafe { - derivation_coinjoin_path( - FFINetwork::Testnet, - 0, // account_index - coinjoin_path.as_mut_ptr() as *mut c_char, - coinjoin_path.len(), - &mut error, - ) - }; - assert!(success); - - // Test identity registration path - takes 2 args: network and identity_index - let mut id_reg_path = vec![0u8; 256]; - let success = unsafe { - derivation_identity_registration_path( - FFINetwork::Testnet, - 0, // identity_index - id_reg_path.as_mut_ptr() as *mut c_char, - id_reg_path.len(), - &mut error, - ) - }; - assert!(success); - - // Test identity topup path - takes 3 args: network, identity_index, topup_index - let mut id_topup_path = vec![0u8; 256]; - let success = unsafe { - derivation_identity_topup_path( - FFINetwork::Testnet, - 0, // identity_index - 2, // topup_index - id_topup_path.as_mut_ptr() as *mut c_char, - id_topup_path.len(), - &mut error, - ) - }; - assert!(success); - - // Test identity authentication path - takes 3 args: network, identity_index, key_index - let mut id_auth_path = vec![0u8; 256]; - let success = unsafe { - derivation_identity_authentication_path( - FFINetwork::Testnet, - 0, // identity_index - 3, // key_index - id_auth_path.as_mut_ptr() as *mut c_char, - id_auth_path.len(), - &mut error, - ) - }; - assert!(success); - } - - #[test] - fn test_derive_private_key_from_seed() { - let mut error = FFIError::default(); - - // Generate a seed - let mut seed = [0u8; 64]; - for (i, byte) in seed.iter_mut().enumerate() { - *byte = (i % 256) as u8; - } - - // Create path - let path = CString::new("m/44'/1'/0'/0/0").unwrap(); - - // Derive private key - returns FFIExtendedPrivKey, not raw bytes - let xpriv = unsafe { - derivation_derive_private_key_from_seed( - seed.as_ptr(), - seed.len(), - path.as_ptr(), - FFINetwork::Testnet, - &mut error, - ) - }; - - assert!(!xpriv.is_null()); - - // Clean up - unsafe { - derivation_xpriv_free(xpriv); - } - } - - #[test] - fn test_error_handling() { - let mut error = FFIError::default(); - - // Test with null seed - let xprv = - unsafe { derivation_new_master_key(ptr::null(), 64, FFINetwork::Testnet, &mut error) }; - assert!(xprv.is_null()); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - - // Note: The BIP32 implementation actually accepts seeds as small as 16 bytes - // so we can't test for invalid seed length error here - } - - #[test] - fn test_derivation_string_to_xpub() { - let mut error = FFIError::default(); - - // Generate a master key and xpub first - let mut seed = [0u8; 64]; - for (i, byte) in seed.iter_mut().enumerate() { - *byte = (i % 256) as u8; - } - - let master_key = unsafe { - derivation_new_master_key(seed.as_ptr(), seed.len(), FFINetwork::Testnet, &mut error) - }; - - let xpub = unsafe { derivation_xpriv_to_xpub(master_key, &mut error) }; - - let xpub_string = unsafe { derivation_xpub_to_string(xpub, &mut error) }; - - assert!(!xpub_string.is_null()); - - // Clean up - unsafe { - derivation_string_free(xpub_string); - derivation_xpub_free(xpub); - derivation_xpriv_free(master_key); - } - } - - #[test] - fn test_derivation_xpriv_string_conversion() { - let mut error = FFIError::default(); - - // Generate a master key - let mut seed = [0u8; 64]; - for (i, byte) in seed.iter_mut().enumerate() { - *byte = (i % 256) as u8; - } - - let master_key = unsafe { - derivation_new_master_key(seed.as_ptr(), seed.len(), FFINetwork::Testnet, &mut error) - }; - - let xpriv_string = unsafe { derivation_xpriv_to_string(master_key, &mut error) }; - - assert!(!xpriv_string.is_null()); - - // Verify it's a valid xpriv string - let xpriv_str = unsafe { CStr::from_ptr(xpriv_string).to_str().unwrap() }; - assert!(xpriv_str.starts_with("tprv")); // Testnet private key - - // Clean up - unsafe { - derivation_string_free(xpriv_string); - derivation_xpriv_free(master_key); - } - } - - #[test] - fn test_derivation_xpub_fingerprint() { - let mut error = FFIError::default(); - - // Generate a master key and xpub - let mut seed = [0u8; 64]; - for (i, byte) in seed.iter_mut().enumerate() { - *byte = (i % 256) as u8; - } - - let master_key = unsafe { - derivation_new_master_key(seed.as_ptr(), seed.len(), FFINetwork::Testnet, &mut error) - }; - - let xpub = unsafe { derivation_xpriv_to_xpub(master_key, &mut error) }; - - let mut fingerprint_buf = [0u8; 4]; - let success = - unsafe { derivation_xpub_fingerprint(xpub, fingerprint_buf.as_mut_ptr(), &mut error) }; - - // Function should succeed - assert!(success); - assert_eq!(error.code, FFIErrorCode::Success); - - // Clean up - unsafe { - derivation_xpub_free(xpub); - derivation_xpriv_free(master_key); - } - } - - #[test] - fn test_special_derivation_paths() { - let mut error = FFIError::default(); - - // Test identity registration path - let mut buffer = vec![0u8; 256]; - let success = unsafe { - derivation_identity_registration_path( - FFINetwork::Testnet, - 0, // identity_index - buffer.as_mut_ptr() as *mut c_char, - buffer.len(), - &mut error, - ) - }; - - assert!(success); - let path_str = - unsafe { CStr::from_ptr(buffer.as_ptr() as *const c_char) }.to_str().unwrap(); - assert!(path_str.contains("m/")); - - // Test identity topup path - let mut buffer = vec![0u8; 256]; - let success = unsafe { - derivation_identity_topup_path( - FFINetwork::Testnet, - 0, // identity_index - 0, // topup_index - buffer.as_mut_ptr() as *mut c_char, - buffer.len(), - &mut error, - ) - }; - - assert!(success); - let path_str = - unsafe { CStr::from_ptr(buffer.as_ptr() as *const c_char) }.to_str().unwrap(); - assert!(path_str.contains("m/")); - - // Test identity authentication path - let mut buffer = vec![0u8; 256]; - let success = unsafe { - derivation_identity_authentication_path( - FFINetwork::Testnet, - 0, // identity_index - 0, // key_index - buffer.as_mut_ptr() as *mut c_char, - buffer.len(), - &mut error, - ) - }; - - assert!(success); - let path_str = - unsafe { CStr::from_ptr(buffer.as_ptr() as *const c_char) }.to_str().unwrap(); - assert!(path_str.contains("m/")); - } - - #[test] - fn test_free_functions_safety() { - // Test that free functions handle null pointers gracefully - unsafe { - derivation_xpub_free(ptr::null_mut()); - } - unsafe { - derivation_xpriv_free(ptr::null_mut()); - } - unsafe { - derivation_string_free(ptr::null_mut()); - } - } - - #[test] - fn test_derivation_new_master_key_edge_cases() { - let mut error = FFIError::default(); - - // Test with null seed - let xprv = - unsafe { derivation_new_master_key(ptr::null(), 64, FFINetwork::Testnet, &mut error) }; - assert!(xprv.is_null()); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - - // Test with a valid seed - let seed = [0u8; 64]; - let xprv = unsafe { - derivation_new_master_key(seed.as_ptr(), seed.len(), FFINetwork::Testnet, &mut error) - }; - if !xprv.is_null() { - unsafe { - derivation_xpriv_free(xprv); - } - } - } - - #[test] - fn test_derivation_path_functions_null_inputs() { - let mut error = FFIError::default(); - - // Test BIP44 account path with null buffer - let success = unsafe { - derivation_bip44_account_path(FFINetwork::Testnet, 0, ptr::null_mut(), 256, &mut error) - }; - assert!(!success); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - - // Test BIP44 payment path with null buffer - let success = unsafe { - derivation_bip44_payment_path( - FFINetwork::Testnet, - 0, - false, - 0, - ptr::null_mut(), - 256, - &mut error, - ) - }; - assert!(!success); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - - // Test CoinJoin path with null buffer - let success = unsafe { - derivation_coinjoin_path(FFINetwork::Testnet, 0, ptr::null_mut(), 256, &mut error) - }; - assert!(!success); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - } - - #[test] - fn test_derivation_path_functions_small_buffer() { - let mut error = FFIError::default(); - - // Test with very small buffer (should fail) - let mut small_buffer = [0u8; 5]; - let success = unsafe { - derivation_bip44_account_path( - FFINetwork::Testnet, - 0, - small_buffer.as_mut_ptr() as *mut c_char, - small_buffer.len(), - &mut error, - ) - }; - assert!(!success); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - - // Test BIP44 payment path with small buffer - let success = unsafe { - derivation_bip44_payment_path( - FFINetwork::Testnet, - 0, - false, - 0, - small_buffer.as_mut_ptr() as *mut c_char, - small_buffer.len(), - &mut error, - ) - }; - assert!(!success); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - } - - #[test] - fn test_derivation_different_networks() { - let mut error = FFIError::default(); - let mut seed = [0u8; 64]; - for (i, byte) in seed.iter_mut().enumerate() { - *byte = (i % 256) as u8; - } - - // Test with Mainnet - let xprv_main = unsafe { - derivation_new_master_key(seed.as_ptr(), seed.len(), FFINetwork::Mainnet, &mut error) - }; - assert!(!xprv_main.is_null()); - assert_eq!(error.code, FFIErrorCode::Success); - - // Test with Testnet - let xprv_test = unsafe { - derivation_new_master_key(seed.as_ptr(), seed.len(), FFINetwork::Testnet, &mut error) - }; - assert!(!xprv_test.is_null()); - assert_eq!(error.code, FFIErrorCode::Success); - - // Convert to strings and verify they have different prefixes - let main_str = unsafe { derivation_xpriv_to_string(xprv_main, &mut error) }; - let test_str = unsafe { derivation_xpriv_to_string(xprv_test, &mut error) }; - - let main_string = unsafe { CStr::from_ptr(main_str) }.to_str().unwrap(); - let test_string = unsafe { CStr::from_ptr(test_str) }.to_str().unwrap(); - - assert!(main_string.starts_with("xprv")); // Dash mainnet - assert!(test_string.starts_with("tprv")); // Testnet - - // Clean up - unsafe { - derivation_string_free(main_str); - derivation_string_free(test_str); - derivation_xpriv_free(xprv_main); - derivation_xpriv_free(xprv_test); - } - } - - #[test] - fn test_derivation_xpriv_to_xpub_null_input() { - let mut error = FFIError::default(); - - let xpub = unsafe { derivation_xpriv_to_xpub(ptr::null_mut(), &mut error) }; - - assert!(xpub.is_null()); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - } - - #[test] - fn test_derivation_xpriv_to_string_null_input() { - let mut error = FFIError::default(); - - let xprv_str = unsafe { derivation_xpriv_to_string(ptr::null_mut(), &mut error) }; - - assert!(xprv_str.is_null()); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - } - - #[test] - fn test_derivation_xpub_to_string_null_input() { - let mut error = FFIError::default(); - - let xpub_str = unsafe { derivation_xpub_to_string(ptr::null_mut(), &mut error) }; - - assert!(xpub_str.is_null()); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - } - - #[test] - fn test_derivation_xpub_fingerprint_null_inputs() { - let mut error = FFIError::default(); - let mut fingerprint = [0u8; 4]; - - // Test with null xpub - let success = unsafe { - derivation_xpub_fingerprint(ptr::null_mut(), fingerprint.as_mut_ptr(), &mut error) - }; - assert!(!success); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - - // Test with null fingerprint buffer - let mut seed = [0u8; 64]; - for (i, byte) in seed.iter_mut().enumerate() { - *byte = (i % 256) as u8; - } - - let xprv = unsafe { - derivation_new_master_key(seed.as_ptr(), seed.len(), FFINetwork::Testnet, &mut error) - }; - - let xpub = unsafe { derivation_xpriv_to_xpub(xprv, &mut error) }; - - let success = unsafe { derivation_xpub_fingerprint(xpub, ptr::null_mut(), &mut error) }; - assert!(!success); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - - // Clean up - unsafe { - derivation_xpub_free(xpub); - derivation_xpriv_free(xprv); - } - } - - #[test] - fn test_derivation_derive_private_key_from_seed_null_inputs() { - let mut error = FFIError::default(); - let seed = [0u8; 64]; - let path = CString::new("m/44'/1'/0'/0/0").unwrap(); - - // Test with null seed - let xpriv = unsafe { - derivation_derive_private_key_from_seed( - ptr::null(), - 64, - path.as_ptr(), - FFINetwork::Testnet, - &mut error, - ) - }; - assert!(xpriv.is_null()); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - - // Test with null path - let xpriv = unsafe { - derivation_derive_private_key_from_seed( - seed.as_ptr(), - seed.len(), - ptr::null(), - FFINetwork::Testnet, - &mut error, - ) - }; - assert!(xpriv.is_null()); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - } - - #[test] - fn test_derivation_derive_private_key_invalid_path() { - let mut error = FFIError::default(); - let mut seed = [0u8; 64]; - for (i, byte) in seed.iter_mut().enumerate() { - *byte = (i % 256) as u8; - } - - // Test with invalid path - try a path that should fail - let invalid_path = CString::new("").unwrap(); - let xpriv = unsafe { - derivation_derive_private_key_from_seed( - seed.as_ptr(), - seed.len(), - invalid_path.as_ptr(), - FFINetwork::Testnet, - &mut error, - ) - }; - // Don't assert specific behavior since we're not sure what the implementation does - // Just exercise the code path - if !xpriv.is_null() { - unsafe { - derivation_xpriv_free(xpriv); - } - } - } - - #[test] - fn test_identity_path_functions_null_inputs() { - let mut error = FFIError::default(); - - // Test identity registration with null buffer - let success = unsafe { - derivation_identity_registration_path( - FFINetwork::Testnet, - 0, - ptr::null_mut(), - 256, - &mut error, - ) - }; - assert!(!success); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - - // Test identity topup with null buffer - let success = unsafe { - derivation_identity_topup_path( - FFINetwork::Testnet, - 0, - 0, - ptr::null_mut(), - 256, - &mut error, - ) - }; - assert!(!success); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - - // Test identity authentication with null buffer - let success = unsafe { - derivation_identity_authentication_path( - FFINetwork::Testnet, - 0, - 0, - ptr::null_mut(), - 256, - &mut error, - ) - }; - assert!(!success); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - } - - #[test] - fn test_identity_path_functions_small_buffer() { - let mut error = FFIError::default(); - let mut small_buffer = [0u8; 5]; - - // Test identity registration with small buffer - let success = unsafe { - derivation_identity_registration_path( - FFINetwork::Testnet, - 0, - small_buffer.as_mut_ptr() as *mut c_char, - small_buffer.len(), - &mut error, - ) - }; - assert!(!success); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - - // Test identity topup with small buffer - let success = unsafe { - derivation_identity_topup_path( - FFINetwork::Testnet, - 0, - 0, - small_buffer.as_mut_ptr() as *mut c_char, - small_buffer.len(), - &mut error, - ) - }; - assert!(!success); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - - // Test identity authentication with small buffer - let success = unsafe { - derivation_identity_authentication_path( - FFINetwork::Testnet, - 0, - 0, - small_buffer.as_mut_ptr() as *mut c_char, - small_buffer.len(), - &mut error, - ) - }; - assert!(!success); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - } - - #[test] - fn test_path_functions_different_indices() { - let mut error = FFIError::default(); - let mut buffer1 = vec![0u8; 256]; - let mut buffer2 = vec![0u8; 256]; - - // Test BIP44 account path with different account indices - let success1 = unsafe { - derivation_bip44_account_path( - FFINetwork::Testnet, - 0, - buffer1.as_mut_ptr() as *mut c_char, - buffer1.len(), - &mut error, - ) - }; - assert!(success1); - - let success2 = unsafe { - derivation_bip44_account_path( - FFINetwork::Testnet, - 5, - buffer2.as_mut_ptr() as *mut c_char, - buffer2.len(), - &mut error, - ) - }; - assert!(success2); - - let path1 = unsafe { CStr::from_ptr(buffer1.as_ptr() as *const c_char).to_str().unwrap() }; - let path2 = unsafe { CStr::from_ptr(buffer2.as_ptr() as *const c_char).to_str().unwrap() }; - - assert_eq!(path1, "m/44'/1'/0'"); - assert_eq!(path2, "m/44'/1'/5'"); - assert_ne!(path1, path2); - } - - #[test] - fn test_bip44_payment_path_variations() { - let mut error = FFIError::default(); - - // Test receive address path - let mut buffer = vec![0u8; 256]; - let success = unsafe { - derivation_bip44_payment_path( - FFINetwork::Testnet, - 0, // account_index - false, // is_change (receive) - 5, // address_index - buffer.as_mut_ptr() as *mut c_char, - buffer.len(), - &mut error, - ) - }; - assert!(success); - let path_str = - unsafe { CStr::from_ptr(buffer.as_ptr() as *const c_char) }.to_str().unwrap(); - assert_eq!(path_str, "m/44'/1'/0'/0/5"); - - // Test change address path - let mut buffer = vec![0u8; 256]; - let success = unsafe { - derivation_bip44_payment_path( - FFINetwork::Testnet, - 0, // account_index - true, // is_change - 3, // address_index - buffer.as_mut_ptr() as *mut c_char, - buffer.len(), - &mut error, - ) - }; - assert!(success); - let path_str = - unsafe { CStr::from_ptr(buffer.as_ptr() as *const c_char) }.to_str().unwrap(); - assert_eq!(path_str, "m/44'/1'/0'/1/3"); - } - - #[test] - fn test_comprehensive_derivation_workflow() { - let mut error = FFIError::default(); - - // Generate seed - let mut seed = [0u8; 64]; - for (i, byte) in seed.iter_mut().enumerate() { - *byte = (i % 256) as u8; - } - - // Create master key - let master_xprv = unsafe { - derivation_new_master_key(seed.as_ptr(), seed.len(), FFINetwork::Testnet, &mut error) - }; - assert!(!master_xprv.is_null()); - - // Convert to public key - let master_xpub = unsafe { derivation_xpriv_to_xpub(master_xprv, &mut error) }; - assert!(!master_xpub.is_null()); - - // Get fingerprint - let mut fingerprint = [0u8; 4]; - let success = unsafe { - derivation_xpub_fingerprint(master_xpub, fingerprint.as_mut_ptr(), &mut error) - }; - assert!(success); - - // Derive child key using path - let path = CString::new("m/44'/1'/0'/0/0").unwrap(); - let child_xprv = unsafe { - derivation_derive_private_key_from_seed( - seed.as_ptr(), - seed.len(), - path.as_ptr(), - FFINetwork::Testnet, - &mut error, - ) - }; - assert!(!child_xprv.is_null()); - - // Convert child to public - let child_xpub = unsafe { derivation_xpriv_to_xpub(child_xprv, &mut error) }; - assert!(!child_xpub.is_null()); - - // Convert to strings - let master_xprv_str = unsafe { derivation_xpriv_to_string(master_xprv, &mut error) }; - let master_xpub_str = unsafe { derivation_xpub_to_string(master_xpub, &mut error) }; - let child_xprv_str = unsafe { derivation_xpriv_to_string(child_xprv, &mut error) }; - let child_xpub_str = unsafe { derivation_xpub_to_string(child_xpub, &mut error) }; - - // Verify all strings are different and have correct prefixes - let master_prv_s = unsafe { CStr::from_ptr(master_xprv_str).to_str().unwrap() }; - let master_pub_s = unsafe { CStr::from_ptr(master_xpub_str).to_str().unwrap() }; - let child_prv_s = unsafe { CStr::from_ptr(child_xprv_str).to_str().unwrap() }; - let child_pub_s = unsafe { CStr::from_ptr(child_xpub_str).to_str().unwrap() }; - - assert!(master_prv_s.starts_with("tprv")); - assert!(master_pub_s.starts_with("tpub")); - assert!(child_prv_s.starts_with("tprv")); - assert!(child_pub_s.starts_with("tpub")); - - assert_ne!(master_prv_s, child_prv_s); - assert_ne!(master_pub_s, child_pub_s); - - // Clean up - - unsafe { - derivation_string_free(master_xprv_str); - derivation_string_free(master_xpub_str); - derivation_string_free(child_xprv_str); - derivation_string_free(child_xpub_str); - derivation_xpub_free(child_xpub); - derivation_xpriv_free(child_xprv); - derivation_xpub_free(master_xpub); - derivation_xpriv_free(master_xprv); - } - } -} diff --git a/key-wallet-ffi/src/error.rs b/key-wallet-ffi/src/error.rs deleted file mode 100644 index 683d8e900..000000000 --- a/key-wallet-ffi/src/error.rs +++ /dev/null @@ -1,457 +0,0 @@ -//! Error handling for FFI interface - -use key_wallet::transaction_checking::PlatformAccountConversionError; -use key_wallet::wallet::managed_wallet_info::asset_lock_builder::AssetLockError; -use key_wallet::wallet::managed_wallet_info::transaction_builder::BuilderError; -use std::ffi::CString; -use std::os::raw::c_char; -use std::str::Utf8Error; -use std::{ffi, io, ptr}; - -/// Dereference a raw `*const` pointer as `&T`, or early-return after writing -/// `InvalidInput` into `*error`. The two-arg form returns `Default::default()`. -#[macro_export] -macro_rules! deref_ptr { - ($ptr:expr, $error:expr, $return_value:expr) => {{ - (*$error).clean(); - - if $ptr.is_null() { - return { - (*$error).set( - $crate::error::FFIErrorCode::InvalidInput, - &format!("{} ptr is null", stringify!($ptr)), - ); - $return_value - }; - } - unsafe { &*$ptr } - }}; - - ($ptr:expr, $error:expr) => {{ - (*$error).clean(); - - if $ptr.is_null() { - return { - (*$error).set( - $crate::error::FFIErrorCode::InvalidInput, - &format!("{} ptr is null", stringify!($ptr)), - ); - Default::default() - }; - } - unsafe { &*$ptr } - }}; -} - -/// Mutable variant of [`deref_ptr!`]: yields `&mut T` on success, otherwise -/// sets `*error` to `InvalidInput` and early-returns. -#[macro_export] -macro_rules! deref_ptr_mut { - ($ptr:expr, $error:expr, $return_value:expr) => {{ - (*$error).clean(); - - if $ptr.is_null() { - return { - (*$error).set( - $crate::error::FFIErrorCode::InvalidInput, - &format!("{} ptr is null", stringify!($ptr)), - ); - $return_value - }; - } - unsafe { &mut *$ptr } - }}; - - ($ptr:expr, $error:expr) => {{ - (*$error).clean(); - - if $ptr.is_null() { - return { - (*$error).set( - $crate::error::FFIErrorCode::InvalidInput, - &format!("{} ptr is null", stringify!($ptr)), - ); - Default::default() - }; - } - unsafe { &mut *$ptr } - }}; -} - -/// Null-check a raw pointer without dereferencing it. On null, sets -/// `*error` to `InvalidInput` and early-returns. Use this for out-parameters -/// where the pointer may point to uninitialized memory and forming a Rust -/// reference would be unsound. -#[macro_export] -macro_rules! check_ptr { - ($ptr:expr, $error:expr, $return_value:expr) => {{ - (*$error).clean(); - - if $ptr.is_null() { - (*$error).set( - $crate::error::FFIErrorCode::InvalidInput, - &format!("{} ptr is null", stringify!($ptr)), - ); - return $return_value; - } - }}; - - ($ptr:expr, $error:expr) => {{ - (*$error).clean(); - - if $ptr.is_null() { - (*$error).set( - $crate::error::FFIErrorCode::InvalidInput, - &format!("{} ptr is null", stringify!($ptr)), - ); - return Default::default(); - } - }}; -} - -/// Unwrap a `Result`/`Option` via [`FfiErrMapper`], writing any error into -/// `*error` and early-returning. The two-arg form returns `Default::default()`. -#[macro_export] -macro_rules! unwrap_or_return { - ($expr:expr, $error:expr, $return_value:expr) => {{ - match $crate::error::FfiErrMapper::map_to_ffi_err($expr, &mut *$error) { - Some(v) => v, - None => return $return_value, - } - }}; - - ($expr:expr, $error:expr) => {{ - match $crate::error::FfiErrMapper::map_to_ffi_err($expr, &mut *$error) { - Some(v) => v, - None => return Default::default(), - } - }}; -} - -/// FFI Error code -#[repr(C)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum FFIErrorCode { - Success = 0, - InvalidInput = 1, - AllocationFailed = 2, - InvalidMnemonic = 3, - InvalidDerivationPath = 4, - InvalidNetwork = 5, - InvalidAddress = 6, - InvalidTransaction = 7, - WalletError = 8, - SerializationError = 9, - NotFound = 10, - InvalidState = 11, - InternalError = 12, - NulByteError = 13, - TransactionBuildingError = 14, - AssetLockerError = 15, - PlatformAccountConversionError = 16, - IOError = 17, -} - -/// FFI Error structure -#[repr(C)] -#[derive(Debug)] -pub struct FFIError { - pub code: FFIErrorCode, - pub message: *mut c_char, -} - -impl FFIError { - /// # Safety - /// - /// This will call FFIError::clean, to ensure message is deallocated - /// before poinitng to a new string. FFIError::clean Safety consideation apply here. - pub unsafe fn set(&mut self, code: FFIErrorCode, msg: &str) { - self.clean(); - - self.message = CString::new(msg).unwrap_or_default().into_raw(); - self.code = code; - } - - /// Returns the error to the default state, deallocating the message if it exists - /// - /// # Safety - /// - /// The message pointer must have been allocated by this library. - pub unsafe fn clean(&mut self) { - self.code = FFIErrorCode::Success; - - if !self.message.is_null() { - let _ = unsafe { CString::from_raw(self.message) }; - self.message = ptr::null_mut(); - } - } -} - -impl Default for FFIError { - fn default() -> Self { - FFIError { - code: FFIErrorCode::Success, - message: ptr::null_mut(), - } - } -} - -pub trait FfiErrMapper: Sized { - /// Map `self` into an `FFIError` via `FfiErrMapperImpl`, clearing any prior - /// error message stored in `error` first. - /// - /// # Safety - /// - /// If `error` currently holds a message pointer, it must have been allocated - /// by this library; it will be freed before being overwritten. - unsafe fn map_to_ffi_err(self, error: &mut FFIError) -> Option { - error.clean(); - - self.map_to_ffi_err_impl(error) - } - - fn map_to_ffi_err_impl(self, err: &mut FFIError) -> Option; -} - -impl FfiErrMapper for Result -where - E: Into, -{ - fn map_to_ffi_err_impl(self, err: &mut FFIError) -> Option { - match self { - Ok(item) => Some(item), - Err(e) => { - *err = e.into(); - None - } - } - } -} - -impl FfiErrMapper for Option { - fn map_to_ffi_err_impl(self, err: &mut FFIError) -> Option { - if self.is_none() { - err.code = FFIErrorCode::NotFound; - err.message = CString::new("Item not found").unwrap().into_raw(); - } - - self - } -} - -impl From for FFIError { - fn from(value: key_wallet::Error) -> Self { - use key_wallet::Error; - - let code = match &value { - Error::InvalidDerivationPath(_) => FFIErrorCode::InvalidDerivationPath, - Error::InvalidMnemonic(_) => FFIErrorCode::InvalidMnemonic, - Error::InvalidParameter(_) => FFIErrorCode::InvalidInput, - Error::InvalidNetwork => FFIErrorCode::InvalidNetwork, - Error::InvalidAddress(_) => FFIErrorCode::InvalidAddress, - Error::Serialization(_) => FFIErrorCode::SerializationError, - Error::WatchOnly | Error::CoinJoinNotEnabled | Error::NoKeySource => { - FFIErrorCode::InvalidState - } - Error::Bip32(_) - | Error::Slip10(_) - | Error::BLS(_) - | Error::Secp256k1(_) - | Error::Base58 - | Error::KeyError(_) => FFIErrorCode::WalletError, - }; - - FFIError { - code, - message: CString::new(value.to_string()) - .unwrap_or( - CString::new("Rust key_wallet::Error message contains null byte").unwrap(), - ) - .into_raw(), - } - } -} - -impl From for FFIError { - fn from(value: key_wallet::bip32::Error) -> Self { - FFIError { - code: FFIErrorCode::InvalidInput, - message: CString::new(value.to_string()) - .unwrap_or( - CString::new("Rust key_wallet::bip32::Error message contains null byte") - .unwrap(), - ) - .into_raw(), - } - } -} - -impl From for FFIError { - fn from(value: key_wallet_manager::WalletError) -> Self { - use key_wallet_manager::WalletError; - - let code = match &value { - WalletError::WalletCreation(_) => FFIErrorCode::WalletError, - WalletError::WalletNotFound(_) => FFIErrorCode::NotFound, - WalletError::WalletExists(_) => FFIErrorCode::InvalidState, - WalletError::InvalidMnemonic(_) => FFIErrorCode::InvalidMnemonic, - WalletError::AccountCreation(_) => FFIErrorCode::WalletError, - WalletError::AccountNotFound(_) => FFIErrorCode::NotFound, - WalletError::AddressGeneration(_) => FFIErrorCode::InvalidAddress, - WalletError::InvalidNetwork => FFIErrorCode::InvalidNetwork, - WalletError::InvalidParameter(_) => FFIErrorCode::InvalidInput, - WalletError::TransactionBuild(_) => FFIErrorCode::InvalidTransaction, - WalletError::InsufficientFunds => FFIErrorCode::InvalidState, - }; - - FFIError { - code, - message: CString::new(value.to_string()) - .unwrap_or( - CString::new("Rust key_wallet_manager::WalletError message contains null byte") - .unwrap(), - ) - .into_raw(), - } - } -} - -impl From for FFIError { - fn from(value: ffi::NulError) -> Self { - FFIError { - code: FFIErrorCode::NulByteError, - message: CString::new(value.to_string()) - .unwrap_or(CString::new("Rust ffi::NulError message contains null byte").unwrap()) - .into_raw(), - } - } -} - -impl From for FFIError { - fn from(value: Utf8Error) -> Self { - FFIError { - code: FFIErrorCode::InvalidInput, - message: CString::new(value.to_string()) - .unwrap_or(CString::new("Rust Utf8Error message contains null byte").unwrap()) - .into_raw(), - } - } -} - -impl From for FFIError { - fn from(value: dashcore::address::Error) -> Self { - FFIError { - code: FFIErrorCode::InvalidAddress, - message: CString::new(value.to_string()) - .unwrap_or( - CString::new("Rust dashcore::address::Error message contains null byte") - .unwrap(), - ) - .into_raw(), - } - } -} - -impl From for FFIError { - fn from(value: dashcore::consensus::encode::Error) -> Self { - FFIError { - code: FFIErrorCode::InvalidInput, - message: CString::new(value.to_string()) - .unwrap_or( - CString::new( - "Rust dashcore::consensus::encode::Error message contains null byte", - ) - .unwrap(), - ) - .into_raw(), - } - } -} - -impl From for FFIError { - fn from(value: BuilderError) -> Self { - FFIError { - code: FFIErrorCode::TransactionBuildingError, - message: CString::new(value.to_string()) - .unwrap_or( - CString::new( - "Rust key_wallet::wallet::managed_wallet_info::transaction_builder::BuilderError message contains null byte", - ).unwrap() - ).into_raw(), - } - } -} - -impl From for FFIError { - fn from(value: AssetLockError) -> Self { - FFIError { - code: FFIErrorCode::AssetLockerError, - message: CString::new(value.to_string()) - .unwrap_or( - CString::new( - "Rust key_wallet::wallet::managed_wallet_info::asset_lock_builder::AssetLockError message contains null byte", - ).unwrap() - ).into_raw(), - } - } -} - -impl From for FFIError { - fn from(value: PlatformAccountConversionError) -> Self { - FFIError { - code: FFIErrorCode::PlatformAccountConversionError, - message: CString::new(value.to_string()) - .unwrap_or( - CString::new( - "Rust key_wallet::transaction_checking::transaction_router::PlatformAccountConversionError message contains null byte", - ).unwrap() - ).into_raw(), - } - } -} - -impl From for FFIError { - fn from(value: io::Error) -> Self { - FFIError { - code: FFIErrorCode::IOError, - message: CString::new(value.to_string()) - .unwrap_or(CString::new("Rust io::Error message contains null byte").unwrap()) - .into_raw(), - } - } -} - -// TODO: Some Results contain a str as the error, need to change that -// so the conversion can be more specific, this is to generic -impl From<&str> for FFIError { - fn from(value: &str) -> Self { - FFIError { - code: FFIErrorCode::InternalError, - message: CString::new(value.to_string()) - .unwrap_or(CString::new("Rust error message contains null byte").unwrap()) - .into_raw(), - } - } -} - -impl Drop for FFIError { - fn drop(&mut self) { - unsafe { - self.clean(); - } - } -} - -/// Free an error message -/// -/// # Safety -/// -/// - `message` must be a valid pointer to a C string that was allocated by this library -/// - The pointer must not be used after calling this function -/// - This function must only be called once per allocation -#[no_mangle] -pub unsafe extern "C" fn error_message_free(message: *mut c_char) { - if !message.is_null() { - let _ = CString::from_raw(message); - } -} diff --git a/key-wallet-ffi/src/keys.rs b/key-wallet-ffi/src/keys.rs deleted file mode 100644 index 9a39e76f7..000000000 --- a/key-wallet-ffi/src/keys.rs +++ /dev/null @@ -1,557 +0,0 @@ -//! Key derivation and management - -use crate::error::{FFIError, FFIErrorCode}; -use crate::types::FFIWallet; -use crate::{check_ptr, deref_ptr, unwrap_or_return}; -use dash_network::ffi::FFINetwork; -use std::ffi::{CStr, CString}; -use std::os::raw::{c_char, c_uint}; -use std::ptr; - -/// Opaque type for a private key (SecretKey) -pub struct FFIPrivateKey { - inner: secp256k1::SecretKey, -} - -/// Opaque type for an extended private key -pub struct FFIExtendedPrivKey { - inner: key_wallet::bip32::ExtendedPrivKey, -} - -/// Opaque type for a public key -pub struct FFIPublicKey { - inner: secp256k1::PublicKey, -} - -/// Opaque type for an extended public key -pub struct FFIExtendedPubKey { - inner: key_wallet::bip32::ExtendedPubKey, -} - -impl FFIExtendedPrivKey { - #[inline] - pub(crate) fn inner(&self) -> &key_wallet::bip32::ExtendedPrivKey { - &self.inner - } - - #[inline] - pub(crate) fn from_inner(inner: key_wallet::bip32::ExtendedPrivKey) -> Self { - FFIExtendedPrivKey { - inner, - } - } -} - -impl FFIExtendedPubKey { - #[inline] - pub(crate) fn inner(&self) -> &key_wallet::bip32::ExtendedPubKey { - &self.inner - } - - #[inline] - pub(crate) fn from_inner(inner: key_wallet::bip32::ExtendedPubKey) -> Self { - FFIExtendedPubKey { - inner, - } - } -} - -impl FFIPrivateKey { - #[inline] - pub(crate) fn from_secret(inner: secp256k1::SecretKey) -> Self { - FFIPrivateKey { - inner, - } - } -} - -/// Get extended private key for account -/// -/// # Safety -/// -/// - `wallet` must be a valid pointer to an FFIWallet -/// - `error` must be a valid pointer to an FFIError -/// - The returned string must be freed with `string_free` -#[no_mangle] -pub unsafe extern "C" fn wallet_get_account_xpriv( - wallet: *const FFIWallet, - account_index: c_uint, - error: *mut FFIError, -) -> *mut c_char { - let wallet = deref_ptr!(wallet, error); - - let account = unwrap_or_return!(wallet.inner().get_bip44_account(account_index), error); - - if account.is_watch_only { - (*error).set(FFIErrorCode::NotFound, "Private key not available (watch-only wallet)"); - return ptr::null_mut(); - } - - (*error).set(FFIErrorCode::InternalError, "Private key extraction not implemented"); - ptr::null_mut() -} - -/// Get extended public key for account -/// -/// # Safety -/// -/// - `wallet` must be a valid pointer to an FFIWallet -/// - `error` must be a valid pointer to an FFIError -/// - The returned string must be freed with `string_free` -#[no_mangle] -pub unsafe extern "C" fn wallet_get_account_xpub( - wallet: *const FFIWallet, - account_index: c_uint, - error: *mut FFIError, -) -> *mut c_char { - let wallet = deref_ptr!(wallet, error); - let account = unwrap_or_return!(wallet.inner().get_bip44_account(account_index), error); - unwrap_or_return!(CString::new(account.extended_public_key().to_string()), error).into_raw() -} - -/// Derive private key at a specific path -/// Returns an opaque FFIPrivateKey pointer that must be freed with private_key_free -/// -/// # Safety -/// -/// - `wallet` must be a valid pointer to an FFIWallet -/// - `derivation_path` must be a valid null-terminated C string -/// - `error` must be a valid pointer to an FFIError -/// - The returned pointer must be freed with `private_key_free` -#[no_mangle] -pub unsafe extern "C" fn wallet_derive_private_key( - wallet: *const FFIWallet, - derivation_path: *const c_char, - error: *mut FFIError, -) -> *mut FFIPrivateKey { - use key_wallet::DerivationPath; - use std::str::FromStr; - - let wallet = deref_ptr!(wallet, error); - let derivation_path = deref_ptr!(derivation_path, error); - let path_str = unwrap_or_return!(CStr::from_ptr(derivation_path).to_str(), error); - let path = unwrap_or_return!(DerivationPath::from_str(path_str), error); - let private_key = unwrap_or_return!(wallet.inner().derive_private_key(&path), error); - Box::into_raw(Box::new(FFIPrivateKey { - inner: private_key, - })) -} - -/// Derive extended private key at a specific path -/// Returns an opaque FFIExtendedPrivKey pointer that must be freed with extended_private_key_free -/// -/// # Safety -/// -/// - `wallet` must be a valid pointer to an FFIWallet -/// - `derivation_path` must be a valid null-terminated C string -/// - `error` must be a valid pointer to an FFIError -/// - The returned pointer must be freed with `extended_private_key_free` -#[no_mangle] -pub unsafe extern "C" fn wallet_derive_extended_private_key( - wallet: *const FFIWallet, - derivation_path: *const c_char, - error: *mut FFIError, -) -> *mut FFIExtendedPrivKey { - use key_wallet::DerivationPath; - use std::str::FromStr; - - let wallet = deref_ptr!(wallet, error); - let derivation_path = deref_ptr!(derivation_path, error); - let path_str = unwrap_or_return!(CStr::from_ptr(derivation_path).to_str(), error); - let path = unwrap_or_return!(DerivationPath::from_str(path_str), error); - let extended_private_key = - unwrap_or_return!(wallet.inner().derive_extended_private_key(&path), error); - Box::into_raw(Box::new(FFIExtendedPrivKey { - inner: extended_private_key, - })) -} - -/// Derive private key at a specific path and return as WIF string -/// -/// # Safety -/// -/// - `wallet` must be a valid pointer to an FFIWallet -/// - `derivation_path` must be a valid null-terminated C string -/// - `error` must be a valid pointer to an FFIError -/// - The returned string must be freed with `string_free` -#[no_mangle] -pub unsafe extern "C" fn wallet_derive_private_key_as_wif( - wallet: *const FFIWallet, - derivation_path: *const c_char, - error: *mut FFIError, -) -> *mut c_char { - use key_wallet::DerivationPath; - use std::str::FromStr; - - let wallet = deref_ptr!(wallet, error); - let derivation_path = deref_ptr!(derivation_path, error); - let path_str = unwrap_or_return!(CStr::from_ptr(derivation_path).to_str(), error); - let path = unwrap_or_return!(DerivationPath::from_str(path_str), error); - let wif = unwrap_or_return!(wallet.inner().derive_private_key_as_wif(&path), error); - unwrap_or_return!(CString::new(wif), error).into_raw() -} - -/// Free a private key -/// -/// # Safety -/// -/// - `key` must be a valid pointer created by private key functions or null -/// - After calling this function, the pointer becomes invalid -#[no_mangle] -pub unsafe extern "C" fn private_key_free(key: *mut FFIPrivateKey) { - if !key.is_null() { - let _ = unsafe { Box::from_raw(key) }; - } -} - -/// Free an extended private key -/// -/// # Safety -/// -/// - `key` must be a valid pointer created by extended private key functions or null -/// - After calling this function, the pointer becomes invalid -#[no_mangle] -pub unsafe extern "C" fn extended_private_key_free(key: *mut FFIExtendedPrivKey) { - if !key.is_null() { - let _ = unsafe { Box::from_raw(key) }; - } -} - -/// Get extended private key as string (xprv format) -/// -/// Returns the extended private key in base58 format (xprv... for mainnet, tprv... for testnet) -/// -/// # Safety -/// -/// - `key` must be a valid pointer to an FFIExtendedPrivKey -/// - `network` is ignored; the network is encoded in the extended key -/// - `error` must be a valid pointer to an FFIError -/// - The returned string must be freed with `string_free` -#[no_mangle] -pub unsafe extern "C" fn extended_private_key_to_string( - key: *const FFIExtendedPrivKey, - _network: FFINetwork, - error: *mut FFIError, -) -> *mut c_char { - // Network is already encoded in the extended key. - let key = deref_ptr!(key, error); - unwrap_or_return!(CString::new(key.inner.to_string()), error).into_raw() -} - -/// Get the private key from an extended private key -/// -/// Extracts the non-extended private key from an extended private key. -/// -/// # Safety -/// -/// - `extended_key` must be a valid pointer to an FFIExtendedPrivKey -/// - `error` must be a valid pointer to an FFIError -/// - The returned FFIPrivateKey must be freed with `private_key_free` -#[no_mangle] -pub unsafe extern "C" fn extended_private_key_get_private_key( - extended_key: *const FFIExtendedPrivKey, - error: *mut FFIError, -) -> *mut FFIPrivateKey { - let extended = deref_ptr!(extended_key, error); - Box::into_raw(Box::new(FFIPrivateKey { - inner: extended.inner.private_key, - })) -} - -/// Get private key as WIF string from FFIPrivateKey -/// -/// # Safety -/// -/// - `key` must be a valid pointer to an FFIPrivateKey -/// - `error` must be a valid pointer to an FFIError -/// - The returned string must be freed with `string_free` -#[no_mangle] -pub unsafe extern "C" fn private_key_to_wif( - key: *const FFIPrivateKey, - network: FFINetwork, - error: *mut FFIError, -) -> *mut c_char { - let key = deref_ptr!(key, error); - - let network_rust: key_wallet::Network = network.into(); - - // Convert to WIF format - use dashcore::PrivateKey as DashPrivateKey; - let dash_key = DashPrivateKey { - compressed: true, - network: network_rust, - inner: key.inner, - }; - - let wif = dash_key.to_wif(); - unwrap_or_return!(CString::new(wif), error).into_raw() -} - -/// Derive public key at a specific path -/// Returns an opaque FFIPublicKey pointer that must be freed with public_key_free -/// -/// # Safety -/// -/// - `wallet` must be a valid pointer to an FFIWallet -/// - `derivation_path` must be a valid null-terminated C string -/// - `error` must be a valid pointer to an FFIError -/// - The returned pointer must be freed with `public_key_free` -#[no_mangle] -pub unsafe extern "C" fn wallet_derive_public_key( - wallet: *const FFIWallet, - derivation_path: *const c_char, - error: *mut FFIError, -) -> *mut FFIPublicKey { - use key_wallet::DerivationPath; - use std::str::FromStr; - - let wallet = deref_ptr!(wallet, error); - let derivation_path = deref_ptr!(derivation_path, error); - let path_str = unwrap_or_return!(CStr::from_ptr(derivation_path).to_str(), error); - let path = unwrap_or_return!(DerivationPath::from_str(path_str), error); - let public_key = unwrap_or_return!(wallet.inner().derive_public_key(&path), error); - Box::into_raw(Box::new(FFIPublicKey { - inner: public_key, - })) -} - -/// Derive extended public key at a specific path -/// Returns an opaque FFIExtendedPubKey pointer that must be freed with extended_public_key_free -/// -/// # Safety -/// -/// - `wallet` must be a valid pointer to an FFIWallet -/// - `derivation_path` must be a valid null-terminated C string -/// - `error` must be a valid pointer to an FFIError -/// - The returned pointer must be freed with `extended_public_key_free` -#[no_mangle] -pub unsafe extern "C" fn wallet_derive_extended_public_key( - wallet: *const FFIWallet, - derivation_path: *const c_char, - error: *mut FFIError, -) -> *mut FFIExtendedPubKey { - use key_wallet::DerivationPath; - use std::str::FromStr; - - let wallet = deref_ptr!(wallet, error); - let derivation_path = deref_ptr!(derivation_path, error); - let path_str = unwrap_or_return!(CStr::from_ptr(derivation_path).to_str(), error); - let path = unwrap_or_return!(DerivationPath::from_str(path_str), error); - let extended_public_key = - unwrap_or_return!(wallet.inner().derive_extended_public_key(&path), error); - Box::into_raw(Box::new(FFIExtendedPubKey { - inner: extended_public_key, - })) -} - -/// Derive public key at a specific path and return as hex string -/// -/// # Safety -/// -/// - `wallet` must be a valid pointer to an FFIWallet -/// - `derivation_path` must be a valid null-terminated C string -/// - `error` must be a valid pointer to an FFIError -/// - The returned string must be freed with `string_free` -#[no_mangle] -pub unsafe extern "C" fn wallet_derive_public_key_as_hex( - wallet: *const FFIWallet, - derivation_path: *const c_char, - error: *mut FFIError, -) -> *mut c_char { - use key_wallet::DerivationPath; - use std::str::FromStr; - - let wallet = deref_ptr!(wallet, error); - let derivation_path = deref_ptr!(derivation_path, error); - let path_str = unwrap_or_return!(CStr::from_ptr(derivation_path).to_str(), error); - let path = unwrap_or_return!(DerivationPath::from_str(path_str), error); - let hex = unwrap_or_return!(wallet.inner().derive_public_key_as_hex(&path), error); - unwrap_or_return!(CString::new(hex), error).into_raw() -} - -/// Free a public key -/// -/// # Safety -/// -/// - `key` must be a valid pointer created by public key functions or null -/// - After calling this function, the pointer becomes invalid -#[no_mangle] -pub unsafe extern "C" fn public_key_free(key: *mut FFIPublicKey) { - if !key.is_null() { - unsafe { - let _ = Box::from_raw(key); - } - } -} - -/// Free an extended public key -/// -/// # Safety -/// -/// - `key` must be a valid pointer created by extended public key functions or null -/// - After calling this function, the pointer becomes invalid -#[no_mangle] -pub unsafe extern "C" fn extended_public_key_free(key: *mut FFIExtendedPubKey) { - if !key.is_null() { - unsafe { - let _ = Box::from_raw(key); - } - } -} - -/// Get extended public key as string (xpub format) -/// -/// Returns the extended public key in base58 format (xpub... for mainnet, tpub... for testnet) -/// -/// # Safety -/// -/// - `key` must be a valid pointer to an FFIExtendedPubKey -/// - `network` is ignored; the network is encoded in the extended key -/// - `error` must be a valid pointer to an FFIError -/// - The returned string must be freed with `string_free` -#[no_mangle] -pub unsafe extern "C" fn extended_public_key_to_string( - key: *const FFIExtendedPubKey, - _network: FFINetwork, - error: *mut FFIError, -) -> *mut c_char { - // Network is already encoded in the extended key. - let key = deref_ptr!(key, error); - unwrap_or_return!(CString::new(key.inner.to_string()), error).into_raw() -} - -/// Get the public key from an extended public key -/// -/// Extracts the non-extended public key from an extended public key. -/// -/// # Safety -/// -/// - `extended_key` must be a valid pointer to an FFIExtendedPubKey -/// - `error` must be a valid pointer to an FFIError -/// - The returned FFIPublicKey must be freed with `public_key_free` -#[no_mangle] -pub unsafe extern "C" fn extended_public_key_get_public_key( - extended_key: *const FFIExtendedPubKey, - error: *mut FFIError, -) -> *mut FFIPublicKey { - let extended = deref_ptr!(extended_key, error); - Box::into_raw(Box::new(FFIPublicKey { - inner: extended.inner.public_key, - })) -} - -/// Get public key as hex string from FFIPublicKey -/// -/// # Safety -/// -/// - `key` must be a valid pointer to an FFIPublicKey -/// - `error` must be a valid pointer to an FFIError -/// - The returned string must be freed with `string_free` -#[no_mangle] -pub unsafe extern "C" fn public_key_to_hex( - key: *const FFIPublicKey, - error: *mut FFIError, -) -> *mut c_char { - let key = deref_ptr!(key, error); - unwrap_or_return!(CString::new(hex::encode(key.inner.serialize())), error).into_raw() -} - -/// Convert derivation path string to indices -/// -/// # Safety -/// -/// - `path` must be a valid null-terminated C string or null -/// - `indices_out` must be a valid pointer to store the indices array pointer -/// - `hardened_out` must be a valid pointer to store the hardened flags array pointer -/// - `count_out` must be a valid pointer to store the count -/// - `error` must be a valid pointer to an FFIError -/// - The returned arrays must be freed with `derivation_path_free` -#[no_mangle] -pub unsafe extern "C" fn derivation_path_parse( - path: *const c_char, - indices_out: *mut *mut u32, - hardened_out: *mut *mut bool, - count_out: *mut usize, - error: *mut FFIError, -) -> bool { - use key_wallet::DerivationPath; - use std::str::FromStr; - - let path = deref_ptr!(path, error); - check_ptr!(indices_out, error); - check_ptr!(hardened_out, error); - check_ptr!(count_out, error); - let path_str = unwrap_or_return!(CStr::from_ptr(path).to_str(), error); - let derivation_path = unwrap_or_return!(DerivationPath::from_str(path_str), error); - - let children: Vec<_> = derivation_path.into_iter().collect(); - let count = children.len(); - - let mut indices = Vec::with_capacity(count); - let mut hardened = Vec::with_capacity(count); - - for child in children { - let (index, is_hardened) = match child { - key_wallet::ChildNumber::Normal { - index, - } => (*index, false), - key_wallet::ChildNumber::Hardened { - index, - } => (*index, true), - _ => { - (*error).set( - FFIErrorCode::InvalidDerivationPath, - "Unsupported ChildNumber variant encountered", - ); - return false; - } - }; - indices.push(index); - hardened.push(is_hardened); - } - - unsafe { - *count_out = count; - if count > 0 { - *indices_out = Box::into_raw(indices.into_boxed_slice()) as *mut u32; - *hardened_out = Box::into_raw(hardened.into_boxed_slice()) as *mut bool; - } else { - *indices_out = ptr::null_mut(); - *hardened_out = ptr::null_mut(); - } - } - true -} - -/// Free derivation path arrays -/// Note: This function expects the count to properly free the slices -/// -/// # Safety -/// -/// - `indices` must be a valid pointer created by `derivation_path_parse` or null -/// - `hardened` must be a valid pointer created by `derivation_path_parse` or null -/// - `count` must match the count from `derivation_path_parse` -/// - After calling this function, the pointers become invalid -#[no_mangle] -pub unsafe extern "C" fn derivation_path_free( - indices: *mut u32, - hardened: *mut bool, - count: usize, -) { - if !indices.is_null() && count > 0 { - unsafe { - // Reconstruct the boxed slice from the raw pointer and let it drop - let _ = Box::from_raw(std::ptr::slice_from_raw_parts_mut(indices, count)); - } - } - if !hardened.is_null() && count > 0 { - unsafe { - // Reconstruct the boxed slice from the raw pointer and let it drop - let _ = Box::from_raw(std::ptr::slice_from_raw_parts_mut(hardened, count)); - } - } -} - -#[cfg(test)] -#[path = "keys_tests.rs"] -mod tests; diff --git a/key-wallet-ffi/src/keys_tests.rs b/key-wallet-ffi/src/keys_tests.rs deleted file mode 100644 index 4fb6d21ea..000000000 --- a/key-wallet-ffi/src/keys_tests.rs +++ /dev/null @@ -1,607 +0,0 @@ -//! Tests for key derivation FFI functions - -#[cfg(test)] -#[allow(clippy::module_inception)] -mod tests { - use crate::error::{FFIError, FFIErrorCode}; - use crate::keys::*; - use crate::wallet; - use std::ffi::{CStr, CString}; - use std::ptr; - - #[test] - fn test_extended_key_string_conversion() { - let mut error = FFIError::default(); - - // Create a wallet to get extended keys from - let mnemonic = CString::new("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").unwrap(); - let wallet = unsafe { - wallet::wallet_create_from_mnemonic(mnemonic.as_ptr(), FFINetwork::Testnet, &mut error) - }; - assert!(!wallet.is_null()); - assert_eq!(error.code, FFIErrorCode::Success); - - // Derive an extended private key - let path = CString::new("m/44'/1'/0'").unwrap(); - let ext_priv = - unsafe { wallet_derive_extended_private_key(wallet, path.as_ptr(), &mut error) }; - assert!(!ext_priv.is_null()); - assert_eq!(error.code, FFIErrorCode::Success); - - // Test extended_private_key_to_string - let xprv_str = - unsafe { extended_private_key_to_string(ext_priv, FFINetwork::Testnet, &mut error) }; - assert!(!xprv_str.is_null()); - assert_eq!(error.code, FFIErrorCode::Success); - - let xprv = unsafe { CStr::from_ptr(xprv_str) }.to_str().unwrap(); - assert!(xprv.starts_with("tprv")); // Testnet extended private key - unsafe { crate::utils::string_free(xprv_str) }; - - // Test extended_private_key_get_private_key - let priv_key = unsafe { extended_private_key_get_private_key(ext_priv, &mut error) }; - assert!(!priv_key.is_null()); - assert_eq!(error.code, FFIErrorCode::Success); - - // Get WIF from the extracted private key - let wif = unsafe { private_key_to_wif(priv_key, FFINetwork::Testnet, &mut error) }; - assert!(!wif.is_null()); - assert_eq!(error.code, FFIErrorCode::Success); - - let wif_str = unsafe { CStr::from_ptr(wif) }.to_str().unwrap(); - // Assert testnet WIF prefix (compressed or uncompressed) - assert!(wif_str.starts_with('c') || wif_str.starts_with('9')); - unsafe { crate::utils::string_free(wif) }; - - // Clean up - unsafe { private_key_free(priv_key) }; - unsafe { extended_private_key_free(ext_priv) }; - - // Now test extended public key - let ext_pub = - unsafe { wallet_derive_extended_public_key(wallet, path.as_ptr(), &mut error) }; - assert!(!ext_pub.is_null()); - assert_eq!(error.code, FFIErrorCode::Success); - - // Test extended_public_key_to_string - let xpub_str = - unsafe { extended_public_key_to_string(ext_pub, FFINetwork::Testnet, &mut error) }; - assert!(!xpub_str.is_null()); - assert_eq!(error.code, FFIErrorCode::Success); - - let xpub = unsafe { CStr::from_ptr(xpub_str) }.to_str().unwrap(); - assert!(xpub.starts_with("tpub")); // Testnet extended public key - unsafe { crate::utils::string_free(xpub_str) }; - - // Test extended_public_key_get_public_key - let pub_key = unsafe { extended_public_key_get_public_key(ext_pub, &mut error) }; - assert!(!pub_key.is_null()); - assert_eq!(error.code, FFIErrorCode::Success); - - // Get hex from the extracted public key - let hex = unsafe { public_key_to_hex(pub_key, &mut error) }; - assert!(!hex.is_null()); - assert_eq!(error.code, FFIErrorCode::Success); - - let hex_str = unsafe { CStr::from_ptr(hex) }.to_str().unwrap(); - assert_eq!(hex_str.len(), 66); // 33 bytes = 66 hex chars - unsafe { crate::utils::string_free(hex) }; - - // Clean up - unsafe { public_key_free(pub_key) }; - unsafe { extended_public_key_free(ext_pub) }; - unsafe { wallet::wallet_free(wallet) }; - } - - // Note: wallet_get_account_xpriv is not implemented for security reasons - // The function always returns null to prevent private key extraction - #[test] - fn test_wallet_get_account_xpriv_not_implemented() { - let mut error = FFIError::default(); - - // Create a wallet - let mnemonic = CString::new("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").unwrap(); - - let wallet = unsafe { - wallet::wallet_create_from_mnemonic(mnemonic.as_ptr(), FFINetwork::Testnet, &mut error) - }; - assert!(!wallet.is_null()); - - // Try to get account xpriv - should fail - let xpriv_str = unsafe { wallet_get_account_xpriv(wallet, 0, &mut error) }; - - // Should return null (not implemented for security) - assert!(xpriv_str.is_null()); - assert_eq!(error.code, FFIErrorCode::InternalError); - - // Clean up - unsafe { - wallet::wallet_free(wallet); - } - } - - #[test] - fn test_wallet_get_account_xpub() { - let mut error = FFIError::default(); - - // Create a wallet - let mnemonic = CString::new("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").unwrap(); - - let wallet = unsafe { - wallet::wallet_create_from_mnemonic(mnemonic.as_ptr(), FFINetwork::Testnet, &mut error) - }; - assert!(!wallet.is_null()); - - // Get account xpub - let xpub_str = unsafe { wallet_get_account_xpub(wallet, 0, &mut error) }; - - assert!(!xpub_str.is_null()); - - let xpub = unsafe { CStr::from_ptr(xpub_str).to_str().unwrap() }; - assert!(xpub.starts_with("tpub")); // Testnet public key - - // Clean up - unsafe { - crate::utils::string_free(xpub_str); - wallet::wallet_free(wallet); - } - } - - // wallet_derive_private_key is now implemented - #[test] - fn test_wallet_derive_private_key_now_implemented() { - let mut error = FFIError::default(); - - // Create a wallet - let mnemonic = CString::new("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").unwrap(); - - let wallet = unsafe { - wallet::wallet_create_from_mnemonic(mnemonic.as_ptr(), FFINetwork::Testnet, &mut error) - }; - - // Try to derive private key - should now succeed (44'/1'/0'/0/0 for Dash) - let path = CString::new("m/44'/1'/0'/0/0").unwrap(); - let privkey_ptr = unsafe { wallet_derive_private_key(wallet, path.as_ptr(), &mut error) }; - - // Should succeed and return a valid pointer - assert!(!privkey_ptr.is_null()); - assert_eq!(error.code, FFIErrorCode::Success); - - // Convert to WIF to verify it's valid - let wif_str = unsafe { private_key_to_wif(privkey_ptr, FFINetwork::Testnet, &mut error) }; - assert!(!wif_str.is_null()); - assert_eq!(error.code, FFIErrorCode::Success); - - let wif = unsafe { CStr::from_ptr(wif_str).to_str().unwrap() }; - // Assert testnet WIF prefix (compressed or uncompressed) - assert!(wif.starts_with('c') || wif.starts_with('9')); - - // Clean up - if !wif_str.is_null() { - unsafe { - crate::utils::string_free(wif_str); - } - } - unsafe { - private_key_free(privkey_ptr); - wallet::wallet_free(wallet); - } - } - - #[test] - fn test_wallet_derive_public_key() { - let mut error = FFIError::default(); - - // Create a wallet - let mnemonic = CString::new("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").unwrap(); - - let wallet = unsafe { - wallet::wallet_create_from_mnemonic(mnemonic.as_ptr(), FFINetwork::Testnet, &mut error) - }; - - // Ensure wallet was created successfully - assert!(!wallet.is_null(), "Failed to create wallet"); - assert_eq!(error.code, FFIErrorCode::Success, "Wallet creation error: {:?}", error.code); - - // Derive public key using derivation path (44'/1'/0'/0/0 for Dash) - let path = CString::new("m/44'/1'/0'/0/0").unwrap(); - let pubkey_ptr = unsafe { wallet_derive_public_key(wallet, path.as_ptr(), &mut error) }; - - if pubkey_ptr.is_null() { - panic!("pubkey_ptr is null, error: {:?}", error); - } - assert_eq!(error.code, FFIErrorCode::Success); - - // Get the hex representation to verify - let hex_str = unsafe { public_key_to_hex(pubkey_ptr, &mut error) }; - assert!(!hex_str.is_null()); - - let hex = unsafe { CStr::from_ptr(hex_str).to_str().unwrap() }; - // Public key should start with 02 or 03 (compressed) - assert!(hex.starts_with("02") || hex.starts_with("03")); - assert_eq!(hex.len(), 66); // 33 bytes * 2 hex chars - - // Clean up - if !hex_str.is_null() { - unsafe { - crate::utils::string_free(hex_str); - } - } - unsafe { - public_key_free(pubkey_ptr); - } - - // Clean up - unsafe { - wallet::wallet_free(wallet); - } - } - - #[test] - fn test_wallet_derive_public_key_as_hex() { - let mut error = FFIError::default(); - - // Create a wallet - let mnemonic = CString::new("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").unwrap(); - let wallet = unsafe { - wallet::wallet_create_from_mnemonic(mnemonic.as_ptr(), FFINetwork::Testnet, &mut error) - }; - assert!(!wallet.is_null()); - assert_eq!(error.code, FFIErrorCode::Success); - - // Derive public key as hex directly - let path = CString::new("m/44'/1'/0'/0/0").unwrap(); - let hex_str = unsafe { wallet_derive_public_key_as_hex(wallet, path.as_ptr(), &mut error) }; - assert!(!hex_str.is_null()); - assert_eq!(error.code, FFIErrorCode::Success); - - let hex = unsafe { CStr::from_ptr(hex_str) }.to_str().unwrap(); - // Public key should start with 02 or 03 (compressed) - assert!(hex.starts_with("02") || hex.starts_with("03")); - assert_eq!(hex.len(), 66); // 33 bytes * 2 hex chars - - // Clean up - unsafe { crate::utils::string_free(hex_str) }; - unsafe { wallet::wallet_free(wallet) }; - } - - #[test] - fn test_derivation_path_parse() { - let mut error = FFIError::default(); - - // Parse a BIP44 path - let path = CString::new("m/44'/1'/0'/0/5").unwrap(); - - let mut indices_out: *mut u32 = ptr::null_mut(); - let mut hardened_out: *mut bool = ptr::null_mut(); - let mut count_out: usize = 0; - - let success = unsafe { - derivation_path_parse( - path.as_ptr(), - &mut indices_out, - &mut hardened_out, - &mut count_out, - &mut error, - ) - }; - - assert!(success); - assert_eq!(count_out, 5); - assert!(!indices_out.is_null()); - assert!(!hardened_out.is_null()); - - // Check the parsed values - let indices = unsafe { std::slice::from_raw_parts(indices_out, count_out) }; - let hardened = unsafe { std::slice::from_raw_parts(hardened_out, count_out) }; - - assert_eq!(indices[0], 44); - assert!(hardened[0]); // 44' - assert_eq!(indices[1], 1); - assert!(hardened[1]); // 1' - assert_eq!(indices[2], 0); - assert!(hardened[2]); // 0' - assert_eq!(indices[3], 0); - assert!(!hardened[3]); // 0 - assert_eq!(indices[4], 5); - assert!(!hardened[4]); // 5 - - // Clean up - unsafe { - derivation_path_free(indices_out, hardened_out, count_out); - } - } - - #[test] - fn test_derivation_path_parse_root() { - let mut error = FFIError::default(); - - // Parse root path - let path = CString::new("m").unwrap(); - - let mut indices_out: *mut u32 = ptr::null_mut(); - let mut hardened_out: *mut bool = ptr::null_mut(); - let mut count_out: usize = 0; - - let success = unsafe { - derivation_path_parse( - path.as_ptr(), - &mut indices_out, - &mut hardened_out, - &mut count_out, - &mut error, - ) - }; - - assert!(success); - assert_eq!(count_out, 0); // Root path has no indices - - // Clean up (should handle null pointers gracefully) - unsafe { - derivation_path_free(indices_out, hardened_out, count_out); - } - } - - #[test] - fn test_error_handling() { - let mut error = FFIError::default(); - - // Test with null wallet - let xpriv = unsafe { wallet_get_account_xpriv(ptr::null(), 0, &mut error) }; - assert!(xpriv.is_null()); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - - // Test with invalid path - let invalid_path = CString::new("invalid/path").unwrap(); - let mut indices_out: *mut u32 = ptr::null_mut(); - let mut hardened_out: *mut bool = ptr::null_mut(); - let mut count_out: usize = 0; - - let success = unsafe { - derivation_path_parse( - invalid_path.as_ptr(), - &mut indices_out, - &mut hardened_out, - &mut count_out, - &mut error, - ) - }; - - assert!(!success); - } - - #[test] - fn test_wallet_derive_public_key_null_inputs() { - let mut error = FFIError::default(); - - // Test with null wallet (44'/1'/0'/0/0 for Dash) - let path = CString::new("m/44'/1'/0'/0/0").unwrap(); - let pubkey_ptr = - unsafe { wallet_derive_public_key(ptr::null(), path.as_ptr(), &mut error) }; - - assert!(pubkey_ptr.is_null()); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - - // Create a wallet for subsequent tests - let mnemonic = CString::new("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").unwrap(); - - let wallet = unsafe { - wallet::wallet_create_from_mnemonic(mnemonic.as_ptr(), FFINetwork::Testnet, &mut error) - }; - - // Test with null path - let pubkey_ptr = unsafe { wallet_derive_public_key(wallet, ptr::null(), &mut error) }; - - assert!(pubkey_ptr.is_null()); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - - // Clean up - unsafe { - wallet::wallet_free(wallet); - } - } - - #[test] - fn test_derivation_path_parse_null_inputs() { - let mut error = FFIError::default(); - - // Test with null path - let mut indices_out: *mut u32 = ptr::null_mut(); - let mut hardened_out: *mut bool = ptr::null_mut(); - let mut count_out: usize = 0; - - let success = unsafe { - derivation_path_parse( - ptr::null(), - &mut indices_out, - &mut hardened_out, - &mut count_out, - &mut error, - ) - }; - - assert!(!success); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - - // Test with null output pointers - let path = CString::new("m/44'/1'/0'").unwrap(); - let success = unsafe { - derivation_path_parse( - path.as_ptr(), - ptr::null_mut(), - &mut hardened_out, - &mut count_out, - &mut error, - ) - }; - - assert!(!success); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - } - - #[test] - fn test_derivation_path_complex_cases() { - let mut error = FFIError::default(); - - // Test single hardened index - let path = CString::new("m/44'").unwrap(); - - let mut indices_out: *mut u32 = ptr::null_mut(); - let mut hardened_out: *mut bool = ptr::null_mut(); - let mut count_out: usize = 0; - - let success = unsafe { - derivation_path_parse( - path.as_ptr(), - &mut indices_out, - &mut hardened_out, - &mut count_out, - &mut error, - ) - }; - - assert!(success); - assert_eq!(count_out, 1); - - let indices = unsafe { std::slice::from_raw_parts(indices_out, count_out) }; - let hardened = unsafe { std::slice::from_raw_parts(hardened_out, count_out) }; - - assert_eq!(indices[0], 44); - assert!(hardened[0]); - - // Clean up - unsafe { - derivation_path_free(indices_out, hardened_out, count_out); - } - - // Test mixed hardened and non-hardened - let path = CString::new("m/1'/2/3'").unwrap(); - - let success = unsafe { - derivation_path_parse( - path.as_ptr(), - &mut indices_out, - &mut hardened_out, - &mut count_out, - &mut error, - ) - }; - - assert!(success); - assert_eq!(count_out, 3); - - let indices = unsafe { std::slice::from_raw_parts(indices_out, count_out) }; - let hardened = unsafe { std::slice::from_raw_parts(hardened_out, count_out) }; - - assert_eq!(indices[0], 1); - assert!(hardened[0]); - assert_eq!(indices[1], 2); - assert!(!hardened[1]); - assert_eq!(indices[2], 3); - assert!(hardened[2]); - - // Clean up - unsafe { - derivation_path_free(indices_out, hardened_out, count_out); - } - } - - #[test] - fn test_wallet_get_account_xpub_edge_cases() { - let mut error = FFIError::default(); - - // Create a wallet - let mnemonic = CString::new("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").unwrap(); - - let wallet = unsafe { - wallet::wallet_create_from_mnemonic(mnemonic.as_ptr(), FFINetwork::Testnet, &mut error) - }; - assert!(!wallet.is_null()); - - // Test different account indices - for account_index in 0..3 { - let xpub_str = unsafe { wallet_get_account_xpub(wallet, account_index, &mut error) }; - - if !xpub_str.is_null() { - let xpub = unsafe { CStr::from_ptr(xpub_str).to_str().unwrap() }; - assert!(xpub.starts_with("tpub")); // Testnet public key - - // Clean up - unsafe { - crate::utils::string_free(xpub_str); - } - } - } - - // Test with null wallet - let xpub_str = unsafe { wallet_get_account_xpub(ptr::null(), 0, &mut error) }; - - assert!(xpub_str.is_null()); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - - // Clean up - unsafe { - wallet::wallet_free(wallet); - } - } - - #[test] - fn test_wallet_derive_public_key_different_paths() { - let mut error = FFIError::default(); - - // Create a wallet - let mnemonic = CString::new("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").unwrap(); - - let wallet = unsafe { - wallet::wallet_create_from_mnemonic(mnemonic.as_ptr(), FFINetwork::Testnet, &mut error) - }; - - // Test different derivation paths (Dash coin type 5) - let test_paths = [ - "m/44'/1'/0'/0/0", - "m/44'/1'/0'/0/1", - "m/44'/1'/0'/1/0", // Change address - "m/44'/1'/1'/0/0", // Different account - ]; - - for path_str in test_paths.iter() { - let path = CString::new(*path_str).unwrap(); - - let pubkey_ptr = unsafe { wallet_derive_public_key(wallet, path.as_ptr(), &mut error) }; - - if !pubkey_ptr.is_null() { - // Get hex representation to verify - let hex_str = unsafe { public_key_to_hex(pubkey_ptr, &mut error) }; - assert!(!hex_str.is_null()); - - let hex = unsafe { CStr::from_ptr(hex_str).to_str().unwrap() }; - // Public key should start with 02 or 03 (compressed) - assert!(hex.starts_with("02") || hex.starts_with("03")); - assert_eq!(hex.len(), 66); // 33 bytes * 2 hex chars - - // Clean up - if !hex_str.is_null() { - unsafe { - crate::utils::string_free(hex_str); - } - } - unsafe { - public_key_free(pubkey_ptr); - } - } - } - - // Clean up - unsafe { - wallet::wallet_free(wallet); - } - } - - #[test] - fn test_derivation_path_free_edge_cases() { - // Test freeing null pointers - unsafe { - derivation_path_free(ptr::null_mut(), ptr::null_mut(), 0); - } - } -} diff --git a/key-wallet-ffi/src/lib.rs b/key-wallet-ffi/src/lib.rs deleted file mode 100644 index ec406b33d..000000000 --- a/key-wallet-ffi/src/lib.rs +++ /dev/null @@ -1,63 +0,0 @@ -//! FFI bindings for key-wallet library -//! -//! This library provides C-compatible FFI bindings for the key-wallet Rust library. -//! It does not use uniffi and instead provides direct extern "C" functions. - -// Module declarations -pub mod account; -pub mod account_collection; -pub mod account_derivation; -pub mod address; -pub mod address_pool; -pub mod derivation; -pub mod error; -pub mod keys; -pub mod managed_account; -pub mod managed_account_collection; -pub mod managed_wallet; -pub mod mnemonic; -pub mod transaction; -pub mod transaction_checking; -pub mod types; -pub mod utils; -pub mod utxo; -pub mod wallet; -pub mod wallet_manager; - -#[cfg(feature = "bip38")] -pub mod bip38; - -// Test modules are now included in each source file - -// Re-export main types for convenience -pub use error::{FFIError, FFIErrorCode}; -pub use types::{FFIBalance, FFIWallet}; -pub use utxo::FFIUTXO; -pub use wallet_manager::{ - wallet_manager_create, wallet_manager_describe, wallet_manager_free, - wallet_manager_free_string, wallet_manager_free_wallet_ids, wallet_manager_get_wallet, - wallet_manager_get_wallet_balance, wallet_manager_get_wallet_ids, wallet_manager_wallet_count, - FFIWalletManager, -}; - -// ============================================================================ -// Initialization and Version -// ============================================================================ - -use std::os::raw::c_char; - -/// Initialize the library -#[no_mangle] -pub extern "C" fn key_wallet_ffi_initialize() -> bool { - // Any global initialization - true -} - -/// Get library version -/// -/// Returns a static string that should NOT be freed by the caller -#[no_mangle] -pub extern "C" fn key_wallet_ffi_version() -> *const c_char { - // Use a static CStr to avoid allocation and ensure the string is never freed - concat!(env!("CARGO_PKG_VERSION"), "\0").as_ptr() as *const c_char -} diff --git a/key-wallet-ffi/src/managed_account.rs b/key-wallet-ffi/src/managed_account.rs deleted file mode 100644 index bf8e3e8c0..000000000 --- a/key-wallet-ffi/src/managed_account.rs +++ /dev/null @@ -1,2679 +0,0 @@ -//! Managed account FFI bindings -//! -//! This module provides FFI-compatible managed account functionality that wraps -//! ManagedAccount instances from the key-wallet crate. FFIManagedCoreAccount is a -//! simple wrapper around `Arc` without additional fields. - -use dash_network::ffi::FFINetwork; -use dashcore::hashes::Hash; -use std::os::raw::{c_char, c_uint}; -#[cfg(feature = "keep-finalized-transactions")] -use std::ptr::slice_from_raw_parts_mut; -use std::sync::Arc; - -use crate::address_pool::{FFIAddressPool, FFIAddressPoolType}; -use crate::error::{FFIError, FFIErrorCode}; -use crate::types::{ - FFIAccountKind, FFIInputDetail, FFIOutputDetail, FFITransactionContext, - FFITransactionDirection, FFITransactionType, -}; -use crate::wallet_manager::FFIWalletManager; -use crate::{check_ptr, deref_ptr}; -use key_wallet::account::account_collection::{DashpayAccountKey, PlatformPaymentAccountKey}; -use key_wallet::account::TransactionRecord; -use key_wallet::managed_account::address_pool::AddressPool; -use key_wallet::managed_account::managed_account_trait::ManagedAccountTrait; -use key_wallet::managed_account::managed_platform_account::ManagedPlatformAccount; -use key_wallet::managed_account::{ManagedCoreFundsAccount, ManagedCoreKeysAccount}; -use key_wallet::AccountType; - -/// Internal handle variant: a funds-bearing account or a keys-only account. -pub(crate) enum FFIManagedCoreAccountInner { - Funds(Arc), - Keys(Arc), -} - -/// Opaque managed account handle. -/// -/// Wraps either a [`ManagedCoreFundsAccount`] (Standard, CoinJoin, DashPay) -/// or a [`ManagedCoreKeysAccount`] (identity, asset-lock, provider). Funds-only -/// accessors (`get_balance`, `get_utxo_count`, …) return zero / null / -/// false on the keys variant; trait-shared accessors (`get_network`, -/// `get_account_type`, address pools, transactions) work on both. -pub struct FFIManagedCoreAccount { - pub(crate) inner: FFIManagedCoreAccountInner, -} - -impl FFIManagedCoreAccount { - /// Create a new FFI managed account handle wrapping a funds-bearing account. - pub fn new(account: &ManagedCoreFundsAccount) -> Self { - FFIManagedCoreAccount { - inner: FFIManagedCoreAccountInner::Funds(Arc::new(account.clone())), - } - } - - /// Create a new FFI managed account handle wrapping a keys-only account. - pub fn new_keys(account: &ManagedCoreKeysAccount) -> Self { - FFIManagedCoreAccount { - inner: FFIManagedCoreAccountInner::Keys(Arc::new(account.clone())), - } - } - - /// Returns the funds-bearing account if this handle wraps one, `None` - /// otherwise. Use this in funds-only FFI entry points. - pub fn as_funds(&self) -> Option<&ManagedCoreFundsAccount> { - match &self.inner { - FFIManagedCoreAccountInner::Funds(a) => Some(a.as_ref()), - FFIManagedCoreAccountInner::Keys(_) => None, - } - } - - /// Returns the keys-only account if this handle wraps one, `None` - /// otherwise. - pub fn as_keys(&self) -> Option<&ManagedCoreKeysAccount> { - match &self.inner { - FFIManagedCoreAccountInner::Funds(_) => None, - FFIManagedCoreAccountInner::Keys(a) => Some(a.as_ref()), - } - } - - /// Returns the inner [`ManagedCoreKeysAccount`] regardless of variant — - /// for the funds-bearing variant this returns the composed-inner keys - /// account; for the keys-only variant it returns the account itself. - /// Use this when the desired data lives on the shared keys-account state - /// (address pools, transactions, network, monitor revision). - pub fn keys_account(&self) -> &ManagedCoreKeysAccount { - match &self.inner { - FFIManagedCoreAccountInner::Funds(a) => a.keys(), - FFIManagedCoreAccountInner::Keys(a) => a.as_ref(), - } - } -} - -/// Opaque managed platform account handle that wraps ManagedPlatformAccount -/// -/// This is different from FFIManagedCoreAccount because ManagedPlatformAccount -/// has a different structure optimized for Platform Payment accounts (DIP-17): -/// - Simple u64 credit balance instead of WalletCoreBalance -/// - Per-address balances tracked directly -/// - No transactions or UTXOs (Platform handles these) -pub struct FFIManagedPlatformAccount { - /// The underlying managed platform account - pub(crate) account: Arc, -} - -impl FFIManagedPlatformAccount { - /// Create a new FFI managed platform account handle - pub fn new(account: &ManagedPlatformAccount) -> Self { - FFIManagedPlatformAccount { - account: Arc::new(account.clone()), - } - } - - /// Get a reference to the inner managed platform account - pub fn inner(&self) -> &ManagedPlatformAccount { - self.account.as_ref() - } -} - -/// FFI Result type for ManagedPlatformAccount operations -#[repr(C)] -pub struct FFIManagedPlatformAccountResult { - /// The managed platform account handle if successful, NULL if error - pub account: *mut FFIManagedPlatformAccount, - /// Error code (0 = success) - pub error_code: i32, - /// Error message (NULL if success, must be freed by caller if not NULL) - pub error_message: *mut std::os::raw::c_char, -} - -impl FFIManagedPlatformAccountResult { - /// Create a success result - pub fn success(account: *mut FFIManagedPlatformAccount) -> Self { - FFIManagedPlatformAccountResult { - account, - error_code: 0, - error_message: std::ptr::null_mut(), - } - } - - /// Create an error result - pub fn error(code: FFIErrorCode, message: String) -> Self { - use std::ffi::CString; - let c_message = CString::new(message).unwrap_or_else(|_| { - CString::new("Unknown error").expect("Hardcoded string should never fail") - }); - FFIManagedPlatformAccountResult { - account: std::ptr::null_mut(), - error_code: code as i32, - error_message: c_message.into_raw(), - } - } -} - -/// C-compatible platform payment account key -#[repr(C)] -#[derive(Debug, Clone, Copy)] -pub struct FFIPlatformPaymentAccountKey { - /// Account index (hardened) - pub account: c_uint, - /// Key class (hardened) - pub key_class: c_uint, -} - -impl From<&PlatformPaymentAccountKey> for FFIPlatformPaymentAccountKey { - fn from(key: &PlatformPaymentAccountKey) -> Self { - FFIPlatformPaymentAccountKey { - account: key.account, - key_class: key.key_class, - } - } -} - -impl From for PlatformPaymentAccountKey { - fn from(key: FFIPlatformPaymentAccountKey) -> Self { - PlatformPaymentAccountKey { - account: key.account, - key_class: key.key_class, - } - } -} - -/// FFI Result type for ManagedAccount operations -#[repr(C)] -pub struct FFIManagedCoreAccountResult { - /// The managed account handle if successful, NULL if error - pub account: *mut FFIManagedCoreAccount, - /// Error code (0 = success) - pub error_code: i32, - /// Error message (NULL if success, must be freed by caller if not NULL) - pub error_message: *mut std::os::raw::c_char, -} - -impl FFIManagedCoreAccountResult { - /// Create a success result - pub fn success(account: *mut FFIManagedCoreAccount) -> Self { - FFIManagedCoreAccountResult { - account, - error_code: 0, - error_message: std::ptr::null_mut(), - } - } - - /// Create an error result - pub fn error(code: FFIErrorCode, message: String) -> Self { - use std::ffi::CString; - let c_message = CString::new(message).unwrap_or_else(|_| { - CString::new("Unknown error").expect("Hardcoded string should never fail") - }); - FFIManagedCoreAccountResult { - account: std::ptr::null_mut(), - error_code: code as i32, - error_message: c_message.into_raw(), - } - } -} - -/// Get a managed account from a managed wallet -/// -/// This function gets a ManagedAccount from the wallet manager's managed wallet info, -/// returning a managed account handle that wraps the ManagedAccount. -/// -/// # Safety -/// -/// - `manager` must be a valid pointer to an FFIWalletManager instance -/// - `wallet_id` must be a valid pointer to a 32-byte wallet ID -/// - The caller must ensure all pointers remain valid for the duration of this call -/// - The returned account must be freed with `managed_core_account_free` when no longer needed -#[no_mangle] -pub unsafe extern "C" fn managed_wallet_get_account( - manager: *const FFIWalletManager, - wallet_id: *const u8, - account_index: c_uint, - account_type: FFIAccountKind, -) -> FFIManagedCoreAccountResult { - if manager.is_null() { - return FFIManagedCoreAccountResult::error( - FFIErrorCode::InvalidInput, - "Manager is null".to_string(), - ); - } - - if wallet_id.is_null() { - return FFIManagedCoreAccountResult::error( - FFIErrorCode::InvalidInput, - "Wallet ID is null".to_string(), - ); - } - - // Get the managed wallet info from the manager - let mut error = FFIError::default(); - let managed_wallet_ptr = crate::wallet_manager::wallet_manager_get_managed_wallet_info( - manager, wallet_id, &mut error, - ); - - if managed_wallet_ptr.is_null() { - return FFIManagedCoreAccountResult::error( - error.code, - if error.message.is_null() { - "Failed to get managed wallet info".to_string() - } else { - let c_str = std::ffi::CStr::from_ptr(error.message); - c_str.to_string_lossy().to_string() - }, - ); - } - - let managed_wallet = &*managed_wallet_ptr; - let account_type_rust = account_type.to_account_type(account_index); - - let result = { - use key_wallet::account::StandardAccountType; - - let managed_collection = &managed_wallet.inner().accounts; - let ffi_account: Option = match account_type_rust { - AccountType::Standard { - index, - standard_account_type, - } => match standard_account_type { - StandardAccountType::BIP44Account => managed_collection - .standard_bip44_accounts - .get(&index) - .map(FFIManagedCoreAccount::new), - StandardAccountType::BIP32Account => managed_collection - .standard_bip32_accounts - .get(&index) - .map(FFIManagedCoreAccount::new), - }, - AccountType::CoinJoin { - index, - } => managed_collection.coinjoin_accounts.get(&index).map(FFIManagedCoreAccount::new), - AccountType::IdentityRegistration => managed_collection - .identity_registration - .as_ref() - .map(FFIManagedCoreAccount::new_keys), - AccountType::IdentityTopUp { - registration_index, - } => managed_collection - .identity_topup - .get(®istration_index) - .map(FFIManagedCoreAccount::new_keys), - AccountType::IdentityTopUpNotBoundToIdentity => managed_collection - .identity_topup_not_bound - .as_ref() - .map(FFIManagedCoreAccount::new_keys), - AccountType::IdentityInvitation => { - managed_collection.identity_invitation.as_ref().map(FFIManagedCoreAccount::new_keys) - } - AccountType::AssetLockAddressTopUp => managed_collection - .asset_lock_address_topup - .as_ref() - .map(FFIManagedCoreAccount::new_keys), - AccountType::AssetLockShieldedAddressTopUp => managed_collection - .asset_lock_shielded_address_topup - .as_ref() - .map(FFIManagedCoreAccount::new_keys), - AccountType::ProviderVotingKeys => managed_collection - .provider_voting_keys - .as_ref() - .map(FFIManagedCoreAccount::new_keys), - AccountType::ProviderOwnerKeys => { - managed_collection.provider_owner_keys.as_ref().map(FFIManagedCoreAccount::new_keys) - } - AccountType::ProviderOperatorKeys => managed_collection - .provider_operator_keys - .as_ref() - .map(FFIManagedCoreAccount::new_keys), - AccountType::ProviderPlatformKeys => managed_collection - .provider_platform_keys - .as_ref() - .map(FFIManagedCoreAccount::new_keys), - AccountType::DashpayReceivingFunds { - .. - } - | AccountType::DashpayExternalAccount { - .. - } - | AccountType::PlatformPayment { - .. - } => None, - }; - - match ffi_account { - Some(ffi_account) => { - FFIManagedCoreAccountResult::success(Box::into_raw(Box::new(ffi_account))) - } - None => FFIManagedCoreAccountResult::error( - FFIErrorCode::NotFound, - "Account not found".to_string(), - ), - } - }; - - // Clean up the managed wallet pointer - crate::managed_wallet::managed_wallet_info_free(managed_wallet_ptr); - - result -} - -/// Get a managed IdentityTopUp account with a specific registration index -/// -/// This is used for top-up accounts that are bound to a specific identity. -/// Returns a managed account handle that wraps the ManagedAccount. -/// -/// # Safety -/// -/// - `manager` must be a valid pointer to an FFIWalletManager instance -/// - `wallet_id` must be a valid pointer to a 32-byte wallet ID -/// - The caller must ensure all pointers remain valid for the duration of this call -/// - The returned account must be freed with `managed_core_account_free` when no longer needed -#[no_mangle] -pub unsafe extern "C" fn managed_wallet_get_top_up_account_with_registration_index( - manager: *const FFIWalletManager, - wallet_id: *const u8, - registration_index: c_uint, -) -> FFIManagedCoreAccountResult { - if manager.is_null() { - return FFIManagedCoreAccountResult::error( - FFIErrorCode::InvalidInput, - "Manager is null".to_string(), - ); - } - - if wallet_id.is_null() { - return FFIManagedCoreAccountResult::error( - FFIErrorCode::InvalidInput, - "Wallet ID is null".to_string(), - ); - } - - // Get the managed wallet info from the manager - let mut error = FFIError::default(); - let managed_wallet_ptr = crate::wallet_manager::wallet_manager_get_managed_wallet_info( - manager, wallet_id, &mut error, - ); - - if managed_wallet_ptr.is_null() { - return FFIManagedCoreAccountResult::error( - error.code, - if error.message.is_null() { - "Failed to get managed wallet info".to_string() - } else { - let c_str = std::ffi::CStr::from_ptr(error.message); - c_str.to_string_lossy().to_string() - }, - ); - } - - let managed_wallet = &*managed_wallet_ptr; - - let result = match managed_wallet.inner().accounts.identity_topup.get(®istration_index) { - Some(account) => { - let ffi_account = FFIManagedCoreAccount::new_keys(account); - FFIManagedCoreAccountResult::success(Box::into_raw(Box::new(ffi_account))) - } - None => FFIManagedCoreAccountResult::error( - FFIErrorCode::NotFound, - format!( - "IdentityTopUp account for registration index {} not found", - registration_index - ), - ), - }; - - // Clean up the managed wallet pointer - crate::managed_wallet::managed_wallet_info_free(managed_wallet_ptr); - - result -} - -/// Get a managed DashPay receiving funds account by composite key -/// -/// # Safety -/// - `manager`, `wallet_id` must be valid -/// - `user_identity_id` and `friend_identity_id` must each point to 32 bytes -#[no_mangle] -pub unsafe extern "C" fn managed_wallet_get_dashpay_receiving_account( - manager: *const FFIWalletManager, - wallet_id: *const u8, - account_index: c_uint, - user_identity_id: *const u8, - friend_identity_id: *const u8, -) -> FFIManagedCoreAccountResult { - if manager.is_null() - || wallet_id.is_null() - || user_identity_id.is_null() - || friend_identity_id.is_null() - { - return FFIManagedCoreAccountResult::error( - FFIErrorCode::InvalidInput, - "Null pointer provided".to_string(), - ); - } - let mut user_id = [0u8; 32]; - let mut friend_id = [0u8; 32]; - core::ptr::copy_nonoverlapping(user_identity_id, user_id.as_mut_ptr(), 32); - core::ptr::copy_nonoverlapping(friend_identity_id, friend_id.as_mut_ptr(), 32); - let key = DashpayAccountKey { - index: account_index, - user_identity_id: user_id, - friend_identity_id: friend_id, - }; - - let mut error = FFIError::default(); - let managed_wallet_ptr = crate::wallet_manager::wallet_manager_get_managed_wallet_info( - manager, wallet_id, &mut error, - ); - if managed_wallet_ptr.is_null() { - return FFIManagedCoreAccountResult::error( - error.code, - if error.message.is_null() { - "Failed to get managed wallet info".to_string() - } else { - std::ffi::CStr::from_ptr(error.message).to_string_lossy().to_string() - }, - ); - } - let managed_wallet = &*managed_wallet_ptr; - - let result = match managed_wallet.inner().accounts.dashpay_receival_accounts.get(&key) { - Some(account) => FFIManagedCoreAccountResult::success(Box::into_raw(Box::new( - FFIManagedCoreAccount::new(account), - ))), - None => FFIManagedCoreAccountResult::error( - FFIErrorCode::NotFound, - "Account not found".to_string(), - ), - }; - crate::managed_wallet::managed_wallet_info_free(managed_wallet_ptr); - result -} - -/// Get a managed DashPay external account by composite key -/// -/// # Safety -/// - Pointers must be valid -#[no_mangle] -pub unsafe extern "C" fn managed_wallet_get_dashpay_external_account( - manager: *const FFIWalletManager, - wallet_id: *const u8, - account_index: c_uint, - user_identity_id: *const u8, - friend_identity_id: *const u8, -) -> FFIManagedCoreAccountResult { - if manager.is_null() - || wallet_id.is_null() - || user_identity_id.is_null() - || friend_identity_id.is_null() - { - return FFIManagedCoreAccountResult::error( - FFIErrorCode::InvalidInput, - "Null pointer provided".to_string(), - ); - } - let mut user_id = [0u8; 32]; - let mut friend_id = [0u8; 32]; - core::ptr::copy_nonoverlapping(user_identity_id, user_id.as_mut_ptr(), 32); - core::ptr::copy_nonoverlapping(friend_identity_id, friend_id.as_mut_ptr(), 32); - let key = DashpayAccountKey { - index: account_index, - user_identity_id: user_id, - friend_identity_id: friend_id, - }; - - let mut error = FFIError::default(); - let managed_wallet_ptr = crate::wallet_manager::wallet_manager_get_managed_wallet_info( - manager, wallet_id, &mut error, - ); - if managed_wallet_ptr.is_null() { - return FFIManagedCoreAccountResult::error( - error.code, - if error.message.is_null() { - "Failed to get managed wallet info".to_string() - } else { - std::ffi::CStr::from_ptr(error.message).to_string_lossy().to_string() - }, - ); - } - let managed_wallet = &*managed_wallet_ptr; - - let result = match managed_wallet.inner().accounts.dashpay_external_accounts.get(&key) { - Some(account) => FFIManagedCoreAccountResult::success(Box::into_raw(Box::new( - FFIManagedCoreAccount::new(account), - ))), - None => FFIManagedCoreAccountResult::error( - FFIErrorCode::NotFound, - "Account not found".to_string(), - ), - }; - crate::managed_wallet::managed_wallet_info_free(managed_wallet_ptr); - result -} - -/// Get the network of a managed account -/// -/// # Safety -/// -/// - `account` must be a valid pointer to an FFIManagedCoreAccount instance -/// - Returns `FFINetwork::Mainnet` if the account is null -#[no_mangle] -pub unsafe extern "C" fn managed_core_account_get_network( - account: *const FFIManagedCoreAccount, -) -> FFINetwork { - if account.is_null() { - return FFINetwork::Mainnet; - } - - let account = &*account; - account.keys_account().network().into() -} - -/// Get the parent wallet ID of a managed account -/// -/// Note: ManagedAccount doesn't store the parent wallet ID directly. -/// The wallet ID is typically known from the context (e.g., when getting the account from a managed wallet). -/// -/// # Safety -/// -/// - `wallet_id` must be a valid pointer to a 32-byte wallet ID buffer that was provided by the caller -/// - The returned pointer is the same as the input pointer for convenience -/// - The caller must not free the returned pointer as it's the same as the input -#[no_mangle] -pub unsafe extern "C" fn managed_core_account_get_parent_wallet_id( - wallet_id: *const u8, -) -> *const u8 { - // Simply return the wallet_id that was passed in - // This function exists for API consistency but ManagedAccount doesn't store parent wallet ID - wallet_id -} - -/// Get the account type of a managed account -/// -/// # Safety -/// -/// - `account` must be a valid pointer to an FFIManagedCoreAccount instance -/// - `index_out` must be a valid pointer to receive the account index (or null) -#[no_mangle] -pub unsafe extern "C" fn managed_core_account_get_account_type( - account: *const FFIManagedCoreAccount, - index_out: *mut c_uint, -) -> FFIAccountKind { - if account.is_null() { - return FFIAccountKind::StandardBIP44; // Default type - } - - let account = &*account; - let managed_account = account.keys_account(); - let account_type_rust = managed_account.managed_account_type().to_account_type(); - - // Set the index if output pointer is provided - if !index_out.is_null() { - *index_out = account_type_rust.index().unwrap_or(0); - } - - // Convert to FFI account type - match account_type_rust { - AccountType::Standard { - standard_account_type, - .. - } => { - use key_wallet::account::StandardAccountType; - match standard_account_type { - StandardAccountType::BIP44Account => FFIAccountKind::StandardBIP44, - StandardAccountType::BIP32Account => FFIAccountKind::StandardBIP32, - } - } - AccountType::CoinJoin { - .. - } => FFIAccountKind::CoinJoin, - AccountType::IdentityRegistration => FFIAccountKind::IdentityRegistration, - AccountType::IdentityTopUp { - .. - } => FFIAccountKind::IdentityTopUp, - AccountType::IdentityTopUpNotBoundToIdentity => { - FFIAccountKind::IdentityTopUpNotBoundToIdentity - } - AccountType::IdentityInvitation => FFIAccountKind::IdentityInvitation, - AccountType::AssetLockAddressTopUp => FFIAccountKind::AssetLockAddressTopUp, - AccountType::AssetLockShieldedAddressTopUp => FFIAccountKind::AssetLockShieldedAddressTopUp, - AccountType::ProviderVotingKeys => FFIAccountKind::ProviderVotingKeys, - AccountType::ProviderOwnerKeys => FFIAccountKind::ProviderOwnerKeys, - AccountType::ProviderOperatorKeys => FFIAccountKind::ProviderOperatorKeys, - AccountType::ProviderPlatformKeys => FFIAccountKind::ProviderPlatformKeys, - AccountType::DashpayReceivingFunds { - .. - } => FFIAccountKind::DashpayReceivingFunds, - AccountType::DashpayExternalAccount { - .. - } => FFIAccountKind::DashpayExternalAccount, - AccountType::PlatformPayment { - .. - } => FFIAccountKind::PlatformPayment, - } -} - -/// Get the balance of a managed account. -/// -/// Returns `false` (and leaves `balance_out` untouched) when the handle wraps -/// a keys-only account (identity / asset-lock / provider) — those don't track -/// per-account balances. Use [`managed_core_account_get_account_type`] to -/// disambiguate, or only call this for funds-bearing accounts. -/// -/// # Safety -/// -/// - `account` must be a valid pointer to an FFIManagedCoreAccount instance -/// - `balance_out` must be a valid pointer to an FFIBalance structure -#[no_mangle] -pub unsafe extern "C" fn managed_core_account_get_balance( - account: *const FFIManagedCoreAccount, - balance_out: *mut crate::types::FFIBalance, -) -> bool { - if account.is_null() || balance_out.is_null() { - return false; - } - - let account = &*account; - let Some(funds) = account.as_funds() else { - return false; - }; - let balance = funds.balance; - - *balance_out = crate::types::FFIBalance { - confirmed: balance.confirmed(), - unconfirmed: balance.unconfirmed(), - immature: balance.immature(), - locked: balance.locked(), - total: balance.total(), - }; - - true -} - -#[cfg(feature = "keep-finalized-transactions")] -/// Get the number of transactions in a managed account -/// -/// Only available with the `keep-finalized-transactions` Cargo feature. With -/// the feature off (the default), records of chainlocked transactions are -/// dropped from the in-memory map, so the count would not reflect the full -/// history — the function is intentionally not exposed. -/// -/// # Safety -/// -/// - `account` must be a valid pointer to an FFIManagedCoreAccount instance -#[no_mangle] -pub unsafe extern "C" fn managed_core_account_get_transaction_count( - account: *const FFIManagedCoreAccount, -) -> c_uint { - if account.is_null() { - return 0; - } - - let account = &*account; - account.keys_account().transactions().len() as c_uint -} - -/// Get the number of UTXOs in a managed account. -/// -/// Always returns 0 for keys-only accounts (identity / asset-lock / provider), -/// which do not track per-account UTXOs. -/// -/// # Safety -/// -/// - `account` must be a valid pointer to an FFIManagedCoreAccount instance -#[no_mangle] -pub unsafe extern "C" fn managed_core_account_get_utxo_count( - account: *const FFIManagedCoreAccount, -) -> c_uint { - if account.is_null() { - return 0; - } - - let account = &*account; - account.as_funds().map_or(0, |f| f.utxos.len() as c_uint) -} - -/// FFI-compatible owning-account descriptor for a [`FFITransactionRecord`]. -/// -/// Mirrors the Rust-side `TransactionRecord::account_type`. `kind` is the -/// discriminant; `index` is the primary index (`0` for variants that have no -/// meaningful primary index — identity-singletons, provider-key, asset-lock); -/// `index_secondary` carries the secondary index (`registration_index` for -/// `IdentityTopUp`, `key_class` for `PlatformPayment`) or `-1` when not -/// applicable. The `identity_user` and `identity_friend` pointers are non-null -/// only for the Dashpay variants and point to 32-byte identity hashes owned by -/// this struct (freed by its `Drop` impl). `key_class` is `-1` unless -/// this is a `PlatformPayment` record, in which case it carries the `key_class` -/// hardened index (also exposed in `index_secondary` for symmetry with the -/// existing FFI tuple contract). -#[repr(C)] -pub struct FFIAccountType { - /// Discriminant identifying the owning account variant. - pub kind: FFIAccountKind, - /// Primary account index for variants that carry one. - pub index: u32, - /// Secondary account index when applicable, `-1` otherwise. - pub index_secondary: i32, - /// Pointer to the 32-byte `user_identity_id` of the Dashpay account that - /// owns this record, null when the account is not a Dashpay variant. The - /// pointee is owned by this struct and freed when it is dropped. - pub identity_user: *const [u8; 32], - /// Pointer to the 32-byte `friend_identity_id` of the Dashpay account - /// that owns this record, null when the account is not a Dashpay variant. - /// The pointee is owned by this struct and freed when it is dropped. - pub identity_friend: *const [u8; 32], - /// `PlatformPayment` `key_class` hardened index, `-1` for any other - /// account variant. Mirrors `index_secondary` for `PlatformPayment`. - pub key_class: i32, -} - -impl From<&AccountType> for FFIAccountType { - fn from(account_type: &AccountType) -> Self { - use key_wallet::account::StandardAccountType; - let (kind, index, index_secondary) = match *account_type { - AccountType::Standard { - index, - standard_account_type: StandardAccountType::BIP44Account, - } => (FFIAccountKind::StandardBIP44, index, -1), - AccountType::Standard { - index, - standard_account_type: StandardAccountType::BIP32Account, - } => (FFIAccountKind::StandardBIP32, index, -1), - AccountType::CoinJoin { - index, - } => (FFIAccountKind::CoinJoin, index, -1), - AccountType::IdentityRegistration => (FFIAccountKind::IdentityRegistration, 0, -1), - AccountType::IdentityTopUp { - registration_index, - } => (FFIAccountKind::IdentityTopUp, 0, registration_index as i32), - AccountType::IdentityTopUpNotBoundToIdentity => { - (FFIAccountKind::IdentityTopUpNotBoundToIdentity, 0, -1) - } - AccountType::IdentityInvitation => (FFIAccountKind::IdentityInvitation, 0, -1), - AccountType::AssetLockAddressTopUp => (FFIAccountKind::AssetLockAddressTopUp, 0, -1), - AccountType::AssetLockShieldedAddressTopUp => { - (FFIAccountKind::AssetLockShieldedAddressTopUp, 0, -1) - } - AccountType::ProviderVotingKeys => (FFIAccountKind::ProviderVotingKeys, 0, -1), - AccountType::ProviderOwnerKeys => (FFIAccountKind::ProviderOwnerKeys, 0, -1), - AccountType::ProviderOperatorKeys => (FFIAccountKind::ProviderOperatorKeys, 0, -1), - AccountType::ProviderPlatformKeys => (FFIAccountKind::ProviderPlatformKeys, 0, -1), - AccountType::DashpayReceivingFunds { - index, - .. - } => (FFIAccountKind::DashpayReceivingFunds, index, -1), - AccountType::DashpayExternalAccount { - index, - .. - } => (FFIAccountKind::DashpayExternalAccount, index, -1), - AccountType::PlatformPayment { - account, - key_class, - } => (FFIAccountKind::PlatformPayment, account, key_class as i32), - }; - - let (identity_user, identity_friend) = match *account_type { - AccountType::DashpayReceivingFunds { - user_identity_id, - friend_identity_id, - .. - } - | AccountType::DashpayExternalAccount { - user_identity_id, - friend_identity_id, - .. - } => ( - Box::into_raw(Box::new(user_identity_id)) as *const [u8; 32], - Box::into_raw(Box::new(friend_identity_id)) as *const [u8; 32], - ), - _ => (std::ptr::null(), std::ptr::null()), - }; - - let key_class = match *account_type { - AccountType::PlatformPayment { - key_class, - .. - } => key_class as i32, - _ => -1, - }; - - FFIAccountType { - kind, - index, - index_secondary, - identity_user, - identity_friend, - key_class, - } - } -} - -impl Drop for FFIAccountType { - fn drop(&mut self) { - if !self.identity_user.is_null() { - let _ = unsafe { Box::from_raw(self.identity_user as *mut [u8; 32]) }; - self.identity_user = std::ptr::null(); - } - if !self.identity_friend.is_null() { - let _ = unsafe { Box::from_raw(self.identity_friend as *mut [u8; 32]) }; - self.identity_friend = std::ptr::null(); - } - } -} - -/// FFI-compatible transaction record -/// -/// Heap-allocated fields are freed automatically when the record is dropped -/// (see `Drop` impl below). -#[repr(C)] -pub struct FFITransactionRecord { - /// Transaction ID (32 bytes) - pub txid: [u8; 32], - /// Net amount for this account (positive = received, negative = sent) - pub net_amount: i64, - /// Transaction context (mempool, instant-send, in-block, chain-locked + block info) - pub context: FFITransactionContext, - /// Classified transaction type - pub transaction_type: FFITransactionType, - /// Direction of the transaction relative to the wallet - pub direction: FFITransactionDirection, - /// Fee if known, 0 if unknown - pub fee: u64, - /// Owning-account descriptor (discriminant + indices + identity ids). - pub account_type: FFIAccountType, - /// Input details array - pub input_details: *mut FFIInputDetail, - /// Number of input details - pub input_details_count: usize, - /// Output details array - pub output_details: *mut FFIOutputDetail, - /// Number of output details - pub output_details_count: usize, - /// Consensus-serialized transaction bytes - pub tx_data: *mut u8, - /// Length of `tx_data` - pub tx_len: usize, - /// Optional label (null if not set) - pub label: *mut c_char, -} - -impl From<&TransactionRecord> for FFITransactionRecord { - fn from(value: &TransactionRecord) -> Self { - let txid = value.txid.to_byte_array(); - let net_amount = value.net_amount; - let context = FFITransactionContext::from(value.context.clone()); - let transaction_type = FFITransactionType::from(value.transaction_type); - let direction = FFITransactionDirection::from(value.direction); - let fee = value.fee.unwrap_or(0); - - let account_type = FFIAccountType::from(&value.account_type); - - // Serialize transaction bytes - let tx_slice = dashcore::consensus::serialize(&value.transaction).into_boxed_slice(); - let tx_len = tx_slice.len(); - let tx_data = if tx_slice.is_empty() { - std::ptr::null_mut() - } else { - Box::into_raw(tx_slice) as *mut u8 - }; - - // Input details - let input_slice: Box<[FFIInputDetail]> = - value.input_details.iter().map(|d| d.into()).collect::>().into_boxed_slice(); - let input_details_count = input_slice.len(); - let input_details = if input_slice.is_empty() { - std::ptr::null_mut() - } else { - Box::into_raw(input_slice) as *mut FFIInputDetail - }; - - // Label - let label = if value.label.is_empty() { - std::ptr::null_mut() - } else { - std::ffi::CString::new(value.label.as_str()).unwrap_or_default().into_raw() - }; - - // Output details - let output_slice: Box<[FFIOutputDetail]> = - value.output_details.iter().map(|d| d.into()).collect::>().into_boxed_slice(); - let output_details_count = output_slice.len(); - let output_details = if output_slice.is_empty() { - std::ptr::null_mut() - } else { - Box::into_raw(output_slice) as *mut FFIOutputDetail - }; - - FFITransactionRecord { - txid, - net_amount, - context, - transaction_type, - direction, - fee, - account_type, - input_details, - input_details_count, - output_details, - output_details_count, - tx_data, - tx_len, - label, - } - } -} - -impl Drop for FFITransactionRecord { - fn drop(&mut self) { - if !self.input_details.is_null() && self.input_details_count > 0 { - let slice_ptr = - std::ptr::slice_from_raw_parts_mut(self.input_details, self.input_details_count); - let _ = unsafe { Box::from_raw(slice_ptr) }; - - self.input_details = std::ptr::null_mut(); - self.input_details_count = 0; - } - - if !self.output_details.is_null() && self.output_details_count > 0 { - let slice_ptr = - std::ptr::slice_from_raw_parts_mut(self.output_details, self.output_details_count); - let _ = unsafe { Box::from_raw(slice_ptr) }; - - self.output_details = std::ptr::null_mut(); - self.output_details_count = 0; - } - - if !self.tx_data.is_null() && self.tx_len > 0 { - let slice_ptr = std::ptr::slice_from_raw_parts_mut(self.tx_data, self.tx_len); - let _ = unsafe { Box::from_raw(slice_ptr) }; - - self.tx_data = std::ptr::null_mut(); - self.tx_len = 0; - } - - if !self.label.is_null() { - let _ = unsafe { std::ffi::CString::from_raw(self.label) }; - - self.label = std::ptr::null_mut(); - } - } -} - -#[cfg(feature = "keep-finalized-transactions")] -/// Get all transactions from a managed account -/// -/// Returns an array of FFITransactionRecord structures. -/// -/// Only available with the `keep-finalized-transactions` Cargo feature. With -/// the feature off (the default), records of chainlocked transactions are -/// dropped from the in-memory map, so this would only return a partial -/// history — the function is intentionally not exposed. -/// -/// # Safety -/// -/// - `account` must be a valid pointer to an FFIManagedCoreAccount instance -/// - `transactions_out` must be a valid pointer to receive the transactions array pointer -/// - `count_out` must be a valid pointer to receive the count -/// - The caller must free the returned array using `managed_core_account_free_transactions` -#[no_mangle] -pub unsafe extern "C" fn managed_core_account_get_transactions( - account: *const FFIManagedCoreAccount, - transactions_out: *mut *mut FFITransactionRecord, - count_out: *mut usize, -) -> bool { - if account.is_null() || transactions_out.is_null() || count_out.is_null() { - return false; - } - - let account = &*account; - let transactions = account.keys_account().transactions(); - - if transactions.is_empty() { - *transactions_out = std::ptr::null_mut(); - *count_out = 0; - return true; - } - - // Allocate array for transaction records - let ffi_tx = transactions.values().map(FFITransactionRecord::from).collect::>(); - - *count_out = ffi_tx.len(); - *transactions_out = Box::into_raw(ffi_tx.into_boxed_slice()) as *mut FFITransactionRecord; - true -} - -#[cfg(feature = "keep-finalized-transactions")] -/// Free transactions array returned by managed_core_account_get_transactions -/// -/// Only available with the `keep-finalized-transactions` Cargo feature, in -/// which configuration `managed_core_account_get_transactions` is also -/// available — the two functions are paired. -/// -/// # Safety -/// -/// - `transactions` must be a pointer returned by `managed_core_account_get_transactions` -/// - `count` must be the count returned by `managed_core_account_get_transactions` -/// - This function must only be called once per allocation -#[no_mangle] -pub unsafe extern "C" fn managed_core_account_free_transactions( - transactions: *mut FFITransactionRecord, - count: usize, -) { - if transactions.is_null() || count == 0 { - return; - } - - let _ = Box::from_raw(slice_from_raw_parts_mut(transactions, count)); -} - -/// Set or clear a label on a transaction record in the shared wallet manager state -/// -/// # Safety -/// -/// - `manager` must be a valid pointer to an FFIWalletManager instance -/// - `wallet_id` must be a valid pointer to a 32-byte wallet ID -/// - `txid` must be a valid pointer to a 32-byte transaction ID -/// - `label` must be a valid null-terminated UTF-8 string, or null to clear the label -/// - `error` must be a valid pointer to an FFIError structure -/// - The caller must ensure all pointers remain valid for the duration of this call -#[no_mangle] -pub unsafe extern "C" fn wallet_manager_set_transaction_label( - manager: *mut FFIWalletManager, - wallet_id: *const u8, - account_type: FFIAccountKind, - account_index: c_uint, - txid: *const u8, - label: *const c_char, - error: *mut FFIError, -) -> bool { - let manager_ref = deref_ptr!(manager, error, false); - check_ptr!(wallet_id, error, false); - check_ptr!(txid, error, false); - - let mut wallet_id_array = [0u8; 32]; - std::ptr::copy_nonoverlapping(wallet_id, wallet_id_array.as_mut_ptr(), 32); - - let mut txid_bytes = [0u8; 32]; - std::ptr::copy_nonoverlapping(txid, txid_bytes.as_mut_ptr(), 32); - let txid = dashcore::Txid::from_byte_array(txid_bytes); - - let account_type_rust = account_type.to_account_type(account_index); - - let new_label = if label.is_null() { - None - } else { - match std::ffi::CStr::from_ptr(label).to_str() { - Ok(s) => Some(s.to_owned()), - Err(e) => { - (*error).set(FFIErrorCode::InvalidInput, &format!("Invalid UTF-8 in label: {}", e)); - return false; - } - } - }; - - let result: Result<(), (FFIErrorCode, String)> = manager_ref.runtime.block_on(async { - let mut manager_guard = manager_ref.manager.write().await; - let wallet_info = manager_guard - .get_wallet_info_mut(&wallet_id_array) - .ok_or_else(|| (FFIErrorCode::NotFound, "Wallet not found".to_string()))?; - let mut account = crate::address_pool::get_managed_account_by_type_mut( - &mut wallet_info.accounts, - &account_type_rust, - ) - .ok_or_else(|| (FFIErrorCode::NotFound, "Account not found".to_string()))?; - let record = account - .transactions_mut() - .get_mut(&txid) - .ok_or_else(|| (FFIErrorCode::NotFound, "Transaction not found".to_string()))?; - match new_label { - None => { - record.label = String::new(); - Ok(()) - } - Some(s) => record.set_label(s).map_err(|e| (FFIErrorCode::InvalidInput, e.to_string())), - } - }); - - if let Err((code, msg)) = result { - (*error).set(code, &msg); - return false; - } - - true -} - -/// Free a managed account handle -/// -/// # Safety -/// -/// - `account` must be a valid pointer to an FFIManagedCoreAccount that was allocated by this library -/// - The pointer must not be used after calling this function -/// - This function must only be called once per allocation -#[no_mangle] -pub unsafe extern "C" fn managed_core_account_free(account: *mut FFIManagedCoreAccount) { - if !account.is_null() { - let _ = Box::from_raw(account); - } -} - -/// Free a managed account result's error message (if any) -/// Note: This does NOT free the account handle itself - use managed_core_account_free for that -/// -/// # Safety -/// -/// - `result` must be a valid pointer to an FFIManagedCoreAccountResult -/// - The error_message field must be either null or a valid CString allocated by this library -/// - The caller must ensure the result pointer remains valid for the duration of this call -#[no_mangle] -pub unsafe extern "C" fn managed_core_account_result_free_error( - result: *mut FFIManagedCoreAccountResult, -) { - if !result.is_null() { - let result = &mut *result; - if !result.error_message.is_null() { - let _ = std::ffi::CString::from_raw(result.error_message); - result.error_message = std::ptr::null_mut(); - } - } -} - -/// Get number of accounts in a managed wallet -/// -/// # Safety -/// -/// - `manager` must be a valid pointer to an FFIWalletManager instance -/// - `wallet_id` must be a valid pointer to a 32-byte wallet ID -/// - `error` must be a valid pointer to an FFIError structure -/// - The caller must ensure all pointers remain valid for the duration of this call -#[no_mangle] -pub unsafe extern "C" fn managed_wallet_get_account_count( - manager: *const FFIWalletManager, - wallet_id: *const u8, - error: *mut FFIError, -) -> c_uint { - check_ptr!(manager, error); - check_ptr!(wallet_id, error); - - let wallet_ptr = crate::wallet_manager::wallet_manager_get_wallet(manager, wallet_id, error); - if wallet_ptr.is_null() { - // Error already set by wallet_manager_get_wallet - return 0; - } - - let wallet = &*wallet_ptr; - let accounts = &wallet.inner().accounts; - let count = accounts.standard_bip44_accounts.len() - + accounts.standard_bip32_accounts.len() - + accounts.coinjoin_accounts.len() - + accounts.identity_registration.is_some() as usize - + accounts.identity_topup.len() - + accounts.identity_topup_not_bound.is_some() as usize - + accounts.identity_invitation.is_some() as usize - + accounts.asset_lock_address_topup.is_some() as usize - + accounts.asset_lock_shielded_address_topup.is_some() as usize - + accounts.provider_voting_keys.is_some() as usize - + accounts.provider_owner_keys.is_some() as usize - + { - #[cfg(feature = "bls")] - { - accounts.provider_operator_keys.is_some() as usize - } - #[cfg(not(feature = "bls"))] - { - 0 - } - } - + { - #[cfg(feature = "eddsa")] - { - accounts.provider_platform_keys.is_some() as usize - } - #[cfg(not(feature = "eddsa"))] - { - 0 - } - } - + accounts.dashpay_receival_accounts.len() - + accounts.dashpay_external_accounts.len() - + accounts.platform_payment_accounts.len(); - - // Clean up the wallet pointer - crate::wallet::wallet_free_const(wallet_ptr); - - count as c_uint -} - -// Note: BLS and EdDSA accounts are handled through regular FFIManagedCoreAccount -// since ManagedAccountCollection stores all accounts as ManagedAccount type - -/// Get the account index from a managed account -/// -/// Returns the primary account index for Standard and CoinJoin accounts. -/// Returns 0 for account types that don't have an index (like Identity or Provider accounts). -/// -/// # Safety -/// -/// - `account` must be a valid pointer to an FFIManagedCoreAccount instance -#[no_mangle] -pub unsafe extern "C" fn managed_core_account_get_index( - account: *const FFIManagedCoreAccount, -) -> c_uint { - if account.is_null() { - return 0; - } - - let account = &*account; - account.keys_account().managed_account_type().index_or_default() -} - -/// Get the external address pool from a managed account -/// -/// This function returns the external (receive) address pool for Standard accounts. -/// Returns NULL for account types that don't have separate external/internal pools. -/// -/// # Safety -/// -/// - `account` must be a valid pointer to an FFIManagedCoreAccount instance -/// - The returned pool must be freed with `address_pool_free` when no longer needed -#[no_mangle] -pub unsafe extern "C" fn managed_core_account_get_external_address_pool( - account: *const FFIManagedCoreAccount, -) -> *mut FFIAddressPool { - if account.is_null() { - return std::ptr::null_mut(); - } - - let account = &*account; - let managed_account = account.keys_account(); - - // Get external address pool if this is a standard account - match managed_account.managed_account_type() { - key_wallet::managed_account::managed_account_type::ManagedAccountType::Standard { - external_addresses, - .. - } => { - let ffi_pool = FFIAddressPool { - pool: external_addresses as *const AddressPool as *mut AddressPool, - pool_type: FFIAddressPoolType::External, - }; - Box::into_raw(Box::new(ffi_pool)) - } - _ => std::ptr::null_mut(), - } -} - -/// Get the internal address pool from a managed account -/// -/// This function returns the internal (change) address pool for Standard accounts. -/// Returns NULL for account types that don't have separate external/internal pools. -/// -/// # Safety -/// -/// - `account` must be a valid pointer to an FFIManagedCoreAccount instance -/// - The returned pool must be freed with `address_pool_free` when no longer needed -#[no_mangle] -pub unsafe extern "C" fn managed_core_account_get_internal_address_pool( - account: *const FFIManagedCoreAccount, -) -> *mut FFIAddressPool { - if account.is_null() { - return std::ptr::null_mut(); - } - - let account = &*account; - let managed_account = account.keys_account(); - - // Get internal address pool if this is a standard account - match managed_account.managed_account_type() { - key_wallet::managed_account::managed_account_type::ManagedAccountType::Standard { - internal_addresses, - .. - } => { - let ffi_pool = FFIAddressPool { - pool: internal_addresses as *const AddressPool as *mut AddressPool, - pool_type: FFIAddressPoolType::Internal, - }; - Box::into_raw(Box::new(ffi_pool)) - } - _ => std::ptr::null_mut(), - } -} - -/// Get an address pool from a managed account by type -/// -/// This function returns the appropriate address pool based on the pool type parameter. -/// For Standard accounts with External/Internal pool types, returns the corresponding pool. -/// For non-standard accounts with Single pool type, returns their single address pool. -/// -/// # Safety -/// -/// - `manager` must be a valid pointer to an FFIWalletManager instance -/// - `account` must be a valid pointer to an FFIManagedCoreAccount instance -/// - `wallet_id` must be a valid pointer to a 32-byte wallet ID -/// - The returned pool must be freed with `address_pool_free` when no longer needed -#[no_mangle] -pub unsafe extern "C" fn managed_core_account_get_address_pool( - account: *const FFIManagedCoreAccount, - pool_type: FFIAddressPoolType, -) -> *mut FFIAddressPool { - if account.is_null() { - return std::ptr::null_mut(); - } - - let account = &*account; - let managed_account = account.keys_account(); - - use key_wallet::managed_account::managed_account_type::ManagedAccountType; - - match pool_type { - FFIAddressPoolType::External => { - // Only standard accounts have external pools - match managed_account.managed_account_type() { - ManagedAccountType::Standard { - external_addresses, - .. - } => { - let ffi_pool = FFIAddressPool { - pool: external_addresses as *const AddressPool as *mut AddressPool, - pool_type: FFIAddressPoolType::External, - }; - Box::into_raw(Box::new(ffi_pool)) - } - _ => std::ptr::null_mut(), - } - } - FFIAddressPoolType::Internal => { - // Only standard accounts have internal pools - match managed_account.managed_account_type() { - ManagedAccountType::Standard { - internal_addresses, - .. - } => { - let ffi_pool = FFIAddressPool { - pool: internal_addresses as *const AddressPool as *mut AddressPool, - pool_type: FFIAddressPoolType::Internal, - }; - Box::into_raw(Box::new(ffi_pool)) - } - _ => std::ptr::null_mut(), - } - } - FFIAddressPoolType::Single => { - // Get the single address pool for non-standard accounts - let pool_ref = match managed_account.managed_account_type() { - ManagedAccountType::Standard { - .. - } => { - // Standard accounts don't have a "single" pool - return std::ptr::null_mut(); - } - ManagedAccountType::CoinJoin { - addresses, - .. - } => addresses, - ManagedAccountType::IdentityRegistration { - addresses, - } => addresses, - ManagedAccountType::IdentityTopUp { - addresses, - .. - } => addresses, - ManagedAccountType::IdentityTopUpNotBoundToIdentity { - addresses, - } => addresses, - ManagedAccountType::IdentityInvitation { - addresses, - } => addresses, - ManagedAccountType::AssetLockAddressTopUp { - addresses, - } => addresses, - ManagedAccountType::AssetLockShieldedAddressTopUp { - addresses, - } => addresses, - ManagedAccountType::ProviderVotingKeys { - addresses, - } => addresses, - ManagedAccountType::ProviderOwnerKeys { - addresses, - } => addresses, - ManagedAccountType::ProviderOperatorKeys { - addresses, - } => addresses, - ManagedAccountType::ProviderPlatformKeys { - addresses, - } => addresses, - ManagedAccountType::DashpayReceivingFunds { - addresses, - .. - } => addresses, - ManagedAccountType::DashpayExternalAccount { - addresses, - .. - } => addresses, - ManagedAccountType::PlatformPayment { - addresses, - .. - } => addresses, - }; - - let ffi_pool = FFIAddressPool { - pool: pool_ref as *const AddressPool as *mut AddressPool, - pool_type: FFIAddressPoolType::Single, - }; - Box::into_raw(Box::new(ffi_pool)) - } - } -} - -// ==================== Platform Payment Account Functions ==================== - -/// Get a managed platform payment account from a managed wallet -/// -/// Platform Payment accounts (DIP-17) are identified by account index and key_class. -/// Returns a platform account handle that wraps the ManagedPlatformAccount. -/// -/// # Safety -/// -/// - `manager` must be a valid pointer to an FFIWalletManager instance -/// - `wallet_id` must be a valid pointer to a 32-byte wallet ID -/// - The caller must ensure all pointers remain valid for the duration of this call -/// - The returned account must be freed with `managed_platform_account_free` when no longer needed -#[no_mangle] -pub unsafe extern "C" fn managed_wallet_get_platform_payment_account( - manager: *const FFIWalletManager, - wallet_id: *const u8, - account_index: c_uint, - key_class: c_uint, -) -> FFIManagedPlatformAccountResult { - if manager.is_null() { - return FFIManagedPlatformAccountResult::error( - FFIErrorCode::InvalidInput, - "Manager is null".to_string(), - ); - } - - if wallet_id.is_null() { - return FFIManagedPlatformAccountResult::error( - FFIErrorCode::InvalidInput, - "Wallet ID is null".to_string(), - ); - } - - // Get the managed wallet info from the manager - let mut error = FFIError::default(); - let managed_wallet_ptr = crate::wallet_manager::wallet_manager_get_managed_wallet_info( - manager, wallet_id, &mut error, - ); - - if managed_wallet_ptr.is_null() { - return FFIManagedPlatformAccountResult::error( - error.code, - if error.message.is_null() { - "Failed to get managed wallet info".to_string() - } else { - let c_str = std::ffi::CStr::from_ptr(error.message); - c_str.to_string_lossy().to_string() - }, - ); - } - - let managed_wallet = &*managed_wallet_ptr; - let key = PlatformPaymentAccountKey { - account: account_index, - key_class, - }; - - let result = match managed_wallet.inner().accounts.platform_payment_accounts.get(&key) { - Some(account) => { - let ffi_account = FFIManagedPlatformAccount::new(account); - FFIManagedPlatformAccountResult::success(Box::into_raw(Box::new(ffi_account))) - } - None => FFIManagedPlatformAccountResult::error( - FFIErrorCode::NotFound, - format!( - "Platform Payment account (account: {}, key_class: {}) not found", - account_index, key_class - ), - ), - }; - - // Clean up the managed wallet pointer - crate::managed_wallet::managed_wallet_info_free(managed_wallet_ptr); - - result -} - -/// Get the network of a managed platform account -/// -/// # Safety -/// -/// - `account` must be a valid pointer to an FFIManagedPlatformAccount instance -/// - Returns `FFINetwork::Mainnet` if the account is null -#[no_mangle] -pub unsafe extern "C" fn managed_platform_account_get_network( - account: *const FFIManagedPlatformAccount, -) -> FFINetwork { - if account.is_null() { - return FFINetwork::Mainnet; - } - - let account = &*account; - account.inner().network.into() -} - -/// Get the account index of a managed platform account -/// -/// # Safety -/// -/// - `account` must be a valid pointer to an FFIManagedPlatformAccount instance -#[no_mangle] -pub unsafe extern "C" fn managed_platform_account_get_account_index( - account: *const FFIManagedPlatformAccount, -) -> c_uint { - if account.is_null() { - return 0; - } - - let account = &*account; - account.inner().account -} - -/// Get the key class of a managed platform account -/// -/// # Safety -/// -/// - `account` must be a valid pointer to an FFIManagedPlatformAccount instance -#[no_mangle] -pub unsafe extern "C" fn managed_platform_account_get_key_class( - account: *const FFIManagedPlatformAccount, -) -> c_uint { - if account.is_null() { - return 0; - } - - let account = &*account; - account.inner().key_class -} - -/// Get the total credit balance of a managed platform account -/// -/// Returns the balance in credits (1000 credits = 1 duff) -/// -/// # Safety -/// -/// - `account` must be a valid pointer to an FFIManagedPlatformAccount instance -#[no_mangle] -pub unsafe extern "C" fn managed_platform_account_get_credit_balance( - account: *const FFIManagedPlatformAccount, -) -> u64 { - if account.is_null() { - return 0; - } - - let account = &*account; - account.inner().total_credit_balance() -} - -/// Get the total balance in duffs of a managed platform account -/// -/// Returns the balance in duffs (credit_balance / 1000) -/// -/// # Safety -/// -/// - `account` must be a valid pointer to an FFIManagedPlatformAccount instance -#[no_mangle] -pub unsafe extern "C" fn managed_platform_account_get_duff_balance( - account: *const FFIManagedPlatformAccount, -) -> u64 { - if account.is_null() { - return 0; - } - - let account = &*account; - account.inner().duff_balance() -} - -/// Get the number of funded addresses in a managed platform account -/// -/// # Safety -/// -/// - `account` must be a valid pointer to an FFIManagedPlatformAccount instance -#[no_mangle] -pub unsafe extern "C" fn managed_platform_account_get_funded_address_count( - account: *const FFIManagedPlatformAccount, -) -> c_uint { - if account.is_null() { - return 0; - } - - let account = &*account; - account.inner().funded_address_count() as c_uint -} - -/// Get the total number of addresses in a managed platform account -/// -/// # Safety -/// -/// - `account` must be a valid pointer to an FFIManagedPlatformAccount instance -#[no_mangle] -pub unsafe extern "C" fn managed_platform_account_get_total_address_count( - account: *const FFIManagedPlatformAccount, -) -> c_uint { - if account.is_null() { - return 0; - } - - let account = &*account; - account.inner().total_address_count() as c_uint -} - -/// Check if a managed platform account is watch-only -/// -/// # Safety -/// -/// - `account` must be a valid pointer to an FFIManagedPlatformAccount instance -#[no_mangle] -pub unsafe extern "C" fn managed_platform_account_get_is_watch_only( - account: *const FFIManagedPlatformAccount, -) -> bool { - if account.is_null() { - return false; - } - - let account = &*account; - account.inner().is_watch_only -} - -/// Get the address pool from a managed platform account -/// -/// Platform accounts only have a single address pool. -/// -/// # Safety -/// -/// - `account` must be a valid pointer to an FFIManagedPlatformAccount instance -/// - The returned pool must be freed with `address_pool_free` when no longer needed -#[no_mangle] -pub unsafe extern "C" fn managed_platform_account_get_address_pool( - account: *const FFIManagedPlatformAccount, -) -> *mut FFIAddressPool { - if account.is_null() { - return std::ptr::null_mut(); - } - - let account = &*account; - let pool_ref = &account.inner().addresses; - - let ffi_pool = FFIAddressPool { - pool: pool_ref as *const AddressPool as *mut AddressPool, - pool_type: FFIAddressPoolType::Single, - }; - Box::into_raw(Box::new(ffi_pool)) -} - -/// Free a managed platform account handle -/// -/// # Safety -/// -/// - `account` must be a valid pointer to an FFIManagedPlatformAccount that was allocated by this library -/// - The pointer must not be used after calling this function -/// - This function must only be called once per allocation -#[no_mangle] -pub unsafe extern "C" fn managed_platform_account_free(account: *mut FFIManagedPlatformAccount) { - if !account.is_null() { - let _ = Box::from_raw(account); - } -} - -/// Free a managed platform account result's error message (if any) -/// Note: This does NOT free the account handle itself - use managed_platform_account_free for that -/// -/// # Safety -/// -/// - `result` must be a valid pointer to an FFIManagedPlatformAccountResult -/// - The error_message field must be either null or a valid CString allocated by this library -/// - The caller must ensure the result pointer remains valid for the duration of this call -#[no_mangle] -pub unsafe extern "C" fn managed_platform_account_result_free_error( - result: *mut FFIManagedPlatformAccountResult, -) { - if !result.is_null() { - let result = &mut *result; - if !result.error_message.is_null() { - let _ = std::ffi::CString::from_raw(result.error_message); - result.error_message = std::ptr::null_mut(); - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::address_pool::address_pool_free; - use crate::types::{FFIAccountCreationOptionType, FFIWalletAccountCreationOptions}; - // These types are only used by the FFITransactionRecord tests, which run - // only when transactions stay in memory. - #[cfg(feature = "keep-finalized-transactions")] - use crate::types::{ - FFIBlockInfo, FFIInputDetail, FFIOutputDetail, FFIOutputRole, FFITransactionContext, - FFITransactionContextType, FFITransactionDirection, FFITransactionType, - }; - use crate::wallet_manager::{ - wallet_manager_add_wallet_from_mnemonic_with_options, wallet_manager_create, - wallet_manager_free, wallet_manager_free_wallet_ids, wallet_manager_get_wallet_ids, - }; - use dash_network::ffi::FFINetwork; - use dashcore::Transaction; - use key_wallet::managed_account::transaction_record::{ - OutputDetail, OutputRole, TransactionDirection, TransactionRecord, - }; - use key_wallet::transaction_checking::transaction_context::TransactionContext; - use key_wallet::transaction_checking::transaction_router::TransactionType; - use std::ffi::CString; - use std::ptr; - - const TEST_MNEMONIC: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; - - #[test] - fn test_managed_account_basic() { - unsafe { - let mut error = FFIError::default(); - - // Create wallet manager - let manager = wallet_manager_create(FFINetwork::Testnet, &mut error); - assert!(!manager.is_null()); - assert_eq!(error.code, FFIErrorCode::Success); - - // Add a wallet with default accounts - let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); - - let success = wallet_manager_add_wallet_from_mnemonic_with_options( - manager, - mnemonic.as_ptr(), - ptr::null(), - &mut error, - ); - assert!(success); - assert_eq!(error.code, FFIErrorCode::Success); - - // Get wallet IDs - let mut wallet_ids_out: *mut u8 = ptr::null_mut(); - let mut count_out: usize = 0; - - let success = wallet_manager_get_wallet_ids( - manager, - &mut wallet_ids_out, - &mut count_out, - &mut error, - ); - assert!(success); - assert_eq!(count_out, 1); - assert!(!wallet_ids_out.is_null()); - - // Get a managed account - let result = managed_wallet_get_account( - manager, - wallet_ids_out, - 0, - FFIAccountKind::StandardBIP44, - ); - - assert!(!result.account.is_null()); - assert_eq!(result.error_code, 0); - assert!(result.error_message.is_null()); - - // Verify the account was created successfully - let _account = &*result.account; - - // Clean up - managed_core_account_free(result.account); - wallet_manager_free_wallet_ids(wallet_ids_out, count_out); - wallet_manager_free(manager); - } - } - - #[test] - fn test_managed_account_not_found() { - unsafe { - let mut error = FFIError::default(); - - // Create wallet manager - let manager = wallet_manager_create(FFINetwork::Testnet, &mut error); - assert!(!manager.is_null()); - - // Add a wallet with minimal accounts - let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); - - let mut options = FFIWalletAccountCreationOptions::default_options(); - options.option_type = FFIAccountCreationOptionType::BIP44AccountsOnly; - let bip44_indices = [0]; - options.bip44_indices = bip44_indices.as_ptr(); - options.bip44_count = bip44_indices.len(); - - let success = wallet_manager_add_wallet_from_mnemonic_with_options( - manager, - mnemonic.as_ptr(), - &options, - &mut error, - ); - assert!(success); - - // Get wallet IDs - let mut wallet_ids_out: *mut u8 = ptr::null_mut(); - let mut count_out: usize = 0; - - let success = wallet_manager_get_wallet_ids( - manager, - &mut wallet_ids_out, - &mut count_out, - &mut error, - ); - assert!(success); - assert_eq!(count_out, 1); - - // Try to get a non-existent CoinJoin account - let mut result = - managed_wallet_get_account(manager, wallet_ids_out, 0, FFIAccountKind::CoinJoin); - - assert!(result.account.is_null()); - assert_ne!(result.error_code, 0); - assert!(!result.error_message.is_null()); - - // Clean up error message - managed_core_account_result_free_error(&mut result as *mut _); - - // Clean up - wallet_manager_free_wallet_ids(wallet_ids_out, count_out); - wallet_manager_free(manager); - } - } - - #[test] - fn test_managed_core_account_free_null() { - unsafe { - // Should not crash when freeing null - managed_core_account_free(ptr::null_mut()); - } - } - - #[test] - fn test_managed_wallet_get_account_count() { - unsafe { - let mut error = FFIError::default(); - - // Create wallet manager - let manager = wallet_manager_create(FFINetwork::Testnet, &mut error); - assert!(!manager.is_null()); - - // Add a wallet with multiple accounts - let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); - - let mut options = FFIWalletAccountCreationOptions::default_options(); - options.option_type = FFIAccountCreationOptionType::AllAccounts; - - let bip44_indices = [0, 1, 2]; - let bip32_indices = [0]; - let coinjoin_indices = [0]; - - options.bip44_indices = bip44_indices.as_ptr(); - options.bip44_count = bip44_indices.len(); - options.bip32_indices = bip32_indices.as_ptr(); - options.bip32_count = bip32_indices.len(); - options.coinjoin_indices = coinjoin_indices.as_ptr(); - options.coinjoin_count = coinjoin_indices.len(); - - let success = wallet_manager_add_wallet_from_mnemonic_with_options( - manager, - mnemonic.as_ptr(), - &options, - &mut error, - ); - assert!(success); - - // Get wallet IDs - let mut wallet_ids_out: *mut u8 = ptr::null_mut(); - let mut count_out: usize = 0; - - let success = wallet_manager_get_wallet_ids( - manager, - &mut wallet_ids_out, - &mut count_out, - &mut error, - ); - assert!(success); - - // Get account count - let count = managed_wallet_get_account_count(manager, wallet_ids_out, &mut error); - - // Should have at least the accounts we created - assert!(count >= 5); // 3 BIP44 + 1 BIP32 + 1 CoinJoin - assert_eq!(error.code, FFIErrorCode::Success); - - // Clean up - wallet_manager_free_wallet_ids(wallet_ids_out, count_out); - wallet_manager_free(manager); - } - } - - #[test] - fn test_managed_account_getters() { - unsafe { - let mut error = FFIError::default(); - - // Create wallet manager - let manager = wallet_manager_create(FFINetwork::Testnet, &mut error); - assert!(!manager.is_null()); - assert_eq!(error.code, FFIErrorCode::Success); - - // Add a wallet with default accounts - let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); - - let success = wallet_manager_add_wallet_from_mnemonic_with_options( - manager, - mnemonic.as_ptr(), - ptr::null(), - &mut error, - ); - assert!(success); - assert_eq!(error.code, FFIErrorCode::Success); - - // Get wallet IDs - let mut wallet_ids_out: *mut u8 = ptr::null_mut(); - let mut count_out: usize = 0; - - let success = wallet_manager_get_wallet_ids( - manager, - &mut wallet_ids_out, - &mut count_out, - &mut error, - ); - assert!(success); - assert_eq!(count_out, 1); - assert!(!wallet_ids_out.is_null()); - - // Get a managed account - let result = managed_wallet_get_account( - manager, - wallet_ids_out, - 0, - FFIAccountKind::StandardBIP44, - ); - - assert!(!result.account.is_null()); - assert_eq!(result.error_code, 0); - assert!(result.error_message.is_null()); - - let account = result.account; - - // Test get_network - let network = managed_core_account_get_network(account); - assert_eq!(network, FFINetwork::Testnet); - - // Test get_account_type - let mut index_out: c_uint = 999; // Initialize with unexpected value - let account_type = managed_core_account_get_account_type(account, &mut index_out); - assert_eq!(account_type, FFIAccountKind::StandardBIP44); - assert_eq!(index_out, 0); - - // Test get_balance - let mut balance_out = crate::types::FFIBalance { - confirmed: 999, - unconfirmed: 999, - immature: 999, - locked: 999, - total: 999, - }; - let success = managed_core_account_get_balance(account, &mut balance_out); - assert!(success); - // Initially, balance should be 0 - assert_eq!(balance_out.confirmed, 0); - assert_eq!(balance_out.unconfirmed, 0); - assert_eq!(balance_out.immature, 0); - assert_eq!(balance_out.locked, 0); - assert_eq!(balance_out.total, 0); - - // Test get_transaction_count (only available with the - // `keep-finalized-transactions` feature; without it the function - // is not exposed because chainlocked records are pruned and the - // count would be incomplete) - #[cfg(feature = "keep-finalized-transactions")] - { - let tx_count = managed_core_account_get_transaction_count(account); - assert_eq!(tx_count, 0); // Initially no transactions - } - - // Test get_utxo_count - let utxo_count = managed_core_account_get_utxo_count(account); - assert_eq!(utxo_count, 0); // Initially no UTXOs - - // Test get_parent_wallet_id - let parent_id = managed_core_account_get_parent_wallet_id(wallet_ids_out); - assert_eq!(parent_id, wallet_ids_out); // Should return the same pointer - - // Clean up - managed_core_account_free(account); - wallet_manager_free_wallet_ids(wallet_ids_out, count_out); - wallet_manager_free(manager); - } - } - - #[test] - fn test_managed_account_getter_edge_cases() { - unsafe { - // Test null account for get_network - let network = managed_core_account_get_network(ptr::null()); - assert_eq!(network, FFINetwork::Mainnet); - - let mut index_out: c_uint = 0; - let account_type = managed_core_account_get_account_type(ptr::null(), &mut index_out); - assert_eq!(account_type, FFIAccountKind::StandardBIP44); // Default type - - #[cfg(feature = "keep-finalized-transactions")] - { - let tx_count = managed_core_account_get_transaction_count(ptr::null()); - assert_eq!(tx_count, 0); - } - - let utxo_count = managed_core_account_get_utxo_count(ptr::null()); - assert_eq!(utxo_count, 0); - - // Test new getters with null account - let index = managed_core_account_get_index(ptr::null()); - assert_eq!(index, 0); - - // Test null balance_out - let mut error = FFIError::default(); - let manager = wallet_manager_create(FFINetwork::Testnet, &mut error); - assert!(!manager.is_null()); - - // Add a wallet - let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); - - let success = wallet_manager_add_wallet_from_mnemonic_with_options( - manager, - mnemonic.as_ptr(), - ptr::null(), - &mut error, - ); - assert!(success); - - // Get wallet IDs - let mut wallet_ids_out: *mut u8 = ptr::null_mut(); - let mut count_out: usize = 0; - - let success = wallet_manager_get_wallet_ids( - manager, - &mut wallet_ids_out, - &mut count_out, - &mut error, - ); - assert!(success); - - // Get an account - let result = managed_wallet_get_account( - manager, - wallet_ids_out, - 0, - FFIAccountKind::StandardBIP44, - ); - assert!(!result.account.is_null()); - - // Test balance with null output - let success = managed_core_account_get_balance(result.account, ptr::null_mut()); - assert!(!success); - - // Clean up - managed_core_account_free(result.account); - wallet_manager_free_wallet_ids(wallet_ids_out, count_out); - wallet_manager_free(manager); - } - } - - #[test] - fn test_managed_account_address_pools() { - unsafe { - let mut error = FFIError::default(); - - // Create wallet manager - let mut manager = wallet_manager_create(FFINetwork::Testnet, &mut error); - assert!(!manager.is_null()); - assert_eq!(error.code, FFIErrorCode::Success); - - // Add a wallet with default accounts - let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); - - let success = wallet_manager_add_wallet_from_mnemonic_with_options( - manager, - mnemonic.as_ptr(), - ptr::null(), - &mut error, - ); - assert!(success); - assert_eq!(error.code, FFIErrorCode::Success); - - // Get wallet IDs - let mut wallet_ids_out: *mut u8 = ptr::null_mut(); - let mut count_out: usize = 0; - - let success = wallet_manager_get_wallet_ids( - manager, - &mut wallet_ids_out, - &mut count_out, - &mut error, - ); - assert!(success); - assert_eq!(count_out, 1); - assert!(!wallet_ids_out.is_null()); - - // Get a standard BIP44 managed account - let result = managed_wallet_get_account( - manager, - wallet_ids_out, - 0, - FFIAccountKind::StandardBIP44, - ); - - assert!(!result.account.is_null()); - assert_eq!(result.error_code, 0); - - let account = result.account; - - // Test get_index - let index = managed_core_account_get_index(account); - assert_eq!(index, 0); - - // Test get_external_address_pool - let external_pool = managed_core_account_get_external_address_pool(account); - assert!(!external_pool.is_null()); - - // Test get_internal_address_pool - let internal_pool = managed_core_account_get_internal_address_pool(account); - assert!(!internal_pool.is_null()); - - // Test get_address_pool with External type - let external_pool2 = - managed_core_account_get_address_pool(account, FFIAddressPoolType::External); - assert!(!external_pool2.is_null()); - - // Test get_address_pool with Internal type - let internal_pool2 = - managed_core_account_get_address_pool(account, FFIAddressPoolType::Internal); - assert!(!internal_pool2.is_null()); - - // Test get_address_pool with Single type (should return null for Standard account) - let single_pool = - managed_core_account_get_address_pool(account, FFIAddressPoolType::Single); - assert!(single_pool.is_null()); - - // Clean up address pools - address_pool_free(external_pool); - address_pool_free(internal_pool); - address_pool_free(external_pool2); - address_pool_free(internal_pool2); - - // Clean up account - managed_core_account_free(account); - - // Now test with different account types from the same wallet - // The default wallet should have been created with StandardBIP44 index 0 - // Let's try creating a wallet with CoinJoin accounts first - - // Clean up and start fresh for the second test - wallet_manager_free_wallet_ids(wallet_ids_out, count_out); - wallet_manager_free(manager); - - // Create a new manager - manager = wallet_manager_create(FFINetwork::Testnet, &mut error); - assert!(!manager.is_null()); - - // Create wallet with CoinJoin account - let mut options = FFIWalletAccountCreationOptions::default_options(); - options.option_type = FFIAccountCreationOptionType::SpecificAccounts; - let coinjoin_indices = [0]; - options.coinjoin_indices = coinjoin_indices.as_ptr(); - options.coinjoin_count = coinjoin_indices.len(); - - let mnemonic2 = CString::new(TEST_MNEMONIC).unwrap(); - let success = wallet_manager_add_wallet_from_mnemonic_with_options( - manager, - mnemonic2.as_ptr(), - &options, - &mut error, - ); - assert!(success); - - // Get wallet IDs - let success = wallet_manager_get_wallet_ids( - manager, - &mut wallet_ids_out, - &mut count_out, - &mut error, - ); - assert!(success); - assert_eq!(count_out, 1); - - // Get CoinJoin account - let cj_result = - managed_wallet_get_account(manager, wallet_ids_out, 0, FFIAccountKind::CoinJoin); - assert!(!cj_result.account.is_null()); - - let cj_account = cj_result.account; - - // Test that external/internal return null for CoinJoin account - let cj_external = managed_core_account_get_external_address_pool(cj_account); - assert!(cj_external.is_null()); - - let cj_internal = managed_core_account_get_internal_address_pool(cj_account); - assert!(cj_internal.is_null()); - - // Test that Single pool works for CoinJoin account - let cj_single = - managed_core_account_get_address_pool(cj_account, FFIAddressPoolType::Single); - assert!(!cj_single.is_null()); - - // Clean up - address_pool_free(cj_single); - managed_core_account_free(cj_account); - wallet_manager_free_wallet_ids(wallet_ids_out, count_out); - wallet_manager_free(manager); - } - } - - #[test] - fn test_address_pool_free_null() { - unsafe { - // Should not crash when freeing null - address_pool_free(ptr::null_mut()); - } - } - - #[cfg(feature = "keep-finalized-transactions")] - #[test] - fn test_free_transactions_null_safety() { - unsafe { - managed_core_account_free_transactions(std::ptr::null_mut(), 0); - managed_core_account_free_transactions(std::ptr::null_mut(), 5); - } - } - - #[cfg(feature = "keep-finalized-transactions")] - /// Helper to create a wallet manager with a default testnet wallet and account. - /// Returns (manager, wallet_ids_out, wallet_ids_count, account). - unsafe fn setup_account( - ) -> (*mut crate::wallet_manager::FFIWalletManager, *mut u8, usize, *mut FFIManagedCoreAccount) - { - let mut error = FFIError::default(); - let manager = wallet_manager_create(FFINetwork::Testnet, &mut error); - assert!(!manager.is_null()); - - let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); - let success = wallet_manager_add_wallet_from_mnemonic_with_options( - manager, - mnemonic.as_ptr(), - ptr::null(), - &mut error, - ); - assert!(success); - - let mut wallet_ids_out: *mut u8 = ptr::null_mut(); - let mut count_out: usize = 0; - let success = - wallet_manager_get_wallet_ids(manager, &mut wallet_ids_out, &mut count_out, &mut error); - assert!(success); - assert_eq!(count_out, 1); - - let result = - managed_wallet_get_account(manager, wallet_ids_out, 0, FFIAccountKind::StandardBIP44); - assert!(!result.account.is_null()); - - (manager, wallet_ids_out, count_out, result.account) - } - - #[cfg(feature = "keep-finalized-transactions")] - /// Helper to insert a test `TransactionRecord` into the shared wallet manager state - /// and return its txid bytes. - unsafe fn insert_test_transaction( - manager: *mut crate::wallet_manager::FFIWalletManager, - wallet_id: *const u8, - account_index: c_uint, - account_type: FFIAccountKind, - label: Option<&str>, - ) -> [u8; 32] { - let tx = Transaction { - version: 2, - lock_time: 0, - input: vec![], - output: vec![], - special_transaction_payload: None, - }; - let txid = tx.txid(); - let txid_bytes = txid.to_byte_array(); - - let account_type_rust = account_type.to_account_type(account_index); - - let mut record = TransactionRecord::new( - tx, - account_type_rust, - TransactionContext::Mempool, - TransactionType::Standard, - TransactionDirection::Incoming, - vec![], - vec![OutputDetail { - index: 0, - role: OutputRole::Received, - address: None, - value: 50_000, - }], - 50_000, - ); - record.set_fee(226); - if let Some(l) = label { - record.set_label(l.to_owned()).unwrap(); - } - - let mut wallet_id_array = [0u8; 32]; - ptr::copy_nonoverlapping(wallet_id, wallet_id_array.as_mut_ptr(), 32); - - let manager_ref = &*manager; - manager_ref.runtime.block_on(async { - let mut manager_guard = manager_ref.manager.write().await; - let wallet_info = manager_guard.get_wallet_info_mut(&wallet_id_array).expect("wallet"); - let mut account = crate::address_pool::get_managed_account_by_type_mut( - &mut wallet_info.accounts, - &account_type_rust, - ) - .expect("account"); - account.transactions_mut().insert(txid, record); - }); - - txid_bytes - } - - #[cfg(feature = "keep-finalized-transactions")] - /// Helper to get transactions from an account. Returns (pointer, count). - unsafe fn get_transactions( - account: *mut FFIManagedCoreAccount, - ) -> (*mut FFITransactionRecord, usize) { - let mut txs_out: *mut FFITransactionRecord = ptr::null_mut(); - let mut txs_count: usize = 0; - let success = managed_core_account_get_transactions(account, &mut txs_out, &mut txs_count); - assert!(success); - (txs_out, txs_count) - } - - #[cfg(feature = "keep-finalized-transactions")] - #[test] - fn test_transaction_label_lifecycle() { - unsafe { - let (manager, wallet_ids_out, count_out, account) = setup_account(); - managed_core_account_free(account); - - let txid_bytes = insert_test_transaction( - manager, - wallet_ids_out, - 0, - FFIAccountKind::StandardBIP44, - Some("Coffee payment"), - ); - - let result = managed_wallet_get_account( - manager, - wallet_ids_out, - 0, - FFIAccountKind::StandardBIP44, - ); - assert!(!result.account.is_null()); - let account = result.account; - assert_eq!(managed_core_account_get_transaction_count(account), 1); - - let (txs_out, txs_count) = get_transactions(account); - assert_eq!(txs_count, 1); - assert!(!txs_out.is_null()); - let record = &*txs_out; - assert_eq!(record.txid, txid_bytes); - assert_eq!(record.net_amount, 50_000); - assert_eq!(record.fee, 226); - assert!(!record.label.is_null()); - let label_str = std::ffi::CStr::from_ptr(record.label).to_str().unwrap(); - assert_eq!(label_str, "Coffee payment"); - managed_core_account_free_transactions(txs_out, txs_count); - managed_core_account_free(account); - - let mut error = FFIError::default(); - let new_label = CString::new("Rent payment").unwrap(); - let success = wallet_manager_set_transaction_label( - manager, - wallet_ids_out, - FFIAccountKind::StandardBIP44, - 0, - txid_bytes.as_ptr(), - new_label.as_ptr(), - &mut error, - ); - assert!(success); - assert_eq!(error.code, FFIErrorCode::Success); - - let result = managed_wallet_get_account( - manager, - wallet_ids_out, - 0, - FFIAccountKind::StandardBIP44, - ); - assert!(!result.account.is_null()); - let account = result.account; - let (txs_out, txs_count) = get_transactions(account); - assert_eq!(txs_count, 1); - let label_str = std::ffi::CStr::from_ptr((*txs_out).label).to_str().unwrap(); - assert_eq!(label_str, "Rent payment"); - managed_core_account_free_transactions(txs_out, txs_count); - managed_core_account_free(account); - - let success = wallet_manager_set_transaction_label( - manager, - wallet_ids_out, - FFIAccountKind::StandardBIP44, - 0, - txid_bytes.as_ptr(), - ptr::null(), - &mut error, - ); - assert!(success); - - let result = managed_wallet_get_account( - manager, - wallet_ids_out, - 0, - FFIAccountKind::StandardBIP44, - ); - assert!(!result.account.is_null()); - let account = result.account; - let (txs_out, txs_count) = get_transactions(account); - assert_eq!(txs_count, 1); - assert!((*txs_out).label.is_null()); - managed_core_account_free_transactions(txs_out, txs_count); - managed_core_account_free(account); - - wallet_manager_free_wallet_ids(wallet_ids_out, count_out); - wallet_manager_free(manager); - } - } - - #[cfg(feature = "keep-finalized-transactions")] - #[test] - fn test_set_transaction_label_errors() { - unsafe { - let mut error = FFIError::default(); - let txid = [0u8; 32]; - let wallet_id = [0u8; 32]; - let label = CString::new("label").unwrap(); - - let success = wallet_manager_set_transaction_label( - ptr::null_mut(), - wallet_id.as_ptr(), - FFIAccountKind::StandardBIP44, - 0, - txid.as_ptr(), - label.as_ptr(), - &mut error, - ); - assert!(!success); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - error.clean(); - - let (manager, wallet_ids_out, count_out, account) = setup_account(); - managed_core_account_free(account); - - let success = wallet_manager_set_transaction_label( - manager, - ptr::null(), - FFIAccountKind::StandardBIP44, - 0, - txid.as_ptr(), - label.as_ptr(), - &mut error, - ); - assert!(!success); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - error.clean(); - - let success = wallet_manager_set_transaction_label( - manager, - wallet_ids_out, - FFIAccountKind::StandardBIP44, - 0, - ptr::null(), - label.as_ptr(), - &mut error, - ); - assert!(!success); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - error.clean(); - - let unknown_wallet_id = [0u8; 32]; - let fake_txid_for_unknown_wallet = [0xaau8; 32]; - let success = wallet_manager_set_transaction_label( - manager, - unknown_wallet_id.as_ptr(), - FFIAccountKind::StandardBIP44, - 0, - fake_txid_for_unknown_wallet.as_ptr(), - label.as_ptr(), - &mut error, - ); - assert!(!success); - assert_eq!(error.code, FFIErrorCode::NotFound); - error.clean(); - - let fake_txid = [0xffu8; 32]; - let success = wallet_manager_set_transaction_label( - manager, - wallet_ids_out, - FFIAccountKind::StandardBIP44, - 0, - fake_txid.as_ptr(), - label.as_ptr(), - &mut error, - ); - assert!(!success); - assert_eq!(error.code, FFIErrorCode::NotFound); - error.clean(); - - wallet_manager_free_wallet_ids(wallet_ids_out, count_out); - wallet_manager_free(manager); - } - } - - #[cfg(feature = "keep-finalized-transactions")] - #[test] - fn test_ffi_transaction_record_roundtrip() { - let mut records = Vec::new(); - - // First record: with sub-allocations - let output_slice = vec![FFIOutputDetail { - index: 0, - role: FFIOutputRole::Received, - value: 0, - address: std::ptr::null_mut(), - }] - .into_boxed_slice(); - // Create input details - let input_slice = vec![FFIInputDetail { - index: 0, - value: 0, - address: CString::new("XtestAddress123").unwrap().into_raw(), - }] - .into_boxed_slice(); - // Create tx data - let tx_slice = vec![0u8; 10].into_boxed_slice(); - - let r0 = FFITransactionRecord { - txid: [0xaa; 32], - net_amount: 50000, - context: FFITransactionContext { - context_type: FFITransactionContextType::Mempool, - block_info: FFIBlockInfo::empty(), - islock_data: std::ptr::null(), - islock_len: 0, - }, - transaction_type: FFITransactionType::Standard, - direction: FFITransactionDirection::Incoming, - fee: 226, - account_type: FFIAccountType { - kind: FFIAccountKind::StandardBIP44, - index: 0, - index_secondary: -1, - identity_user: std::ptr::null(), - identity_friend: std::ptr::null(), - key_class: -1, - }, - input_details_count: input_slice.len(), - input_details: Box::into_raw(input_slice) as *mut FFIInputDetail, - output_details_count: output_slice.len(), - output_details: Box::into_raw(output_slice) as *mut FFIOutputDetail, - tx_len: tx_slice.len(), - tx_data: Box::into_raw(tx_slice) as *mut u8, - - // Create label - label: CString::new("Payment for coffee").unwrap().into_raw(), - }; - - // Second record: empty sub-arrays - let r1 = FFITransactionRecord { - txid: [0xbb; 32], - net_amount: -10000, - context: FFITransactionContext { - context_type: FFITransactionContextType::Mempool, - block_info: FFIBlockInfo::empty(), - islock_data: std::ptr::null(), - islock_len: 0, - }, - transaction_type: FFITransactionType::Standard, - direction: FFITransactionDirection::Outgoing, - fee: 0, - account_type: FFIAccountType { - kind: FFIAccountKind::StandardBIP44, - index: 0, - index_secondary: -1, - identity_user: std::ptr::null(), - identity_friend: std::ptr::null(), - key_class: -1, - }, - input_details: std::ptr::null_mut(), - input_details_count: 0, - output_details: std::ptr::null_mut(), - output_details_count: 0, - tx_data: std::ptr::null_mut(), - tx_len: 0, - label: std::ptr::null_mut(), - }; - - records.push(r0); - records.push(r1); - - let count = records.len(); - let records = records.into_boxed_slice(); - let records_ptr = Box::into_raw(records) as *mut FFITransactionRecord; - - // Free should not crash - unsafe { - managed_core_account_free_transactions(records_ptr, count); - } - } -} diff --git a/key-wallet-ffi/src/managed_account_collection.rs b/key-wallet-ffi/src/managed_account_collection.rs deleted file mode 100644 index 5912abd1e..000000000 --- a/key-wallet-ffi/src/managed_account_collection.rs +++ /dev/null @@ -1,1259 +0,0 @@ -//! FFI bindings for managed account collections -//! -//! This module provides FFI-compatible account collection functionality that works -//! with managed wallets through the wallet manager. It mirrors the functionality -//! of account_collection.rs but accesses accounts through the wallet manager's -//! wallet reference. - -use std::ffi::CString; -use std::os::raw::{c_char, c_uint}; -use std::ptr; - -use crate::check_ptr; -use crate::error::FFIError; -use crate::managed_account::FFIManagedCoreAccount; -use crate::wallet_manager::FFIWalletManager; - -/// Opaque handle to a managed account collection -pub struct FFIManagedCoreAccountCollection { - /// The underlying managed account collection - collection: key_wallet::managed_account::managed_account_collection::ManagedAccountCollection, -} - -impl FFIManagedCoreAccountCollection { - /// Create a new FFI managed account collection - pub fn new( - collection: &key_wallet::managed_account::managed_account_collection::ManagedAccountCollection, - ) -> Self { - FFIManagedCoreAccountCollection { - collection: collection.clone(), - } - } -} - -/// C-compatible summary of all accounts in a managed collection -/// -/// This struct provides Swift with structured data about all accounts -/// that exist in the managed collection, allowing programmatic access to account -/// indices and presence information. -#[repr(C)] -pub struct FFIManagedCoreAccountCollectionSummary { - /// Array of BIP44 account indices - pub bip44_indices: *mut c_uint, - /// Number of BIP44 accounts - pub bip44_count: usize, - - /// Array of BIP32 account indices - pub bip32_indices: *mut c_uint, - /// Number of BIP32 accounts - pub bip32_count: usize, - - /// Array of CoinJoin account indices - pub coinjoin_indices: *mut c_uint, - /// Number of CoinJoin accounts - pub coinjoin_count: usize, - - /// Array of identity top-up registration indices - pub identity_topup_indices: *mut c_uint, - /// Number of identity top-up accounts - pub identity_topup_count: usize, - - /// Whether identity registration account exists - pub has_identity_registration: bool, - /// Whether identity invitation account exists - pub has_identity_invitation: bool, - /// Whether identity top-up not bound account exists - pub has_identity_topup_not_bound: bool, - /// Whether provider voting keys account exists - pub has_provider_voting_keys: bool, - /// Whether provider owner keys account exists - pub has_provider_owner_keys: bool, - - #[cfg(feature = "bls")] - /// Whether provider operator keys account exists - pub has_provider_operator_keys: bool, - - #[cfg(feature = "eddsa")] - /// Whether provider platform keys account exists - pub has_provider_platform_keys: bool, - - /// Array of Platform Payment account keys (account, key_class pairs) - pub platform_payment_keys: *mut crate::managed_account::FFIPlatformPaymentAccountKey, - /// Number of Platform Payment accounts - pub platform_payment_count: usize, -} - -/// Get managed account collection for a specific network from wallet manager -/// -/// # Safety -/// -/// - `manager` must be a valid pointer to an FFIWalletManager instance -/// - `wallet_id` must be a valid pointer to a 32-byte wallet ID -/// - `error` must be a valid pointer to an FFIError structure -/// - The returned pointer must be freed with `managed_account_collection_free` when no longer needed -#[no_mangle] -pub unsafe extern "C" fn managed_wallet_get_account_collection( - manager: *const FFIWalletManager, - wallet_id: *const u8, - error: *mut FFIError, -) -> *mut FFIManagedCoreAccountCollection { - check_ptr!(manager, error); - check_ptr!(wallet_id, error); - - // Get the managed wallet info from the manager - let managed_wallet_ptr = - crate::wallet_manager::wallet_manager_get_managed_wallet_info(manager, wallet_id, error); - - if managed_wallet_ptr.is_null() { - // Error already set by wallet_manager_get_managed_wallet_info - return ptr::null_mut(); - } - - // Get the managed account collection from the managed wallet info - let managed_wallet = &*managed_wallet_ptr; - - let ffi_collection = FFIManagedCoreAccountCollection::new(&managed_wallet.inner().accounts); - - // Clean up the managed wallet pointer since we've extracted what we need - crate::managed_wallet::managed_wallet_info_free(managed_wallet_ptr); - - Box::into_raw(Box::new(ffi_collection)) -} - -/// Free a managed account collection handle -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection created by this library -/// - `collection` must not be used after calling this function -#[no_mangle] -pub unsafe extern "C" fn managed_account_collection_free( - collection: *mut FFIManagedCoreAccountCollection, -) { - if !collection.is_null() { - let _ = Box::from_raw(collection); - } -} - -// Standard BIP44 accounts functions - -/// Get a BIP44 account by index from the managed collection -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection -/// - The returned pointer must be freed with `managed_core_account_free` when no longer needed -#[no_mangle] -pub unsafe extern "C" fn managed_account_collection_get_bip44_account( - collection: *const FFIManagedCoreAccountCollection, - index: c_uint, -) -> *mut FFIManagedCoreAccount { - if collection.is_null() { - return ptr::null_mut(); - } - - let collection = &*collection; - match collection.collection.standard_bip44_accounts.get(&index) { - Some(account) => { - // Get the network from the account - let ffi_account = FFIManagedCoreAccount::new(account); - Box::into_raw(Box::new(ffi_account)) - } - None => ptr::null_mut(), - } -} - -/// Get all BIP44 account indices from managed collection -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection -/// - `out_indices` must be a valid pointer to store the indices array -/// - `out_count` must be a valid pointer to store the count -/// - The returned array must be freed with `free_u32_array` when no longer needed -#[no_mangle] -pub unsafe extern "C" fn managed_account_collection_get_bip44_indices( - collection: *const FFIManagedCoreAccountCollection, - out_indices: *mut *mut c_uint, - out_count: *mut usize, -) -> bool { - if collection.is_null() || out_indices.is_null() || out_count.is_null() { - return false; - } - - let collection = &*collection; - let mut indices: Vec = - collection.collection.standard_bip44_accounts.keys().copied().collect(); - - if indices.is_empty() { - *out_indices = ptr::null_mut(); - *out_count = 0; - return true; - } - - indices.sort(); - - let mut boxed_slice = indices.into_boxed_slice(); - let ptr = boxed_slice.as_mut_ptr(); - let len = boxed_slice.len(); - std::mem::forget(boxed_slice); - - *out_indices = ptr; - *out_count = len; - true -} - -// Standard BIP32 accounts functions - -/// Get a BIP32 account by index from the managed collection -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection -/// - The returned pointer must be freed with `managed_core_account_free` when no longer needed -#[no_mangle] -pub unsafe extern "C" fn managed_account_collection_get_bip32_account( - collection: *const FFIManagedCoreAccountCollection, - index: c_uint, -) -> *mut FFIManagedCoreAccount { - if collection.is_null() { - return ptr::null_mut(); - } - - let collection = &*collection; - match collection.collection.standard_bip32_accounts.get(&index) { - Some(account) => { - let ffi_account = FFIManagedCoreAccount::new(account); - Box::into_raw(Box::new(ffi_account)) - } - None => ptr::null_mut(), - } -} - -/// Get all BIP32 account indices from managed collection -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection -/// - `out_indices` must be a valid pointer to store the indices array -/// - `out_count` must be a valid pointer to store the count -/// - The returned array must be freed with `free_u32_array` when no longer needed -#[no_mangle] -pub unsafe extern "C" fn managed_account_collection_get_bip32_indices( - collection: *const FFIManagedCoreAccountCollection, - out_indices: *mut *mut c_uint, - out_count: *mut usize, -) -> bool { - if collection.is_null() || out_indices.is_null() || out_count.is_null() { - return false; - } - - let collection = &*collection; - let indices: Vec = - collection.collection.standard_bip32_accounts.keys().copied().collect(); - - if indices.is_empty() { - *out_indices = ptr::null_mut(); - *out_count = 0; - return true; - } - - let mut boxed_slice = indices.into_boxed_slice(); - let ptr = boxed_slice.as_mut_ptr(); - let len = boxed_slice.len(); - std::mem::forget(boxed_slice); - - *out_indices = ptr; - *out_count = len; - true -} - -// CoinJoin accounts functions - -/// Get a CoinJoin account by index from the managed collection -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection -/// - The returned pointer must be freed with `managed_core_account_free` when no longer needed -#[no_mangle] -pub unsafe extern "C" fn managed_account_collection_get_coinjoin_account( - collection: *const FFIManagedCoreAccountCollection, - index: c_uint, -) -> *mut FFIManagedCoreAccount { - if collection.is_null() { - return ptr::null_mut(); - } - - let collection = &*collection; - match collection.collection.coinjoin_accounts.get(&index) { - Some(account) => { - let ffi_account = FFIManagedCoreAccount::new(account); - Box::into_raw(Box::new(ffi_account)) - } - None => ptr::null_mut(), - } -} - -/// Get all CoinJoin account indices from managed collection -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection -/// - `out_indices` must be a valid pointer to store the indices array -/// - `out_count` must be a valid pointer to store the count -/// - The returned array must be freed with `free_u32_array` when no longer needed -#[no_mangle] -pub unsafe extern "C" fn managed_account_collection_get_coinjoin_indices( - collection: *const FFIManagedCoreAccountCollection, - out_indices: *mut *mut c_uint, - out_count: *mut usize, -) -> bool { - if collection.is_null() || out_indices.is_null() || out_count.is_null() { - return false; - } - - let collection = &*collection; - let mut indices: Vec = - collection.collection.coinjoin_accounts.keys().copied().collect(); - - if indices.is_empty() { - *out_indices = ptr::null_mut(); - *out_count = 0; - return true; - } - - indices.sort(); - - let mut boxed_slice = indices.into_boxed_slice(); - let ptr = boxed_slice.as_mut_ptr(); - let len = boxed_slice.len(); - std::mem::forget(boxed_slice); - - *out_indices = ptr; - *out_count = len; - true -} - -// Identity accounts functions - -/// Get the identity registration account if it exists in managed collection -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection -/// - The returned pointer must be freed with `managed_core_account_free` when no longer needed -#[no_mangle] -pub unsafe extern "C" fn managed_account_collection_get_identity_registration( - collection: *const FFIManagedCoreAccountCollection, -) -> *mut FFIManagedCoreAccount { - if collection.is_null() { - return ptr::null_mut(); - } - - let collection = &*collection; - match &collection.collection.identity_registration { - Some(account) => { - let ffi_account = FFIManagedCoreAccount::new_keys(account); - Box::into_raw(Box::new(ffi_account)) - } - None => ptr::null_mut(), - } -} - -/// Check if identity registration account exists in managed collection -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection -#[no_mangle] -pub unsafe extern "C" fn managed_account_collection_has_identity_registration( - collection: *const FFIManagedCoreAccountCollection, -) -> bool { - if collection.is_null() { - return false; - } - - let collection = &*collection; - collection.collection.identity_registration.is_some() -} - -/// Get an identity topup account by registration index from managed collection -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection -/// - The returned pointer must be freed with `managed_core_account_free` when no longer needed -#[no_mangle] -pub unsafe extern "C" fn managed_account_collection_get_identity_topup( - collection: *const FFIManagedCoreAccountCollection, - registration_index: c_uint, -) -> *mut FFIManagedCoreAccount { - if collection.is_null() { - return ptr::null_mut(); - } - - let collection = &*collection; - match collection.collection.identity_topup.get(®istration_index) { - Some(account) => { - let ffi_account = FFIManagedCoreAccount::new_keys(account); - Box::into_raw(Box::new(ffi_account)) - } - None => ptr::null_mut(), - } -} - -/// Get all identity topup registration indices from managed collection -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection -/// - `out_indices` must be a valid pointer to store the indices array -/// - `out_count` must be a valid pointer to store the count -/// - The returned array must be freed with `free_u32_array` when no longer needed -#[no_mangle] -pub unsafe extern "C" fn managed_account_collection_get_identity_topup_indices( - collection: *const FFIManagedCoreAccountCollection, - out_indices: *mut *mut c_uint, - out_count: *mut usize, -) -> bool { - if collection.is_null() || out_indices.is_null() || out_count.is_null() { - return false; - } - - let collection = &*collection; - let mut indices: Vec = collection.collection.identity_topup.keys().copied().collect(); - - if indices.is_empty() { - *out_indices = ptr::null_mut(); - *out_count = 0; - return true; - } - - indices.sort(); - - let mut boxed_slice = indices.into_boxed_slice(); - let ptr = boxed_slice.as_mut_ptr(); - let len = boxed_slice.len(); - std::mem::forget(boxed_slice); - - *out_indices = ptr; - *out_count = len; - true -} - -/// Get the identity topup not bound account if it exists in managed collection -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection -/// - `manager` must be a valid pointer to an FFIWalletManager -/// - The returned pointer must be freed with `managed_core_account_free` when no longer needed -#[no_mangle] -pub unsafe extern "C" fn managed_account_collection_get_identity_topup_not_bound( - collection: *const FFIManagedCoreAccountCollection, -) -> *mut FFIManagedCoreAccount { - if collection.is_null() { - return ptr::null_mut(); - } - - let collection = &*collection; - match &collection.collection.identity_topup_not_bound { - Some(account) => { - let ffi_account = FFIManagedCoreAccount::new_keys(account); - Box::into_raw(Box::new(ffi_account)) - } - None => ptr::null_mut(), - } -} - -/// Check if identity topup not bound account exists in managed collection -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection -#[no_mangle] -pub unsafe extern "C" fn managed_account_collection_has_identity_topup_not_bound( - collection: *const FFIManagedCoreAccountCollection, -) -> bool { - if collection.is_null() { - return false; - } - - let collection = &*collection; - collection.collection.identity_topup_not_bound.is_some() -} - -/// Get the identity invitation account if it exists in managed collection -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection -/// - The returned pointer must be freed with `managed_core_account_free` when no longer needed -#[no_mangle] -pub unsafe extern "C" fn managed_account_collection_get_identity_invitation( - collection: *const FFIManagedCoreAccountCollection, -) -> *mut FFIManagedCoreAccount { - if collection.is_null() { - return ptr::null_mut(); - } - - let collection = &*collection; - match &collection.collection.identity_invitation { - Some(account) => { - let ffi_account = FFIManagedCoreAccount::new_keys(account); - Box::into_raw(Box::new(ffi_account)) - } - None => ptr::null_mut(), - } -} - -/// Check if identity invitation account exists in managed collection -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection -#[no_mangle] -pub unsafe extern "C" fn managed_account_collection_has_identity_invitation( - collection: *const FFIManagedCoreAccountCollection, -) -> bool { - if collection.is_null() { - return false; - } - - let collection = &*collection; - collection.collection.identity_invitation.is_some() -} - -// Provider accounts functions - -/// Get the provider voting keys account if it exists in managed collection -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection -/// - The returned pointer must be freed with `managed_core_account_free` when no longer needed -#[no_mangle] -pub unsafe extern "C" fn managed_account_collection_get_provider_voting_keys( - collection: *const FFIManagedCoreAccountCollection, -) -> *mut FFIManagedCoreAccount { - if collection.is_null() { - return ptr::null_mut(); - } - - let collection = &*collection; - match &collection.collection.provider_voting_keys { - Some(account) => { - let ffi_account = FFIManagedCoreAccount::new_keys(account); - Box::into_raw(Box::new(ffi_account)) - } - None => ptr::null_mut(), - } -} - -/// Check if provider voting keys account exists in managed collection -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection -#[no_mangle] -pub unsafe extern "C" fn managed_account_collection_has_provider_voting_keys( - collection: *const FFIManagedCoreAccountCollection, -) -> bool { - if collection.is_null() { - return false; - } - - let collection = &*collection; - collection.collection.provider_voting_keys.is_some() -} - -/// Get the provider owner keys account if it exists in managed collection -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection -/// - The returned pointer must be freed with `managed_core_account_free` when no longer needed -#[no_mangle] -pub unsafe extern "C" fn managed_account_collection_get_provider_owner_keys( - collection: *const FFIManagedCoreAccountCollection, -) -> *mut FFIManagedCoreAccount { - if collection.is_null() { - return ptr::null_mut(); - } - - let collection = &*collection; - match &collection.collection.provider_owner_keys { - Some(account) => { - let ffi_account = FFIManagedCoreAccount::new_keys(account); - Box::into_raw(Box::new(ffi_account)) - } - None => ptr::null_mut(), - } -} - -/// Check if provider owner keys account exists in managed collection -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection -#[no_mangle] -pub unsafe extern "C" fn managed_account_collection_has_provider_owner_keys( - collection: *const FFIManagedCoreAccountCollection, -) -> bool { - if collection.is_null() { - return false; - } - - let collection = &*collection; - collection.collection.provider_owner_keys.is_some() -} - -/// Get the provider operator keys account if it exists in managed collection -/// Note: Returns null if the `bls` feature is not enabled -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection -/// - The returned pointer must be freed with `managed_core_account_free` when no longer needed (when BLS is enabled) -#[no_mangle] -pub unsafe extern "C" fn managed_account_collection_get_provider_operator_keys( - collection: *const FFIManagedCoreAccountCollection, -) -> *mut std::os::raw::c_void { - #[cfg(feature = "bls")] - { - if collection.is_null() { - return ptr::null_mut(); - } - - let collection = &*collection; - match &collection.collection.provider_operator_keys { - Some(account) => { - let ffi_account = FFIManagedCoreAccount::new_keys(account); - Box::into_raw(Box::new(ffi_account)) as *mut std::os::raw::c_void - } - None => ptr::null_mut(), - } - } - - #[cfg(not(feature = "bls"))] - { - // BLS feature not enabled, always return null - let _ = collection; // Avoid unused parameter warning - ptr::null_mut() - } -} - -/// Check if provider operator keys account exists in managed collection -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection -#[no_mangle] -pub unsafe extern "C" fn managed_account_collection_has_provider_operator_keys( - collection: *const FFIManagedCoreAccountCollection, -) -> bool { - if collection.is_null() { - return false; - } - - #[cfg(feature = "bls")] - { - let collection = &*collection; - collection.collection.provider_operator_keys.is_some() - } - - #[cfg(not(feature = "bls"))] - { - false - } -} - -/// Get the provider platform keys account if it exists in managed collection -/// Note: Returns null if the `eddsa` feature is not enabled -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection -/// - The returned pointer must be freed with `managed_core_account_free` when no longer needed (when EdDSA is enabled) -#[no_mangle] -pub unsafe extern "C" fn managed_account_collection_get_provider_platform_keys( - collection: *const FFIManagedCoreAccountCollection, -) -> *mut std::os::raw::c_void { - #[cfg(feature = "eddsa")] - { - if collection.is_null() { - return ptr::null_mut(); - } - - let collection = &*collection; - match &collection.collection.provider_platform_keys { - Some(account) => { - let ffi_account = FFIManagedCoreAccount::new_keys(account); - Box::into_raw(Box::new(ffi_account)) as *mut std::os::raw::c_void - } - None => ptr::null_mut(), - } - } - - #[cfg(not(feature = "eddsa"))] - { - // EdDSA feature not enabled, always return null - let _ = collection; // Avoid unused parameter warning - ptr::null_mut() - } -} - -/// Check if provider platform keys account exists in managed collection -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection -#[no_mangle] -pub unsafe extern "C" fn managed_account_collection_has_provider_platform_keys( - collection: *const FFIManagedCoreAccountCollection, -) -> bool { - if collection.is_null() { - return false; - } - - #[cfg(feature = "eddsa")] - { - let collection = &*collection; - collection.collection.provider_platform_keys.is_some() - } - - #[cfg(not(feature = "eddsa"))] - { - false - } -} - -// Platform Payment accounts functions - -/// Get a Platform Payment account by account index and key class from the managed collection -/// -/// Platform Payment accounts (DIP-17) are identified by two indices: -/// - account_index: The account' level in the derivation path -/// - key_class: The key_class' level in the derivation path (typically 0) -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection -/// - The returned pointer must be freed with `managed_platform_account_free` when no longer needed -#[no_mangle] -pub unsafe extern "C" fn managed_account_collection_get_platform_payment_account( - collection: *const FFIManagedCoreAccountCollection, - account_index: c_uint, - key_class: c_uint, -) -> *mut crate::managed_account::FFIManagedPlatformAccount { - if collection.is_null() { - return ptr::null_mut(); - } - - let collection = &*collection; - let key = key_wallet::account::account_collection::PlatformPaymentAccountKey { - account: account_index, - key_class, - }; - - match collection.collection.platform_payment_accounts.get(&key) { - Some(account) => { - let ffi_account = crate::managed_account::FFIManagedPlatformAccount::new(account); - Box::into_raw(Box::new(ffi_account)) - } - None => ptr::null_mut(), - } -} - -/// Get all Platform Payment account keys from managed collection -/// -/// Returns an array of FFIPlatformPaymentAccountKey structures. -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection -/// - `out_keys` must be a valid pointer to store the keys array -/// - `out_count` must be a valid pointer to store the count -/// - The returned array must be freed with `managed_account_collection_free_platform_payment_keys` when no longer needed -#[no_mangle] -pub unsafe extern "C" fn managed_account_collection_get_platform_payment_keys( - collection: *const FFIManagedCoreAccountCollection, - out_keys: *mut *mut crate::managed_account::FFIPlatformPaymentAccountKey, - out_count: *mut usize, -) -> bool { - if collection.is_null() || out_keys.is_null() || out_count.is_null() { - return false; - } - - let collection = &*collection; - let keys: Vec = collection - .collection - .platform_payment_accounts - .keys() - .map(crate::managed_account::FFIPlatformPaymentAccountKey::from) - .collect(); - - if keys.is_empty() { - *out_keys = ptr::null_mut(); - *out_count = 0; - return true; - } - - let mut boxed_slice = keys.into_boxed_slice(); - let ptr = boxed_slice.as_mut_ptr(); - let len = boxed_slice.len(); - std::mem::forget(boxed_slice); - - *out_keys = ptr; - *out_count = len; - true -} - -/// Free platform payment keys array returned by managed_account_collection_get_platform_payment_keys -/// -/// # Safety -/// -/// - `keys` must be a pointer returned by `managed_account_collection_get_platform_payment_keys` -/// - `count` must be the count returned by `managed_account_collection_get_platform_payment_keys` -/// - This function must only be called once per allocation -#[no_mangle] -pub unsafe extern "C" fn managed_account_collection_free_platform_payment_keys( - keys: *mut crate::managed_account::FFIPlatformPaymentAccountKey, - count: usize, -) { - if !keys.is_null() && count > 0 { - let _ = Vec::from_raw_parts(keys, count, count); - } -} - -/// Check if there are any Platform Payment accounts in the managed collection -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection -#[no_mangle] -pub unsafe extern "C" fn managed_account_collection_has_platform_payment_accounts( - collection: *const FFIManagedCoreAccountCollection, -) -> bool { - if collection.is_null() { - return false; - } - - let collection = &*collection; - !collection.collection.platform_payment_accounts.is_empty() -} - -/// Get the number of Platform Payment accounts in the managed collection -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection -#[no_mangle] -pub unsafe extern "C" fn managed_account_collection_platform_payment_count( - collection: *const FFIManagedCoreAccountCollection, -) -> c_uint { - if collection.is_null() { - return 0; - } - - let collection = &*collection; - collection.collection.platform_payment_accounts.len() as c_uint -} - -// Utility functions - -/// Get the total number of accounts in the managed collection -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection -#[no_mangle] -pub unsafe extern "C" fn managed_account_collection_count( - collection: *const FFIManagedCoreAccountCollection, -) -> c_uint { - if collection.is_null() { - return 0; - } - - let collection = &*collection; - let mut count = 0u32; - - count += collection.collection.standard_bip44_accounts.len() as u32; - count += collection.collection.standard_bip32_accounts.len() as u32; - count += collection.collection.coinjoin_accounts.len() as u32; - count += collection.collection.identity_topup.len() as u32; - - if collection.collection.identity_registration.is_some() { - count += 1; - } - if collection.collection.identity_topup_not_bound.is_some() { - count += 1; - } - if collection.collection.identity_invitation.is_some() { - count += 1; - } - if collection.collection.provider_voting_keys.is_some() { - count += 1; - } - if collection.collection.provider_owner_keys.is_some() { - count += 1; - } - - #[cfg(feature = "bls")] - if collection.collection.provider_operator_keys.is_some() { - count += 1; - } - - #[cfg(feature = "eddsa")] - if collection.collection.provider_platform_keys.is_some() { - count += 1; - } - - // Platform payment accounts - count += collection.collection.platform_payment_accounts.len() as u32; - - count -} - -/// Get a human-readable summary of all accounts in the managed collection -/// -/// Returns a formatted string showing all account types and their indices. -/// The format is designed to be clear and readable for end users. -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection -/// - The returned string must be freed with `string_free` when no longer needed -/// - Returns null if the collection pointer is null -#[no_mangle] -pub unsafe extern "C" fn managed_account_collection_summary( - collection: *const FFIManagedCoreAccountCollection, -) -> *mut c_char { - if collection.is_null() { - return ptr::null_mut(); - } - - let collection = &*collection; - let mut summary_parts = Vec::new(); - - summary_parts.push("Managed Account Summary:".to_string()); - - // BIP44 Accounts - if !collection.collection.standard_bip44_accounts.is_empty() { - let mut indices: Vec = - collection.collection.standard_bip44_accounts.keys().copied().collect(); - indices.sort(); - let count = indices.len(); - let indices_str = format!("{:?}", indices); - summary_parts.push(format!( - "• BIP44 Accounts: {} {} at indices {}", - count, - if count == 1 { - "account" - } else { - "accounts" - }, - indices_str - )); - } - - // BIP32 Accounts - if !collection.collection.standard_bip32_accounts.is_empty() { - let mut indices: Vec = - collection.collection.standard_bip32_accounts.keys().copied().collect(); - indices.sort(); - let count = indices.len(); - let indices_str = format!("{:?}", indices); - summary_parts.push(format!( - "• BIP32 Accounts: {} {} at indices {}", - count, - if count == 1 { - "account" - } else { - "accounts" - }, - indices_str - )); - } - - // CoinJoin Accounts - if !collection.collection.coinjoin_accounts.is_empty() { - let mut indices: Vec = - collection.collection.coinjoin_accounts.keys().copied().collect(); - indices.sort(); - let count = indices.len(); - let indices_str = format!("{:?}", indices); - summary_parts.push(format!( - "• CoinJoin Accounts: {} {} at indices {}", - count, - if count == 1 { - "account" - } else { - "accounts" - }, - indices_str - )); - } - - // Identity TopUp Accounts - if !collection.collection.identity_topup.is_empty() { - let mut indices: Vec = collection.collection.identity_topup.keys().copied().collect(); - indices.sort(); - let count = indices.len(); - let indices_str = format!("{:?}", indices); - summary_parts.push(format!( - "• Identity TopUp: {} {} at indices {}", - count, - if count == 1 { - "account" - } else { - "accounts" - }, - indices_str - )); - } - - // Special accounts (single instances) - if collection.collection.identity_registration.is_some() { - summary_parts.push("• Identity Registration Account".to_string()); - } - - if collection.collection.identity_topup_not_bound.is_some() { - summary_parts.push("• Identity TopUp Not Bound Account".to_string()); - } - - if collection.collection.identity_invitation.is_some() { - summary_parts.push("• Identity Invitation Account".to_string()); - } - - if collection.collection.provider_voting_keys.is_some() { - summary_parts.push("• Provider Voting Keys Account".to_string()); - } - - if collection.collection.provider_owner_keys.is_some() { - summary_parts.push("• Provider Owner Keys Account".to_string()); - } - - #[cfg(feature = "bls")] - if collection.collection.provider_operator_keys.is_some() { - summary_parts.push("• Provider Operator Keys Account (BLS)".to_string()); - } - - #[cfg(feature = "eddsa")] - if collection.collection.provider_platform_keys.is_some() { - summary_parts.push("• Provider Platform Keys Account (EdDSA)".to_string()); - } - - // Platform Payment Accounts - if !collection.collection.platform_payment_accounts.is_empty() { - let count = collection.collection.platform_payment_accounts.len(); - let keys: Vec = collection - .collection - .platform_payment_accounts - .keys() - .map(|k| format!("({},{})", k.account, k.key_class)) - .collect(); - summary_parts.push(format!( - "• Platform Payment: {} {} at keys {}", - count, - if count == 1 { - "account" - } else { - "accounts" - }, - keys.join(", ") - )); - } - - // If there are no accounts at all - if summary_parts.len() == 1 { - summary_parts.push("No accounts configured".to_string()); - } - - let summary = summary_parts.join("\n"); - - match CString::new(summary) { - Ok(c_str) => c_str.into_raw(), - Err(_) => ptr::null_mut(), - } -} - -/// Get structured account collection summary data for managed collection -/// -/// Returns a struct containing arrays of indices for each account type and boolean -/// flags for special accounts. This provides Swift with programmatic access to -/// account information. -/// -/// # Safety -/// -/// - `collection` must be a valid pointer to an FFIManagedCoreAccountCollection -/// - The returned pointer must be freed with `managed_account_collection_summary_free` when no longer needed -/// - Returns null if the collection pointer is null -#[no_mangle] -pub unsafe extern "C" fn managed_account_collection_summary_data( - collection: *const FFIManagedCoreAccountCollection, -) -> *mut FFIManagedCoreAccountCollectionSummary { - if collection.is_null() { - return ptr::null_mut(); - } - - let collection = &*collection; - - // Collect BIP44 indices - let mut bip44_indices: Vec = - collection.collection.standard_bip44_accounts.keys().copied().collect(); - bip44_indices.sort(); - let (bip44_ptr, bip44_count) = if bip44_indices.is_empty() { - (ptr::null_mut(), 0) - } else { - let count = bip44_indices.len(); - let mut boxed_slice = bip44_indices.into_boxed_slice(); - let ptr = boxed_slice.as_mut_ptr(); - std::mem::forget(boxed_slice); - (ptr, count) - }; - - // Collect BIP32 indices - let mut bip32_indices: Vec = - collection.collection.standard_bip32_accounts.keys().copied().collect(); - bip32_indices.sort(); - let (bip32_ptr, bip32_count) = if bip32_indices.is_empty() { - (ptr::null_mut(), 0) - } else { - let count = bip32_indices.len(); - let mut boxed_slice = bip32_indices.into_boxed_slice(); - let ptr = boxed_slice.as_mut_ptr(); - std::mem::forget(boxed_slice); - (ptr, count) - }; - - // Collect CoinJoin indices - let mut coinjoin_indices: Vec = - collection.collection.coinjoin_accounts.keys().copied().collect(); - coinjoin_indices.sort(); - let (coinjoin_ptr, coinjoin_count) = if coinjoin_indices.is_empty() { - (ptr::null_mut(), 0) - } else { - let count = coinjoin_indices.len(); - let mut boxed_slice = coinjoin_indices.into_boxed_slice(); - let ptr = boxed_slice.as_mut_ptr(); - std::mem::forget(boxed_slice); - (ptr, count) - }; - - // Collect identity topup indices - let mut topup_indices: Vec = - collection.collection.identity_topup.keys().copied().collect(); - topup_indices.sort(); - let (topup_ptr, topup_count) = if topup_indices.is_empty() { - (ptr::null_mut(), 0) - } else { - let count = topup_indices.len(); - let mut boxed_slice = topup_indices.into_boxed_slice(); - let ptr = boxed_slice.as_mut_ptr(); - std::mem::forget(boxed_slice); - (ptr, count) - }; - - // Collect platform payment keys - let platform_payment_keys: Vec = - collection - .collection - .platform_payment_accounts - .keys() - .map(crate::managed_account::FFIPlatformPaymentAccountKey::from) - .collect(); - let (platform_payment_ptr, platform_payment_count) = if platform_payment_keys.is_empty() { - (ptr::null_mut(), 0) - } else { - let count = platform_payment_keys.len(); - let mut boxed_slice = platform_payment_keys.into_boxed_slice(); - let ptr = boxed_slice.as_mut_ptr(); - std::mem::forget(boxed_slice); - (ptr, count) - }; - - // Create the summary struct - let summary = FFIManagedCoreAccountCollectionSummary { - bip44_indices: bip44_ptr, - bip44_count, - bip32_indices: bip32_ptr, - bip32_count, - coinjoin_indices: coinjoin_ptr, - coinjoin_count, - identity_topup_indices: topup_ptr, - identity_topup_count: topup_count, - has_identity_registration: collection.collection.identity_registration.is_some(), - has_identity_invitation: collection.collection.identity_invitation.is_some(), - has_identity_topup_not_bound: collection.collection.identity_topup_not_bound.is_some(), - has_provider_voting_keys: collection.collection.provider_voting_keys.is_some(), - has_provider_owner_keys: collection.collection.provider_owner_keys.is_some(), - #[cfg(feature = "bls")] - has_provider_operator_keys: collection.collection.provider_operator_keys.is_some(), - #[cfg(feature = "eddsa")] - has_provider_platform_keys: collection.collection.provider_platform_keys.is_some(), - platform_payment_keys: platform_payment_ptr, - platform_payment_count, - }; - - Box::into_raw(Box::new(summary)) -} - -/// Free a managed account collection summary and all its allocated memory -/// -/// # Safety -/// -/// - `summary` must be a valid pointer to an FFIManagedCoreAccountCollectionSummary created by `managed_account_collection_summary_data` -/// - `summary` must not be used after calling this function -#[no_mangle] -pub unsafe extern "C" fn managed_account_collection_summary_free( - summary: *mut FFIManagedCoreAccountCollectionSummary, -) { - if !summary.is_null() { - let summary = Box::from_raw(summary); - - // Free all the allocated arrays - if !summary.bip44_indices.is_null() && summary.bip44_count > 0 { - let _ = Vec::from_raw_parts( - summary.bip44_indices, - summary.bip44_count, - summary.bip44_count, - ); - } - - if !summary.bip32_indices.is_null() && summary.bip32_count > 0 { - let _ = Vec::from_raw_parts( - summary.bip32_indices, - summary.bip32_count, - summary.bip32_count, - ); - } - - if !summary.coinjoin_indices.is_null() && summary.coinjoin_count > 0 { - let _ = Vec::from_raw_parts( - summary.coinjoin_indices, - summary.coinjoin_count, - summary.coinjoin_count, - ); - } - - if !summary.identity_topup_indices.is_null() && summary.identity_topup_count > 0 { - let _ = Vec::from_raw_parts( - summary.identity_topup_indices, - summary.identity_topup_count, - summary.identity_topup_count, - ); - } - - if !summary.platform_payment_keys.is_null() && summary.platform_payment_count > 0 { - let _ = Vec::from_raw_parts( - summary.platform_payment_keys, - summary.platform_payment_count, - summary.platform_payment_count, - ); - } - - // The summary struct itself is dropped automatically when the Box is dropped - } -} diff --git a/key-wallet-ffi/src/managed_wallet.rs b/key-wallet-ffi/src/managed_wallet.rs deleted file mode 100644 index 8f6c6fd71..000000000 --- a/key-wallet-ffi/src/managed_wallet.rs +++ /dev/null @@ -1,860 +0,0 @@ -//! Managed wallet FFI bindings -//! -//! This module provides FFI bindings for ManagedWalletInfo which includes -//! address management, UTXO tracking, and transaction building capabilities. -//! - -use std::ffi::CString; -use std::os::raw::{c_char, c_uint}; -use std::ptr; - -use crate::error::{FFIError, FFIErrorCode}; -use crate::types::FFIWallet; -use crate::{check_ptr, deref_ptr, deref_ptr_mut, unwrap_or_return}; -use key_wallet::managed_account::address_pool::KeySource; -use key_wallet::managed_account::managed_account_trait::ManagedAccountTrait; -use key_wallet::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface; -use key_wallet::wallet::managed_wallet_info::ManagedWalletInfo; -use std::ffi::c_void; - -/// FFI wrapper for ManagedWalletInfo (single canonical type) -#[repr(C)] -pub struct FFIManagedWalletInfo { - // Opaque pointer to avoid leaking ManagedWalletInfo into C headers - inner: *mut c_void, -} - -impl FFIManagedWalletInfo { - /// Create a new FFIManagedWalletInfo from a ManagedWalletInfo - pub fn new(inner: ManagedWalletInfo) -> Self { - Self { - inner: Box::into_raw(Box::new(inner)) as *mut c_void, - } - } - - pub fn inner(&self) -> &ManagedWalletInfo { - unsafe { &*(self.inner as *const ManagedWalletInfo) } - } - - pub fn inner_mut(&mut self) -> &mut ManagedWalletInfo { - unsafe { &mut *(self.inner as *mut ManagedWalletInfo) } - } -} - -/// Get the next unused receive address -/// -/// Generates the next unused receive address for the specified account. -/// This properly manages address gaps and updates the managed wallet state. -/// -/// # Safety -/// -/// - `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo -/// - `wallet` must be a valid pointer to an FFIWallet -/// - `error` must be a valid pointer to an FFIError -/// - The returned string must be freed by the caller -#[no_mangle] -pub unsafe extern "C" fn managed_wallet_get_next_bip44_receive_address( - managed_wallet: *mut FFIManagedWalletInfo, - wallet: *const FFIWallet, - account_index: std::os::raw::c_uint, - error: *mut FFIError, -) -> *mut c_char { - let managed_wallet = deref_ptr_mut!(managed_wallet, error); - let wallet = deref_ptr!(wallet, error); - - // Get the specific managed account (default to BIP44) - let managed_account = unwrap_or_return!( - managed_wallet.inner_mut().accounts.standard_bip44_accounts.get_mut(&account_index), - error - ); - - // Get the account from the wallet to get the extended public key - let account = unwrap_or_return!( - wallet.wallet.accounts.standard_bip44_accounts.get(&account_index), - error - ); - - // Generate the next receive address - let xpub = account.extended_public_key(); - let address = unwrap_or_return!(managed_account.next_receive_address(Some(&xpub), true), error) - .to_string(); - unwrap_or_return!(CString::new(address), error).into_raw() -} - -/// Get the next unused change address -/// -/// Generates the next unused change address for the specified account. -/// This properly manages address gaps and updates the managed wallet state. -/// -/// # Safety -/// -/// - `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo -/// - `wallet` must be a valid pointer to an FFIWallet -/// - `error` must be a valid pointer to an FFIError -/// - The returned string must be freed by the caller -#[no_mangle] -pub unsafe extern "C" fn managed_wallet_get_next_bip44_change_address( - managed_wallet: *mut FFIManagedWalletInfo, - wallet: *const FFIWallet, - account_index: std::os::raw::c_uint, - error: *mut FFIError, -) -> *mut c_char { - let managed_wallet = deref_ptr_mut!(managed_wallet, error); - let wallet = deref_ptr!(wallet, error); - - // Get the specific managed account (default to BIP44) - let managed_account = unwrap_or_return!( - managed_wallet.inner_mut().accounts.standard_bip44_accounts.get_mut(&account_index), - error - ); - - // Get the account from the wallet to get the extended public key - let account = unwrap_or_return!( - wallet.wallet.accounts.standard_bip44_accounts.get(&account_index), - error - ); - - // Generate the next change address - let xpub = account.extended_public_key(); - let next_change_address = - unwrap_or_return!(managed_account.next_change_address(Some(&xpub), true), error) - .to_string(); - unwrap_or_return!(CString::new(next_change_address), error).into_raw() -} - -/// Get BIP44 external (receive) addresses in the specified range -/// -/// Returns external addresses from start_index (inclusive) to end_index (exclusive). -/// If addresses in the range haven't been generated yet, they will be generated. -/// -/// # Safety -/// -/// - `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo -/// - `wallet` must be a valid pointer to an FFIWallet -/// - `addresses_out` must be a valid pointer to store the address array pointer -/// - `count_out` must be a valid pointer to store the count -/// - `error` must be a valid pointer to an FFIError -/// - Free the result with address_array_free(addresses_out, count_out) -#[no_mangle] -pub unsafe extern "C" fn managed_wallet_get_bip_44_external_address_range( - managed_wallet: *mut FFIManagedWalletInfo, - wallet: *const FFIWallet, - account_index: std::os::raw::c_uint, - start_index: std::os::raw::c_uint, - end_index: std::os::raw::c_uint, - addresses_out: *mut *mut *mut c_char, - count_out: *mut usize, - error: *mut FFIError, -) -> bool { - check_ptr!(addresses_out, error); - check_ptr!(count_out, error); - let managed_wallet = deref_ptr_mut!(managed_wallet, error); - let wallet = deref_ptr!(wallet, error); - - // Get the specific managed account (BIP44) - let managed_account = unwrap_or_return!( - managed_wallet.inner_mut().accounts.standard_bip44_accounts.get_mut(&account_index), - error - ); - - // Get the account from the wallet to get the extended public key - let account = unwrap_or_return!( - wallet.wallet.accounts.standard_bip44_accounts.get(&account_index), - error - ); - - // Get external addresses in the range - let xpub = account.extended_public_key(); - let key_source = KeySource::Public(xpub); - - // Access the external address pool from the managed account - let addresses = if let key_wallet::account::ManagedAccountType::Standard { - external_addresses, - .. - } = managed_account.managed_account_type_mut() - { - unwrap_or_return!( - external_addresses.address_range(start_index, end_index, &key_source), - error - ) - } else { - (*error).set(FFIErrorCode::WalletError, "Account is not a standard BIP44 account"); - *count_out = 0; - *addresses_out = ptr::null_mut(); - return false; - }; - - // Convert addresses to C strings - let mut c_addresses = Vec::with_capacity(addresses.len()); - for address in addresses { - let c_str = unwrap_or_return!(CString::new(address.to_string()), error).into_raw(); - c_addresses.push(c_str); - } - - // Convert Vec to Box<[*mut c_char]> and leak it properly - let boxed_slice = c_addresses.into_boxed_slice(); - let len = boxed_slice.len(); - let ptr = Box::into_raw(boxed_slice) as *mut *mut c_char; - - *count_out = len; - *addresses_out = ptr; - (*error).clean(); - true -} - -/// Get BIP44 internal (change) addresses in the specified range -/// -/// Returns internal addresses from start_index (inclusive) to end_index (exclusive). -/// If addresses in the range haven't been generated yet, they will be generated. -/// -/// # Safety -/// -/// - `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo -/// - `wallet` must be a valid pointer to an FFIWallet -/// - `addresses_out` must be a valid pointer to store the address array pointer -/// - `count_out` must be a valid pointer to store the count -/// - `error` must be a valid pointer to an FFIError -/// - Free the result with address_array_free(addresses_out, count_out) -#[no_mangle] -pub unsafe extern "C" fn managed_wallet_get_bip_44_internal_address_range( - managed_wallet: *mut FFIManagedWalletInfo, - wallet: *const FFIWallet, - account_index: std::os::raw::c_uint, - start_index: std::os::raw::c_uint, - end_index: std::os::raw::c_uint, - addresses_out: *mut *mut *mut c_char, - count_out: *mut usize, - error: *mut FFIError, -) -> bool { - check_ptr!(addresses_out, error); - check_ptr!(count_out, error); - let managed_wallet = deref_ptr_mut!(managed_wallet, error); - let wallet = deref_ptr!(wallet, error); - - // Get the specific managed account (BIP44) - let managed_account = unwrap_or_return!( - managed_wallet.inner_mut().accounts.standard_bip44_accounts.get_mut(&account_index), - error - ); - - // Get the account from the wallet to get the extended public key - let account = unwrap_or_return!( - wallet.wallet.accounts.standard_bip44_accounts.get(&account_index), - error - ); - - // Get internal addresses in the range - let xpub = account.extended_public_key(); - let key_source = KeySource::Public(xpub); - - // Access the internal address pool from the managed account - let addresses = if let key_wallet::account::ManagedAccountType::Standard { - internal_addresses, - .. - } = managed_account.managed_account_type_mut() - { - unwrap_or_return!( - internal_addresses.address_range(start_index, end_index, &key_source), - error - ) - } else { - (*error).set(FFIErrorCode::WalletError, "Account is not a standard BIP44 account"); - *count_out = 0; - *addresses_out = ptr::null_mut(); - return false; - }; - - // Convert addresses to C strings - let mut c_addresses = Vec::with_capacity(addresses.len()); - for address in addresses { - let c_str = unwrap_or_return!(CString::new(address.to_string()), error).into_raw(); - c_addresses.push(c_str); - } - - // Convert Vec to Box<[*mut c_char]> and leak it properly - let boxed_slice = c_addresses.into_boxed_slice(); - let len = boxed_slice.len(); - let ptr = Box::into_raw(boxed_slice) as *mut *mut c_char; - - *count_out = len; - *addresses_out = ptr; - (*error).clean(); - true -} - -/// Get wallet balance from managed wallet info -/// -/// Returns the balance breakdown including confirmed, unconfirmed, immature, locked, and total amounts. -/// -/// # Safety -/// -/// - `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo -/// - `confirmed_out` must be a valid pointer to store the confirmed balance -/// - `unconfirmed_out` must be a valid pointer to store the unconfirmed balance -/// - `immature_out` must be a valid pointer to store the immature balance -/// - `locked_out` must be a valid pointer to store the locked balance -/// - `total_out` must be a valid pointer to store the total balance -/// - `error` must be a valid pointer to an FFIError -#[no_mangle] -pub unsafe extern "C" fn managed_wallet_get_balance( - managed_wallet: *const FFIManagedWalletInfo, - confirmed_out: *mut u64, - unconfirmed_out: *mut u64, - immature_out: *mut u64, - locked_out: *mut u64, - total_out: *mut u64, - error: *mut FFIError, -) -> bool { - let managed_wallet = deref_ptr!(managed_wallet, error); - check_ptr!(confirmed_out, error); - check_ptr!(unconfirmed_out, error); - check_ptr!(immature_out, error); - check_ptr!(locked_out, error); - check_ptr!(total_out, error); - - let balance = &managed_wallet.inner().balance; - *confirmed_out = balance.confirmed(); - *unconfirmed_out = balance.unconfirmed(); - *immature_out = balance.immature(); - *locked_out = balance.locked(); - *total_out = balance.total(); - true -} - -/// Get current last processed height from wallet info -/// -/// # Safety -/// -/// - `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo -/// - `error` must be a valid pointer to an FFIError structure -/// - The caller must ensure all pointers remain valid for the duration of this call -#[no_mangle] -pub unsafe extern "C" fn managed_wallet_last_processed_height( - managed_wallet: *const FFIManagedWalletInfo, - error: *mut FFIError, -) -> c_uint { - let managed_wallet = deref_ptr!(managed_wallet, error); - managed_wallet.inner().last_processed_height() -} - -/// Free managed wallet info -/// -/// # Safety -/// -/// - `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo or null -/// - After calling this function, the pointer becomes invalid and must not be used -#[no_mangle] -pub unsafe extern "C" fn managed_wallet_free(managed_wallet: *mut FFIManagedWalletInfo) { - if !managed_wallet.is_null() { - // Reclaim outer struct, then free inner if present - let wrapper = Box::from_raw(managed_wallet); - if !wrapper.inner.is_null() { - let _ = Box::from_raw(wrapper.inner as *mut ManagedWalletInfo); - } - } -} - -/// Free managed wallet info returned by wallet_manager_get_managed_wallet_info -/// -/// # Safety -/// -/// - `wallet_info` must be a valid pointer returned by wallet_manager_get_managed_wallet_info or null -/// - After calling this function, the pointer becomes invalid and must not be used -#[no_mangle] -pub unsafe extern "C" fn managed_wallet_info_free(wallet_info: *mut FFIManagedWalletInfo) { - if !wallet_info.is_null() { - let wrapper = Box::from_raw(wallet_info); - if !wrapper.inner.is_null() { - let _ = Box::from_raw(wrapper.inner as *mut ManagedWalletInfo); - } - } -} - -#[cfg(test)] -mod tests { - use crate::error::{FFIError, FFIErrorCode}; - use crate::managed_wallet::*; - use crate::wallet; - use dash_network::ffi::FFINetwork; - use key_wallet::managed_account::managed_account_type::ManagedAccountType; - use std::ffi::{CStr, CString}; - use std::ptr; - - const TEST_MNEMONIC: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; - - // Note: managed_wallet_create has been removed as client libraries - // should only get ManagedWalletInfo through WalletManager - - #[test] - fn test_managed_wallet_free_null() { - // Should not crash when freeing null - unsafe { - managed_wallet_free(ptr::null_mut()); - } - } - - #[test] - fn test_managed_wallet_get_next_receive_address_null_pointers() { - let mut error = FFIError::default(); - - // Test with null managed wallet - let address = unsafe { - managed_wallet_get_next_bip44_receive_address( - ptr::null_mut(), - ptr::null(), - 0, - &mut error, - ) - }; - - assert!(address.is_null()); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - } - - #[test] - fn test_managed_wallet_get_next_change_address_null_pointers() { - let mut error = FFIError::default(); - - // Test with null managed wallet - let address = unsafe { - managed_wallet_get_next_bip44_change_address( - ptr::null_mut(), - ptr::null(), - 0, - &mut error, - ) - }; - - assert!(address.is_null()); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - } - - #[test] - fn test_managed_wallet_get_bip_44_external_address_range_null_pointers() { - let mut error = FFIError::default(); - let mut addresses_out: *mut *mut c_char = ptr::null_mut(); - let mut count_out: usize = 0; - - // Test with null managed wallet - let success = unsafe { - managed_wallet_get_bip_44_external_address_range( - ptr::null_mut(), - ptr::null(), - 0, - 0, - 10, - &mut addresses_out, - &mut count_out, - &mut error, - ) - }; - - assert!(!success); - assert_eq!(count_out, 0); - assert!(addresses_out.is_null()); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - } - - #[test] - fn test_managed_wallet_get_bip_44_internal_address_range_null_pointers() { - let mut error = FFIError::default(); - let mut addresses_out: *mut *mut c_char = ptr::null_mut(); - let mut count_out: usize = 0; - - // Test with null managed wallet - let success = unsafe { - managed_wallet_get_bip_44_internal_address_range( - ptr::null_mut(), - ptr::null(), - 0, - 0, - 10, - &mut addresses_out, - &mut count_out, - &mut error, - ) - }; - - assert!(!success); - assert_eq!(count_out, 0); - assert!(addresses_out.is_null()); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - } - - #[test] - fn test_managed_wallet_address_generation_with_valid_wallet() { - let mut error = FFIError::default(); - - // Create a wallet - let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); - - let wallet = unsafe { - wallet::wallet_create_from_mnemonic(mnemonic.as_ptr(), FFINetwork::Testnet, &mut error) - }; - assert!(!wallet.is_null()); - assert_eq!(error.code, FFIErrorCode::Success); - - // Create managed wallet info from the wallet heap-allocated like C would do - let wallet_rust = unsafe { &(*wallet).wallet }; - let managed_info = ManagedWalletInfo::from_wallet(wallet_rust, 0); - let ffi_managed = Box::into_raw(Box::new(FFIManagedWalletInfo::new(managed_info))); - - // Test get_next_receive_address with valid pointers - let receive_addr = unsafe { - managed_wallet_get_next_bip44_receive_address(ffi_managed, wallet, 0, &mut error) - }; - - if !receive_addr.is_null() { - // If successful, verify the address - let addr_str = unsafe { CStr::from_ptr(receive_addr).to_string_lossy() }; - assert!(!addr_str.is_empty()); - - // Free the address string - unsafe { - let _ = CString::from_raw(receive_addr); - } - } else { - // It's ok if it fails due to no accounts being initialized - // This would happen in a real scenario where WalletManager would - // properly initialize the accounts - assert_eq!(error.code, FFIErrorCode::WalletError); - } - - // Test get_next_change_address with valid pointers - let change_addr = unsafe { - managed_wallet_get_next_bip44_change_address(ffi_managed, wallet, 0, &mut error) - }; - - if !change_addr.is_null() { - // If successful, verify the address - let addr_str = unsafe { CStr::from_ptr(change_addr).to_string_lossy() }; - assert!(!addr_str.is_empty()); - - // Free the address string - unsafe { - let _ = CString::from_raw(change_addr); - } - } else { - // It's ok if it fails due to no accounts being initialized - assert_eq!(error.code, FFIErrorCode::WalletError); - } - - // Clean up - unsafe { - managed_wallet_free(ffi_managed); - wallet::wallet_free(wallet); - } - } - - #[test] - fn test_comprehensive_address_generation() { - use key_wallet::account::{ - ManagedAccountCollection, ManagedCoreFundsAccount, StandardAccountType, - }; - use key_wallet::bip32::DerivationPath; - use key_wallet::managed_account::address_pool::{AddressPool, AddressPoolType}; - - let mut error = FFIError::default(); - - // Create a wallet with a known mnemonic - let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); - - let wallet_ptr = unsafe { - wallet::wallet_create_from_mnemonic(mnemonic.as_ptr(), FFINetwork::Testnet, &mut error) - }; - assert!(!wallet_ptr.is_null()); - assert_eq!(error.code, FFIErrorCode::Success); - - // Get the actual wallet - let wallet_arc = unsafe { &(*wallet_ptr).wallet }; - - // We need to work with the existing wallet structure - // Create managed wallet info from the existing wallet - let mut managed_info = ManagedWalletInfo::from_wallet(wallet_arc, 0); - - let network = key_wallet::Network::Testnet; - - // Initialize the managed account collection properly - let mut managed_collection = ManagedAccountCollection::new(); - - // Create a managed account with address pools - // Using NoKeySource for test data - let key_source = KeySource::NoKeySource; - let external_pool = AddressPool::new( - DerivationPath::from(vec![key_wallet::bip32::ChildNumber::from_normal_idx(0).unwrap()]), - AddressPoolType::External, - 20, - network, - &key_source, - ) - .expect("Failed to create external pool"); - let internal_pool = AddressPool::new( - DerivationPath::from(vec![key_wallet::bip32::ChildNumber::from_normal_idx(1).unwrap()]), - AddressPoolType::Internal, - 20, - network, - &key_source, - ) - .expect("Failed to create internal pool"); - - let managed_account = ManagedCoreFundsAccount::new( - ManagedAccountType::Standard { - index: 0, - standard_account_type: StandardAccountType::BIP44Account, - external_addresses: external_pool, - internal_addresses: internal_pool, - }, - network, - ); - - managed_collection.standard_bip44_accounts.insert(0, managed_account.clone()); - // Insert the managed account directly into managed_info's accounts - managed_info - .accounts - .insert(managed_account) - .expect("insert should succeed for Standard account"); - - // Create wrapper for managed info heap-allocated like C would do - let ffi_managed = Box::into_raw(Box::new(FFIManagedWalletInfo::new(managed_info))); - - // Use the existing wallet pointer - let ffi_wallet_ptr = wallet_ptr; - - // Test 1: Get next receive address - let receive_addr = unsafe { - managed_wallet_get_next_bip44_receive_address( - ffi_managed, - ffi_wallet_ptr, - 0, - &mut error, - ) - }; - - assert!(!receive_addr.is_null()); - let receive_str = unsafe { CStr::from_ptr(receive_addr).to_string_lossy() }; - assert!(!receive_str.is_empty()); - println!("Generated receive address: {}", receive_str); - unsafe { - let _ = CString::from_raw(receive_addr); - } - - // Test 2: Get next change address - let change_addr = unsafe { - managed_wallet_get_next_bip44_change_address(ffi_managed, ffi_wallet_ptr, 0, &mut error) - }; - - assert!(!change_addr.is_null()); - let change_str = unsafe { CStr::from_ptr(change_addr).to_string_lossy() }; - assert!(!change_str.is_empty()); - println!("Generated change address: {}", change_str); - unsafe { - let _ = CString::from_raw(change_addr); - } - - // Test 3: Get external address range - let mut addresses_out: *mut *mut c_char = ptr::null_mut(); - let mut count_out: usize = 0; - - let success = unsafe { - managed_wallet_get_bip_44_external_address_range( - ffi_managed, - ffi_wallet_ptr, - 0, - 0, - 5, - &mut addresses_out, - &mut count_out, - &mut error, - ) - }; - - assert!(success); - assert_eq!(count_out, 5); - assert!(!addresses_out.is_null()); - - // Verify and free addresses - unsafe { - let addresses = std::slice::from_raw_parts(addresses_out, count_out); - for &addr_ptr in addresses { - let addr_str = CStr::from_ptr(addr_ptr).to_string_lossy(); - assert!(!addr_str.is_empty()); - println!("External address: {}", addr_str); - let _ = CString::from_raw(addr_ptr); - } - libc::free(addresses_out as *mut libc::c_void); - } - - // Test 4: Get internal address range - let mut addresses_out: *mut *mut c_char = ptr::null_mut(); - let mut count_out: usize = 0; - - let success = unsafe { - managed_wallet_get_bip_44_internal_address_range( - ffi_managed, - ffi_wallet_ptr, - 0, - 0, - 3, - &mut addresses_out, - &mut count_out, - &mut error, - ) - }; - - assert!(success); - assert_eq!(count_out, 3); - assert!(!addresses_out.is_null()); - - // Verify and free addresses - unsafe { - let addresses = std::slice::from_raw_parts(addresses_out, count_out); - for &addr_ptr in addresses { - let addr_str = CStr::from_ptr(addr_ptr).to_string_lossy(); - assert!(!addr_str.is_empty()); - println!("Internal address: {}", addr_str); - // Don't manually free individual strings - address_array_free handles it - } - // Use the proper FFI function to free the array and all strings - crate::address::address_array_free(addresses_out, count_out); - } - - // Clean up - unsafe { - managed_wallet_free(ffi_managed); - wallet::wallet_free(wallet_ptr); - } - } - - #[test] - fn test_managed_wallet_get_balance() { - use key_wallet::wallet::balance::WalletCoreBalance; - - let mut error = FFIError::default(); - - // Create a wallet - let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); - - let wallet_ptr = unsafe { - wallet::wallet_create_from_mnemonic(mnemonic.as_ptr(), FFINetwork::Testnet, &mut error) - }; - assert!(!wallet_ptr.is_null()); - - // Create managed wallet info - let wallet_arc = unsafe { &(*wallet_ptr).wallet }; - let mut managed_info = ManagedWalletInfo::from_wallet(wallet_arc, 0); - - // Set some test balance values - managed_info.balance = WalletCoreBalance::new(1000000, 50000, 10000, 25000); - - let ffi_managed = FFIManagedWalletInfo::new(managed_info); - let ffi_managed_ptr = Box::into_raw(Box::new(ffi_managed)); - - // Test getting balance - let mut confirmed: u64 = 0; - let mut unconfirmed: u64 = 0; - let mut immature: u64 = 0; - let mut locked: u64 = 0; - let mut total: u64 = 0; - - let success = unsafe { - managed_wallet_get_balance( - ffi_managed_ptr, - &mut confirmed, - &mut unconfirmed, - &mut immature, - &mut locked, - &mut total, - &mut error, - ) - }; - - assert!(success); - assert_eq!(confirmed, 1000000); - assert_eq!(unconfirmed, 50000); - assert_eq!(immature, 10000); - assert_eq!(locked, 25000); - assert_eq!(total, 1085000); - - // Test with null managed wallet - let success = unsafe { - managed_wallet_get_balance( - ptr::null(), - &mut confirmed, - &mut unconfirmed, - &mut immature, - &mut locked, - &mut total, - &mut error, - ) - }; - - assert!(!success); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - - // Test with null output pointers - let success = unsafe { - managed_wallet_get_balance( - ffi_managed_ptr, - ptr::null_mut(), - &mut unconfirmed, - &mut immature, - &mut locked, - &mut total, - &mut error, - ) - }; - - assert!(!success); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - - // Clean up - unsafe { - managed_wallet_free(ffi_managed_ptr); - wallet::wallet_free(wallet_ptr); - } - } - - #[test] - fn test_managed_wallet_get_address_range_null_outputs() { - let mut error = FFIError::default(); - - // Test with null addresses_out for external range - let success = unsafe { - managed_wallet_get_bip_44_external_address_range( - ptr::null_mut(), - ptr::null(), - 0, - 0, - 10, - ptr::null_mut(), - &mut 0, - &mut error, - ) - }; - - assert!(!success); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - - // Test with null count_out for internal range - let mut addresses_out: *mut *mut c_char = ptr::null_mut(); - let success = unsafe { - managed_wallet_get_bip_44_internal_address_range( - ptr::null_mut(), - ptr::null(), - 0, - 0, - 10, - &mut addresses_out, - ptr::null_mut(), - &mut error, - ) - }; - - assert!(!success); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - } -} - -#[cfg(test)] -#[path = "managed_wallet_tests.rs"] -mod managed_wallet_tests; diff --git a/key-wallet-ffi/src/managed_wallet_tests.rs b/key-wallet-ffi/src/managed_wallet_tests.rs deleted file mode 100644 index 83d8d97ef..000000000 --- a/key-wallet-ffi/src/managed_wallet_tests.rs +++ /dev/null @@ -1,320 +0,0 @@ -//! Tests for managed wallet FFI module - -#[cfg(test)] -mod tests { - use crate::address_pool::managed_wallet_mark_address_used; - use crate::error::{FFIError, FFIErrorCode}; - use crate::managed_wallet::*; - use crate::types::FFIWallet; - use crate::wallet; - use crate::wallet_manager::FFIWalletManager; - use crate::wallet_manager::{ - wallet_manager_add_wallet_from_mnemonic, wallet_manager_create, wallet_manager_free, - wallet_manager_free_wallet_ids, wallet_manager_get_managed_wallet_info, - wallet_manager_get_wallet, wallet_manager_get_wallet_ids, - }; - use dash_network::ffi::FFINetwork; - use std::ffi::CString; - use std::ptr; - - const TEST_MNEMONIC: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; - - /// Helper: build a manager populated with a single wallet derived from TEST_MNEMONIC - /// and return the manager, the retrieved wallet, the managed wallet info, the raw - /// wallet-id buffer owned by the manager, and the wallet-id length. The caller is - /// responsible for calling `cleanup_fixture` once done. - unsafe fn setup_fixture( - error: &mut FFIError, - ) -> (*mut FFIWalletManager, *const FFIWallet, *mut FFIManagedWalletInfo, *mut u8, usize) { - let manager = wallet_manager_create(FFINetwork::Testnet, error); - assert!(!manager.is_null()); - - let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); - - let added = wallet_manager_add_wallet_from_mnemonic(manager, mnemonic.as_ptr(), error); - assert!(added); - assert_eq!(error.code, FFIErrorCode::Success); - - let mut wallet_ids: *mut u8 = ptr::null_mut(); - let mut id_count: usize = 0; - let got_ids = wallet_manager_get_wallet_ids( - manager, - &mut wallet_ids as *mut *mut u8, - &mut id_count as *mut usize, - error, - ); - assert!(got_ids); - assert_eq!(id_count, 1); - assert!(!wallet_ids.is_null()); - - let wallet = wallet_manager_get_wallet(manager, wallet_ids, error); - assert!(!wallet.is_null()); - - let managed_wallet = wallet_manager_get_managed_wallet_info(manager, wallet_ids, error); - assert!(!managed_wallet.is_null()); - - (manager, wallet, managed_wallet, wallet_ids, id_count) - } - - unsafe fn cleanup_fixture( - manager: *mut FFIWalletManager, - wallet: *const FFIWallet, - managed_wallet: *mut FFIManagedWalletInfo, - wallet_ids: *mut u8, - id_count: usize, - ) { - if !managed_wallet.is_null() { - managed_wallet_info_free(managed_wallet); - } - if !wallet.is_null() { - wallet::wallet_free_const(wallet); - } - if !wallet_ids.is_null() { - wallet_manager_free_wallet_ids(wallet_ids, id_count); - } - if !manager.is_null() { - wallet_manager_free(manager); - } - } - - #[test] - fn test_managed_wallet_info_from_manager_success() { - let mut error = FFIError::default(); - let (manager, wallet, managed_wallet, wallet_ids, id_count) = - unsafe { setup_fixture(&mut error) }; - - assert!(!managed_wallet.is_null()); - assert_eq!(error.code, FFIErrorCode::Success); - - unsafe { - cleanup_fixture(manager, wallet, managed_wallet, wallet_ids, id_count); - } - } - - #[test] - fn test_managed_wallet_info_from_manager_null_manager() { - let mut error = FFIError::default(); - let wallet_id = [0u8; 32]; - - let managed_wallet = unsafe { - wallet_manager_get_managed_wallet_info(ptr::null(), wallet_id.as_ptr(), &mut error) - }; - - assert!(managed_wallet.is_null()); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - } - - #[test] - fn test_managed_wallet_info_from_manager_unknown_wallet_id() { - let mut error = FFIError::default(); - let manager = unsafe { wallet_manager_create(FFINetwork::Testnet, &mut error) }; - assert!(!manager.is_null()); - - let bogus_wallet_id = [0u8; 32]; - let managed_wallet = unsafe { - wallet_manager_get_managed_wallet_info(manager, bogus_wallet_id.as_ptr(), &mut error) - }; - - assert!(managed_wallet.is_null()); - assert_eq!(error.code, FFIErrorCode::NotFound); - - unsafe { - wallet_manager_free(manager); - } - } - - #[test] - fn test_managed_wallet_mark_address_used_valid() { - let mut error = FFIError::default(); - let (manager, wallet, managed_wallet, wallet_ids, id_count) = - unsafe { setup_fixture(&mut error) }; - - // Well-formed testnet address. It may or may not belong to any pool of this - // wallet, but the function must at minimum parse it without panicking. - let address = CString::new("yXdxAYfK7KGx7gNpVHUfRsQMNpMj5cAadG").unwrap(); - let success = unsafe { - managed_wallet_mark_address_used(managed_wallet, address.as_ptr(), &mut error) - }; - - // Should succeed or fail gracefully depending on address validation - // The function validates the address format internally - if success { - assert_eq!(error.code, FFIErrorCode::Success); - } else { - // Address validation might fail due to library version differences - assert_eq!(error.code, FFIErrorCode::InvalidAddress); - } - - unsafe { - cleanup_fixture(manager, wallet, managed_wallet, wallet_ids, id_count); - } - } - - #[test] - fn test_managed_wallet_mark_address_used_invalid() { - let mut error = FFIError::default(); - let (manager, wallet, managed_wallet, wallet_ids, id_count) = - unsafe { setup_fixture(&mut error) }; - - let address = CString::new("invalid_address").unwrap(); - let success = unsafe { - managed_wallet_mark_address_used(managed_wallet, address.as_ptr(), &mut error) - }; - - assert!(!success); - assert_eq!(error.code, FFIErrorCode::InvalidAddress); - - unsafe { - cleanup_fixture(manager, wallet, managed_wallet, wallet_ids, id_count); - } - } - - #[test] - fn test_managed_wallet_mark_address_used_null_inputs() { - let mut error = FFIError::default(); - - let success = - unsafe { managed_wallet_mark_address_used(ptr::null_mut(), ptr::null(), &mut error) }; - - assert!(!success); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - } - - #[test] - fn test_managed_wallet_get_next_bip44_receive_address_null_inputs() { - let mut error = FFIError::default(); - - let address = unsafe { - managed_wallet_get_next_bip44_receive_address( - ptr::null_mut(), - ptr::null(), - 0, - &mut error, - ) - }; - - assert!(address.is_null()); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - } - - #[test] - fn test_managed_wallet_get_next_bip44_change_address_null_inputs() { - let mut error = FFIError::default(); - - let address = unsafe { - managed_wallet_get_next_bip44_change_address( - ptr::null_mut(), - ptr::null(), - 0, - &mut error, - ) - }; - - assert!(address.is_null()); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - } - - #[test] - fn test_managed_wallet_free_null() { - // Should handle null gracefully - unsafe { - managed_wallet_free(ptr::null_mut()); - managed_wallet_info_free(ptr::null_mut()); - } - } - - #[test] - fn test_managed_wallet_info_free_valid() { - let mut error = FFIError::default(); - let (manager, wallet, managed_wallet, wallet_ids, id_count) = - unsafe { setup_fixture(&mut error) }; - assert!(!managed_wallet.is_null()); - - // Free the managed wallet info independently — should not crash. - unsafe { managed_wallet_info_free(managed_wallet) }; - - // Pass null to cleanup_fixture so it doesn't double-free managed_wallet. - unsafe { - cleanup_fixture(manager, wallet, ptr::null_mut(), wallet_ids, id_count); - } - } - - #[test] - fn test_ffi_managed_wallet_info_methods() { - let mut error = FFIError::default(); - let (manager, wallet, managed_wallet, wallet_ids, id_count) = - unsafe { setup_fixture(&mut error) }; - assert!(!managed_wallet.is_null()); - - // Verify we can access the inner methods on FFIManagedWalletInfo. - unsafe { - let managed_ref = &*managed_wallet; - let _inner = managed_ref.inner(); - - let managed_mut = &mut *managed_wallet; - let _inner_mut = managed_mut.inner_mut(); - } - - unsafe { - cleanup_fixture(manager, wallet, managed_wallet, wallet_ids, id_count); - } - } - - #[test] - fn test_managed_wallet_mark_address_used_utf8_error() { - let mut error = FFIError::default(); - let (manager, wallet, managed_wallet, wallet_ids, id_count) = - unsafe { setup_fixture(&mut error) }; - - // Invalid UTF-8 bytes with null terminator. - let invalid_utf8 = [0xFFu8, 0xFE, 0xFD, 0x00]; - let success = unsafe { - managed_wallet_mark_address_used( - managed_wallet, - invalid_utf8.as_ptr() as *const std::os::raw::c_char, - &mut error, - ) - }; - - assert!(!success); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - - unsafe { - cleanup_fixture(manager, wallet, managed_wallet, wallet_ids, id_count); - } - } - - #[test] - fn test_managed_wallet_address_operations_with_real_wallet() { - let mut error = FFIError::default(); - let (manager, wallet, managed_wallet, wallet_ids, id_count) = - unsafe { setup_fixture(&mut error) }; - assert!(!managed_wallet.is_null()); - - // Get the next receive address for BIP44 account 0 — with a fully populated - // managed wallet this should succeed. - let address_ptr = unsafe { - managed_wallet_get_next_bip44_receive_address(managed_wallet, wallet, 0, &mut error) - }; - assert!(!address_ptr.is_null(), "expected a receive address, error: {:?}", error.code); - assert_eq!(error.code, FFIErrorCode::Success); - unsafe { - // Reclaim the C string allocated by the FFI function. - let _ = CString::from_raw(address_ptr); - } - - // Same for the next change address. - let address_ptr = unsafe { - managed_wallet_get_next_bip44_change_address(managed_wallet, wallet, 0, &mut error) - }; - assert!(!address_ptr.is_null(), "expected a change address, error: {:?}", error.code); - assert_eq!(error.code, FFIErrorCode::Success); - unsafe { - let _ = CString::from_raw(address_ptr); - } - - unsafe { - cleanup_fixture(manager, wallet, managed_wallet, wallet_ids, id_count); - } - } -} diff --git a/key-wallet-ffi/src/mnemonic.rs b/key-wallet-ffi/src/mnemonic.rs deleted file mode 100644 index 55da2c1a2..000000000 --- a/key-wallet-ffi/src/mnemonic.rs +++ /dev/null @@ -1,264 +0,0 @@ -//! Mnemonic generation and handling - -#[cfg(test)] -#[path = "mnemonic_tests.rs"] -mod tests; - -use std::ffi::{CStr, CString}; -use std::os::raw::{c_char, c_uint}; -use std::ptr; - -use key_wallet::Mnemonic; - -use crate::error::{FFIError, FFIErrorCode}; -use crate::{check_ptr, deref_ptr, unwrap_or_return}; - -/// Language enumeration for mnemonic generation -/// -/// This enum must be kept in sync with key_wallet::mnemonic::Language. -/// When adding new languages to the key_wallet crate, remember to update -/// this FFI enum and both From implementations below. -#[repr(C)] -#[derive(Debug, Clone, Copy)] -pub enum FFILanguage { - English = 0, - ChineseSimplified = 1, - ChineseTraditional = 2, - Czech = 3, - French = 4, - Italian = 5, - Japanese = 6, - Korean = 7, - Portuguese = 8, - Spanish = 9, -} - -impl From for key_wallet::mnemonic::Language { - fn from(l: FFILanguage) -> Self { - use key_wallet::mnemonic::Language; - match l { - FFILanguage::English => Language::English, - FFILanguage::ChineseSimplified => Language::ChineseSimplified, - FFILanguage::ChineseTraditional => Language::ChineseTraditional, - FFILanguage::Czech => Language::Czech, - FFILanguage::French => Language::French, - FFILanguage::Italian => Language::Italian, - FFILanguage::Japanese => Language::Japanese, - FFILanguage::Korean => Language::Korean, - FFILanguage::Portuguese => Language::Portuguese, - FFILanguage::Spanish => Language::Spanish, - } - } -} - -impl From for FFILanguage { - fn from(l: key_wallet::mnemonic::Language) -> Self { - use key_wallet::mnemonic::Language; - match l { - Language::English => FFILanguage::English, - Language::ChineseSimplified => FFILanguage::ChineseSimplified, - Language::ChineseTraditional => FFILanguage::ChineseTraditional, - Language::Czech => FFILanguage::Czech, - Language::French => FFILanguage::French, - Language::Italian => FFILanguage::Italian, - Language::Japanese => FFILanguage::Japanese, - Language::Korean => FFILanguage::Korean, - Language::Portuguese => FFILanguage::Portuguese, - Language::Spanish => FFILanguage::Spanish, - } - } -} - -/// Generate a new mnemonic with specified word count (12, 15, 18, 21, or 24) -/// -/// # Safety -/// -/// `error` must be a valid pointer to an `FFIError`. The returned string must be -/// freed with `mnemonic_free`. -#[no_mangle] -pub unsafe extern "C" fn mnemonic_generate( - word_count: c_uint, - error: *mut FFIError, -) -> *mut c_char { - let entropy_bits = match word_count { - 12 => 128, - 15 => 160, - 18 => 192, - 21 => 224, - 24 => 256, - _ => { - (*error).set( - FFIErrorCode::InvalidInput, - &format!("Invalid word count: {}. Must be 12, 15, 18, 21, or 24", word_count), - ); - return ptr::null_mut(); - } - }; - - use key_wallet::mnemonic::Language; - let word_count = match entropy_bits { - 128 => 12, - 160 => 15, - 192 => 18, - 224 => 21, - 256 => 24, - _ => 12, - }; - let mnemonic = unwrap_or_return!(Mnemonic::generate(word_count, Language::English), error); - unwrap_or_return!(CString::new(mnemonic.to_string()), error).into_raw() -} - -/// Generate a new mnemonic with specified language and word count -/// -/// # Safety -/// -/// `error` must be a valid pointer to an `FFIError`. The returned string must be -/// freed with `mnemonic_free`. -#[no_mangle] -pub unsafe extern "C" fn mnemonic_generate_with_language( - word_count: c_uint, - language: FFILanguage, - error: *mut FFIError, -) -> *mut c_char { - let entropy_bits = match word_count { - 12 => 128, - 15 => 160, - 18 => 192, - 21 => 224, - 24 => 256, - _ => { - (*error).set( - FFIErrorCode::InvalidInput, - &format!("Invalid word count: {}. Must be 12, 15, 18, 21, or 24", word_count), - ); - return ptr::null_mut(); - } - }; - - use key_wallet::mnemonic::Language; - let lang: Language = language.into(); - let word_count = match entropy_bits { - 128 => 12, - 160 => 15, - 192 => 18, - 224 => 21, - 256 => 24, - _ => 12, - }; - let mnemonic = unwrap_or_return!(Mnemonic::generate(word_count, lang), error); - unwrap_or_return!(CString::new(mnemonic.to_string()), error).into_raw() -} - -/// Validate a mnemonic phrase -/// -/// # Safety -/// -/// - `mnemonic` must be a valid null-terminated C string or null -/// - `error` must be a valid pointer to an FFIError -#[no_mangle] -pub unsafe extern "C" fn mnemonic_validate(mnemonic: *const c_char, error: *mut FFIError) -> bool { - let mnemonic = deref_ptr!(mnemonic, error); - let mnemonic_str = unwrap_or_return!(CStr::from_ptr(mnemonic).to_str(), error); - - use key_wallet::mnemonic::Language; - - // Try validation against all supported languages - let languages = [ - Language::English, - Language::ChineseSimplified, - Language::ChineseTraditional, - Language::Czech, - Language::French, - Language::Italian, - Language::Japanese, - Language::Korean, - Language::Portuguese, - Language::Spanish, - ]; - - for language in languages.iter() { - if Mnemonic::validate(mnemonic_str, *language) { - return true; - } - } - (*error).set( - FFIErrorCode::InvalidMnemonic, - "Invalid mnemonic: does not match any supported language", - ); - false -} - -/// Convert mnemonic to seed with optional passphrase -/// -/// # Safety -/// -/// - `mnemonic` must be a valid null-terminated C string -/// - `passphrase` must be a valid null-terminated C string or null -/// - `seed_out` must be a valid pointer to a buffer of at least 64 bytes -/// - `seed_len` must be a valid pointer to store the seed length -/// - `error` must be a valid pointer to an FFIError -#[no_mangle] -pub unsafe extern "C" fn mnemonic_to_seed( - mnemonic: *const c_char, - passphrase: *const c_char, - seed_out: *mut u8, - seed_len: *mut usize, - error: *mut FFIError, -) -> bool { - let mnemonic = deref_ptr!(mnemonic, error); - check_ptr!(seed_out, error); - check_ptr!(seed_len, error); - let mnemonic_str = unwrap_or_return!(CStr::from_ptr(mnemonic).to_str(), error); - - let passphrase_str = if passphrase.is_null() { - "" - } else { - unwrap_or_return!(CStr::from_ptr(passphrase).to_str(), error) - }; - - use key_wallet::mnemonic::Language; - let m = unwrap_or_return!(Mnemonic::from_phrase(mnemonic_str, Language::English), error); - let seed = m.to_seed(passphrase_str); - let seed_bytes: &[u8] = seed.as_ref(); - - unsafe { - *seed_len = seed_bytes.len(); - if *seed_len > 64 { - (*error).set(FFIErrorCode::InvalidState, "Seed too large"); - return false; - } - std::ptr::copy_nonoverlapping(seed_bytes.as_ptr(), seed_out, seed_bytes.len()); - } - true -} - -/// Get word count from mnemonic -/// -/// # Safety -/// -/// - `mnemonic` must be a valid null-terminated C string or null -/// - `error` must be a valid pointer to an FFIError -#[no_mangle] -pub unsafe extern "C" fn mnemonic_word_count( - mnemonic: *const c_char, - error: *mut FFIError, -) -> c_uint { - let mnemonic = deref_ptr!(mnemonic, error); - let mnemonic_str = unwrap_or_return!(CStr::from_ptr(mnemonic).to_str(), error); - mnemonic_str.split_whitespace().count() as c_uint -} - -/// Free a mnemonic string -/// -/// # Safety -/// -/// - `mnemonic` must be a valid pointer created by mnemonic generation functions or null -/// - After calling this function, the pointer becomes invalid -#[no_mangle] -pub unsafe extern "C" fn mnemonic_free(mnemonic: *mut c_char) { - if !mnemonic.is_null() { - unsafe { - let _ = CString::from_raw(mnemonic); - } - } -} diff --git a/key-wallet-ffi/src/mnemonic_tests.rs b/key-wallet-ffi/src/mnemonic_tests.rs deleted file mode 100644 index 5a4dc6b1f..000000000 --- a/key-wallet-ffi/src/mnemonic_tests.rs +++ /dev/null @@ -1,693 +0,0 @@ -//! Unit tests for mnemonic FFI module - -#[cfg(test)] -#[allow(clippy::module_inception)] -mod tests { - use crate::error::{FFIError, FFIErrorCode}; - use crate::mnemonic; - use std::ffi::CString; - - use std::ptr; - - const TEST_MNEMONIC: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; - const TEST_MNEMONIC_24: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art"; - - #[test] - fn test_mnemonic_validation() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - // Test valid 12-word mnemonic - let valid_mnemonic = CString::new(TEST_MNEMONIC).unwrap(); - let is_valid = unsafe { mnemonic::mnemonic_validate(valid_mnemonic.as_ptr(), error) }; - assert!(is_valid); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); - - // Test valid 24-word mnemonic - let valid_mnemonic_24 = CString::new(TEST_MNEMONIC_24).unwrap(); - let is_valid = unsafe { mnemonic::mnemonic_validate(valid_mnemonic_24.as_ptr(), error) }; - assert!(is_valid); - - // Test invalid mnemonic - let invalid_mnemonic = CString::new("invalid mnemonic phrase here").unwrap(); - let is_valid = unsafe { mnemonic::mnemonic_validate(invalid_mnemonic.as_ptr(), error) }; - assert!(!is_valid); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidMnemonic); - - // Test null mnemonic - let is_valid = unsafe { mnemonic::mnemonic_validate(ptr::null(), error) }; - assert!(!is_valid); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); - } - - #[test] - fn test_mnemonic_generation() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - // Test 12-word generation - let mnemonic_12 = unsafe { mnemonic::mnemonic_generate(12, error) }; - assert!(!mnemonic_12.is_null()); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); - - let mnemonic_str = unsafe { std::ffi::CStr::from_ptr(mnemonic_12).to_str().unwrap() }; - let word_count = mnemonic_str.split_whitespace().count(); - assert_eq!(word_count, 12); - - // Validate the generated mnemonic - let is_valid = unsafe { mnemonic::mnemonic_validate(mnemonic_12, error) }; - assert!(is_valid); - - unsafe { - mnemonic::mnemonic_free(mnemonic_12); - } - - // Test 24-word generation - let mnemonic_24 = unsafe { mnemonic::mnemonic_generate(24, error) }; - assert!(!mnemonic_24.is_null()); - - let mnemonic_str = unsafe { std::ffi::CStr::from_ptr(mnemonic_24).to_str().unwrap() }; - let word_count = mnemonic_str.split_whitespace().count(); - assert_eq!(word_count, 24); - - unsafe { - mnemonic::mnemonic_free(mnemonic_24); - } - - // Test invalid word count - let invalid = unsafe { mnemonic::mnemonic_generate(13, error) }; - assert!(invalid.is_null()); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); - } - - #[test] - fn test_mnemonic_to_seed() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); - let passphrase = CString::new("").unwrap(); - - let mut seed = [0u8; 64]; - let mut seed_len: usize = 0; - - let success = unsafe { - mnemonic::mnemonic_to_seed( - mnemonic.as_ptr(), - passphrase.as_ptr(), - seed.as_mut_ptr(), - &mut seed_len, - error, - ) - }; - - assert!(success); - assert_eq!(seed_len, 64); - assert_ne!(seed, [0u8; 64]); // Seed should not be all zeros - - // Test with passphrase - let passphrase = CString::new("test passphrase").unwrap(); - let mut seed_with_pass = [0u8; 64]; - - let success = unsafe { - mnemonic::mnemonic_to_seed( - mnemonic.as_ptr(), - passphrase.as_ptr(), - seed_with_pass.as_mut_ptr(), - &mut seed_len, - error, - ) - }; - - assert!(success); - assert_ne!(seed, seed_with_pass); // Different passphrase should produce different seed - } - - #[test] - fn test_mnemonic_word_counts() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - // Test all valid word counts - let valid_counts = [12, 15, 18, 21, 24]; - - for count in valid_counts.iter() { - let mnemonic = unsafe { mnemonic::mnemonic_generate(*count, error) }; - assert!(!mnemonic.is_null()); - - let mnemonic_str = unsafe { std::ffi::CStr::from_ptr(mnemonic).to_str().unwrap() }; - let word_count = mnemonic_str.split_whitespace().count(); - assert_eq!(word_count, *count as usize); - - unsafe { - mnemonic::mnemonic_free(mnemonic); - } - } - } - - #[test] - fn test_mnemonic_invalid_word_count() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - // Test invalid word counts - let invalid_counts = [0, 1, 11, 13, 14, 16, 17, 19, 20, 22, 23, 25, 100]; - - for count in invalid_counts.iter() { - let mnemonic = unsafe { mnemonic::mnemonic_generate(*count, error) }; - assert!(mnemonic.is_null()); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); - } - } - - #[test] - fn test_mnemonic_edge_cases() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - // Test with null mnemonic - let success = unsafe { mnemonic::mnemonic_validate(ptr::null(), error) }; - assert!(!success); - - // Test with empty mnemonic - let empty = CString::new("").unwrap(); - let success = unsafe { mnemonic::mnemonic_validate(empty.as_ptr(), error) }; - assert!(!success); - - // Test with wrong word count - let wrong_count = CString::new("abandon abandon abandon").unwrap(); - let success = unsafe { mnemonic::mnemonic_validate(wrong_count.as_ptr(), error) }; - assert!(!success); - - // Test mnemonic to seed with null passphrase - let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); - let mut seed = [0u8; 64]; - let mut seed_len: usize = 0; - - let success = unsafe { - mnemonic::mnemonic_to_seed( - mnemonic.as_ptr(), - ptr::null(), // null passphrase - seed.as_mut_ptr(), - &mut seed_len, - error, - ) - }; - assert!(success); - assert_eq!(seed_len, 64); - } - - #[test] - fn test_mnemonic_generate_with_language() { - let mut error = FFIError::default(); - - // Test generating with different languages - let languages = [ - mnemonic::FFILanguage::English, - mnemonic::FFILanguage::Spanish, - mnemonic::FFILanguage::French, - mnemonic::FFILanguage::Italian, - mnemonic::FFILanguage::Japanese, - mnemonic::FFILanguage::Korean, - mnemonic::FFILanguage::ChineseSimplified, - mnemonic::FFILanguage::ChineseTraditional, - mnemonic::FFILanguage::Czech, - mnemonic::FFILanguage::Portuguese, - ]; - - unsafe { - for lang in languages.iter() { - let mnemonic_ptr = mnemonic::mnemonic_generate_with_language(12, *lang, &mut error); - - assert!(!mnemonic_ptr.is_null()); - assert_eq!(error.code, FFIErrorCode::Success); - - // Verify it's valid - let is_valid = mnemonic::mnemonic_validate(mnemonic_ptr, &mut error); - assert!(is_valid); - - // Clean up - mnemonic::mnemonic_free(mnemonic_ptr); - } - } - } - - #[test] - fn test_mnemonic_czech_portuguese_languages() { - let mut error = FFIError::default(); - - // Test Czech language specifically - unsafe { - let czech_mnemonic = mnemonic::mnemonic_generate_with_language( - 12, - mnemonic::FFILanguage::Czech, - &mut error, - ); - assert!(!czech_mnemonic.is_null()); - assert_eq!(error.code, FFIErrorCode::Success); - - // Verify it's valid - let is_valid = mnemonic::mnemonic_validate(czech_mnemonic, &mut error); - assert!(is_valid); - - mnemonic::mnemonic_free(czech_mnemonic); - } - - // Test Portuguese language specifically - unsafe { - let portuguese_mnemonic = mnemonic::mnemonic_generate_with_language( - 24, - mnemonic::FFILanguage::Portuguese, - &mut error, - ); - assert!(!portuguese_mnemonic.is_null()); - assert_eq!(error.code, FFIErrorCode::Success); - - // Verify it's valid - let is_valid = mnemonic::mnemonic_validate(portuguese_mnemonic, &mut error); - assert!(is_valid); - - mnemonic::mnemonic_free(portuguese_mnemonic); - } - } - - #[test] - fn test_mnemonic_generate_and_validate_languages() { - let mut error = FFIError::default(); - - // Generate a mnemonic with a specific language - let mnemonic_ptr = unsafe { - mnemonic::mnemonic_generate_with_language( - 12, - mnemonic::FFILanguage::Spanish, - &mut error, - ) - }; - assert!(!mnemonic_ptr.is_null()); - - // Validate it (validation doesn't need language since it checks all word lists) - let is_valid = unsafe { mnemonic::mnemonic_validate(mnemonic_ptr, &mut error) }; - assert!(is_valid); - - // Clean up - unsafe { - mnemonic::mnemonic_free(mnemonic_ptr); - } - } - - #[test] - fn test_mnemonic_free_null() { - // Should handle null gracefully - unsafe { - mnemonic::mnemonic_free(ptr::null_mut()); - } - } - - #[test] - fn test_seed_from_mnemonic_with_different_passphrases() { - let mut error = FFIError::default(); - let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); - - // Test with empty passphrase - let empty_pass = CString::new("").unwrap(); - let mut seed1 = [0u8; 64]; - let mut seed_len = 64usize; - - let success = unsafe { - mnemonic::mnemonic_to_seed( - mnemonic.as_ptr(), - empty_pass.as_ptr(), - seed1.as_mut_ptr(), - &mut seed_len, - &mut error, - ) - }; - assert!(success); - - // Test with non-empty passphrase - let pass = CString::new("TREZOR").unwrap(); - let mut seed2 = [0u8; 64]; - seed_len = 64; - - let success = unsafe { - mnemonic::mnemonic_to_seed( - mnemonic.as_ptr(), - pass.as_ptr(), - seed2.as_mut_ptr(), - &mut seed_len, - &mut error, - ) - }; - assert!(success); - - // Seeds should be different - assert_ne!(seed1, seed2); - } - - #[test] - fn test_mnemonic_word_count_function() { - let mut error = FFIError::default(); - - // Test different mnemonics - let test_cases = [ - ("word", 1), - ("two words", 2), - ("three word mnemonic", 3), - (TEST_MNEMONIC, 12), - (TEST_MNEMONIC_24, 24), - ]; - - unsafe { - for (mnemonic_str, expected_count) in test_cases { - let mnemonic = CString::new(mnemonic_str).unwrap(); - let count = mnemonic::mnemonic_word_count(mnemonic.as_ptr(), &mut error); - - assert_eq!(count, expected_count); - assert_eq!(error.code, FFIErrorCode::Success); - } - } - } - - #[test] - fn test_mnemonic_word_count_null_input() { - let mut error = FFIError::default(); - - let count = unsafe { mnemonic::mnemonic_word_count(ptr::null(), &mut error) }; - - assert_eq!(count, 0); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - } - - #[test] - fn test_mnemonic_word_count_utf8_error() { - let mut error = FFIError::default(); - - // Create invalid UTF-8 string - let invalid_utf8 = [0xFF, 0xFE, 0xFD, 0x00]; - let count = unsafe { - mnemonic::mnemonic_word_count( - invalid_utf8.as_ptr() as *const std::os::raw::c_char, - &mut error, - ) - }; - - assert_eq!(count, 0); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - } - - #[test] - fn test_mnemonic_to_seed_null_inputs() { - let mut error = FFIError::default(); - let mut seed = [0u8; 64]; - let mut seed_len = 0usize; - - // Test null mnemonic - let success = unsafe { - mnemonic::mnemonic_to_seed( - ptr::null(), - ptr::null(), - seed.as_mut_ptr(), - &mut seed_len, - &mut error, - ) - }; - assert!(!success); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - - // Test null seed_out - let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); - let success = unsafe { - mnemonic::mnemonic_to_seed( - mnemonic.as_ptr(), - ptr::null(), - ptr::null_mut(), - &mut seed_len, - &mut error, - ) - }; - assert!(!success); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - - // Test null seed_len - let success = unsafe { - mnemonic::mnemonic_to_seed( - mnemonic.as_ptr(), - ptr::null(), - seed.as_mut_ptr(), - ptr::null_mut(), - &mut error, - ) - }; - assert!(!success); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - } - - #[test] - fn test_mnemonic_to_seed_invalid_mnemonic() { - let mut error = FFIError::default(); - let mut seed = [0u8; 64]; - let mut seed_len = 0usize; - - let invalid_mnemonic = CString::new("invalid mnemonic phrase").unwrap(); - let success = unsafe { - mnemonic::mnemonic_to_seed( - invalid_mnemonic.as_ptr(), - ptr::null(), - seed.as_mut_ptr(), - &mut seed_len, - &mut error, - ) - }; - - assert!(!success); - assert_eq!(error.code, FFIErrorCode::InvalidMnemonic); - } - - #[test] - fn test_mnemonic_to_seed_utf8_errors() { - let mut error = FFIError::default(); - let mut seed = [0u8; 64]; - let mut seed_len = 0usize; - - // Test invalid UTF-8 in mnemonic - let invalid_utf8 = [0xFF, 0xFE, 0xFD, 0x00]; - let success = unsafe { - mnemonic::mnemonic_to_seed( - invalid_utf8.as_ptr() as *const std::os::raw::c_char, - ptr::null(), - seed.as_mut_ptr(), - &mut seed_len, - &mut error, - ) - }; - assert!(!success); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - - // Test invalid UTF-8 in passphrase - let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); - let success = unsafe { - mnemonic::mnemonic_to_seed( - mnemonic.as_ptr(), - invalid_utf8.as_ptr() as *const std::os::raw::c_char, - seed.as_mut_ptr(), - &mut seed_len, - &mut error, - ) - }; - assert!(!success); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - } - - #[test] - fn test_mnemonic_validate_utf8_error() { - let mut error = FFIError::default(); - - // Create invalid UTF-8 string - let invalid_utf8 = [0xFF, 0xFE, 0xFD, 0x00]; - let is_valid = unsafe { - mnemonic::mnemonic_validate( - invalid_utf8.as_ptr() as *const std::os::raw::c_char, - &mut error, - ) - }; - - assert!(!is_valid); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - } - - #[test] - fn test_mnemonic_generate_with_language_invalid_word_count() { - let mut error = FFIError::default(); - - // Test invalid word count with language - let mnemonic = unsafe { - mnemonic::mnemonic_generate_with_language( - 13, - mnemonic::FFILanguage::English, - &mut error, - ) - }; - - assert!(mnemonic.is_null()); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - } - - #[test] - fn test_mnemonic_generate_with_language_all_word_counts() { - let mut error = FFIError::default(); - - // Test all valid word counts with language - let valid_counts = [12, 15, 18, 21, 24]; - - for word_count in valid_counts { - let mnemonic = unsafe { - mnemonic::mnemonic_generate_with_language( - word_count, - mnemonic::FFILanguage::English, - &mut error, - ) - }; - - assert!(!mnemonic.is_null()); - assert_eq!(error.code, FFIErrorCode::Success); - - let mnemonic_str = unsafe { std::ffi::CStr::from_ptr(mnemonic).to_str().unwrap() }; - assert_eq!(mnemonic_str.split_whitespace().count(), word_count as usize); - - unsafe { - mnemonic::mnemonic_free(mnemonic); - } - } - } - - #[test] - fn test_mnemonic_generate_different_languages() { - let mut error = FFIError::default(); - - // Test generating with all supported languages - let languages = [ - mnemonic::FFILanguage::English, - mnemonic::FFILanguage::ChineseSimplified, - mnemonic::FFILanguage::ChineseTraditional, - mnemonic::FFILanguage::French, - mnemonic::FFILanguage::Italian, - mnemonic::FFILanguage::Japanese, - mnemonic::FFILanguage::Korean, - mnemonic::FFILanguage::Spanish, - ]; - - for lang in languages { - let mnemonic_ptr = - unsafe { mnemonic::mnemonic_generate_with_language(12, lang, &mut error) }; - - // Some languages might not be fully supported by the underlying library - unsafe { - if !mnemonic_ptr.is_null() { - assert_eq!(error.code, FFIErrorCode::Success); - - let mnemonic_str = std::ffi::CStr::from_ptr(mnemonic_ptr).to_str().unwrap(); - assert_eq!(mnemonic_str.split_whitespace().count(), 12); - - // Verify it validates - let is_valid = mnemonic::mnemonic_validate(mnemonic_ptr, &mut error); - assert!(is_valid); - - mnemonic::mnemonic_free(mnemonic_ptr); - } - } - } - } - - #[test] - fn test_generated_mnemonic_deterministic_seed() { - let mut error = FFIError::default(); - - // Generate mnemonic - let mnemonic = unsafe { mnemonic::mnemonic_generate(12, &mut error) }; - assert!(!mnemonic.is_null()); - - let passphrase = CString::new("").unwrap(); - - // Generate seed twice with same passphrase - should be identical - let mut seed1 = [0u8; 64]; - let mut seed_len1 = 0usize; - let mut seed2 = [0u8; 64]; - let mut seed_len2 = 0usize; - - let success1 = unsafe { - mnemonic::mnemonic_to_seed( - mnemonic, - passphrase.as_ptr(), - seed1.as_mut_ptr(), - &mut seed_len1, - &mut error, - ) - }; - - let success2 = unsafe { - mnemonic::mnemonic_to_seed( - mnemonic, - passphrase.as_ptr(), - seed2.as_mut_ptr(), - &mut seed_len2, - &mut error, - ) - }; - - assert!(success1); - assert!(success2); - assert_eq!(seed_len1, 64); - assert_eq!(seed_len2, 64); - assert_eq!(seed1, seed2); // Should be identical - - unsafe { - mnemonic::mnemonic_free(mnemonic); - } - } - - #[test] - fn test_mnemonic_comprehensive_workflow() { - let mut error = FFIError::default(); - - // Generate -> Validate -> Get word count -> Convert to seed -> Free - let mnemonic = unsafe { mnemonic::mnemonic_generate(15, &mut error) }; - assert!(!mnemonic.is_null()); - assert_eq!(error.code, FFIErrorCode::Success); - - let passphrase = CString::new("").unwrap(); - - // Validate - let is_valid = unsafe { mnemonic::mnemonic_validate(mnemonic, &mut error) }; - assert!(is_valid); - assert_eq!(error.code, FFIErrorCode::Success); - - // Check word count - let word_count = unsafe { mnemonic::mnemonic_word_count(mnemonic, &mut error) }; - assert_eq!(word_count, 15); - assert_eq!(error.code, FFIErrorCode::Success); - - // Convert to seed - let mut seed = [0u8; 64]; - let mut seed_len = 0usize; - - let success = unsafe { - mnemonic::mnemonic_to_seed( - mnemonic, - passphrase.as_ptr(), - seed.as_mut_ptr(), - &mut seed_len, - &mut error, - ) - }; - - assert!(success); - assert_eq!(seed_len, 64); - assert_ne!(seed, [0u8; 64]); - assert_eq!(error.code, FFIErrorCode::Success); - - // Free - unsafe { - mnemonic::mnemonic_free(mnemonic); - } - } -} diff --git a/key-wallet-ffi/src/transaction.rs b/key-wallet-ffi/src/transaction.rs deleted file mode 100644 index 2f38b742e..000000000 --- a/key-wallet-ffi/src/transaction.rs +++ /dev/null @@ -1,873 +0,0 @@ -//! Transaction building and management - -use crate::error::{FFIError, FFIErrorCode}; -use crate::types::{ - transaction_context_from_ffi, FFIBlockInfo, FFITransactionContextType, FFIWallet, -}; -use crate::{check_ptr, FFIWalletManager}; -use crate::{deref_ptr, deref_ptr_mut, unwrap_or_return}; -use dash_network::ffi::FFINetwork; -use dashcore::{ - consensus, hashes::Hash, sighash::SighashCache, EcdsaSighashType, Network, OutPoint, Script, - ScriptBuf, Transaction, TxIn, TxOut, Txid, -}; -use key_wallet::wallet::managed_wallet_info::asset_lock_builder::{ - AssetLockFundingType, CreditOutputFunding, -}; -use key_wallet::wallet::managed_wallet_info::fee::FeeRate; -use secp256k1::{Message, Secp256k1, SecretKey}; -use std::ffi::{CStr, CString}; -use std::os::raw::c_char; -use std::ptr; -use std::slice; -use std::str::FromStr; -// MARK: - Transaction Types - -/// Opaque handle for a transaction -pub struct FFITransaction { - inner: Transaction, -} - -/// FFI-compatible transaction input -#[repr(C)] -pub struct FFITxIn { - /// Transaction ID (32 bytes) - pub txid: [u8; 32], - /// Output index - pub vout: u32, - /// Script signature length - pub script_sig_len: u32, - /// Script signature data pointer - pub script_sig: *const u8, - /// Sequence number - pub sequence: u32, -} - -/// FFI-compatible transaction output -#[repr(C)] -pub struct FFITxOut { - /// Amount in duffs - pub amount: u64, - /// Script pubkey length - pub script_pubkey_len: u32, - /// Script pubkey data pointer - pub script_pubkey: *const u8, -} - -/// Transaction output for building (legacy structure) -#[repr(C)] -pub struct FFITxOutput { - pub address: *const c_char, - pub amount: u64, -} - -/// Build and sign a transaction using the wallet's managed info -/// -/// This is the recommended way to build transactions. It handles: -/// - UTXO selection using coin selection algorithms -/// - Fee calculation -/// - Change address generation -/// - Transaction signing -/// -/// # Safety -/// -/// - `manager` must be a valid pointer to an FFIWalletManager -/// - `wallet` must be a valid pointer to an FFIWallet -/// - `account_index` must be a valid BIP44 account index present in the wallet -/// - `outputs` must be a valid pointer to an array of FFITxOutput with at least `outputs_count` elements -/// - `fee_rate` must be a valid variant of FFIFeeRate -/// - `fee_out` must be a valid, non-null pointer to a `u64`; on success it receives the -/// calculated transaction fee in duffs -/// - `tx_bytes_out` must be a valid pointer to store the transaction bytes pointer -/// - `tx_len_out` must be a valid pointer to store the transaction length -/// - `error` must be a valid pointer to an FFIError -/// - The returned transaction bytes must be freed with `transaction_bytes_free` -#[no_mangle] -pub unsafe extern "C" fn wallet_build_and_sign_transaction( - manager: *const FFIWalletManager, - wallet: *const FFIWallet, - account_index: u32, - outputs: *const FFITxOutput, - outputs_count: usize, - fee_per_kb: u64, - fee_out: *mut u64, - tx_bytes_out: *mut *mut u8, - tx_len_out: *mut usize, - error: *mut FFIError, -) -> bool { - let manager_ref = deref_ptr!(manager, error); - let wallet_ref = deref_ptr!(wallet, error); - check_ptr!(outputs, error); - check_ptr!(tx_bytes_out, error); - check_ptr!(tx_len_out, error); - check_ptr!(fee_out, error); - - if outputs_count == 0 { - (*error).set(FFIErrorCode::InvalidInput, "At least one output required"); - return false; - } - - let ffi_outputs = slice::from_raw_parts(outputs, outputs_count); - let mut outputs = Vec::with_capacity(outputs_count); - - for output in ffi_outputs { - if output.address.is_null() { - (*error).set(FFIErrorCode::InvalidInput, "Output address pointer is null"); - return false; - } - - // Convert address from C string - let address_str = unwrap_or_return!(CStr::from_ptr(output.address).to_str(), error); - - // Parse address using dashcore - let address = unwrap_or_return!(dashcore::Address::from_str(address_str), error); - - outputs.push((address, output.amount)); - } - - let wallet_id = wallet_ref.inner().wallet_id; - - unsafe { - manager_ref.runtime.block_on(async { - let mut manager = manager_ref.manager.write().await; - - let (transaction, fee) = unwrap_or_return!( - manager - .build_and_sign_transaction( - &wallet_id, - account_index, - outputs, - FeeRate::new(fee_per_kb) - ) - .await, - error - ); - *fee_out = fee; - - // Serialize the transaction - let serialized = consensus::serialize(&transaction); - let size = serialized.len(); - - let boxed = serialized.into_boxed_slice(); - let tx_bytes = Box::into_raw(boxed) as *mut u8; - - *tx_bytes_out = tx_bytes; - *tx_len_out = size; - - (*error).clean(); - true - }) - } -} - -// Transaction context for checking -// FFITransactionContextType is imported from types module at the top -/// Transaction check result -#[repr(C)] -pub struct FFITransactionCheckResult { - /// Whether the transaction belongs to the wallet - pub is_relevant: bool, - /// Total amount received - pub total_received: u64, - /// Total amount sent - pub total_sent: u64, - /// Number of affected accounts - pub affected_accounts_count: u32, -} - -/// Check if a transaction belongs to the wallet using ManagedWalletInfo -/// -/// # Safety -/// -/// - `wallet` must be a valid mutable pointer to an FFIWallet -/// - `tx_bytes` must be a valid pointer to transaction bytes with at least `tx_len` bytes -/// - `result_out` must be a valid pointer to store the result -/// - `error` must be a valid pointer to an FFIError -#[no_mangle] -pub unsafe extern "C" fn wallet_check_transaction( - wallet: *mut FFIWallet, - tx_bytes: *const u8, - tx_len: usize, - context_type: FFITransactionContextType, - block_info: FFIBlockInfo, - islock_data: *const u8, - islock_len: usize, - update_state: bool, - result_out: *mut FFITransactionCheckResult, - error: *mut FFIError, -) -> bool { - let wallet = deref_ptr_mut!(wallet, error); - check_ptr!(tx_bytes, error); - check_ptr!(result_out, error); - - unsafe { - let tx_slice = slice::from_raw_parts(tx_bytes, tx_len); - - use dashcore::consensus::Decodable; - let tx = - unwrap_or_return!(dashcore::Transaction::consensus_decode(&mut &tx_slice[..]), error); - - // Build the transaction context - let context = unwrap_or_return!( - transaction_context_from_ffi(context_type, &block_info, islock_data, islock_len), - error - ); - - // Create a ManagedWalletInfo from the wallet - use key_wallet::transaction_checking::WalletTransactionChecker; - use key_wallet::wallet::managed_wallet_info::ManagedWalletInfo; - - let mut managed_info = ManagedWalletInfo::from_wallet(wallet.inner(), 0); - - // Check the transaction - wallet is always required now - let wallet_mut = unwrap_or_return!(wallet.inner_mut(), error); - - // Block on the async check_transaction call - let check_result = tokio::runtime::Handle::current().block_on( - managed_info.check_core_transaction(&tx, context, wallet_mut, update_state, true), - ); - - // If we updated state, we need to update the wallet's managed info - // Note: This would require storing ManagedWalletInfo in FFIWallet - // For now, we just return the result without persisting changes - - // Fill the result - *result_out = FFITransactionCheckResult { - is_relevant: check_result.is_relevant, - total_received: check_result.total_received, - total_sent: check_result.total_sent, - affected_accounts_count: check_result.affected_accounts.len() as u32, - }; - - (*error).clean(); - true - } -} - -/// Free transaction bytes -/// -/// # Safety -/// -/// - `tx_bytes` must be a valid pointer created by transaction functions or null -/// - After calling this function, the pointer becomes invalid -#[no_mangle] -pub unsafe extern "C" fn transaction_bytes_free(tx_bytes: *mut u8) { - if !tx_bytes.is_null() { - unsafe { - let _ = Box::from_raw(tx_bytes); - } - } -} - -// MARK: - Transaction Creation - -/// Create a new empty transaction -/// -/// # Returns -/// - Pointer to FFITransaction on success -/// - NULL on error -#[no_mangle] -pub extern "C" fn transaction_create() -> *mut FFITransaction { - let tx = Transaction { - version: 2, - lock_time: 0, - input: vec![], - output: vec![], - special_transaction_payload: None, - }; - - Box::into_raw(Box::new(FFITransaction { - inner: tx, - })) -} - -/// Add an input to a transaction -/// -/// # Safety -/// - `tx` must be a valid pointer to an FFITransaction -/// - `input` must be a valid pointer to an FFITxIn -/// -/// # Returns -/// - 0 on success -/// - -1 on error -#[no_mangle] -pub unsafe extern "C" fn transaction_add_input( - tx: *mut FFITransaction, - input: *const FFITxIn, -) -> i32 { - if tx.is_null() || input.is_null() { - return -1; - } - - let tx = &mut *tx; - let input = &*input; - - // Convert txid - let txid = match Txid::from_slice(&input.txid) { - Ok(txid) => txid, - Err(_) => { - return -1; - } - }; - - // Convert script - let script_sig = if input.script_sig.is_null() || input.script_sig_len == 0 { - ScriptBuf::new() - } else { - let script_slice = slice::from_raw_parts(input.script_sig, input.script_sig_len as usize); - ScriptBuf::from(script_slice.to_vec()) - }; - - let tx_in = TxIn { - previous_output: OutPoint { - txid, - vout: input.vout, - }, - script_sig, - sequence: input.sequence, - witness: Default::default(), - }; - - tx.inner.input.push(tx_in); - 0 -} - -/// Add an output to a transaction -/// -/// # Safety -/// - `tx` must be a valid pointer to an FFITransaction -/// - `output` must be a valid pointer to an FFITxOut -/// -/// # Returns -/// - 0 on success -/// - -1 on error -#[no_mangle] -pub unsafe extern "C" fn transaction_add_output( - tx: *mut FFITransaction, - output: *const FFITxOut, -) -> i32 { - if tx.is_null() || output.is_null() { - return -1; - } - - let tx = &mut *tx; - let output = &*output; - - // Convert script - let script_pubkey = if output.script_pubkey.is_null() || output.script_pubkey_len == 0 { - return -1; - } else { - let script_slice = - slice::from_raw_parts(output.script_pubkey, output.script_pubkey_len as usize); - ScriptBuf::from(script_slice.to_vec()) - }; - - let tx_out = TxOut { - value: output.amount, - script_pubkey, - }; - - tx.inner.output.push(tx_out); - 0 -} - -/// Get the transaction ID -/// -/// # Safety -/// - `tx` must be a valid pointer to an FFITransaction -/// - `txid_out` must be a valid pointer to a buffer of at least 32 bytes -/// -/// # Returns -/// - 0 on success -/// - -1 on error -#[no_mangle] -pub unsafe extern "C" fn transaction_get_txid(tx: *const FFITransaction, txid_out: *mut u8) -> i32 { - if tx.is_null() || txid_out.is_null() { - return -1; - } - - let tx = &*tx; - let txid = tx.inner.txid(); - - let txid_bytes = txid.as_byte_array(); - ptr::copy_nonoverlapping(txid_bytes.as_ptr(), txid_out, 32); - 0 -} - -/// Get transaction ID from raw transaction bytes -/// -/// # Safety -/// - `tx_bytes` must be a valid pointer to transaction bytes -/// - `tx_len` must be the correct length of the transaction -/// - `error` must be a valid pointer to an FFIError -/// -/// # Returns -/// - Pointer to null-terminated hex string of TXID (must be freed with string_free) -/// - NULL on error -#[no_mangle] -pub unsafe extern "C" fn transaction_get_txid_from_bytes( - tx_bytes: *const u8, - tx_len: usize, - error: *mut FFIError, -) -> *mut c_char { - check_ptr!(tx_bytes, error); - let tx_slice = slice::from_raw_parts(tx_bytes, tx_len); - let tx: Transaction = unwrap_or_return!(consensus::deserialize(tx_slice), error); - unwrap_or_return!(CString::new(tx.txid().to_string()), error).into_raw() -} - -/// Serialize a transaction -/// -/// # Safety -/// - `tx` must be a valid pointer to an FFITransaction -/// - `out_buf` can be NULL to get size only -/// - `out_len` must be a valid pointer to store the size -/// -/// # Returns -/// - 0 on success -/// - -1 on error -#[no_mangle] -pub unsafe extern "C" fn transaction_serialize( - tx: *const FFITransaction, - out_buf: *mut u8, - out_len: *mut u32, -) -> i32 { - if tx.is_null() || out_len.is_null() { - return -1; - } - - let tx = &*tx; - let serialized = consensus::serialize(&tx.inner); - let size = serialized.len() as u32; - - if out_buf.is_null() { - // Just return size - *out_len = size; - return 0; - } - - let provided_size = *out_len; - if provided_size < size { - *out_len = size; - return -1; - } - - ptr::copy_nonoverlapping(serialized.as_ptr(), out_buf, serialized.len()); - *out_len = size; - 0 -} - -/// Deserialize a transaction -/// -/// # Safety -/// - `data` must be a valid pointer to serialized transaction data -/// - `len` must be the correct length of the data -/// -/// # Returns -/// - Pointer to FFITransaction on success -/// - NULL on error -#[no_mangle] -pub unsafe extern "C" fn transaction_deserialize(data: *const u8, len: u32) -> *mut FFITransaction { - if data.is_null() { - return ptr::null_mut(); - } - - let slice = slice::from_raw_parts(data, len as usize); - - match consensus::deserialize::(slice) { - Ok(tx) => Box::into_raw(Box::new(FFITransaction { - inner: tx, - })), - Err(_) => ptr::null_mut(), - } -} - -/// Destroy a transaction -/// -/// # Safety -/// - `tx` must be a valid pointer to an FFITransaction created by transaction functions or null -/// - After calling this function, the pointer becomes invalid -#[no_mangle] -pub unsafe extern "C" fn transaction_destroy(tx: *mut FFITransaction) { - if !tx.is_null() { - let _ = Box::from_raw(tx); - } -} - -// MARK: - Transaction Signing - -/// Calculate signature hash for an input -/// -/// # Safety -/// - `tx` must be a valid pointer to an FFITransaction -/// - `script_pubkey` must be a valid pointer to the script pubkey -/// - `hash_out` must be a valid pointer to a buffer of at least 32 bytes -/// -/// # Returns -/// - 0 on success -/// - -1 on error -#[no_mangle] -pub unsafe extern "C" fn transaction_sighash( - tx: *const FFITransaction, - input_index: u32, - script_pubkey: *const u8, - script_pubkey_len: u32, - sighash_type: u32, - hash_out: *mut u8, -) -> i32 { - if tx.is_null() || script_pubkey.is_null() || hash_out.is_null() { - return -1; - } - - let tx = &*tx; - let script_slice = slice::from_raw_parts(script_pubkey, script_pubkey_len as usize); - let script = Script::from_bytes(script_slice); - - let sighash_type = EcdsaSighashType::from_consensus(sighash_type); - let cache = SighashCache::new(&tx.inner); - - match cache.legacy_signature_hash(input_index as usize, script, sighash_type.to_u32()) { - Ok(hash) => { - let hash_bytes: &[u8] = hash.as_ref(); - ptr::copy_nonoverlapping(hash_bytes.as_ptr(), hash_out, 32); - 0 - } - Err(_) => -1, - } -} - -/// Sign a transaction input -/// -/// # Safety -/// - `tx` must be a valid pointer to an FFITransaction -/// - `private_key` must be a valid pointer to a 32-byte private key -/// - `script_pubkey` must be a valid pointer to the script pubkey -/// -/// # Returns -/// - 0 on success -/// - -1 on error -#[no_mangle] -pub unsafe extern "C" fn transaction_sign_input( - tx: *mut FFITransaction, - input_index: u32, - private_key: *const u8, - script_pubkey: *const u8, - script_pubkey_len: u32, - sighash_type: u32, -) -> i32 { - if tx.is_null() || private_key.is_null() || script_pubkey.is_null() { - return -1; - } - - let tx = &mut *tx; - let input_index = input_index as usize; - - if input_index >= tx.inner.input.len() { - return -1; - } - - // Calculate sighash - let mut sighash = [0u8; 32]; - if transaction_sighash( - tx as *const FFITransaction, - input_index as u32, - script_pubkey, - script_pubkey_len, - sighash_type, - sighash.as_mut_ptr(), - ) != 0 - { - return -1; - } - - // Parse private key - let privkey_slice = slice::from_raw_parts(private_key, 32); - let privkey = match SecretKey::from_slice(privkey_slice) { - Ok(k) => k, - Err(_) => { - return -1; - } - }; - - // Sign - let secp = Secp256k1::new(); - let message = Message::from_digest(sighash); - let sig = secp.sign_ecdsa(&message, &privkey); - - // Build signature script (simplified P2PKH) - let mut sig_bytes = sig.serialize_der().to_vec(); - sig_bytes.push(sighash_type as u8); - - let pubkey = secp256k1::PublicKey::from_secret_key(&secp, &privkey); - let pubkey_bytes = pubkey.serialize(); - - let mut script_sig = vec![]; - script_sig.push(sig_bytes.len() as u8); - script_sig.extend_from_slice(&sig_bytes); - script_sig.push(pubkey_bytes.len() as u8); - script_sig.extend_from_slice(&pubkey_bytes); - - tx.inner.input[input_index].script_sig = ScriptBuf::from(script_sig); - 0 -} - -// MARK: - Script Utilities - -/// Create a P2PKH script pubkey -/// -/// # Safety -/// - `pubkey_hash` must be a valid pointer to a 20-byte public key hash -/// - `out_buf` can be NULL to get size only -/// - `out_len` must be a valid pointer to store the size -/// -/// # Returns -/// - 0 on success -/// - -1 on error -#[no_mangle] -pub unsafe extern "C" fn script_p2pkh( - pubkey_hash: *const u8, - out_buf: *mut u8, - out_len: *mut u32, -) -> i32 { - if pubkey_hash.is_null() || out_len.is_null() { - return -1; - } - - let hash_slice = slice::from_raw_parts(pubkey_hash, 20); - - // Build P2PKH script: OP_DUP OP_HASH160 OP_EQUALVERIFY OP_CHECKSIG - let mut script = vec![0x76, 0xa9, 0x14]; // OP_DUP OP_HASH160 PUSH(20) - script.extend_from_slice(hash_slice); - script.extend_from_slice(&[0x88, 0xac]); // OP_EQUALVERIFY OP_CHECKSIG - - let size = script.len() as u32; - - if out_buf.is_null() { - *out_len = size; - return 0; - } - - let provided_size = *out_len; - if provided_size < size { - *out_len = size; - return -1; - } - - ptr::copy_nonoverlapping(script.as_ptr(), out_buf, script.len()); - *out_len = size; - 0 -} - -/// Extract public key hash from P2PKH address -/// -/// # Safety -/// - `address` must be a valid pointer to a null-terminated C string -/// - `hash_out` must be a valid pointer to a buffer of at least 20 bytes -/// -/// # Returns -/// - 0 on success -/// - -1 on error -#[no_mangle] -pub unsafe extern "C" fn address_to_pubkey_hash( - address: *const c_char, - network: FFINetwork, - hash_out: *mut u8, -) -> i32 { - if address.is_null() || hash_out.is_null() { - return -1; - } - - let address_str = match CStr::from_ptr(address).to_str() { - Ok(s) => s, - Err(_) => { - return -1; - } - }; - - let expected_network: Network = network.into(); - - match address_str.parse::>() { - Ok(addr) => { - if *addr.network() != expected_network { - return -1; - } - - match addr.payload() { - dashcore::address::Payload::PubkeyHash(hash) => { - let hash_bytes = hash.as_byte_array(); - ptr::copy_nonoverlapping(hash_bytes.as_ptr(), hash_out, 20); - 0 - } - _ => -1, - } - } - Err(_) => -1, - } -} - -// MARK: - Asset Lock Transaction - -/// The type of funding account used for asset lock key derivation. -#[repr(C)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum FFIAssetLockFundingType { - /// Identity registration: m/9'/coinType'/5'/0'/index' - IdentityRegistration = 0, - /// Identity top-up (bound to a specific identity): m/9'/coinType'/5'/1'/reg_index'/index' - IdentityTopUp = 1, - /// Identity top-up (not bound to identity): m/9'/coinType'/5'/1'/index' - IdentityTopUpNotBound = 2, - /// Identity invitation: m/9'/coinType'/5'/3'/index' - IdentityInvitation = 3, - /// Asset lock address top-up: m/9'/coinType'/5'/4'/index' - AssetLockAddressTopUp = 4, - /// Asset lock shielded address top-up: m/9'/coinType'/5'/5'/index' - AssetLockShieldedAddressTopUp = 5, -} - -impl From for AssetLockFundingType { - fn from(ffi: FFIAssetLockFundingType) -> Self { - match ffi { - FFIAssetLockFundingType::IdentityRegistration => Self::IdentityRegistration, - FFIAssetLockFundingType::IdentityTopUp => Self::IdentityTopUp, - FFIAssetLockFundingType::IdentityTopUpNotBound => Self::IdentityTopUpNotBound, - FFIAssetLockFundingType::IdentityInvitation => Self::IdentityInvitation, - FFIAssetLockFundingType::AssetLockAddressTopUp => Self::AssetLockAddressTopUp, - FFIAssetLockFundingType::AssetLockShieldedAddressTopUp => { - Self::AssetLockShieldedAddressTopUp - } - } - } -} - -/// Build and sign an asset lock transaction for Core to Platform transfers. -/// -/// Creates a special transaction (type 8) with `AssetLockPayload` that locks -/// Dash for Platform credits. Derives one unique private key per credit output -/// from the specified funding account types. -/// -/// # Parameters -/// -/// - `funding_types`: Array of `credit_outputs_count` funding account types, -/// one per credit output (registration, top-up, invitation, etc.) -/// - `identity_indices`: Array of `credit_outputs_count` identity indices. -/// Only used for `IdentityTopUp` entries; ignored for other funding types. -/// - `private_keys_out`: Caller-allocated array of `credit_outputs_count` × 32-byte -/// buffers. On success, each `private_keys_out[i]` receives the one-time private -/// key corresponding to `credit_output_scripts[i]`. -/// -/// # Safety -/// -/// - All pointer parameters must be valid and non-null -/// - All parallel arrays must have at least `credit_outputs_count` elements -/// - `private_keys_out` must point to an array of `credit_outputs_count` × `[u8; 32]` buffers -/// - Caller must free `tx_bytes_out` with `transaction_bytes_free` -#[no_mangle] -pub unsafe extern "C" fn wallet_build_and_sign_asset_lock_transaction( - manager: *const FFIWalletManager, - wallet: *const FFIWallet, - account_index: u32, - funding_types: *const FFIAssetLockFundingType, - identity_indices: *const u32, - credit_output_scripts: *const *const u8, - credit_output_script_lens: *const usize, - credit_output_amounts: *const u64, - credit_outputs_count: usize, - fee_per_kb: u64, - fee_out: *mut u64, - tx_bytes_out: *mut *mut u8, - tx_len_out: *mut usize, - private_keys_out: *mut [u8; 32], - error: *mut FFIError, -) -> bool { - check_ptr!(manager, error); - check_ptr!(wallet, error); - check_ptr!(funding_types, error); - check_ptr!(identity_indices, error); - check_ptr!(credit_output_scripts, error); - check_ptr!(credit_output_script_lens, error); - check_ptr!(credit_output_amounts, error); - check_ptr!(tx_bytes_out, error); - check_ptr!(tx_len_out, error); - check_ptr!(fee_out, error); - check_ptr!(private_keys_out, error); - - if credit_outputs_count == 0 { - (*error).set(FFIErrorCode::InvalidInput, "At least one credit output required"); - return false; - } - - unsafe { - let manager_ref = &*manager; - let wallet_ref = &*wallet; - - let scripts_slice = slice::from_raw_parts(credit_output_scripts, credit_outputs_count); - let lens_slice = slice::from_raw_parts(credit_output_script_lens, credit_outputs_count); - let amounts_slice = slice::from_raw_parts(credit_output_amounts, credit_outputs_count); - let funding_types_slice = slice::from_raw_parts(funding_types, credit_outputs_count); - let identity_indices_slice = slice::from_raw_parts(identity_indices, credit_outputs_count); - - // Convert FFI arrays to domain types - let mut fundings = Vec::with_capacity(credit_outputs_count); - for i in 0..credit_outputs_count { - if scripts_slice[i].is_null() { - (*error).set( - FFIErrorCode::InvalidInput, - &format!("Credit output script {} is null", i), - ); - return false; - } - let script_bytes = slice::from_raw_parts(scripts_slice[i], lens_slice[i]); - fundings.push(CreditOutputFunding { - output: TxOut { - value: amounts_slice[i], - script_pubkey: ScriptBuf::from(script_bytes.to_vec()), - }, - funding_type: funding_types_slice[i].into(), - identity_index: identity_indices_slice[i], - }); - } - - manager_ref.runtime.block_on(async { - let mut manager = manager_ref.manager.write().await; - let wallet_id = wallet_ref.inner().wallet_id; - - let managed_wallet = unwrap_or_return!(manager.get_wallet_info_mut(&wallet_id), error); - - let result = unwrap_or_return!(managed_wallet.build_asset_lock( - wallet_ref.inner(), - account_index, - fundings, - fee_per_kb, - ).await, error); - - // Write outputs - *fee_out = result.fee; - - // `build_asset_lock` always returns private keys; the signer-variant - // path uses a different FFI entry point. - let private_keys = match &result.keys { - key_wallet::wallet::managed_wallet_info::asset_lock_builder::AssetLockCreditKeys::Private(k) => k, - key_wallet::wallet::managed_wallet_info::asset_lock_builder::AssetLockCreditKeys::Public(_) => { - (*error).set(FFIErrorCode::WalletError, "Unexpected public-key result from build_asset_lock"); - return false; - } - }; - let keys_out = slice::from_raw_parts_mut(private_keys_out, credit_outputs_count); - for (i, key) in private_keys.iter().enumerate() { - if i < keys_out.len() { - keys_out[i] = *key; - } - } - - let serialized = consensus::serialize(&result.transaction); - let size = serialized.len(); - let boxed = serialized.into_boxed_slice(); - *tx_bytes_out = Box::into_raw(boxed) as *mut u8; - *tx_len_out = size; - - (*error).clean(); - true - }) - } -} diff --git a/key-wallet-ffi/src/transaction_checking.rs b/key-wallet-ffi/src/transaction_checking.rs deleted file mode 100644 index c751c10cc..000000000 --- a/key-wallet-ffi/src/transaction_checking.rs +++ /dev/null @@ -1,493 +0,0 @@ -//! Transaction checking FFI bindings -//! -//! This module provides FFI bindings for the advanced transaction checking -//! functionality introduced in the key-wallet library, including transaction -//! routing, classification, and account matching. - -use std::ffi::CString; -use std::os::raw::{c_char, c_uint}; -use std::slice; - -use crate::error::{FFIError, FFIErrorCode}; -use crate::managed_wallet::{managed_wallet_info_free, FFIManagedWalletInfo}; -use crate::types::{ - transaction_context_from_ffi, FFIBlockInfo, FFITransactionContextType, FFIWallet, -}; -use crate::{check_ptr, deref_ptr_mut, unwrap_or_return}; -use dashcore::consensus::Decodable; -use dashcore::Transaction; -use key_wallet::transaction_checking::{ - account_checker::CoreAccountTypeMatch, TransactionContext, WalletTransactionChecker, -}; -use key_wallet::wallet::managed_wallet_info::ManagedWalletInfo; - -// Transaction context for checking -// FFITransactionContextType is imported from types module at the top -/// Account type match result -#[repr(C)] -pub struct FFIAccountMatch { - /// Account type ID (matches FFIAccountKind enum values) - pub account_type: c_uint, - /// Account index (if applicable) - pub account_index: c_uint, - /// Registration index for identity top-up (if applicable) - pub registration_index: c_uint, - /// Amount received by this account - pub received: u64, - /// Amount sent from this account - pub sent: u64, - /// Number of external addresses involved - pub external_addresses_count: c_uint, - /// Number of internal addresses involved - pub internal_addresses_count: c_uint, - /// Whether external addresses were involved - pub has_external_addresses: bool, - /// Whether internal addresses were involved - pub has_internal_addresses: bool, -} - -/// Transaction check result -#[repr(C)] -pub struct FFITransactionCheckResult { - /// Whether the transaction belongs to the wallet - pub is_relevant: bool, - /// Total amount received across all accounts - pub total_received: u64, - /// Total amount sent across all accounts - pub total_sent: u64, - /// Total amount received for credit conversion - pub total_received_for_credit_conversion: u64, - /// Array of affected accounts (must be freed) - pub affected_accounts: *mut FFIAccountMatch, - /// Number of affected accounts - pub affected_accounts_count: c_uint, -} - -/// Check if a transaction belongs to the wallet -/// -/// This function checks a transaction against all relevant account types in the wallet -/// and returns detailed information about which accounts are affected. -/// -/// # Safety -/// -/// - `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo -/// - `wallet` must be a valid pointer to an FFIWallet (needed for address generation and DashPay queries) -/// - `tx_bytes` must be a valid pointer to transaction bytes with at least `tx_len` bytes -/// - `result_out` must be a valid pointer to store the result -/// - `error` must be a valid pointer to an FFIError -/// - The affected_accounts array in the result must be freed with `transaction_check_result_free` -#[no_mangle] -pub unsafe extern "C" fn managed_wallet_check_transaction( - managed_wallet: *mut FFIManagedWalletInfo, - wallet: *mut FFIWallet, - tx_bytes: *const u8, - tx_len: usize, - context_type: FFITransactionContextType, - block_info: FFIBlockInfo, - islock_data: *const u8, - islock_len: usize, - update_state: bool, - result_out: *mut FFITransactionCheckResult, - error: *mut FFIError, -) -> bool { - let managed_wallet: &mut ManagedWalletInfo = deref_ptr_mut!(managed_wallet, error).inner_mut(); - check_ptr!(tx_bytes, error); - check_ptr!(result_out, error); - - let tx_slice = slice::from_raw_parts(tx_bytes, tx_len); - - let tx = unwrap_or_return!(Transaction::consensus_decode(&mut &tx_slice[..]), error); - - // Build the transaction context - let context = unwrap_or_return!( - transaction_context_from_ffi(context_type, &block_info, islock_data, islock_len,), - error - ); - - if let TransactionContext::InstantSend(ref lock) = context { - if lock.txid != tx.txid() { - (*error).set(FFIErrorCode::InvalidInput, "InstantLock txid does not match transaction"); - return false; - } - } - - let ff_wallet_mut = deref_ptr_mut!(wallet, error); - let wallet_mut = unwrap_or_return!(ff_wallet_mut.inner_mut(), error); - - // Block on the async check_transaction call - let check_result = tokio::runtime::Handle::current().block_on( - managed_wallet.check_core_transaction(&tx, context, wallet_mut, update_state, true), - ); - - // Convert the result to FFI format - let affected_accounts = if check_result.affected_accounts.is_empty() { - std::ptr::null_mut() - } else { - let mut ffi_accounts = Vec::with_capacity(check_result.affected_accounts.len()); - - for account_match in &check_result.affected_accounts { - match &account_match.account_type_match { - CoreAccountTypeMatch::StandardBIP44 { - account_index, - involved_receive_addresses, - involved_change_addresses, - } => { - let external_count = involved_receive_addresses.len() as c_uint; - let internal_count = involved_change_addresses.len() as c_uint; - let ffi_match = FFIAccountMatch { - account_type: 0, // StandardBIP44 - account_index: *account_index, - registration_index: 0, - received: account_match.received, - sent: account_match.sent, - external_addresses_count: external_count, - internal_addresses_count: internal_count, - has_external_addresses: external_count > 0, - has_internal_addresses: internal_count > 0, - }; - ffi_accounts.push(ffi_match); - continue; - } - CoreAccountTypeMatch::StandardBIP32 { - account_index, - involved_receive_addresses, - involved_change_addresses, - } => { - let external_count = involved_receive_addresses.len() as c_uint; - let internal_count = involved_change_addresses.len() as c_uint; - let ffi_match = FFIAccountMatch { - account_type: 1, // StandardBIP32 - account_index: *account_index, - registration_index: 0, - received: account_match.received, - sent: account_match.sent, - external_addresses_count: external_count, - internal_addresses_count: internal_count, - has_external_addresses: external_count > 0, - has_internal_addresses: internal_count > 0, - }; - ffi_accounts.push(ffi_match); - continue; - } - CoreAccountTypeMatch::CoinJoin { - account_index, - involved_addresses, - } => { - let ffi_match = FFIAccountMatch { - account_type: 2, // CoinJoin - account_index: *account_index, - registration_index: 0, - received: account_match.received, - sent: account_match.sent, - external_addresses_count: involved_addresses.len() as c_uint, - internal_addresses_count: 0, - has_external_addresses: !involved_addresses.is_empty(), - has_internal_addresses: false, - }; - ffi_accounts.push(ffi_match); - continue; - } - CoreAccountTypeMatch::IdentityRegistration { - involved_addresses, - } => { - let ffi_match = FFIAccountMatch { - account_type: 3, // IdentityRegistration - account_index: 0, - registration_index: 0, - received: account_match.received, - sent: account_match.sent, - external_addresses_count: involved_addresses.len() as c_uint, - internal_addresses_count: 0, - has_external_addresses: !involved_addresses.is_empty(), - has_internal_addresses: false, - }; - ffi_accounts.push(ffi_match); - continue; - } - CoreAccountTypeMatch::IdentityTopUp { - account_index, - involved_addresses, - } => { - let ffi_match = FFIAccountMatch { - account_type: 4, // IdentityTopUp - account_index: 0, - registration_index: *account_index, - received: account_match.received, - sent: account_match.sent, - external_addresses_count: involved_addresses.len() as c_uint, - internal_addresses_count: 0, - has_external_addresses: !involved_addresses.is_empty(), - has_internal_addresses: false, - }; - ffi_accounts.push(ffi_match); - continue; - } - CoreAccountTypeMatch::IdentityTopUpNotBound { - involved_addresses, - } => { - let ffi_match = FFIAccountMatch { - account_type: 5, // IdentityTopUpNotBound - account_index: 0, - registration_index: 0, - received: account_match.received, - sent: account_match.sent, - external_addresses_count: involved_addresses.len() as c_uint, - internal_addresses_count: 0, - has_external_addresses: !involved_addresses.is_empty(), - has_internal_addresses: false, - }; - ffi_accounts.push(ffi_match); - continue; - } - CoreAccountTypeMatch::IdentityInvitation { - involved_addresses, - } => { - let ffi_match = FFIAccountMatch { - account_type: 6, // IdentityInvitation - account_index: 0, - registration_index: 0, - received: account_match.received, - sent: account_match.sent, - external_addresses_count: involved_addresses.len() as c_uint, - internal_addresses_count: 0, - has_external_addresses: !involved_addresses.is_empty(), - has_internal_addresses: false, - }; - ffi_accounts.push(ffi_match); - continue; - } - CoreAccountTypeMatch::AssetLockAddressTopUp { - involved_addresses, - } => { - let ffi_match = FFIAccountMatch { - account_type: 14, // AssetLockAddressTopUp - account_index: 0, - registration_index: 0, - received: account_match.received, - sent: account_match.sent, - external_addresses_count: involved_addresses.len() as c_uint, - internal_addresses_count: 0, - has_external_addresses: !involved_addresses.is_empty(), - has_internal_addresses: false, - }; - ffi_accounts.push(ffi_match); - continue; - } - CoreAccountTypeMatch::AssetLockShieldedAddressTopUp { - involved_addresses, - } => { - let ffi_match = FFIAccountMatch { - account_type: 15, // AssetLockShieldedAddressTopUp - account_index: 0, - registration_index: 0, - received: account_match.received, - sent: account_match.sent, - external_addresses_count: involved_addresses.len() as c_uint, - internal_addresses_count: 0, - has_external_addresses: !involved_addresses.is_empty(), - has_internal_addresses: false, - }; - ffi_accounts.push(ffi_match); - continue; - } - CoreAccountTypeMatch::ProviderVotingKeys { - involved_addresses, - } => { - let ffi_match = FFIAccountMatch { - account_type: 7, // ProviderVotingKeys - account_index: 0, - registration_index: 0, - received: account_match.received, - sent: account_match.sent, - external_addresses_count: involved_addresses.len() as c_uint, - internal_addresses_count: 0, - has_external_addresses: !involved_addresses.is_empty(), - has_internal_addresses: false, - }; - ffi_accounts.push(ffi_match); - continue; - } - CoreAccountTypeMatch::ProviderOwnerKeys { - involved_addresses, - } => { - let ffi_match = FFIAccountMatch { - account_type: 8, // ProviderOwnerKeys - account_index: 0, - registration_index: 0, - received: account_match.received, - sent: account_match.sent, - external_addresses_count: involved_addresses.len() as c_uint, - internal_addresses_count: 0, - has_external_addresses: !involved_addresses.is_empty(), - has_internal_addresses: false, - }; - ffi_accounts.push(ffi_match); - continue; - } - CoreAccountTypeMatch::ProviderOperatorKeys { - involved_addresses, - } => { - let ffi_match = FFIAccountMatch { - account_type: 9, // ProviderOperatorKeys - account_index: 0, - registration_index: 0, - received: account_match.received, - sent: account_match.sent, - external_addresses_count: involved_addresses.len() as c_uint, - internal_addresses_count: 0, - has_external_addresses: !involved_addresses.is_empty(), - has_internal_addresses: false, - }; - ffi_accounts.push(ffi_match); - continue; - } - CoreAccountTypeMatch::ProviderPlatformKeys { - involved_addresses, - } => { - let ffi_match = FFIAccountMatch { - account_type: 10, // ProviderPlatformKeys - account_index: 0, - registration_index: 0, - received: account_match.received, - sent: account_match.sent, - external_addresses_count: involved_addresses.len() as c_uint, - internal_addresses_count: 0, - has_external_addresses: !involved_addresses.is_empty(), - has_internal_addresses: false, - }; - ffi_accounts.push(ffi_match); - continue; - } - CoreAccountTypeMatch::DashpayReceivingFunds { - account_index, - involved_addresses, - } => { - let ffi_match = FFIAccountMatch { - account_type: 11, // DashpayReceivingFunds - account_index: *account_index, - registration_index: 0, - received: account_match.received, - sent: account_match.sent, - external_addresses_count: involved_addresses.len() as c_uint, - internal_addresses_count: 0, - has_external_addresses: !involved_addresses.is_empty(), - has_internal_addresses: false, - }; - ffi_accounts.push(ffi_match); - continue; - } - CoreAccountTypeMatch::DashpayExternalAccount { - account_index, - involved_addresses, - } => { - let ffi_match = FFIAccountMatch { - account_type: 12, // DashpayExternalAccount - account_index: *account_index, - registration_index: 0, - received: account_match.received, - sent: account_match.sent, - external_addresses_count: involved_addresses.len() as c_uint, - internal_addresses_count: 0, - has_external_addresses: !involved_addresses.is_empty(), - has_internal_addresses: false, - }; - ffi_accounts.push(ffi_match); - continue; - } - } - } - - // Convert vector to raw array - let _len = ffi_accounts.len(); - let ptr = ffi_accounts.as_mut_ptr(); - std::mem::forget(ffi_accounts); - ptr - }; - - // Fill the result - *result_out = FFITransactionCheckResult { - is_relevant: check_result.is_relevant, - total_received: check_result.total_received, - total_sent: check_result.total_sent, - total_received_for_credit_conversion: check_result.total_received_for_credit_conversion, - affected_accounts, - affected_accounts_count: check_result.affected_accounts.len() as c_uint, - }; - - (*error).clean(); - true -} - -/// Free a transaction check result -/// -/// # Safety -/// -/// - `result` must be a valid pointer to an FFITransactionCheckResult -/// - This function must only be called once per result -#[no_mangle] -pub unsafe extern "C" fn transaction_check_result_free(result: *mut FFITransactionCheckResult) { - if !result.is_null() { - let result = &mut *result; - if !result.affected_accounts.is_null() && result.affected_accounts_count > 0 { - // Reconstruct the vector and drop it - let _ = Vec::from_raw_parts( - result.affected_accounts, - result.affected_accounts_count as usize, - result.affected_accounts_count as usize, - ); - result.affected_accounts = std::ptr::null_mut(); - result.affected_accounts_count = 0; - } - } -} - -/// Free a managed wallet (FFIManagedWalletInfo type) -/// -/// # Safety -/// -/// - `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo -/// - This function must only be called once per managed wallet -#[no_mangle] -pub unsafe extern "C" fn ffi_managed_wallet_free(managed_wallet: *mut FFIManagedWalletInfo) { - // For compatibility, forward to canonical free - managed_wallet_info_free(managed_wallet); -} - -/// Get the transaction classification for routing -/// -/// Returns a string describing the transaction type (e.g., "Standard", "CoinJoin", -/// "AssetLock", "AssetUnlock", "ProviderRegistration", etc.) -/// -/// # Safety -/// -/// - `tx_bytes` must be a valid pointer to transaction bytes with at least `tx_len` bytes -/// - `error` must be a valid pointer to an FFIError -/// - The returned string must be freed by the caller -#[no_mangle] -pub unsafe extern "C" fn transaction_classify( - tx_bytes: *const u8, - tx_len: usize, - error: *mut FFIError, -) -> *mut c_char { - check_ptr!(tx_bytes, error); - let tx_slice = slice::from_raw_parts(tx_bytes, tx_len); - let tx = unwrap_or_return!(Transaction::consensus_decode(&mut &tx_slice[..]), error); - - use key_wallet::transaction_checking::transaction_router::TransactionRouter; - let tx_type = TransactionRouter::classify_transaction(&tx); - unwrap_or_return!(CString::new(format!("{:?}", tx_type)), error).into_raw() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_transaction_context_conversion() { - // Test that FFI transaction context values match expectations - assert_eq!(FFITransactionContextType::Mempool as u32, 0); - assert_eq!(FFITransactionContextType::InstantSend as u32, 1); - assert_eq!(FFITransactionContextType::InBlock as u32, 2); - assert_eq!(FFITransactionContextType::InChainLockedBlock as u32, 3); - } -} diff --git a/key-wallet-ffi/src/types.rs b/key-wallet-ffi/src/types.rs deleted file mode 100644 index f11a840e3..000000000 --- a/key-wallet-ffi/src/types.rs +++ /dev/null @@ -1,1184 +0,0 @@ -//! Common types for FFI interface - -use dashcore::ephemerealdata::instant_lock::InstantLock; -use dashcore::hashes::Hash; -use key_wallet::account::{InputDetail, OutputDetail}; -use key_wallet::managed_account::transaction_record::{OutputRole, TransactionDirection}; -use key_wallet::transaction_checking::transaction_router::TransactionType; -use key_wallet::transaction_checking::{BlockInfo, TransactionContext}; -use key_wallet::Wallet; -use std::os::raw::c_char; -use std::sync::Arc; - -/// FFI-compatible block metadata (height, hash, timestamp). -#[repr(C)] -#[derive(Debug, Clone, Copy)] -pub struct FFIBlockInfo { - /// Block height - pub height: u32, - /// Block hash (32 bytes) - pub block_hash: [u8; 32], - /// Unix timestamp - pub timestamp: u32, -} - -impl FFIBlockInfo { - /// All-zeros placeholder used for unconfirmed contexts. - pub fn empty() -> Self { - Self { - height: 0, - block_hash: [0u8; 32], - timestamp: 0, - } - } - - /// Convert to native `BlockInfo`. - pub fn to_block_info(&self) -> BlockInfo { - let block_hash = dashcore::BlockHash::from_byte_array(self.block_hash); - BlockInfo::new(self.height, block_hash, self.timestamp) - } -} - -impl From for FFIBlockInfo { - fn from(info: BlockInfo) -> Self { - Self { - height: info.height(), - block_hash: info.block_hash().to_byte_array(), - timestamp: info.timestamp(), - } - } -} - -/// Convert an `FFIBlockInfo` and context type to a native `TransactionContext`. -/// -/// Returns `None` when: -/// - Block info is all-zeros for confirmed contexts (`InBlock`, `InChainLockedBlock`) -/// - IS lock data is null/empty for `InstantSend` contexts -/// - IS lock data fails deserialization -pub(crate) fn transaction_context_from_ffi( - context_type: FFITransactionContextType, - block_info: &FFIBlockInfo, - islock_data: *const u8, - islock_len: usize, -) -> Option { - match context_type { - FFITransactionContextType::Mempool => Some(TransactionContext::Mempool), - FFITransactionContextType::InstantSend => { - if islock_data.is_null() || islock_len == 0 { - return None; - } - let bytes = unsafe { std::slice::from_raw_parts(islock_data, islock_len) }; - let lock = match dashcore::consensus::deserialize::(bytes) { - Ok(lock) => lock, - Err(_) => return None, - }; - Some(TransactionContext::InstantSend(lock)) - } - FFITransactionContextType::InBlock => { - if block_info.block_hash == [0u8; 32] && block_info.timestamp == 0 { - return None; - } - Some(TransactionContext::InBlock(block_info.to_block_info())) - } - FFITransactionContextType::InChainLockedBlock => { - if block_info.block_hash == [0u8; 32] && block_info.timestamp == 0 { - return None; - } - Some(TransactionContext::InChainLockedBlock(block_info.to_block_info())) - } - } -} - -/// FFI Balance type for representing wallet balances -#[repr(C)] -#[derive(Debug, Clone, Copy, Default)] -pub struct FFIBalance { - /// Confirmed balance in duffs - pub confirmed: u64, - /// Unconfirmed balance in duffs - pub unconfirmed: u64, - /// Immature balance in duffs (e.g., mining rewards not yet mature) - pub immature: u64, - /// Locked balance in duffs (e.g., CoinJoin reserves) - pub locked: u64, - /// Total balance in duffs - pub total: u64, -} - -impl From for FFIBalance { - fn from(balance: key_wallet::WalletCoreBalance) -> Self { - FFIBalance { - confirmed: balance.confirmed(), - unconfirmed: balance.unconfirmed(), - immature: balance.immature(), - locked: balance.locked(), - total: balance.total(), - } - } -} - -/// Opaque wallet handle -pub struct FFIWallet { - pub(crate) wallet: Arc, -} - -impl FFIWallet { - /// Create a new FFI wallet handle - pub fn new(wallet: Wallet) -> Self { - FFIWallet { - wallet: Arc::new(wallet), - } - } - - /// Get a reference to the inner wallet - pub fn inner(&self) -> &Wallet { - self.wallet.as_ref() - } - - /// Get a mutable reference to the inner wallet (requires Arc::get_mut) - pub fn inner_mut(&mut self) -> Option<&mut Wallet> { - Arc::get_mut(&mut self.wallet) - } -} - -/// FFI Result type for Account operations -#[repr(C)] -pub struct FFIAccountResult { - /// The account handle if successful, NULL if error - pub account: *mut FFIAccount, - /// Error code (0 = success) - pub error_code: i32, - /// Error message (NULL if success, must be freed by caller if not NULL) - pub error_message: *mut c_char, -} - -impl FFIAccountResult { - /// Create a success result - pub fn success(account: *mut FFIAccount) -> Self { - FFIAccountResult { - account, - error_code: 0, - error_message: std::ptr::null_mut(), - } - } - - /// Create an error result - pub fn error(code: crate::error::FFIErrorCode, message: String) -> Self { - use std::ffi::CString; - let c_message = CString::new(message).unwrap_or_else(|_| { - // Fallback to a safe literal that cannot fail - CString::new("Unknown error").expect("Hardcoded string should never fail") - }); - FFIAccountResult { - account: std::ptr::null_mut(), - error_code: code as i32, - error_message: c_message.into_raw(), - } - } -} - -/// Forward declaration for FFIAccount (defined in account.rs) -pub use crate::account::FFIAccount; -#[cfg(feature = "bls")] -pub use crate::account::FFIBLSAccount; -#[cfg(feature = "eddsa")] -pub use crate::account::FFIEdDSAAccount; - -/// Standard account subtype -#[repr(C)] -#[derive(Debug, Clone, Copy)] -pub enum FFIStandardAccountType { - BIP44 = 0, - BIP32 = 1, -} - -/// Account type enumeration matching all key_wallet AccountType variants -/// -/// This enum provides a complete FFI representation of all account types -/// supported by the key_wallet library: -/// -/// - Standard accounts: BIP44 and BIP32 variants for regular transactions -/// - CoinJoin: Privacy-enhanced transactions -/// - Identity accounts: Registration, top-up, and invitation funding -/// - Provider accounts: Various masternode provider key types (voting, owner, operator, platform) -#[repr(C)] -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum FFIAccountKind { - /// Standard BIP44 account (m/44'/coin_type'/account'/x/x) - StandardBIP44 = 0, - /// Standard BIP32 account (m/account'/x/x) - StandardBIP32 = 1, - /// CoinJoin account for private transactions - CoinJoin = 2, - /// Identity registration funding - IdentityRegistration = 3, - /// Identity top-up funding (requires registration_index) - IdentityTopUp = 4, - /// Identity top-up funding not bound to a specific identity - IdentityTopUpNotBoundToIdentity = 5, - /// Identity invitation funding - IdentityInvitation = 6, - /// Provider voting keys (DIP-3) - Path: m/9'/5'/3'/1'/\[key_index\] - ProviderVotingKeys = 7, - /// Provider owner keys (DIP-3) - Path: m/9'/5'/3'/2'/\[key_index\] - ProviderOwnerKeys = 8, - /// Provider operator keys (DIP-3) - Path: m/9'/5'/3'/3'/\[key_index\] - ProviderOperatorKeys = 9, - /// Provider platform P2P keys (DIP-3, ED25519) - Path: m/9'/5'/3'/4'/\[key_index\] - ProviderPlatformKeys = 10, - /// DashPay incoming funds account using 256-bit derivation - DashpayReceivingFunds = 11, - /// DashPay external (watch-only) account using 256-bit derivation - DashpayExternalAccount = 12, - /// Platform Payment address (DIP-17) - Path: m/9'/5'/17'/account'/key_class'/index - PlatformPayment = 13, - /// Asset lock address top-up funding (subfeature 4) - AssetLockAddressTopUp = 14, - /// Asset lock shielded address top-up funding (subfeature 5) - AssetLockShieldedAddressTopUp = 15, -} - -impl FFIAccountKind { - /// Convert to AccountType with the provided index (used where applicable). - /// For types needing an index (e.g., IdentityTopUp.registration_index), the provided index is used. - pub fn to_account_type(self, index: u32) -> key_wallet::AccountType { - use key_wallet::account::account_type::StandardAccountType; - match self { - FFIAccountKind::StandardBIP44 => key_wallet::AccountType::Standard { - index, - standard_account_type: StandardAccountType::BIP44Account, - }, - FFIAccountKind::StandardBIP32 => key_wallet::AccountType::Standard { - index, - standard_account_type: StandardAccountType::BIP32Account, - }, - FFIAccountKind::CoinJoin => key_wallet::AccountType::CoinJoin { - index, - }, - FFIAccountKind::IdentityRegistration => key_wallet::AccountType::IdentityRegistration, - FFIAccountKind::IdentityTopUp => { - // IdentityTopUp requires a registration_index - key_wallet::AccountType::IdentityTopUp { - registration_index: index, - } - } - FFIAccountKind::IdentityTopUpNotBoundToIdentity => { - key_wallet::AccountType::IdentityTopUpNotBoundToIdentity - } - FFIAccountKind::IdentityInvitation => key_wallet::AccountType::IdentityInvitation, - FFIAccountKind::AssetLockAddressTopUp => key_wallet::AccountType::AssetLockAddressTopUp, - FFIAccountKind::AssetLockShieldedAddressTopUp => { - key_wallet::AccountType::AssetLockShieldedAddressTopUp - } - FFIAccountKind::ProviderVotingKeys => key_wallet::AccountType::ProviderVotingKeys, - FFIAccountKind::ProviderOwnerKeys => key_wallet::AccountType::ProviderOwnerKeys, - FFIAccountKind::ProviderOperatorKeys => key_wallet::AccountType::ProviderOperatorKeys, - FFIAccountKind::ProviderPlatformKeys => key_wallet::AccountType::ProviderPlatformKeys, - // DashPay variants require additional identity IDs (user_identity_id and friend_identity_id) - // that are not part of the current FFI API. These types cannot be constructed via this - // conversion path. Attempting to use them is a programming error. - // - // TODO: Extend the FFI API to accept identity IDs for DashPay account creation: - // - Add new FFI functions like: - // * ffi_account_type_to_dashpay_receiving_funds(index, user_id[32], friend_id[32]) - // * ffi_account_type_to_dashpay_external_account(index, user_id[32], friend_id[32]) - // - Or extend to_account_type to accept optional identity ID parameters - // - // Until then, attempting to convert these variants will panic to prevent silent misrouting. - FFIAccountKind::DashpayReceivingFunds => { - panic!( - "FFIAccountKind::DashpayReceivingFunds cannot be converted to AccountType \ - without user_identity_id and friend_identity_id. The FFI API does not yet \ - support passing these 32-byte identity IDs. This is a programming error - \ - DashPay account creation must use a different API path." - ); - } - FFIAccountKind::DashpayExternalAccount => { - panic!( - "FFIAccountKind::DashpayExternalAccount cannot be converted to AccountType \ - without user_identity_id and friend_identity_id. The FFI API does not yet \ - support passing these 32-byte identity IDs. This is a programming error - \ - DashPay account creation must use a different API path." - ); - } - FFIAccountKind::PlatformPayment => { - panic!( - "FFIAccountKind::PlatformPayment cannot be converted to AccountType \ - without account and key_class indices. The FFI API does not yet \ - support passing these values. This is a programming error - \ - Platform Payment account creation must use a different API path." - ); - } - } - } - - /// Convert from AccountType to FFI representation - /// - /// Returns: (FFIAccountKind, primary_index, optional_secondary_index) - /// - /// # Panics - /// - /// Panics when attempting to convert DashPay account types (DashpayReceivingFunds, - /// DashpayExternalAccount) because they contain 32-byte identity IDs that cannot be - /// represented in the current FFI tuple format. This prevents silent data loss. - /// - /// TODO: Extend the return type or create separate FFI functions that can return - /// the full DashPay account information including identity IDs. - pub fn from_account_type(account_type: &key_wallet::AccountType) -> (Self, u32, Option) { - use key_wallet::account::account_type::StandardAccountType; - match account_type { - key_wallet::AccountType::Standard { - index, - standard_account_type, - } => match standard_account_type { - StandardAccountType::BIP44Account => (FFIAccountKind::StandardBIP44, *index, None), - StandardAccountType::BIP32Account => (FFIAccountKind::StandardBIP32, *index, None), - }, - key_wallet::AccountType::CoinJoin { - index, - } => (FFIAccountKind::CoinJoin, *index, None), - key_wallet::AccountType::IdentityRegistration => { - (FFIAccountKind::IdentityRegistration, 0, None) - } - key_wallet::AccountType::IdentityTopUp { - registration_index, - } => (FFIAccountKind::IdentityTopUp, 0, Some(*registration_index)), - key_wallet::AccountType::IdentityTopUpNotBoundToIdentity => { - (FFIAccountKind::IdentityTopUpNotBoundToIdentity, 0, None) - } - key_wallet::AccountType::IdentityInvitation => { - (FFIAccountKind::IdentityInvitation, 0, None) - } - key_wallet::AccountType::AssetLockAddressTopUp => { - (FFIAccountKind::AssetLockAddressTopUp, 0, None) - } - key_wallet::AccountType::AssetLockShieldedAddressTopUp => { - (FFIAccountKind::AssetLockShieldedAddressTopUp, 0, None) - } - key_wallet::AccountType::ProviderVotingKeys => { - (FFIAccountKind::ProviderVotingKeys, 0, None) - } - key_wallet::AccountType::ProviderOwnerKeys => { - (FFIAccountKind::ProviderOwnerKeys, 0, None) - } - key_wallet::AccountType::ProviderOperatorKeys => { - (FFIAccountKind::ProviderOperatorKeys, 0, None) - } - key_wallet::AccountType::ProviderPlatformKeys => { - (FFIAccountKind::ProviderPlatformKeys, 0, None) - } - key_wallet::AccountType::DashpayReceivingFunds { - index, - user_identity_id, - friend_identity_id, - } => { - // Cannot convert DashPay accounts to FFI without losing identity ID information - panic!( - "Cannot convert AccountType::DashpayReceivingFunds (index={}, user_id={:?}, friend_id={:?}) \ - to FFI representation. The current FFI tuple format (FFIAccountKind, u32, Option) \ - cannot represent the two 32-byte identity IDs required by DashPay accounts. \ - This would result in silent data loss. A dedicated FFI API for DashPay accounts is needed.", - index, - &user_identity_id[..8], // Show first 8 bytes for debugging - &friend_identity_id[..8] - ); - } - key_wallet::AccountType::DashpayExternalAccount { - index, - user_identity_id, - friend_identity_id, - } => { - // Cannot convert DashPay accounts to FFI without losing identity ID information - panic!( - "Cannot convert AccountType::DashpayExternalAccount (index={}, user_id={:?}, friend_id={:?}) \ - to FFI representation. The current FFI tuple format (FFIAccountKind, u32, Option) \ - cannot represent the two 32-byte identity IDs required by DashPay accounts. \ - This would result in silent data loss. A dedicated FFI API for DashPay accounts is needed.", - index, - &user_identity_id[..8], // Show first 8 bytes for debugging - &friend_identity_id[..8] - ); - } - key_wallet::AccountType::PlatformPayment { - account, - key_class, - } => (FFIAccountKind::PlatformPayment, *account, Some(*key_class)), - } - } -} - -/// Address type enumeration -#[repr(C)] -#[derive(Debug, Clone, Copy)] -pub enum FFIAddressType { - P2PKH = 0, - P2SH = 1, - Unknown = 255, -} - -impl From for FFIAddressType { - fn from(t: key_wallet::AddressType) -> Self { - match t { - key_wallet::AddressType::P2pkh => FFIAddressType::P2PKH, - key_wallet::AddressType::P2sh => FFIAddressType::P2SH, - // SegWit and Taproot address types are not supported yet in Dash - key_wallet::AddressType::P2wpkh => FFIAddressType::Unknown, - key_wallet::AddressType::P2wsh => FFIAddressType::Unknown, - key_wallet::AddressType::P2tr => FFIAddressType::Unknown, - // Handle any future address types - _ => FFIAddressType::Unknown, - } - } -} - -impl From for key_wallet::AddressType { - fn from(t: FFIAddressType) -> Self { - match t { - FFIAddressType::P2PKH => key_wallet::AddressType::P2pkh, - FFIAddressType::P2SH => key_wallet::AddressType::P2sh, - FFIAddressType::Unknown => key_wallet::AddressType::P2pkh, // Default to P2PKH for unknown types - } - } -} - -/// FFI specification for a PlatformPayment account to create -/// -/// PlatformPayment accounts (DIP-17) use the derivation path: -/// `m/9'/coin_type'/17'/account'/key_class'/index` -#[repr(C)] -#[derive(Debug, Clone, Copy)] -pub struct FFIPlatformPaymentAccountSpec { - /// Account index (hardened) - the account' level in the derivation path - pub account: u32, - /// Key class (hardened) - defaults to 0', 1' is reserved for change-like segregation - pub key_class: u32, -} - -/// FFI Account Creation Option Type -#[repr(C)] -#[derive(Debug, Clone, Copy)] -pub enum FFIAccountCreationOptionType { - /// Create default accounts (BIP44 account 0, CoinJoin account 0, and special accounts) - Default = 0, - /// Create all specified accounts plus all special purpose accounts - AllAccounts = 1, - /// Create only BIP44 accounts (no CoinJoin or special accounts) - BIP44AccountsOnly = 2, - /// Create specific accounts with full control - SpecificAccounts = 3, - /// Create no accounts at all - NoAccounts = 4, -} - -/// FFI structure for wallet account creation options -/// This single struct represents all possible account creation configurations -#[repr(C)] -pub struct FFIWalletAccountCreationOptions { - /// The type of account creation option - pub option_type: FFIAccountCreationOptionType, - - /// Array of BIP44 account indices to create - pub bip44_indices: *const u32, - pub bip44_count: usize, - - /// Array of BIP32 account indices to create - pub bip32_indices: *const u32, - pub bip32_count: usize, - - /// Array of CoinJoin account indices to create - pub coinjoin_indices: *const u32, - pub coinjoin_count: usize, - - /// Array of identity top-up registration indices to create - pub topup_indices: *const u32, - pub topup_count: usize, - - /// Array of PlatformPayment account specs to create - pub platform_payment_specs: *const FFIPlatformPaymentAccountSpec, - pub platform_payment_count: usize, - - /// For SpecificAccounts: Additional special account types to create - /// (e.g., IdentityRegistration, ProviderKeys, etc.) - /// This is an array of FFIAccountKind values - pub special_account_types: *const FFIAccountKind, - pub special_account_types_count: usize, -} - -impl FFIWalletAccountCreationOptions { - /// Create default options - pub fn default_options() -> Self { - FFIWalletAccountCreationOptions { - option_type: FFIAccountCreationOptionType::Default, - bip44_indices: std::ptr::null(), - bip44_count: 0, - bip32_indices: std::ptr::null(), - bip32_count: 0, - coinjoin_indices: std::ptr::null(), - coinjoin_count: 0, - topup_indices: std::ptr::null(), - topup_count: 0, - platform_payment_specs: std::ptr::null(), - platform_payment_count: 0, - special_account_types: std::ptr::null(), - special_account_types_count: 0, - } - } - - /// Convert FFI options to Rust WalletAccountCreationOptions - /// - /// # Safety - /// - /// - If `account_indices` is not null, it must point to a valid array of at least `account_indices_count` elements - /// - The indices in the array must be valid u32 values - pub unsafe fn to_wallet_options( - &self, - ) -> key_wallet::wallet::initialization::WalletAccountCreationOptions { - use key_wallet::wallet::initialization::WalletAccountCreationOptions; - use std::collections::BTreeSet; - - match self.option_type { - FFIAccountCreationOptionType::Default => WalletAccountCreationOptions::Default, - FFIAccountCreationOptionType::NoAccounts => WalletAccountCreationOptions::None, - FFIAccountCreationOptionType::BIP44AccountsOnly => { - let mut bip44_set = BTreeSet::new(); - if !self.bip44_indices.is_null() && self.bip44_count > 0 { - let slice = std::slice::from_raw_parts(self.bip44_indices, self.bip44_count); - bip44_set.extend(slice.iter().copied()); - } else { - // Default to account 0 if no indices provided - bip44_set.insert(0); - } - WalletAccountCreationOptions::BIP44AccountsOnly(bip44_set) - } - FFIAccountCreationOptionType::AllAccounts => { - use key_wallet::wallet::initialization::PlatformPaymentAccountSpec; - - let mut bip44_set = BTreeSet::new(); - if !self.bip44_indices.is_null() && self.bip44_count > 0 { - let slice = std::slice::from_raw_parts(self.bip44_indices, self.bip44_count); - bip44_set.extend(slice.iter().copied()); - } - - let mut bip32_set = BTreeSet::new(); - if !self.bip32_indices.is_null() && self.bip32_count > 0 { - let slice = std::slice::from_raw_parts(self.bip32_indices, self.bip32_count); - bip32_set.extend(slice.iter().copied()); - } - - let mut coinjoin_set = BTreeSet::new(); - if !self.coinjoin_indices.is_null() && self.coinjoin_count > 0 { - let slice = - std::slice::from_raw_parts(self.coinjoin_indices, self.coinjoin_count); - coinjoin_set.extend(slice.iter().copied()); - } - - let mut topup_set = BTreeSet::new(); - if !self.topup_indices.is_null() && self.topup_count > 0 { - let slice = std::slice::from_raw_parts(self.topup_indices, self.topup_count); - topup_set.extend(slice.iter().copied()); - } - - let mut platform_payment_set = BTreeSet::new(); - if !self.platform_payment_specs.is_null() && self.platform_payment_count > 0 { - let slice = std::slice::from_raw_parts( - self.platform_payment_specs, - self.platform_payment_count, - ); - for spec in slice { - platform_payment_set.insert(PlatformPaymentAccountSpec { - account: spec.account, - key_class: spec.key_class, - }); - } - } - - WalletAccountCreationOptions::AllAccounts( - bip44_set, - bip32_set, - coinjoin_set, - topup_set, - platform_payment_set, - ) - } - FFIAccountCreationOptionType::SpecificAccounts => { - use key_wallet::wallet::initialization::PlatformPaymentAccountSpec; - - let mut bip44_set = BTreeSet::new(); - if !self.bip44_indices.is_null() && self.bip44_count > 0 { - let slice = std::slice::from_raw_parts(self.bip44_indices, self.bip44_count); - bip44_set.extend(slice.iter().copied()); - } - - let mut bip32_set = BTreeSet::new(); - if !self.bip32_indices.is_null() && self.bip32_count > 0 { - let slice = std::slice::from_raw_parts(self.bip32_indices, self.bip32_count); - bip32_set.extend(slice.iter().copied()); - } - - let mut coinjoin_set = BTreeSet::new(); - if !self.coinjoin_indices.is_null() && self.coinjoin_count > 0 { - let slice = - std::slice::from_raw_parts(self.coinjoin_indices, self.coinjoin_count); - coinjoin_set.extend(slice.iter().copied()); - } - - let mut topup_set = BTreeSet::new(); - if !self.topup_indices.is_null() && self.topup_count > 0 { - let slice = std::slice::from_raw_parts(self.topup_indices, self.topup_count); - topup_set.extend(slice.iter().copied()); - } - - let mut platform_payment_set = BTreeSet::new(); - if !self.platform_payment_specs.is_null() && self.platform_payment_count > 0 { - let slice = std::slice::from_raw_parts( - self.platform_payment_specs, - self.platform_payment_count, - ); - for spec in slice { - platform_payment_set.insert(PlatformPaymentAccountSpec { - account: spec.account, - key_class: spec.key_class, - }); - } - } - - // Convert special account types if provided - let special_accounts = if !self.special_account_types.is_null() - && self.special_account_types_count > 0 - { - let slice = std::slice::from_raw_parts( - self.special_account_types, - self.special_account_types_count, - ); - let mut accounts = Vec::new(); - for &ffi_type in slice { - accounts.push(ffi_type.to_account_type(0)); - } - Some(accounts) - } else { - None - }; - - WalletAccountCreationOptions::SpecificAccounts( - bip44_set, - bip32_set, - coinjoin_set, - topup_set, - platform_payment_set, - special_accounts, - ) - } - } - } -} - -/// FFI-compatible transaction context type -#[repr(C)] -#[derive(Debug, Clone, Copy)] -pub enum FFITransactionContextType { - /// Transaction is in the mempool (unconfirmed) - Mempool = 0, - /// Transaction is in the mempool with an InstantSend lock - InstantSend = 1, - /// Transaction is in a block at the given height - InBlock = 2, - /// Transaction is in a chain-locked block at the given height - InChainLockedBlock = 3, -} - -impl From for FFITransactionContextType { - fn from(ctx: TransactionContext) -> Self { - match ctx { - TransactionContext::Mempool => FFITransactionContextType::Mempool, - TransactionContext::InstantSend(_) => FFITransactionContextType::InstantSend, - TransactionContext::InBlock(_) => FFITransactionContextType::InBlock, - TransactionContext::InChainLockedBlock(_) => { - FFITransactionContextType::InChainLockedBlock - } - } - } -} - -/// FFI-compatible transaction context (type + optional block info + optional IS lock) -#[repr(C)] -#[derive(Debug)] -pub struct FFITransactionContext { - /// The context type - pub context_type: FFITransactionContextType, - /// Block info (zeroed for mempool/instant-send contexts) - pub block_info: FFIBlockInfo, - /// Consensus-serialized `InstantLock` bytes (null for non-IS contexts) - pub islock_data: *const u8, - /// Length of the `islock_data` buffer - pub islock_len: usize, -} - -impl FFITransactionContext { - /// Create a mempool context - pub fn mempool() -> Self { - Self { - context_type: FFITransactionContextType::Mempool, - block_info: FFIBlockInfo::empty(), - islock_data: std::ptr::null(), - islock_len: 0, - } - } - - /// Create an in-block context - pub fn in_block(block_info: FFIBlockInfo) -> Self { - Self { - context_type: FFITransactionContextType::InBlock, - block_info, - islock_data: std::ptr::null(), - islock_len: 0, - } - } - - /// Create a chain-locked block context - pub fn in_chain_locked_block(block_info: FFIBlockInfo) -> Self { - Self { - context_type: FFITransactionContextType::InChainLockedBlock, - block_info, - islock_data: std::ptr::null(), - islock_len: 0, - } - } - - /// Convert to the native `TransactionContext`. - /// - /// Returns `None` when block info is all-zeros for confirmed contexts. - pub fn to_transaction_context(&self) -> Option { - transaction_context_from_ffi( - self.context_type, - &self.block_info, - self.islock_data, - self.islock_len, - ) - } -} - -impl From for FFITransactionContext { - fn from(ctx: TransactionContext) -> Self { - let block_info = ctx - .block_info() - .map(|info| FFIBlockInfo::from(*info)) - .unwrap_or_else(FFIBlockInfo::empty); - - let (islock_data, islock_len) = if let TransactionContext::InstantSend(ref lock) = ctx { - let bytes = dashcore::consensus::serialize(lock).into_boxed_slice(); - let len = bytes.len(); - let ptr = Box::into_raw(bytes) as *const u8; - (ptr, len) - } else { - (std::ptr::null(), 0) - }; - - let context_type = FFITransactionContextType::from(ctx); - Self { - context_type, - block_info, - islock_data, - islock_len, - } - } -} - -impl Drop for FFITransactionContext { - fn drop(&mut self) { - if !self.islock_data.is_null() && self.islock_len > 0 { - let slice_ptr = - std::ptr::slice_from_raw_parts_mut(self.islock_data as *mut u8, self.islock_len); - let _ = unsafe { Box::from_raw(slice_ptr) }; - - self.islock_data = std::ptr::null(); - self.islock_len = 0; - } - } -} - -/// FFI-compatible transaction direction -#[repr(C)] -#[derive(Debug, Clone, Copy)] -pub enum FFITransactionDirection { - Incoming = 0, - Outgoing = 1, - Internal = 2, - CoinJoin = 3, -} - -impl From for FFITransactionDirection { - fn from(dir: TransactionDirection) -> Self { - match dir { - TransactionDirection::Incoming => Self::Incoming, - TransactionDirection::Outgoing => Self::Outgoing, - TransactionDirection::Internal => Self::Internal, - TransactionDirection::CoinJoin => Self::CoinJoin, - } - } -} - -/// FFI-compatible transaction type classification -#[repr(C)] -#[derive(Debug, Clone, Copy)] -pub enum FFITransactionType { - Standard = 0, - CoinJoin = 1, - ProviderRegistration = 2, - ProviderUpdateRegistrar = 3, - ProviderUpdateService = 4, - ProviderUpdateRevocation = 5, - AssetLock = 6, - AssetUnlock = 7, - Coinbase = 8, - Ignored = 9, -} - -impl From for FFITransactionType { - fn from(tt: TransactionType) -> Self { - match tt { - TransactionType::Standard => Self::Standard, - TransactionType::CoinJoin => Self::CoinJoin, - TransactionType::ProviderRegistration => Self::ProviderRegistration, - TransactionType::ProviderUpdateRegistrar => Self::ProviderUpdateRegistrar, - TransactionType::ProviderUpdateService => Self::ProviderUpdateService, - TransactionType::ProviderUpdateRevocation => Self::ProviderUpdateRevocation, - TransactionType::AssetLock => Self::AssetLock, - TransactionType::AssetUnlock => Self::AssetUnlock, - TransactionType::Coinbase => Self::Coinbase, - TransactionType::Ignored => Self::Ignored, - } - } -} - -/// FFI-compatible output role -#[repr(C)] -#[derive(Debug, Clone, Copy)] -pub enum FFIOutputRole { - Received = 0, - Change = 1, - Sent = 2, - Unspendable = 3, -} - -impl From for FFIOutputRole { - fn from(role: OutputRole) -> Self { - match role { - OutputRole::Received => Self::Received, - OutputRole::Change => Self::Change, - OutputRole::Sent => Self::Sent, - OutputRole::Unspendable => Self::Unspendable, - } - } -} - -/// FFI-compatible input detail -#[repr(C)] -pub struct FFIInputDetail { - pub index: u32, - pub value: u64, - pub address: *mut std::os::raw::c_char, -} - -impl From<&InputDetail> for FFIInputDetail { - fn from(d: &InputDetail) -> Self { - FFIInputDetail { - index: d.index, - value: d.value, - address: std::ffi::CString::new(d.address.to_string()).unwrap_or_default().into_raw(), - } - } -} - -impl Drop for FFIInputDetail { - fn drop(&mut self) { - if !self.address.is_null() { - let _ = unsafe { std::ffi::CString::from_raw(self.address) }; - - self.address = std::ptr::null_mut(); - } - } -} - -/// FFI-compatible output detail -#[repr(C)] -pub struct FFIOutputDetail { - pub index: u32, - pub role: FFIOutputRole, - pub value: u64, - pub address: *mut c_char, -} - -impl From<&OutputDetail> for FFIOutputDetail { - fn from(d: &OutputDetail) -> Self { - FFIOutputDetail { - index: d.index, - role: FFIOutputRole::from(d.role), - value: d.value, - address: match &d.address { - Some(addr) => { - std::ffi::CString::new(addr.to_string()).unwrap_or_default().into_raw() - } - None => std::ptr::null_mut(), - }, - } - } -} - -impl Drop for FFIOutputDetail { - fn drop(&mut self) { - if !self.address.is_null() { - let _ = unsafe { std::ffi::CString::from_raw(self.address) }; - - self.address = std::ptr::null_mut(); - } - } -} - -#[cfg(test)] -mod tests { - use std::ptr; - - use dashcore::consensus::serialize; - use dashcore::ephemerealdata::instant_lock::InstantLock; - use key_wallet::transaction_checking::BlockInfo; - - use super::*; - - fn valid_block_info() -> FFIBlockInfo { - FFIBlockInfo { - height: 1000, - block_hash: [0xab; 32], - timestamp: 1700000000, - } - } - - #[test] - #[should_panic(expected = "DashpayReceivingFunds cannot be converted to AccountType")] - fn test_dashpay_receiving_funds_to_account_type_panics() { - // This should panic because we cannot construct a DashPay account without identity IDs - let _ = FFIAccountKind::DashpayReceivingFunds.to_account_type(0); - } - - #[test] - #[should_panic(expected = "DashpayExternalAccount cannot be converted to AccountType")] - fn test_dashpay_external_account_to_account_type_panics() { - // This should panic because we cannot construct a DashPay account without identity IDs - let _ = FFIAccountKind::DashpayExternalAccount.to_account_type(0); - } - - #[test] - #[should_panic(expected = "PlatformPayment cannot be converted to AccountType")] - fn test_platform_payment_to_account_type_panics() { - // This should panic because we cannot construct a Platform Payment account without indices - let _ = FFIAccountKind::PlatformPayment.to_account_type(0); - } - - #[test] - #[should_panic(expected = "Cannot convert AccountType::DashpayReceivingFunds")] - fn test_dashpay_receiving_funds_from_account_type_panics() { - // This should panic because we cannot represent identity IDs in the FFI tuple - let account_type = key_wallet::AccountType::DashpayReceivingFunds { - index: 0, - user_identity_id: [1u8; 32], - friend_identity_id: [2u8; 32], - }; - let _ = FFIAccountKind::from_account_type(&account_type); - } - - #[test] - #[should_panic(expected = "Cannot convert AccountType::DashpayExternalAccount")] - fn test_dashpay_external_account_from_account_type_panics() { - // This should panic because we cannot represent identity IDs in the FFI tuple - let account_type = key_wallet::AccountType::DashpayExternalAccount { - index: 0, - user_identity_id: [1u8; 32], - friend_identity_id: [2u8; 32], - }; - let _ = FFIAccountKind::from_account_type(&account_type); - } - - #[test] - fn test_non_dashpay_conversions_work() { - // Verify that non-DashPay types still convert correctly - let standard_bip44 = FFIAccountKind::StandardBIP44.to_account_type(5); - assert!(matches!( - standard_bip44, - key_wallet::AccountType::Standard { - index: 5, - .. - } - )); - - let coinjoin = FFIAccountKind::CoinJoin.to_account_type(3); - assert!(matches!( - coinjoin, - key_wallet::AccountType::CoinJoin { - index: 3 - } - )); - - // Test reverse conversion - let (ffi_type, index, _) = FFIAccountKind::from_account_type(&standard_bip44); - assert_eq!(ffi_type, FFIAccountKind::StandardBIP44); - assert_eq!(index, 5); - } - - #[test] - fn transaction_context_from_ffi_mempool_with_empty_block_info() { - let result = transaction_context_from_ffi( - FFITransactionContextType::Mempool, - &FFIBlockInfo::empty(), - ptr::null(), - 0, - ); - assert!(matches!(result, Some(TransactionContext::Mempool))); - } - - #[test] - fn transaction_context_from_ffi_instant_send_with_null_islock() { - let result = transaction_context_from_ffi( - FFITransactionContextType::InstantSend, - &FFIBlockInfo::empty(), - ptr::null(), - 0, - ); - assert!(result.is_none()); - } - - #[test] - fn transaction_context_from_ffi_instant_send_with_valid_islock() { - let islock = InstantLock::default(); - let bytes = serialize(&islock); - let result = transaction_context_from_ffi( - FFITransactionContextType::InstantSend, - &FFIBlockInfo::empty(), - bytes.as_ptr(), - bytes.len(), - ); - assert!(matches!(result, Some(TransactionContext::InstantSend(_)))); - } - - #[test] - fn transaction_context_from_ffi_in_block_with_empty_block_info() { - let result = transaction_context_from_ffi( - FFITransactionContextType::InBlock, - &FFIBlockInfo::empty(), - ptr::null(), - 0, - ); - assert!(result.is_none()); - } - - #[test] - fn transaction_context_from_ffi_in_chain_locked_block_with_empty_block_info() { - let result = transaction_context_from_ffi( - FFITransactionContextType::InChainLockedBlock, - &FFIBlockInfo::empty(), - ptr::null(), - 0, - ); - assert!(result.is_none()); - } - - #[test] - fn transaction_context_from_ffi_in_block_with_valid_block_info() { - let block_info = valid_block_info(); - let result = transaction_context_from_ffi( - FFITransactionContextType::InBlock, - &block_info, - ptr::null(), - 0, - ); - let ctx = result.expect("should return Some for InBlock with valid block info"); - assert!(matches!(ctx, TransactionContext::InBlock(info) if info.height() == 1000)); - } - - #[test] - fn transaction_context_from_ffi_in_chain_locked_block_with_valid_block_info() { - let block_info = valid_block_info(); - let result = transaction_context_from_ffi( - FFITransactionContextType::InChainLockedBlock, - &block_info, - ptr::null(), - 0, - ); - let ctx = result.expect("should return Some for InChainLockedBlock with valid block info"); - assert!( - matches!(ctx, TransactionContext::InChainLockedBlock(info) if info.height() == 1000) - ); - } - - #[test] - fn test_ffi_transaction_direction_from() { - assert!(matches!( - FFITransactionDirection::from(TransactionDirection::Incoming), - FFITransactionDirection::Incoming - )); - assert!(matches!( - FFITransactionDirection::from(TransactionDirection::Outgoing), - FFITransactionDirection::Outgoing - )); - assert!(matches!( - FFITransactionDirection::from(TransactionDirection::Internal), - FFITransactionDirection::Internal - )); - assert!(matches!( - FFITransactionDirection::from(TransactionDirection::CoinJoin), - FFITransactionDirection::CoinJoin - )); - } - - #[test] - fn test_ffi_transaction_type_from() { - assert!(matches!( - FFITransactionType::from(TransactionType::Standard), - FFITransactionType::Standard - )); - assert!(matches!( - FFITransactionType::from(TransactionType::CoinJoin), - FFITransactionType::CoinJoin - )); - assert!(matches!( - FFITransactionType::from(TransactionType::ProviderRegistration), - FFITransactionType::ProviderRegistration - )); - assert!(matches!( - FFITransactionType::from(TransactionType::AssetLock), - FFITransactionType::AssetLock - )); - assert!(matches!( - FFITransactionType::from(TransactionType::Coinbase), - FFITransactionType::Coinbase - )); - assert!(matches!( - FFITransactionType::from(TransactionType::Ignored), - FFITransactionType::Ignored - )); - } - - #[test] - fn test_ffi_transaction_context_from_in_block() { - let hash = dashcore::BlockHash::from_byte_array([0xab; 32]); - let block_info = BlockInfo::new(1000, hash, 1700000000); - let ctx = FFITransactionContext::from(TransactionContext::InBlock(block_info)); - assert!(matches!(ctx.context_type, FFITransactionContextType::InBlock)); - assert_eq!(ctx.block_info.height, 1000); - assert_eq!(ctx.block_info.block_hash, [0xab; 32]); - assert_eq!(ctx.block_info.timestamp, 1700000000); - } - - #[test] - fn test_ffi_transaction_context_from_mempool() { - let ctx = FFITransactionContext::from(TransactionContext::Mempool); - assert!(matches!(ctx.context_type, FFITransactionContextType::Mempool)); - assert_eq!(ctx.block_info.block_hash, [0u8; 32]); - } - - #[test] - fn test_ffi_output_role_from() { - assert!(matches!(FFIOutputRole::from(OutputRole::Received), FFIOutputRole::Received)); - assert!(matches!(FFIOutputRole::from(OutputRole::Change), FFIOutputRole::Change)); - assert!(matches!(FFIOutputRole::from(OutputRole::Sent), FFIOutputRole::Sent)); - assert!(matches!(FFIOutputRole::from(OutputRole::Unspendable), FFIOutputRole::Unspendable)); - } -} diff --git a/key-wallet-ffi/src/utils.rs b/key-wallet-ffi/src/utils.rs deleted file mode 100644 index 42a535f79..000000000 --- a/key-wallet-ffi/src/utils.rs +++ /dev/null @@ -1,47 +0,0 @@ -//! Utility functions for FFI - -#[cfg(test)] -#[path = "utils_tests.rs"] -mod util_tests; - -use std::ffi::CString; -use std::os::raw::c_char; - -/// Free a string -/// -/// # Safety -/// -/// - `s` must be a valid pointer created by C string creation functions or null -/// - After calling this function, the pointer becomes invalid -#[no_mangle] -pub unsafe extern "C" fn string_free(s: *mut c_char) { - if !s.is_null() { - unsafe { - let _ = CString::from_raw(s); - } - } -} - -/// Helper function to convert Rust string to C string -pub fn rust_string_to_c(s: String) -> *mut c_char { - match CString::new(s) { - Ok(c_str) => c_str.into_raw(), - Err(_) => std::ptr::null_mut(), - } -} - -/// Helper function to convert C string to Rust string -/// -/// # Safety -/// -/// - `s` must be a valid null-terminated C string or null -/// - The string must remain valid for the duration of this function call -pub unsafe fn c_string_to_rust(s: *const c_char) -> Result { - use std::ffi::CStr; - - if s.is_null() { - return Ok(String::new()); - } - - CStr::from_ptr(s).to_str().map(|s| s.to_string()) -} diff --git a/key-wallet-ffi/src/utils_tests.rs b/key-wallet-ffi/src/utils_tests.rs deleted file mode 100644 index 74d4a1f10..000000000 --- a/key-wallet-ffi/src/utils_tests.rs +++ /dev/null @@ -1,66 +0,0 @@ -//! Unit tests for utils FFI module - -#[cfg(test)] -#[allow(clippy::module_inception)] -mod util_tests { - use crate::utils; - use std::ffi::CString; - use std::ptr; - - #[test] - fn test_string_utils() { - // Test string allocation and deallocation - let test_str = "Hello, FFI!"; - let c_string = CString::new(test_str).unwrap(); - let raw_ptr = c_string.into_raw(); - - // Verify the string is valid - let retrieved = unsafe { std::ffi::CStr::from_ptr(raw_ptr).to_str().unwrap() }; - assert_eq!(retrieved, test_str); - - // Free the string - unsafe { - utils::string_free(raw_ptr); - } - } - - #[test] - fn test_string_free() { - // Test freeing null pointer (should not crash) - unsafe { - utils::string_free(ptr::null_mut()); - } - - // Test freeing valid string - let c_string = CString::new("test").unwrap(); - let raw_ptr = c_string.into_raw(); - unsafe { - utils::string_free(raw_ptr); - } - } - - #[test] - fn test_c_string_to_rust() { - // Test converting C string to Rust string - let test_str = "Test String"; - let c_string = CString::new(test_str).unwrap(); - - let rust_str = unsafe { std::ffi::CStr::from_ptr(c_string.as_ptr()).to_str().unwrap() }; - - assert_eq!(rust_str, test_str); - } - - #[test] - fn test_version() { - let version = crate::key_wallet_ffi_version(); - assert!(!version.is_null()); - - let version_str = unsafe { std::ffi::CStr::from_ptr(version).to_str().unwrap() }; - - // Version should match Cargo.toml version - assert!(!version_str.is_empty()); - - // Note: We don't free the version string as it's likely a static - // or should be handled by the library's own cleanup - } -} diff --git a/key-wallet-ffi/src/utxo.rs b/key-wallet-ffi/src/utxo.rs deleted file mode 100644 index 3ecb1585b..000000000 --- a/key-wallet-ffi/src/utxo.rs +++ /dev/null @@ -1,193 +0,0 @@ -//! UTXO management - -use crate::error::FFIError; -use crate::managed_wallet::FFIManagedWalletInfo; -use crate::{check_ptr, deref_ptr}; -use key_wallet::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface; -use std::ffi::CString; -use std::os::raw::c_char; -use std::ptr; - -/// UTXO structure for FFI -#[repr(C)] -pub struct FFIUTXO { - pub txid: [u8; 32], - pub vout: u32, - pub amount: u64, - pub address: *mut c_char, - pub script_pubkey: *mut u8, - pub script_len: usize, - pub height: u32, - pub confirmations: u32, -} - -impl FFIUTXO { - /// Create a new FFIUTXO - pub fn new( - txid: [u8; 32], - vout: u32, - amount: u64, - address: String, - script: Vec, - height: u32, - confirmations: u32, - ) -> Self { - let address_cstr = CString::new(address).unwrap_or_default(); - let script_len = script.len(); - let script_ptr = if script.is_empty() { - ptr::null_mut() - } else { - let script_box = script.into_boxed_slice(); - Box::into_raw(script_box) as *mut u8 - }; - - FFIUTXO { - txid, - vout, - amount, - address: address_cstr.into_raw(), - script_pubkey: script_ptr, - script_len, - height, - confirmations, - } - } - - /// Free the FFIUTXO's allocated memory - /// - /// # Safety - /// - /// - `self.address` must be a valid pointer created by CString or null - /// - `self.script_pubkey` must be a valid pointer to a Box allocation or null - /// - After calling this function, the pointers become invalid - pub unsafe fn free(&mut self) { - if !self.address.is_null() { - let _ = CString::from_raw(self.address); - self.address = ptr::null_mut(); - } - if !self.script_pubkey.is_null() && self.script_len > 0 { - // Reconstruct the boxed slice with DST pointer - let _ = - Box::from_raw(ptr::slice_from_raw_parts_mut(self.script_pubkey, self.script_len)); - self.script_pubkey = ptr::null_mut(); - self.script_len = 0; - } - } -} - -/// Get all UTXOs from managed wallet info -/// -/// # Safety -/// -/// - `managed_info` must be a valid pointer to an FFIManagedWalletInfo instance -/// - `utxos_out` must be a valid pointer to store the UTXO array pointer -/// - `count_out` must be a valid pointer to store the UTXO count -/// - `error` must be a valid pointer to an FFIError structure -/// - The caller must ensure all pointers remain valid for the duration of this call -/// - The returned UTXO array must be freed with `utxo_array_free` when no longer needed -#[no_mangle] -pub unsafe extern "C" fn managed_wallet_get_utxos( - managed_info: *const FFIManagedWalletInfo, - utxos_out: *mut *mut FFIUTXO, - count_out: *mut usize, - error: *mut FFIError, -) -> bool { - let managed_info = deref_ptr!(managed_info, error); - check_ptr!(utxos_out, error); - check_ptr!(count_out, error); - - // Get UTXOs from the managed wallet info - let utxos = managed_info.inner().utxos(); - - if utxos.is_empty() { - *count_out = 0; - *utxos_out = ptr::null_mut(); - } else { - // Convert UTXOs to FFI format - let mut ffi_utxos = Vec::with_capacity(utxos.len()); - - for utxo in utxos { - // Convert txid to byte array - let mut txid_bytes = [0u8; 32]; - txid_bytes.copy_from_slice(&utxo.outpoint.txid[..]); - - // Convert address to string - let address_str = utxo.address.to_string(); - - // Get script bytes - let script_bytes = utxo.txout.script_pubkey.as_bytes().to_vec(); - - let current_height = managed_info.inner().last_processed_height(); - let confirmations = utxo.confirmations(current_height); - - let ffi_utxo = FFIUTXO::new( - txid_bytes, - utxo.outpoint.vout, - utxo.value(), - address_str, - script_bytes, - utxo.height, - confirmations, - ); - - ffi_utxos.push(ffi_utxo); - } - - *count_out = ffi_utxos.len(); - // Convert Vec to boxed slice for consistent memory layout - let boxed_utxos = ffi_utxos.into_boxed_slice(); - let ptr = Box::into_raw(boxed_utxos) as *mut FFIUTXO; - *utxos_out = ptr; - } - true -} - -/// Get all UTXOs (deprecated - use managed_wallet_get_utxos instead) -/// -/// # Safety -/// -/// This function is deprecated and returns an empty list. -/// Use `managed_wallet_get_utxos` with a ManagedWalletInfo instead. -#[no_mangle] -#[deprecated(note = "Use managed_wallet_get_utxos with ManagedWalletInfo instead")] -pub unsafe extern "C" fn wallet_get_utxos( - _wallet: *const crate::types::FFIWallet, - utxos_out: *mut *mut FFIUTXO, - count_out: *mut usize, - error: *mut FFIError, -) -> bool { - check_ptr!(utxos_out, error); - check_ptr!(count_out, error); - - *count_out = 0; - *utxos_out = ptr::null_mut(); - true -} - -/// Free UTXO array -/// -/// # Safety -/// -/// - `utxos` must be a valid pointer to an array of FFIUTXO structs allocated by this library -/// - `count` must match the number of UTXOs in the array -/// - The pointer must not be used after calling this function -/// - This function must only be called once per array -#[no_mangle] -pub unsafe extern "C" fn utxo_array_free(utxos: *mut FFIUTXO, count: usize) { - if !utxos.is_null() && count > 0 { - // Create a slice from the raw pointer - let slice = std::slice::from_raw_parts_mut(utxos, count); - - // Free each UTXO's allocated memory (address and script) - for utxo in slice { - utxo.free(); - } - - // Free the array itself by reconstructing the boxed slice with DST pointer - let _ = Box::from_raw(ptr::slice_from_raw_parts_mut(utxos, count)); - } -} - -#[cfg(test)] -#[path = "utxo_tests.rs"] -mod tests; diff --git a/key-wallet-ffi/src/utxo_tests.rs b/key-wallet-ffi/src/utxo_tests.rs deleted file mode 100644 index 74c0dddca..000000000 --- a/key-wallet-ffi/src/utxo_tests.rs +++ /dev/null @@ -1,570 +0,0 @@ -#[cfg(test)] -mod utxo_tests { - use super::super::*; - use crate::error::{FFIError, FFIErrorCode}; - use key_wallet::managed_account::managed_account_type::ManagedAccountType; - use key_wallet::Utxo; - use std::ffi::CStr; - use std::ptr; - - #[test] - fn test_ffi_utxo_new() { - let txid = [1u8; 32]; - let vout = 0; - let amount = 100000; - let address = "yXdxAYfK7KGx7gNpVHUfRsQMNpMj5cAadG".to_string(); - let script = vec![0x76, 0xa9, 0x14]; // Sample script - let height = 12345; - let confirmations = 10; - - let utxo = FFIUTXO::new( - txid, - vout, - amount, - address.clone(), - script.clone(), - height, - confirmations, - ); - - assert_eq!(utxo.txid, txid); - assert_eq!(utxo.vout, vout); - assert_eq!(utxo.amount, amount); - assert!(!utxo.address.is_null()); - assert!(!utxo.script_pubkey.is_null()); - assert_eq!(utxo.script_len, script.len()); - assert_eq!(utxo.height, height); - assert_eq!(utxo.confirmations, confirmations); - - // Verify address - let addr_str = unsafe { CStr::from_ptr(utxo.address).to_str().unwrap() }; - assert_eq!(addr_str, address); - - // Clean up - unsafe { - let mut utxo = utxo; - utxo.free(); - } - } - - #[test] - fn test_ffi_utxo_new_empty_script() { - let txid = [2u8; 32]; - let utxo = FFIUTXO::new( - txid, - 1, - 50000, - "yYNrYTYsV8xCTMAz5wXmKzn7eqUe5p5V8V".to_string(), - vec![], - 100, - 5, - ); - - assert_eq!(utxo.txid, txid); - assert!(utxo.script_pubkey.is_null()); - assert_eq!(utxo.script_len, 0); - - // Clean up - unsafe { - let mut utxo = utxo; - utxo.free(); - } - } - - #[test] - fn test_deprecated_wallet_get_utxos() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - let mut utxos_out: *mut FFIUTXO = ptr::null_mut(); - let mut count_out: usize = 0; - - // The deprecated function should always return an empty list - let success = unsafe { - #[allow(deprecated)] - wallet_get_utxos(ptr::null(), &mut utxos_out, &mut count_out, error) - }; - - assert!(success); - assert_eq!(count_out, 0); - assert!(utxos_out.is_null()); - } - - #[test] - fn test_managed_wallet_get_utxos_null() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - let mut utxos_out: *mut FFIUTXO = ptr::null_mut(); - let mut count_out: usize = 0; - - // Test with null managed_info - let result = - unsafe { managed_wallet_get_utxos(ptr::null(), &mut utxos_out, &mut count_out, error) }; - assert!(!result); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); - } - - #[test] - fn test_managed_wallet_get_utxos_empty() { - use crate::managed_wallet::FFIManagedWalletInfo; - use key_wallet::wallet::managed_wallet_info::ManagedWalletInfo; - use key_wallet::Network; - - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - let mut utxos_out: *mut FFIUTXO = ptr::null_mut(); - let mut count_out: usize = 0; - - // Create an empty managed wallet info heap-allocated like C would do - let managed_info = ManagedWalletInfo::new(Network::Testnet, [0u8; 32]); - let ffi_managed_info = Box::into_raw(Box::new(FFIManagedWalletInfo::new(managed_info))); - - let result = unsafe { - managed_wallet_get_utxos(&*ffi_managed_info, &mut utxos_out, &mut count_out, error) - }; - - assert!(result); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); - assert_eq!(count_out, 0); - assert!(utxos_out.is_null()); - - unsafe { crate::managed_wallet::managed_wallet_free(ffi_managed_info) }; - } - - // Note: There's no individual utxo_free function, only utxo_array_free - - #[test] - fn test_utxo_array_free() { - // Create some test UTXOs in the same format as managed_wallet_get_utxos returns - let mut utxos = Vec::new(); - for i in 0..3 { - let utxo = FFIUTXO::new( - [i as u8; 32], - i as u32, - (i as u64 + 1) * 10000, - format!("address_{}", i), - vec![0x76, 0xa9, i as u8], - i as u32 * 100, - i as u32, - ); - utxos.push(utxo); - } - - // Convert to boxed slice and get raw pointer (same as in managed_wallet_get_utxos) - let count = utxos.len(); - let mut boxed_utxos = utxos.into_boxed_slice(); - let utxos_ptr = boxed_utxos.as_mut_ptr(); - std::mem::forget(boxed_utxos); - - // Free the UTXOs - unsafe { - utxo_array_free(utxos_ptr, count); - } - } - - #[test] - fn test_utxo_array_free_null() { - // Should handle null gracefully - unsafe { - utxo_array_free(ptr::null_mut(), 0); - } - } - - #[test] - fn test_managed_wallet_get_utxos_with_data() { - use crate::managed_wallet::FFIManagedWalletInfo; - use dashcore::blockdata::script::ScriptBuf; - use dashcore::{Address, OutPoint, TxOut, Txid}; - use key_wallet::account::account_type::StandardAccountType; - use key_wallet::managed_account::ManagedCoreFundsAccount; - use key_wallet::utxo::Utxo; - use key_wallet::wallet::managed_wallet_info::ManagedWalletInfo; - use key_wallet::Network; - - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - let mut utxos_out: *mut FFIUTXO = ptr::null_mut(); - let mut count_out: usize = 0; - - // Create a managed wallet info with some UTXOs - let mut managed_info = ManagedWalletInfo::new(Network::Testnet, [1u8; 32]); - - // Create a BIP44 account with UTXOs - let mut bip44_account = ManagedCoreFundsAccount::new( - ManagedAccountType::Standard { - index: 0, - standard_account_type: StandardAccountType::BIP44Account, - external_addresses: - key_wallet::managed_account::address_pool::AddressPoolBuilder::default() - .base_path(key_wallet::DerivationPath::from(vec![])) - .build() - .unwrap(), - internal_addresses: - key_wallet::managed_account::address_pool::AddressPoolBuilder::default() - .base_path(key_wallet::DerivationPath::from(vec![])) - .build() - .unwrap(), - }, - Network::Testnet, - ); - - // Add multiple UTXOs - for i in 0..3 { - let mut txid_bytes = [0u8; 32]; - txid_bytes[0] = i as u8; - let outpoint = OutPoint { - txid: Txid::from(txid_bytes), - vout: i as u32, - }; - let txout = TxOut { - value: (i as u64 + 1) * 50000, - script_pubkey: ScriptBuf::from(vec![]), - }; - // Create a dummy P2PKH address - let dummy_pubkey_hash = dashcore::PubkeyHash::from([0u8; 20]); - let script = ScriptBuf::new_p2pkh(&dummy_pubkey_hash); - let address = Address::from_script(&script, Network::Testnet).unwrap(); - let mut utxo = Utxo::new(outpoint, txout, address, 100 + i as u32, false); - utxo.is_confirmed = true; - - bip44_account.utxos.insert(outpoint, utxo); - } - - managed_info.accounts.insert(bip44_account).unwrap(); - - let ffi_managed_info = Box::into_raw(Box::new(FFIManagedWalletInfo::new(managed_info))); - unsafe { (*ffi_managed_info).inner_mut() }.update_last_processed_height(300); - let result = unsafe { - managed_wallet_get_utxos(&*ffi_managed_info, &mut utxos_out, &mut count_out, error) - }; - - assert!(result); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); - assert_eq!(count_out, 3); - assert!(!utxos_out.is_null()); - - // Verify UTXO data - unsafe { - let utxos = std::slice::from_raw_parts(utxos_out, count_out); - - // Check first UTXO - assert_eq!(utxos[0].txid[0], 0); - assert_eq!(utxos[0].vout, 0); - assert_eq!(utxos[0].amount, 50000); - assert_eq!(utxos[0].height, 100); - assert_eq!(utxos[0].confirmations, 201); - - // Check second UTXO - assert_eq!(utxos[1].txid[0], 1); - assert_eq!(utxos[1].vout, 1); - assert_eq!(utxos[1].amount, 100000); - assert_eq!(utxos[1].height, 101); - assert_eq!(utxos[1].confirmations, 200); - - // Check third UTXO - assert_eq!(utxos[2].txid[0], 2); - assert_eq!(utxos[2].vout, 2); - assert_eq!(utxos[2].amount, 150000); - assert_eq!(utxos[2].height, 102); - assert_eq!(utxos[2].confirmations, 199); - } - - // Clean up - unsafe { - utxo_array_free(utxos_out, count_out); - crate::managed_wallet::managed_wallet_free(ffi_managed_info); - } - } - - #[test] - fn test_managed_wallet_get_utxos_multiple_accounts() { - use crate::managed_wallet::FFIManagedWalletInfo; - use key_wallet::account::account_type::StandardAccountType; - use key_wallet::managed_account::ManagedCoreFundsAccount; - use key_wallet::wallet::managed_wallet_info::ManagedWalletInfo; - use key_wallet::Network; - - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - let mut utxos_out: *mut FFIUTXO = ptr::null_mut(); - let mut count_out: usize = 0; - - let mut managed_info = ManagedWalletInfo::new(Network::Testnet, [2u8; 32]); - - // Create BIP44 account with 2 UTXOs - let mut bip44_account = ManagedCoreFundsAccount::new( - ManagedAccountType::Standard { - index: 0, - standard_account_type: StandardAccountType::BIP44Account, - external_addresses: - key_wallet::managed_account::address_pool::AddressPoolBuilder::default() - .base_path(key_wallet::DerivationPath::from(vec![])) - .build() - .unwrap(), - internal_addresses: - key_wallet::managed_account::address_pool::AddressPoolBuilder::default() - .base_path(key_wallet::DerivationPath::from(vec![])) - .build() - .unwrap(), - }, - Network::Testnet, - ); - - let utxos = Utxo::dummy_batch(0..2, 10000, 100, false, false); - for utxo in utxos { - bip44_account.utxos.insert(utxo.outpoint, utxo); - } - managed_info.accounts.insert(bip44_account).unwrap(); - - // Create BIP32 account with 1 UTXO - let mut bip32_account = ManagedCoreFundsAccount::new( - ManagedAccountType::Standard { - index: 0, - standard_account_type: StandardAccountType::BIP32Account, - external_addresses: - key_wallet::managed_account::address_pool::AddressPoolBuilder::default() - .base_path(key_wallet::DerivationPath::from(vec![])) - .build() - .unwrap(), - internal_addresses: - key_wallet::managed_account::address_pool::AddressPoolBuilder::default() - .base_path(key_wallet::DerivationPath::from(vec![])) - .build() - .unwrap(), - }, - Network::Testnet, - ); - - let utxos = Utxo::dummy_batch(10..11, 20000, 200, false, false); - for utxo in utxos { - bip32_account.utxos.insert(utxo.outpoint, utxo); - } - managed_info.accounts.insert(bip32_account).unwrap(); - - // Create CoinJoin account with 2 UTXOs - let mut coinjoin_account = ManagedCoreFundsAccount::new( - ManagedAccountType::CoinJoin { - index: 0, - addresses: key_wallet::managed_account::address_pool::AddressPoolBuilder::default() - .base_path(key_wallet::DerivationPath::from(vec![])) - .build() - .unwrap(), - }, - Network::Testnet, - ); - - let utxos = Utxo::dummy_batch(20..22, 30000, 300, false, false); - for utxo in utxos { - coinjoin_account.utxos.insert(utxo.outpoint, utxo); - } - managed_info.accounts.insert(coinjoin_account).unwrap(); - - let ffi_managed_info = Box::into_raw(Box::new(FFIManagedWalletInfo::new(managed_info))); - - let result = unsafe { - managed_wallet_get_utxos(&*ffi_managed_info, &mut utxos_out, &mut count_out, error) - }; - - assert!(result); - assert_eq!(count_out, 5); // 2 from BIP44, 1 from BIP32, 2 from CoinJoin - assert!(!utxos_out.is_null()); - - // Clean up - unsafe { - utxo_array_free(utxos_out, count_out); - crate::managed_wallet::managed_wallet_free(ffi_managed_info); - } - } - - #[test] - fn test_managed_wallet_get_utxos() { - use crate::managed_wallet::FFIManagedWalletInfo; - use key_wallet::account::account_type::StandardAccountType; - use key_wallet::managed_account::ManagedCoreFundsAccount; - use key_wallet::wallet::managed_wallet_info::ManagedWalletInfo; - use key_wallet::Network; - - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - let mut utxos_out: *mut FFIUTXO = ptr::null_mut(); - let mut count_out: usize = 0; - - // Create managed wallet info for testnet - let mut managed_info = ManagedWalletInfo::new(Network::Testnet, [3u8; 32]); - - // Add a UTXO to Testnet account - let mut testnet_account = ManagedCoreFundsAccount::new( - ManagedAccountType::Standard { - index: 0, - standard_account_type: StandardAccountType::BIP44Account, - external_addresses: - key_wallet::managed_account::address_pool::AddressPoolBuilder::default() - .base_path(key_wallet::DerivationPath::from(vec![])) - .build() - .unwrap(), - internal_addresses: - key_wallet::managed_account::address_pool::AddressPoolBuilder::default() - .base_path(key_wallet::DerivationPath::from(vec![])) - .build() - .unwrap(), - }, - Network::Testnet, - ); - - let utxos = Utxo::dummy_batch(1..2, 10000, 100, false, false); - for utxo in utxos { - testnet_account.utxos.insert(utxo.outpoint, utxo); - } - managed_info.accounts.insert(testnet_account).unwrap(); - - let ffi_managed_info = Box::into_raw(Box::new(FFIManagedWalletInfo::new(managed_info))); - - // Get UTXOs - let result = unsafe { - managed_wallet_get_utxos(&*ffi_managed_info, &mut utxos_out, &mut count_out, error) - }; - assert!(result); - assert_eq!(count_out, 1); - unsafe { - utxo_array_free(utxos_out, count_out); - crate::managed_wallet::managed_wallet_free(ffi_managed_info); - } - } - - #[test] - fn test_ffi_utxo_with_large_script() { - let txid = [0xAAu8; 32]; - let vout = 42; - let amount = 1000000; - let address = "yXdxAYfK7KGx7gNpVHUfRsQMNpMj5cAadG".to_string(); - let script = vec![0x76; 1000]; // Large script - let height = 654321; - let confirmations = 100; - - let utxo = FFIUTXO::new( - txid, - vout, - amount, - address.clone(), - script.clone(), - height, - confirmations, - ); - - assert_eq!(utxo.txid, txid); - assert_eq!(utxo.vout, vout); - assert_eq!(utxo.amount, amount); - assert_eq!(utxo.script_len, 1000); - assert_eq!(utxo.height, height); - assert_eq!(utxo.confirmations, confirmations); - - // Verify script content - unsafe { - let script_slice = std::slice::from_raw_parts(utxo.script_pubkey, utxo.script_len); - assert!(script_slice.iter().all(|&b| b == 0x76)); - - let mut utxo = utxo; - utxo.free(); - } - } - - #[test] - fn test_ffi_utxo_edge_values() { - // Test with maximum values - let txid = [0xFFu8; 32]; - let vout = u32::MAX; - let amount = u64::MAX; - let address = "x".repeat(100); // Long address - let script = vec![0x00]; - let height = u32::MAX; - let confirmations = u32::MAX; - - let utxo = FFIUTXO::new(txid, vout, amount, address.clone(), script, height, confirmations); - - assert_eq!(utxo.vout, u32::MAX); - assert_eq!(utxo.amount, u64::MAX); - assert_eq!(utxo.height, u32::MAX); - assert_eq!(utxo.confirmations, u32::MAX); - - // Clean up - unsafe { - let mut utxo = utxo; - utxo.free(); - } - } - - #[test] - fn test_utxo_array_free_with_mixed_content() { - // Create UTXOs with different properties - let utxos = vec![ - // UTXO with normal values - FFIUTXO::new([0x01u8; 32], 0, 10000, "address1".to_string(), vec![0x76, 0xa9], 100, 10), - // UTXO with empty script - FFIUTXO::new([0x02u8; 32], 1, 20000, "address2".to_string(), vec![], 200, 20), - // UTXO with large script - FFIUTXO::new([0x03u8; 32], 2, 30000, "address3".to_string(), vec![0xAB; 500], 300, 30), - ]; - - let count = utxos.len(); - let mut boxed_utxos = utxos.into_boxed_slice(); - let utxos_ptr = boxed_utxos.as_mut_ptr(); - std::mem::forget(boxed_utxos); - - // Free should handle all different UTXO types - unsafe { - utxo_array_free(utxos_ptr, count); - } - } - - #[test] - fn test_managed_wallet_get_utxos_null_outputs() { - use crate::managed_wallet::FFIManagedWalletInfo; - use key_wallet::wallet::managed_wallet_info::ManagedWalletInfo; - use key_wallet::Network; - - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - let mut count_out: usize = 0; - - let managed_info = ManagedWalletInfo::new(Network::Testnet, [4u8; 32]); - let ffi_managed_info = Box::into_raw(Box::new(FFIManagedWalletInfo::new(managed_info))); - - // Test with null utxos_out - let result = unsafe { - managed_wallet_get_utxos(&*ffi_managed_info, ptr::null_mut(), &mut count_out, error) - }; - assert!(!result); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); - - // Test with null count_out - let mut utxos_out: *mut FFIUTXO = ptr::null_mut(); - let result = unsafe { - managed_wallet_get_utxos(&*ffi_managed_info, &mut utxos_out, ptr::null_mut(), error) - }; - assert!(!result); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); - - unsafe { - crate::managed_wallet::managed_wallet_free(ffi_managed_info); - } - } - - #[test] - fn test_ffi_utxo_free_idempotent() { - let utxo = - FFIUTXO::new([0x05u8; 32], 0, 10000, "test_address".to_string(), vec![0x76], 100, 1); - - unsafe { - let mut utxo = utxo; - // First free - utxo.free(); - - // After free, pointers should be null - assert!(utxo.address.is_null()); - assert!(utxo.script_pubkey.is_null()); - assert_eq!(utxo.script_len, 0); - - // Second free should be safe (no-op) - utxo.free(); - } - } -} diff --git a/key-wallet-ffi/src/wallet.rs b/key-wallet-ffi/src/wallet.rs deleted file mode 100644 index 113cd7902..000000000 --- a/key-wallet-ffi/src/wallet.rs +++ /dev/null @@ -1,827 +0,0 @@ -//! Wallet creation and management - -#[cfg(test)] -#[path = "wallet_tests.rs"] -mod tests; - -use crate::types::FFIAccountResult; -use dash_network::ffi::FFINetwork; -use key_wallet::wallet::initialization::WalletAccountCreationOptions; -use key_wallet::{Mnemonic, Seed, Wallet}; -use std::ffi::{CStr, CString}; -use std::os::raw::{c_char, c_uint}; -use std::ptr; -use std::slice; - -use crate::error::{FFIError, FFIErrorCode}; -use crate::types::{FFIWallet, FFIWalletAccountCreationOptions}; -use crate::{check_ptr, deref_ptr, unwrap_or_return}; -use key_wallet::Network; - -/// Create a new wallet from mnemonic with options -/// -/// # Safety -/// -/// - `mnemonic` must be a valid pointer to a null-terminated C string -/// - `account_options` must be a valid pointer to FFIWalletAccountCreationOptions or null -/// - `error` must be a valid pointer to an FFIError structure -/// - The caller must ensure all pointers remain valid for the duration of this call -/// - The returned pointer must be freed with `wallet_free` when no longer needed -#[no_mangle] -pub unsafe extern "C" fn wallet_create_from_mnemonic_with_options( - mnemonic: *const c_char, - network: FFINetwork, - account_options: *const FFIWalletAccountCreationOptions, - error: *mut FFIError, -) -> *mut FFIWallet { - use key_wallet::mnemonic::Language; - - let mnemonic = deref_ptr!(mnemonic, error); - let mnemonic_str = unwrap_or_return!(CStr::from_ptr(mnemonic).to_str(), error); - - let mnemonic = unwrap_or_return!(Mnemonic::from_phrase(mnemonic_str, Language::English), error); - - let network_rust: Network = network.into(); - let creation_options = if account_options.is_null() { - WalletAccountCreationOptions::Default - } else { - (*account_options).to_wallet_options() - }; - - let wallet = - unwrap_or_return!(Wallet::from_mnemonic(mnemonic, network_rust, creation_options), error); - - Box::into_raw(Box::new(FFIWallet::new(wallet))) -} - -/// Create a new wallet from mnemonic (backward compatibility - single network) -/// -/// # Safety -/// -/// - `mnemonic` must be a valid pointer to a null-terminated C string -/// - `error` must be a valid pointer to an FFIError structure -/// - The caller must ensure all pointers remain valid for the duration of this call -/// - The returned pointer must be freed with `wallet_free` when no longer needed -#[no_mangle] -pub unsafe extern "C" fn wallet_create_from_mnemonic( - mnemonic: *const c_char, - network: FFINetwork, - error: *mut FFIError, -) -> *mut FFIWallet { - wallet_create_from_mnemonic_with_options( - mnemonic, - network, - ptr::null(), // Use default options - error, - ) -} - -/// Create a new wallet from seed with options -/// -/// # Safety -/// -/// - `seed` must be a valid pointer to a byte array of `seed_len` length -/// - `account_options` must be a valid pointer to FFIWalletAccountCreationOptions or null -/// - `error` must be a valid pointer to an FFIError structure -/// - The caller must ensure all pointers remain valid for the duration of this call -#[no_mangle] -pub unsafe extern "C" fn wallet_create_from_seed_with_options( - seed: *const u8, - seed_len: usize, - network: FFINetwork, - account_options: *const FFIWalletAccountCreationOptions, - error: *mut FFIError, -) -> *mut FFIWallet { - let _seed_byte = deref_ptr!(seed, error); - if seed_len != 64 { - (*error).set( - FFIErrorCode::InvalidInput, - &format!("Invalid seed length: {}, expected 64", seed_len), - ); - return ptr::null_mut(); - } - - let seed_bytes = slice::from_raw_parts(seed, seed_len); - let mut seed_array = [0u8; 64]; - seed_array.copy_from_slice(seed_bytes); - let seed = Seed::new(seed_array); - - let network_rust: Network = network.into(); - let creation_options = if account_options.is_null() { - WalletAccountCreationOptions::Default - } else { - (*account_options).to_wallet_options() - }; - - let wallet = unwrap_or_return!(Wallet::from_seed(seed, network_rust, creation_options), error); - Box::into_raw(Box::new(FFIWallet::new(wallet))) -} - -/// Create a new wallet from seed (backward compatibility) -/// -/// # Safety -/// -/// - `seed` must be a valid pointer to a byte array of `seed_len` length -/// - `error` must be a valid pointer to an FFIError structure -/// - The caller must ensure all pointers remain valid for the duration of this call -#[no_mangle] -pub unsafe extern "C" fn wallet_create_from_seed( - seed: *const u8, - seed_len: usize, - network: FFINetwork, - error: *mut FFIError, -) -> *mut FFIWallet { - wallet_create_from_seed_with_options( - seed, - seed_len, - network, - ptr::null(), // Use default options - error, - ) -} - -/// Create a new random wallet with options -/// -/// # Safety -/// -/// - `account_options` must be a valid pointer to FFIWalletAccountCreationOptions or null -/// - `error` must be a valid pointer to an FFIError structure -/// - The caller must ensure all pointers remain valid for the duration of this call -#[no_mangle] -pub unsafe extern "C" fn wallet_create_random_with_options( - network: FFINetwork, - account_options: *const FFIWalletAccountCreationOptions, - error: *mut FFIError, -) -> *mut FFIWallet { - let network_rust: Network = network.into(); - - // Convert account creation options - let creation_options = if account_options.is_null() { - WalletAccountCreationOptions::Default - } else { - (*account_options).to_wallet_options() - }; - - let wallet = unwrap_or_return!(Wallet::new_random(network_rust, creation_options), error); - Box::into_raw(Box::new(FFIWallet::new(wallet))) -} - -/// Create a new random wallet (backward compatibility) -/// -/// # Safety -/// -/// - `error` must be a valid pointer to an FFIError structure -/// - The caller must ensure the pointer remains valid for the duration of this call -#[no_mangle] -pub unsafe extern "C" fn wallet_create_random( - network: FFINetwork, - error: *mut FFIError, -) -> *mut FFIWallet { - wallet_create_random_with_options( - network, - ptr::null(), // Use default options - error, - ) -} - -/// Get wallet ID (32-byte hash) -/// -/// # Safety -/// -/// - `wallet` must be a valid pointer to an FFIWallet -/// - `id_out` must be a valid pointer to a 32-byte buffer -/// - `error` must be a valid pointer to an FFIError structure -/// - The caller must ensure all pointers remain valid for the duration of this call -#[no_mangle] -pub unsafe extern "C" fn wallet_get_id( - wallet: *const FFIWallet, - id_out: *mut u8, - error: *mut FFIError, -) -> bool { - let wallet = deref_ptr!(wallet, error); - check_ptr!(id_out, error); - let wallet_id = wallet.inner().wallet_id; - ptr::copy_nonoverlapping(wallet_id.as_ptr(), id_out, 32); - true -} - -/// Check if wallet has mnemonic -/// -/// # Safety -/// -/// - `wallet` must be a valid pointer to an FFIWallet instance -/// - `error` must be a valid pointer to an FFIError structure -/// - The caller must ensure all pointers remain valid for the duration of this call -#[no_mangle] -pub unsafe extern "C" fn wallet_has_mnemonic( - wallet: *const FFIWallet, - error: *mut FFIError, -) -> bool { - let wallet = deref_ptr!(wallet, error); - wallet.inner().has_mnemonic() -} - -/// Check if wallet is watch-only -/// -/// # Safety -/// -/// - `wallet` must be a valid pointer to an FFIWallet instance -/// - `error` must be a valid pointer to an FFIError structure -/// - The caller must ensure all pointers remain valid for the duration of this call -#[no_mangle] -pub unsafe extern "C" fn wallet_is_watch_only( - wallet: *const FFIWallet, - error: *mut FFIError, -) -> bool { - let wallet = deref_ptr!(wallet, error); - wallet.inner().is_watch_only() -} - -/// Get extended public key for account -/// -/// # Safety -/// -/// - `wallet` must be a valid pointer to an FFIWallet instance -/// - `error` must be a valid pointer to an FFIError structure -/// - The caller must ensure all pointers remain valid for the duration of this call -/// - The returned C string must be freed by the caller when no longer needed -#[no_mangle] -pub unsafe extern "C" fn wallet_get_xpub( - wallet: *const FFIWallet, - account_index: c_uint, - error: *mut FFIError, -) -> *mut c_char { - let wallet = deref_ptr!(wallet, error); - let account = unwrap_or_return!(wallet.inner().get_bip44_account(account_index), error); - unwrap_or_return!(CString::new(account.extended_public_key().to_string()), error).into_raw() -} - -/// Free a wallet -/// -/// # Safety -/// -/// - `wallet` must be a valid pointer to an FFIWallet that was created by this library -/// - The pointer must not be used after calling this function -/// - This function must only be called once per wallet -#[no_mangle] -pub unsafe extern "C" fn wallet_free(wallet: *mut FFIWallet) { - if !wallet.is_null() { - unsafe { - let _ = Box::from_raw(wallet); - } - } -} - -/// Free a const wallet handle -/// -/// This is a const-safe wrapper for wallet_free() that accepts a const pointer. -/// Use this function when you have a *const FFIWallet that needs to be freed, -/// such as wallets returned from wallet_manager_get_wallet(). -/// -/// # Safety -/// -/// - `wallet` must be a valid pointer created by wallet creation functions or null -/// - After calling this function, the pointer becomes invalid -/// - This function must only be called once per wallet -/// - The wallet must have been allocated by this library (not stack or static memory) -#[no_mangle] -pub unsafe extern "C" fn wallet_free_const(wallet: *const FFIWallet) { - if !wallet.is_null() { - unsafe { - // Cast away const and free - this is safe because we know the wallet - // was originally allocated as mutable memory by Box::into_raw - let _ = Box::from_raw(wallet as *mut FFIWallet); - } - } -} - -/// Add an account to the wallet without xpub -/// -/// # Safety -/// -/// This function dereferences a raw pointer to FFIWallet. -/// The caller must ensure that: -/// - The wallet pointer is either null or points to a valid FFIWallet -/// - The FFIWallet remains valid for the duration of this call -/// -/// # Note -/// -/// This function does NOT support the following account types: -/// - `PlatformPayment`: Use `wallet_add_platform_payment_account()` instead -/// - `DashpayReceivingFunds`: Use `wallet_add_dashpay_receiving_account()` instead -/// - `DashpayExternalAccount`: Use `wallet_add_dashpay_external_account_with_xpub_bytes()` instead -#[no_mangle] -pub unsafe extern "C" fn wallet_add_account( - wallet: *mut FFIWallet, - account_type: crate::types::FFIAccountKind, - account_index: c_uint, -) -> crate::types::FFIAccountResult { - use crate::types::FFIAccountKind; - - if wallet.is_null() { - return crate::types::FFIAccountResult::error( - FFIErrorCode::InvalidInput, - "Wallet is null".to_string(), - ); - } - - // Check for account types that require special handling - match account_type { - FFIAccountKind::PlatformPayment => { - return crate::types::FFIAccountResult::error( - FFIErrorCode::InvalidInput, - "PlatformPayment accounts require account and key_class indices. \ - Use wallet_add_platform_payment_account() instead." - .to_string(), - ); - } - FFIAccountKind::DashpayReceivingFunds => { - return crate::types::FFIAccountResult::error( - FFIErrorCode::InvalidInput, - "DashpayReceivingFunds accounts require identity IDs. \ - Use wallet_add_dashpay_receiving_account() instead." - .to_string(), - ); - } - FFIAccountKind::DashpayExternalAccount => { - return crate::types::FFIAccountResult::error( - FFIErrorCode::InvalidInput, - "DashpayExternalAccount accounts require identity IDs. \ - Use wallet_add_dashpay_external_account_with_xpub_bytes() instead." - .to_string(), - ); - } - _ => {} // Other types are supported - } - - let wallet = &mut *wallet; - - let account_type_rust = account_type.to_account_type(account_index); - - match wallet.inner_mut() { - Some(w) => { - // Use the proper add_account method - match w.add_account(account_type_rust, None) { - Ok(()) => { - // Get the account we just added - if let Some(account) = w.accounts.account_of_type(account_type_rust) { - let ffi_account = crate::types::FFIAccount::new(account); - return crate::types::FFIAccountResult::success(Box::into_raw(Box::new( - ffi_account, - ))); - } - crate::types::FFIAccountResult::error( - FFIErrorCode::WalletError, - "Failed to retrieve account after adding".to_string(), - ) - } - Err(e) => crate::types::FFIAccountResult::error( - FFIErrorCode::WalletError, - format!("Failed to add account: {}", e), - ), - } - } - None => crate::types::FFIAccountResult::error( - FFIErrorCode::InvalidState, - "Cannot modify wallet".to_string(), - ), - } -} - -/// Add a DashPay receiving funds account -/// -/// # Safety -/// - `wallet` must be a valid pointer -/// - `user_identity_id` and `friend_identity_id` must each point to 32 bytes -#[no_mangle] -pub unsafe extern "C" fn wallet_add_dashpay_receiving_account( - wallet: *mut FFIWallet, - account_index: c_uint, - user_identity_id: *const u8, - friend_identity_id: *const u8, -) -> FFIAccountResult { - use key_wallet::account::AccountType; - if wallet.is_null() || user_identity_id.is_null() || friend_identity_id.is_null() { - return FFIAccountResult::error( - crate::error::FFIErrorCode::InvalidInput, - "Null pointer provided".to_string(), - ); - } - let w = &mut *wallet; - let wallet_mut = match w.inner_mut() { - Some(w) => w, - None => { - return FFIAccountResult::error( - crate::error::FFIErrorCode::InvalidInput, - "Wallet is immutable".to_string(), - ) - } - }; - let mut user_id = [0u8; 32]; - let mut friend_id = [0u8; 32]; - core::ptr::copy_nonoverlapping(user_identity_id, user_id.as_mut_ptr(), 32); - core::ptr::copy_nonoverlapping(friend_identity_id, friend_id.as_mut_ptr(), 32); - - let acct = AccountType::DashpayReceivingFunds { - index: account_index, - user_identity_id: user_id, - friend_identity_id: friend_id, - }; - match wallet_mut.add_account(acct, None) { - Ok(()) => { - if let Some(account) = wallet_mut.accounts.account_of_type(acct) { - let ffi_account = crate::types::FFIAccount::new(account); - return FFIAccountResult::success(Box::into_raw(Box::new(ffi_account))); - } - FFIAccountResult::error( - crate::error::FFIErrorCode::WalletError, - "Failed to retrieve account after adding".to_string(), - ) - } - Err(e) => FFIAccountResult::error(crate::error::FFIErrorCode::InvalidInput, e.to_string()), - } -} - -/// Add a DashPay external (watch-only) account with xpub bytes -/// -/// # Safety -/// - `wallet` must be valid, `xpub_bytes` must point to `xpub_len` bytes -/// - `user_identity_id` and `friend_identity_id` must each point to 32 bytes -#[no_mangle] -pub unsafe extern "C" fn wallet_add_dashpay_external_account_with_xpub_bytes( - wallet: *mut FFIWallet, - account_index: c_uint, - user_identity_id: *const u8, - friend_identity_id: *const u8, - xpub_bytes: *const u8, - xpub_len: usize, -) -> FFIAccountResult { - use key_wallet::account::AccountType; - use key_wallet::bip32::ExtendedPubKey; - if wallet.is_null() - || user_identity_id.is_null() - || friend_identity_id.is_null() - || xpub_bytes.is_null() - { - return FFIAccountResult::error( - crate::error::FFIErrorCode::InvalidInput, - "Null pointer provided".to_string(), - ); - } - let w = &mut *wallet; - let wallet_mut = match w.inner_mut() { - Some(w) => w, - None => { - return FFIAccountResult::error( - crate::error::FFIErrorCode::InvalidInput, - "Wallet is immutable".to_string(), - ) - } - }; - let mut user_id = [0u8; 32]; - let mut friend_id = [0u8; 32]; - core::ptr::copy_nonoverlapping(user_identity_id, user_id.as_mut_ptr(), 32); - core::ptr::copy_nonoverlapping(friend_identity_id, friend_id.as_mut_ptr(), 32); - let xpub_slice = core::slice::from_raw_parts(xpub_bytes, xpub_len); - let xpub = match ExtendedPubKey::decode(xpub_slice) { - Ok(x) => x, - Err(_) => { - return FFIAccountResult::error( - crate::error::FFIErrorCode::InvalidInput, - "Invalid xpub bytes".to_string(), - ) - } - }; - let acct = AccountType::DashpayExternalAccount { - index: account_index, - user_identity_id: user_id, - friend_identity_id: friend_id, - }; - match wallet_mut.add_account(acct, Some(xpub)) { - Ok(()) => { - if let Some(account) = wallet_mut.accounts.account_of_type(acct) { - let ffi_account = crate::types::FFIAccount::new(account); - return FFIAccountResult::success(Box::into_raw(Box::new(ffi_account))); - } - FFIAccountResult::error( - crate::error::FFIErrorCode::WalletError, - "Failed to retrieve account after adding".to_string(), - ) - } - Err(e) => FFIAccountResult::error(crate::error::FFIErrorCode::InvalidInput, e.to_string()), - } -} - -/// Add an account to the wallet with xpub as byte array -/// -/// # Safety -/// -/// This function dereferences raw pointers. -/// The caller must ensure that: -/// - The wallet pointer is either null or points to a valid FFIWallet -/// - The xpub_bytes pointer is either null or points to at least xpub_len bytes -/// - The FFIWallet remains valid for the duration of this call -/// -/// # Note -/// -/// This function does NOT support the following account types: -/// - `PlatformPayment`: Use `wallet_add_platform_payment_account()` instead -/// - `DashpayReceivingFunds`: Use `wallet_add_dashpay_receiving_account()` instead -/// - `DashpayExternalAccount`: Use `wallet_add_dashpay_external_account_with_xpub_bytes()` instead -#[no_mangle] -pub unsafe extern "C" fn wallet_add_account_with_xpub_bytes( - wallet: *mut FFIWallet, - account_type: crate::types::FFIAccountKind, - account_index: c_uint, - xpub_bytes: *const u8, - xpub_len: usize, -) -> crate::types::FFIAccountResult { - use crate::types::FFIAccountKind; - - if wallet.is_null() { - return crate::types::FFIAccountResult::error( - FFIErrorCode::InvalidInput, - "Wallet is null".to_string(), - ); - } - - if xpub_bytes.is_null() { - return crate::types::FFIAccountResult::error( - FFIErrorCode::InvalidInput, - "Xpub bytes are null".to_string(), - ); - } - - // Check for account types that require special handling - match account_type { - FFIAccountKind::PlatformPayment => { - return crate::types::FFIAccountResult::error( - FFIErrorCode::InvalidInput, - "PlatformPayment accounts require account and key_class indices. \ - Use wallet_add_platform_payment_account() instead." - .to_string(), - ); - } - FFIAccountKind::DashpayReceivingFunds => { - return crate::types::FFIAccountResult::error( - FFIErrorCode::InvalidInput, - "DashpayReceivingFunds accounts require identity IDs. \ - Use wallet_add_dashpay_receiving_account() instead." - .to_string(), - ); - } - FFIAccountKind::DashpayExternalAccount => { - return crate::types::FFIAccountResult::error( - FFIErrorCode::InvalidInput, - "DashpayExternalAccount accounts require identity IDs. \ - Use wallet_add_dashpay_external_account_with_xpub_bytes() instead." - .to_string(), - ); - } - _ => {} // Other types are supported - } - - let wallet = &mut *wallet; - - use key_wallet::ExtendedPubKey; - - let account_type_rust = account_type.to_account_type(account_index); - - // Parse the xpub from bytes (assuming it's a string representation) - let xpub_slice = slice::from_raw_parts(xpub_bytes, xpub_len); - let xpub_str = match std::str::from_utf8(xpub_slice) { - Ok(s) => s, - Err(_) => { - return crate::types::FFIAccountResult::error( - FFIErrorCode::InvalidInput, - "Invalid UTF-8 in xpub bytes".to_string(), - ); - } - }; - - let xpub = match xpub_str.parse::() { - Ok(xpub) => xpub, - Err(e) => { - return crate::types::FFIAccountResult::error( - FFIErrorCode::InvalidInput, - format!("Invalid extended public key: {}", e), - ); - } - }; - - match wallet.inner_mut() { - Some(w) => match w.add_account(account_type_rust, Some(xpub)) { - Ok(()) => { - // Get the account we just added - if let Some(account) = w.accounts.account_of_type(account_type_rust) { - let ffi_account = crate::types::FFIAccount::new(account); - return crate::types::FFIAccountResult::success(Box::into_raw(Box::new( - ffi_account, - ))); - } - crate::types::FFIAccountResult::error( - FFIErrorCode::WalletError, - "Failed to retrieve account after adding".to_string(), - ) - } - Err(e) => crate::types::FFIAccountResult::error( - FFIErrorCode::WalletError, - format!("Failed to add account with xpub: {}", e), - ), - }, - None => crate::types::FFIAccountResult::error( - FFIErrorCode::InvalidState, - "Cannot modify wallet".to_string(), - ), - } -} - -/// Add an account to the wallet with xpub as string -/// -/// # Safety -/// -/// This function dereferences raw pointers. -/// The caller must ensure that: -/// - The wallet pointer is either null or points to a valid FFIWallet -/// - The xpub_string pointer is either null or points to a valid null-terminated C string -/// - The FFIWallet remains valid for the duration of this call -/// -/// # Note -/// -/// This function does NOT support the following account types: -/// - `PlatformPayment`: Use `wallet_add_platform_payment_account()` instead -/// - `DashpayReceivingFunds`: Use `wallet_add_dashpay_receiving_account()` instead -/// - `DashpayExternalAccount`: Use `wallet_add_dashpay_external_account_with_xpub_bytes()` instead -#[no_mangle] -pub unsafe extern "C" fn wallet_add_account_with_string_xpub( - wallet: *mut FFIWallet, - account_type: crate::types::FFIAccountKind, - account_index: c_uint, - xpub_string: *const c_char, -) -> crate::types::FFIAccountResult { - use crate::types::FFIAccountKind; - - if wallet.is_null() { - return crate::types::FFIAccountResult::error( - FFIErrorCode::InvalidInput, - "Wallet is null".to_string(), - ); - } - - if xpub_string.is_null() { - return crate::types::FFIAccountResult::error( - FFIErrorCode::InvalidInput, - "Xpub string is null".to_string(), - ); - } - - // Check for account types that require special handling - match account_type { - FFIAccountKind::PlatformPayment => { - return crate::types::FFIAccountResult::error( - FFIErrorCode::InvalidInput, - "PlatformPayment accounts require account and key_class indices. \ - Use wallet_add_platform_payment_account() instead." - .to_string(), - ); - } - FFIAccountKind::DashpayReceivingFunds => { - return crate::types::FFIAccountResult::error( - FFIErrorCode::InvalidInput, - "DashpayReceivingFunds accounts require identity IDs. \ - Use wallet_add_dashpay_receiving_account() instead." - .to_string(), - ); - } - FFIAccountKind::DashpayExternalAccount => { - return crate::types::FFIAccountResult::error( - FFIErrorCode::InvalidInput, - "DashpayExternalAccount accounts require identity IDs. \ - Use wallet_add_dashpay_external_account_with_xpub_bytes() instead." - .to_string(), - ); - } - _ => {} // Other types are supported - } - - let wallet = &mut *wallet; - - use key_wallet::ExtendedPubKey; - - let account_type_rust = account_type.to_account_type(account_index); - - // Parse the xpub from C string - let xpub_str = match CStr::from_ptr(xpub_string).to_str() { - Ok(s) => s, - Err(_) => { - return crate::types::FFIAccountResult::error( - FFIErrorCode::InvalidInput, - "Invalid UTF-8 in xpub string".to_string(), - ); - } - }; - - let xpub = match xpub_str.parse::() { - Ok(xpub) => xpub, - Err(e) => { - return crate::types::FFIAccountResult::error( - FFIErrorCode::InvalidInput, - format!("Invalid extended public key: {}", e), - ); - } - }; - - match wallet.inner_mut() { - Some(w) => match w.add_account(account_type_rust, Some(xpub)) { - Ok(()) => { - // Get the account we just added - if let Some(account) = w.accounts.account_of_type(account_type_rust) { - let ffi_account = crate::types::FFIAccount::new(account); - return crate::types::FFIAccountResult::success(Box::into_raw(Box::new( - ffi_account, - ))); - } - crate::types::FFIAccountResult::error( - FFIErrorCode::WalletError, - "Failed to retrieve account after adding".to_string(), - ) - } - Err(e) => crate::types::FFIAccountResult::error( - FFIErrorCode::WalletError, - format!("Failed to add account with xpub: {}", e), - ), - }, - None => crate::types::FFIAccountResult::error( - FFIErrorCode::InvalidState, - "Cannot modify wallet".to_string(), - ), - } -} - -/// Add a Platform Payment account (DIP-17) to the wallet -/// -/// Platform Payment accounts use the derivation path: -/// `m/9'/coin_type'/17'/account'/key_class'/index` -/// -/// # Arguments -/// * `wallet` - Pointer to the wallet -/// * `account_index` - The account index (hardened) in the derivation path -/// * `key_class` - The key class (hardened) - typically 0' for main addresses -/// -/// # Safety -/// -/// This function dereferences a raw pointer to FFIWallet. -/// The caller must ensure that: -/// - The wallet pointer is either null or points to a valid FFIWallet -/// - The FFIWallet remains valid for the duration of this call -#[no_mangle] -pub unsafe extern "C" fn wallet_add_platform_payment_account( - wallet: *mut FFIWallet, - account_index: c_uint, - key_class: c_uint, -) -> crate::types::FFIAccountResult { - use key_wallet::account::AccountType; - - if wallet.is_null() { - return crate::types::FFIAccountResult::error( - FFIErrorCode::InvalidInput, - "Wallet is null".to_string(), - ); - } - - let wallet = &mut *wallet; - - let account_type = AccountType::PlatformPayment { - account: account_index, - key_class, - }; - - match wallet.inner_mut() { - Some(w) => { - // Use the proper add_account method - match w.add_account(account_type, None) { - Ok(()) => { - // Get the account we just added - if let Some(account) = w.accounts.account_of_type(account_type) { - let ffi_account = crate::types::FFIAccount::new(account); - return crate::types::FFIAccountResult::success(Box::into_raw(Box::new( - ffi_account, - ))); - } - crate::types::FFIAccountResult::error( - FFIErrorCode::WalletError, - "Failed to retrieve account after adding".to_string(), - ) - } - Err(e) => crate::types::FFIAccountResult::error( - FFIErrorCode::WalletError, - format!("Failed to add platform payment account: {}", e), - ), - } - } - None => crate::types::FFIAccountResult::error( - FFIErrorCode::InvalidState, - "Cannot modify wallet".to_string(), - ), - } -} diff --git a/key-wallet-ffi/src/wallet_manager.rs b/key-wallet-ffi/src/wallet_manager.rs deleted file mode 100644 index e2736b26b..000000000 --- a/key-wallet-ffi/src/wallet_manager.rs +++ /dev/null @@ -1,617 +0,0 @@ -//! FFI bindings for WalletManager - -#[cfg(test)] -#[path = "wallet_manager_tests.rs"] -mod tests; - -#[cfg(test)] -#[path = "wallet_manager_serialization_tests.rs"] -mod serialization_tests; - -use crate::error::FFIError; -use crate::{check_ptr, deref_ptr, deref_ptr_mut, unwrap_or_return}; -use dash_network::ffi::FFINetwork; -use key_wallet::wallet::managed_wallet_info::ManagedWalletInfo; -use key_wallet_manager::WalletInterface; -use key_wallet_manager::WalletManager; -use std::ffi::{CStr, CString}; -use std::os::raw::{c_char, c_uint}; -use std::ptr; -use std::sync::Arc; -use tokio::sync::RwLock; - -/// FFI wrapper for WalletManager -/// -/// This struct holds a cloned Arc reference to the WalletManager, -/// allowing FFI code to interact with it directly without going through -/// the SPV client. -pub struct FFIWalletManager { - network: FFINetwork, - pub(crate) manager: Arc>>, - pub(crate) runtime: Arc, -} - -impl FFIWalletManager { - /// Create a new FFIWalletManager from an `Arc>` - pub fn from_arc( - manager: Arc>>, - runtime: Arc, - ) -> Self { - let network = runtime.block_on(async { - let manager_guard = manager.read().await; - manager_guard.network() - }); - - FFIWalletManager { - network: FFINetwork::from(network), - manager, - runtime, - } - } - - pub fn network(&self) -> FFINetwork { - self.network - } -} - -/// Describe the wallet manager for a given network and return a newly -/// allocated C string. -/// -/// # Safety -/// - `manager` must be a valid pointer to an `FFIWalletManager` -/// - Callers must free the returned string with `wallet_manager_free_string` -#[no_mangle] -pub unsafe extern "C" fn wallet_manager_describe( - manager: *const FFIWalletManager, - error: *mut FFIError, -) -> *mut c_char { - let manager_ref = deref_ptr!(manager, error); - let runtime = manager_ref.runtime.clone(); - let manager_arc = manager_ref.manager.clone(); - - let description = runtime.block_on(async { - let guard = manager_arc.read().await; - guard.describe().await - }); - - unwrap_or_return!(CString::new(description), error).into_raw() -} - -/// Free a string previously returned by wallet manager APIs. -/// -/// # Safety -/// - `value` must be either null or a pointer obtained from -/// `wallet_manager_describe` (or other wallet manager FFI helpers that -/// specify this free function). -/// - The pointer must not be used after this call returns. -#[no_mangle] -pub unsafe extern "C" fn wallet_manager_free_string(value: *mut c_char) { - if value.is_null() { - return; - } - - drop(CString::from_raw(value)); -} - -/// Create a new wallet manager -/// -/// # Safety -/// -/// `error` must be a valid pointer to an `FFIError`. The returned pointer must be -/// freed with `wallet_manager_free`. -#[no_mangle] -pub unsafe extern "C" fn wallet_manager_create( - network: FFINetwork, - error: *mut FFIError, -) -> *mut FFIWalletManager { - let manager = WalletManager::new(network.into()); - let runtime = unwrap_or_return!(tokio::runtime::Runtime::new(), error); - - Box::into_raw(Box::new(FFIWalletManager { - network, - manager: Arc::new(RwLock::new(manager)), - runtime: Arc::new(runtime), - })) -} - -/// Add a wallet from mnemonic to the manager with options -/// -/// # Safety -/// -/// - `manager` must be a valid pointer to an FFIWalletManager instance -/// - `mnemonic` must be a valid pointer to a null-terminated C string -/// - `account_options` must be a valid pointer to FFIWalletAccountCreationOptions or null -/// - `error` must be a valid pointer to an FFIError structure -/// - The caller must ensure all pointers remain valid for the duration of this call -#[no_mangle] -pub unsafe extern "C" fn wallet_manager_add_wallet_from_mnemonic_with_options( - manager: *mut FFIWalletManager, - mnemonic: *const c_char, - account_options: *const crate::types::FFIWalletAccountCreationOptions, - error: *mut FFIError, -) -> bool { - let manager_ref = deref_ptr!(manager, error); - let mnemonic = deref_ptr!(mnemonic, error); - let mnemonic_str = unwrap_or_return!(CStr::from_ptr(mnemonic).to_str(), error); - - let creation_options = if account_options.is_null() { - key_wallet::wallet::initialization::WalletAccountCreationOptions::Default - } else { - (*account_options).to_wallet_options() - }; - - let result = manager_ref.runtime.block_on(async { - let mut manager_guard = manager_ref.manager.write().await; - manager_guard.create_wallet_from_mnemonic(mnemonic_str, 0, creation_options) - }); - let _ = unwrap_or_return!(result, error); - true -} - -/// Add a wallet from mnemonic to the manager (backward compatibility) -/// -/// # Safety -/// -/// - `manager` must be a valid pointer to an FFIWalletManager instance -/// - `mnemonic` must be a valid pointer to a null-terminated C string -/// - `error` must be a valid pointer to an FFIError structure -/// - The caller must ensure all pointers remain valid for the duration of this call -#[no_mangle] -pub unsafe extern "C" fn wallet_manager_add_wallet_from_mnemonic( - manager: *mut FFIWalletManager, - mnemonic: *const c_char, - error: *mut FFIError, -) -> bool { - wallet_manager_add_wallet_from_mnemonic_with_options( - manager, - mnemonic, - ptr::null(), // Use default options - error, - ) -} - -/// Add a wallet from mnemonic to the manager and return serialized bytes -/// -/// Creates a wallet from a mnemonic phrase, adds it to the manager, optionally downgrading it -/// to a pubkey-only wallet (watch-only or externally signable), and returns the serialized wallet bytes. -/// -/// # Safety -/// -/// - `manager` must be a valid pointer to an FFIWalletManager instance -/// - `mnemonic` must be a valid pointer to a null-terminated C string -/// - `birth_height` is the block height to start syncing from (0 = sync from genesis) -/// - `account_options` must be a valid pointer to FFIWalletAccountCreationOptions or null -/// - `downgrade_to_pubkey_wallet` if true, creates a watch-only or externally signable wallet -/// - `allow_external_signing` if true AND downgrade_to_pubkey_wallet is true, creates an externally signable wallet -/// - `wallet_bytes_out` must be a valid pointer to a pointer that will receive the serialized bytes -/// - `wallet_bytes_len_out` must be a valid pointer that will receive the byte length -/// - `wallet_id_out` must be a valid pointer to a 32-byte array that will receive the wallet ID -/// - `error` must be a valid pointer to an FFIError structure -/// - The caller must ensure all pointers remain valid for the duration of this call -/// - The caller must free the returned wallet_bytes using wallet_manager_free_wallet_bytes() -#[cfg(feature = "bincode")] -#[no_mangle] -#[allow(clippy::too_many_arguments)] -pub unsafe extern "C" fn wallet_manager_add_wallet_from_mnemonic_return_serialized_bytes( - manager: *mut FFIWalletManager, - mnemonic: *const c_char, - birth_height: c_uint, - account_options: *const crate::types::FFIWalletAccountCreationOptions, - downgrade_to_pubkey_wallet: bool, - allow_external_signing: bool, - wallet_bytes_out: *mut *mut u8, - wallet_bytes_len_out: *mut usize, - wallet_id_out: *mut u8, - error: *mut FFIError, -) -> bool { - let manager_ref = deref_ptr!(manager, error); - let mnemonic = deref_ptr!(mnemonic, error); - check_ptr!(wallet_bytes_out, error); - check_ptr!(wallet_bytes_len_out, error); - check_ptr!(wallet_id_out, error); - - let mnemonic_str = unwrap_or_return!(CStr::from_ptr(mnemonic).to_str(), error); - - let creation_options = if account_options.is_null() { - key_wallet::wallet::initialization::WalletAccountCreationOptions::Default - } else { - (*account_options).to_wallet_options() - }; - - let result = manager_ref.runtime.block_on(async { - let mut manager_guard = manager_ref.manager.write().await; - - manager_guard.create_wallet_from_mnemonic_return_serialized_bytes( - mnemonic_str, - birth_height, - creation_options, - downgrade_to_pubkey_wallet, - allow_external_signing, - ) - }); - - let (serialized, wallet_id) = unwrap_or_return!(result, error); - - // Allocate memory for the serialized bytes - let boxed_bytes = serialized.into_boxed_slice(); - let bytes_len = boxed_bytes.len(); - let bytes_ptr = Box::into_raw(boxed_bytes) as *mut u8; - - // Write output values - unsafe { - *wallet_bytes_out = bytes_ptr; - *wallet_bytes_len_out = bytes_len; - ptr::copy_nonoverlapping(wallet_id.as_ptr(), wallet_id_out, 32); - } - - (*error).clean(); - true -} - -/// Free wallet bytes buffer -/// -/// # Safety -/// -/// - `wallet_bytes` must be a valid pointer to a buffer allocated by wallet_manager_add_wallet_from_mnemonic_return_serialized_bytes -/// - `bytes_len` must match the original allocation size -/// - The pointer must not be used after calling this function -/// - This function must only be called once per buffer -#[cfg(feature = "bincode")] -#[no_mangle] -pub unsafe extern "C" fn wallet_manager_free_wallet_bytes(wallet_bytes: *mut u8, bytes_len: usize) { - if !wallet_bytes.is_null() && bytes_len > 0 { - unsafe { - // Reconstruct the boxed slice with the correct DST pointer - ptr::write_bytes(wallet_bytes, 0, bytes_len); - let _ = Box::from_raw(ptr::slice_from_raw_parts_mut(wallet_bytes, bytes_len)); - } - } -} - -/// Import a wallet from bincode-serialized bytes -/// -/// Deserializes a wallet from bytes and adds it to the manager. -/// Returns a 32-byte wallet ID on success. -/// -/// # Safety -/// -/// - `manager` must be a valid pointer to an FFIWalletManager instance -/// - `wallet_bytes` must be a valid pointer to bincode-serialized wallet bytes -/// - `wallet_bytes_len` must be the exact length of the wallet bytes -/// - `wallet_id_out` must be a valid pointer to a 32-byte array that will receive the wallet ID -/// - `error` must be a valid pointer to an FFIError structure -/// - The caller must ensure all pointers remain valid for the duration of this call -#[cfg(feature = "bincode")] -#[no_mangle] -pub unsafe extern "C" fn wallet_manager_import_wallet_from_bytes( - manager: *mut FFIWalletManager, - wallet_bytes: *const u8, - wallet_bytes_len: usize, - wallet_id_out: *mut u8, - error: *mut FFIError, -) -> bool { - let manager_ref = deref_ptr!(manager, error); - check_ptr!(wallet_bytes, error); - check_ptr!(wallet_id_out, error); - - let wallet_bytes_slice = std::slice::from_raw_parts(wallet_bytes, wallet_bytes_len); - - // Import the wallet using async runtime - let result = manager_ref.runtime.block_on(async { - let mut manager_guard = manager_ref.manager.write().await; - manager_guard.import_wallet_from_bytes(wallet_bytes_slice) - }); - - let wallet_id = unwrap_or_return!(result, error); - // Copy the wallet ID to the output buffer - unsafe { - ptr::copy_nonoverlapping(wallet_id.as_ptr(), wallet_id_out, 32); - } - true -} - -/// Get wallet IDs -/// -/// # Safety -/// -/// - `manager` must be a valid pointer to an FFIWalletManager -/// - `wallet_ids_out` must be a valid pointer to a pointer that will receive the wallet IDs -/// - `count_out` must be a valid pointer to receive the count -/// - `error` must be a valid pointer to an FFIError structure -/// - The caller must ensure all pointers remain valid for the duration of this call -#[no_mangle] -pub unsafe extern "C" fn wallet_manager_get_wallet_ids( - manager: *const FFIWalletManager, - wallet_ids_out: *mut *mut u8, - count_out: *mut usize, - error: *mut FFIError, -) -> bool { - let manager_ref = deref_ptr!(manager, error); - check_ptr!(wallet_ids_out, error); - check_ptr!(count_out, error); - - // Get wallet IDs from the manager - let wallet_ids = manager_ref.runtime.block_on(async { - let manager_guard = manager_ref.manager.read().await; - manager_guard.list_wallets().into_iter().cloned().collect::>() - }); - - let count = wallet_ids.len(); - if count == 0 { - *count_out = 0; - *wallet_ids_out = ptr::null_mut(); - } else { - // Allocate memory for wallet IDs (32 bytes each) as a boxed slice - let mut ids_buffer = Vec::with_capacity(count * 32); - for wallet_id in wallet_ids.iter() { - ids_buffer.extend_from_slice(wallet_id); - } - // Convert to boxed slice for consistent memory layout - let boxed_slice = ids_buffer.into_boxed_slice(); - let ids_ptr = Box::into_raw(boxed_slice) as *mut u8; - - *wallet_ids_out = ids_ptr; - *count_out = count; - } - true -} - -/// Get a wallet from the manager -/// -/// Returns a reference to the wallet if found -/// -/// # Safety -/// -/// - `manager` must be a valid pointer to an FFIWalletManager instance -/// - `wallet_id` must be a valid pointer to a 32-byte wallet ID -/// - `error` must be a valid pointer to an FFIError structure -/// - The caller must ensure all pointers remain valid for the duration of this call -/// - The returned wallet must be freed with wallet_free_const() -#[no_mangle] -pub unsafe extern "C" fn wallet_manager_get_wallet( - manager: *const FFIWalletManager, - wallet_id: *const u8, - error: *mut FFIError, -) -> *const crate::types::FFIWallet { - let manager_ref = deref_ptr!(manager, error); - check_ptr!(wallet_id, error); - - let mut wallet_id_array = [0u8; 32]; - ptr::copy_nonoverlapping(wallet_id, wallet_id_array.as_mut_ptr(), 32); - - let wallet_opt = manager_ref.runtime.block_on(async { - let manager_guard = manager_ref.manager.read().await; - manager_guard.get_wallet(&wallet_id_array).cloned() - }); - let wallet = unwrap_or_return!(wallet_opt, error); - Box::into_raw(Box::new(crate::types::FFIWallet::new(wallet))) -} - -/// Get managed wallet info from the manager -/// -/// Returns a reference to the managed wallet info if found -/// -/// # Safety -/// -/// - `manager` must be a valid pointer to an FFIWalletManager instance -/// - `wallet_id` must be a valid pointer to a 32-byte wallet ID -/// - `error` must be a valid pointer to an FFIError structure -/// - The caller must ensure all pointers remain valid for the duration of this call -/// - The returned managed wallet info must be freed with managed_wallet_info_free() -#[no_mangle] -pub unsafe extern "C" fn wallet_manager_get_managed_wallet_info( - manager: *const FFIWalletManager, - wallet_id: *const u8, - error: *mut FFIError, -) -> *mut crate::managed_wallet::FFIManagedWalletInfo { - let manager_ref = deref_ptr!(manager, error); - check_ptr!(wallet_id, error); - - let mut wallet_id_array = [0u8; 32]; - ptr::copy_nonoverlapping(wallet_id, wallet_id_array.as_mut_ptr(), 32); - - let wallet_info_opt = manager_ref.runtime.block_on(async { - let manager_guard = manager_ref.manager.read().await; - manager_guard.get_wallet_info(&wallet_id_array).cloned() - }); - let wallet_info = unwrap_or_return!(wallet_info_opt, error); - Box::into_raw(Box::new(crate::managed_wallet::FFIManagedWalletInfo::new(wallet_info))) -} - -/// Get wallet balance -/// -/// Returns the confirmed and unconfirmed balance for a specific wallet -/// -/// # Safety -/// -/// - `manager` must be a valid pointer to an FFIWalletManager instance -/// - `wallet_id` must be a valid pointer to a 32-byte wallet ID -/// - `confirmed_out` must be a valid pointer to a u64 (maps to C uint64_t) -/// - `unconfirmed_out` must be a valid pointer to a u64 (maps to C uint64_t) -/// - `error` must be a valid pointer to an FFIError structure -/// - The caller must ensure all pointers remain valid for the duration of this call -#[no_mangle] -pub unsafe extern "C" fn wallet_manager_get_wallet_balance( - manager: *const FFIWalletManager, - wallet_id: *const u8, - confirmed_out: *mut u64, - unconfirmed_out: *mut u64, - error: *mut FFIError, -) -> bool { - let manager_ref = deref_ptr!(manager, error); - check_ptr!(wallet_id, error); - check_ptr!(confirmed_out, error); - check_ptr!(unconfirmed_out, error); - - let mut wallet_id_array = [0u8; 32]; - ptr::copy_nonoverlapping(wallet_id, wallet_id_array.as_mut_ptr(), 32); - - let result = manager_ref.runtime.block_on(async { - let manager_guard = manager_ref.manager.read().await; - manager_guard.get_wallet_balance(&wallet_id_array) - }); - let balance = unwrap_or_return!(result, error); - *confirmed_out = balance.confirmed(); - *unconfirmed_out = balance.unconfirmed(); - true -} - -/// Process a transaction through all wallets -/// -/// Checks a transaction against all wallets and updates their states if relevant. -/// Returns true if the transaction was relevant to at least one wallet. -/// -/// # Safety -/// -/// - `manager` must be a valid pointer to an FFIWalletManager instance -/// - `tx_bytes` must be a valid pointer to transaction bytes -/// - `tx_len` must be the length of the transaction bytes -/// - `context` must be a valid pointer to FFITransactionContext -/// - `update_state_if_found` indicates whether to update wallet state when transaction is relevant -/// - `error` must be a valid pointer to an FFIError structure -/// - The caller must ensure all pointers remain valid for the duration of this call -#[no_mangle] -pub unsafe extern "C" fn wallet_manager_process_transaction( - manager: *mut FFIWalletManager, - tx_bytes: *const u8, - tx_len: usize, - context: *const crate::types::FFITransactionContext, - update_state_if_found: bool, - error: *mut FFIError, -) -> bool { - let manager_ref = deref_ptr_mut!(manager, error); - check_ptr!(tx_bytes, error); - check_ptr!(context, error); - - let tx_slice = std::slice::from_raw_parts(tx_bytes, tx_len); - - use dashcore::blockdata::transaction::Transaction; - use dashcore::consensus::encode::deserialize; - - let tx: Transaction = unwrap_or_return!(deserialize::(tx_slice), error); - - // Convert FFI context to native TransactionContext - let context = unwrap_or_return!(unsafe { (*context).to_transaction_context() }, error); - - // Process the transaction using async runtime - let result = manager_ref.runtime.block_on(async { - let mut manager_guard = manager_ref.manager.write().await; - manager_guard - .check_transaction_in_all_wallets(&tx, context, update_state_if_found, true) - .await - }); - - (*error).clean(); - !result.affected_wallets.is_empty() -} - -/// Get the network for this wallet manager -/// -/// # Safety -/// -/// - `manager` must be a valid pointer to an FFIWalletManager -/// - `error` must be a valid pointer to an FFIError structure -/// - The caller must ensure all pointers remain valid for the duration of this call -#[no_mangle] -pub unsafe extern "C" fn wallet_manager_network( - manager: *const FFIWalletManager, - error: *mut FFIError, -) -> FFINetwork { - let manager_ref = deref_ptr!(manager, error, FFINetwork::Mainnet); - manager_ref.network() -} - -/// Get current height for a network -/// -/// # Safety -/// -/// - `manager` must be a valid pointer to an FFIWalletManager -/// - `error` must be a valid pointer to an FFIError structure -/// - The caller must ensure all pointers remain valid for the duration of this call -#[no_mangle] -pub unsafe extern "C" fn wallet_manager_current_height( - manager: *const FFIWalletManager, - error: *mut FFIError, -) -> c_uint { - let manager_ref = deref_ptr!(manager, error); - manager_ref.runtime.block_on(async { - let manager_guard = manager_ref.manager.read().await; - manager_guard.last_processed_height() - }) -} - -/// Get wallet count -/// -/// # Safety -/// -/// - `manager` must be a valid pointer to an FFIWalletManager instance -/// - `error` must be a valid pointer to an FFIError structure -/// - The caller must ensure all pointers remain valid for the duration of this call -#[no_mangle] -pub unsafe extern "C" fn wallet_manager_wallet_count( - manager: *const FFIWalletManager, - error: *mut FFIError, -) -> usize { - let manager_ref = deref_ptr!(manager, error); - manager_ref.runtime.block_on(async { - let manager_guard = manager_ref.manager.read().await; - manager_guard.wallet_count() - }) -} - -/// Free wallet manager -/// -/// # Safety -/// -/// - `manager` must be a valid pointer to an FFIWalletManager that was created by this library -/// - The pointer must not be used after calling this function -/// - This function must only be called once per manager -#[no_mangle] -pub unsafe extern "C" fn wallet_manager_free(manager: *mut FFIWalletManager) { - if !manager.is_null() { - unsafe { - let _ = Box::from_raw(manager); - } - } -} - -/// Free wallet IDs buffer -/// -/// # Safety -/// -/// - `wallet_ids` must be a valid pointer to a buffer allocated by this library -/// - `count` must match the number of wallet IDs in the buffer -/// - The pointer must not be used after calling this function -/// - This function must only be called once per buffer -#[no_mangle] -pub unsafe extern "C" fn wallet_manager_free_wallet_ids(wallet_ids: *mut u8, count: usize) { - if !wallet_ids.is_null() && count > 0 { - unsafe { - // Reconstruct the boxed slice with the correct DST pointer - let _ = Box::from_raw(std::ptr::slice_from_raw_parts_mut(wallet_ids, count * 32)); - } - } -} - -/// Free address array -/// -/// # Safety -/// -/// - `addresses` must be a valid pointer to an array of C string pointers allocated by this library -/// - `count` must match the original allocation size -/// - Each address pointer in the array must be either null or a valid C string allocated by this library -/// - The pointers must not be used after calling this function -/// - This function must only be called once per allocation -#[no_mangle] -pub unsafe extern "C" fn wallet_manager_free_addresses(addresses: *mut *mut c_char, count: usize) { - if !addresses.is_null() { - let slice = std::slice::from_raw_parts_mut(addresses, count); - for addr in slice { - if !addr.is_null() { - let _ = CString::from_raw(*addr); - } - } - // Free the array itself (matches boxed slice allocation) - let _ = Box::from_raw(std::ptr::slice_from_raw_parts_mut(addresses, count)); - } -} diff --git a/key-wallet-ffi/src/wallet_manager_serialization_tests.rs b/key-wallet-ffi/src/wallet_manager_serialization_tests.rs deleted file mode 100644 index df78f06a6..000000000 --- a/key-wallet-ffi/src/wallet_manager_serialization_tests.rs +++ /dev/null @@ -1,364 +0,0 @@ -//! Tests for wallet serialization FFI functions - -#[cfg(all(test, feature = "bincode"))] -mod tests { - use crate::error::{FFIError, FFIErrorCode}; - use crate::types::FFIWalletAccountCreationOptions; - use crate::wallet_manager; - use dash_network::ffi::FFINetwork; - use std::ffi::CString; - use std::ptr; - - const TEST_MNEMONIC: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; - - #[test] - fn test_create_wallet_return_serialized_bytes_full_wallet() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - // Create a wallet manager - let manager = unsafe { wallet_manager::wallet_manager_create(FFINetwork::Testnet, error) }; - assert!(!manager.is_null()); - - let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); - - let mut wallet_bytes_out: *mut u8 = ptr::null_mut(); - let mut wallet_bytes_len_out: usize = 0; - let mut wallet_id_out = [0u8; 32]; - - // Create a full wallet with private keys - let success = unsafe { - wallet_manager::wallet_manager_add_wallet_from_mnemonic_return_serialized_bytes( - manager, - mnemonic.as_ptr(), - 0, // birth_height - ptr::null(), // default account options - false, // don't downgrade to pubkey wallet - false, // allow_external_signing - &mut wallet_bytes_out, - &mut wallet_bytes_len_out, - wallet_id_out.as_mut_ptr(), - error, - ) - }; - - assert!(success, "Failed to create wallet"); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); - assert!(!wallet_bytes_out.is_null()); - assert!(wallet_bytes_len_out > 0); - - // Verify wallet ID is not all zeros - assert!(wallet_id_out.iter().any(|&b| b != 0), "Wallet ID should not be all zeros"); - - // Clean up - unsafe { - wallet_manager::wallet_manager_free_wallet_bytes( - wallet_bytes_out, - wallet_bytes_len_out, - ); - wallet_manager::wallet_manager_free(manager); - } - } - - #[test] - fn test_create_wallet_return_serialized_bytes_watch_only() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - // Create a wallet manager - let manager = unsafe { wallet_manager::wallet_manager_create(FFINetwork::Testnet, error) }; - assert!(!manager.is_null()); - - let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); - - let mut wallet_bytes_out: *mut u8 = ptr::null_mut(); - let mut wallet_bytes_len_out: usize = 0; - let mut wallet_id_out = [0u8; 32]; - - // Create a watch-only wallet (no private keys) - let success = unsafe { - wallet_manager::wallet_manager_add_wallet_from_mnemonic_return_serialized_bytes( - manager, - mnemonic.as_ptr(), - 0, - ptr::null(), - true, // downgrade to pubkey wallet - false, // watch-only - &mut wallet_bytes_out, - &mut wallet_bytes_len_out, - wallet_id_out.as_mut_ptr(), - error, - ) - }; - - assert!(success, "Failed to create watch-only wallet"); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); - assert!(!wallet_bytes_out.is_null()); - assert!(wallet_bytes_len_out > 0); - - // Clean up - unsafe { - wallet_manager::wallet_manager_free_wallet_bytes( - wallet_bytes_out, - wallet_bytes_len_out, - ); - wallet_manager::wallet_manager_free(manager); - } - } - - #[test] - fn test_create_wallet_return_serialized_bytes_externally_signable() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - // Create a wallet manager - let manager = unsafe { wallet_manager::wallet_manager_create(FFINetwork::Testnet, error) }; - assert!(!manager.is_null()); - - let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); - - let mut wallet_bytes_out: *mut u8 = ptr::null_mut(); - let mut wallet_bytes_len_out: usize = 0; - let mut wallet_id_out = [0u8; 32]; - - // Create an externally signable wallet - let success = unsafe { - wallet_manager::wallet_manager_add_wallet_from_mnemonic_return_serialized_bytes( - manager, - mnemonic.as_ptr(), - 0, - ptr::null(), - true, // downgrade to pubkey wallet - true, // externally signable - &mut wallet_bytes_out, - &mut wallet_bytes_len_out, - wallet_id_out.as_mut_ptr(), - error, - ) - }; - - assert!(success, "Failed to create externally signable wallet"); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); - assert!(!wallet_bytes_out.is_null()); - assert!(wallet_bytes_len_out > 0); - - // Clean up - unsafe { - wallet_manager::wallet_manager_free_wallet_bytes( - wallet_bytes_out, - wallet_bytes_len_out, - ); - wallet_manager::wallet_manager_free(manager); - } - } - - #[test] - fn test_import_serialized_wallet() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - // Create a wallet manager - let manager1 = unsafe { wallet_manager::wallet_manager_create(FFINetwork::Testnet, error) }; - assert!(!manager1.is_null()); - - let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); - - let mut wallet_bytes_out: *mut u8 = ptr::null_mut(); - let mut wallet_bytes_len_out: usize = 0; - let mut original_wallet_id = [0u8; 32]; - - // First create and serialize a wallet - let success = unsafe { - wallet_manager::wallet_manager_add_wallet_from_mnemonic_return_serialized_bytes( - manager1, - mnemonic.as_ptr(), - 0, - ptr::null(), - false, - false, - &mut wallet_bytes_out, - &mut wallet_bytes_len_out, - original_wallet_id.as_mut_ptr(), - error, - ) - }; - - assert!(success); - assert!(!wallet_bytes_out.is_null()); - - // Now import the wallet into a new manager - let manager2 = unsafe { wallet_manager::wallet_manager_create(FFINetwork::Testnet, error) }; - assert!(!manager2.is_null()); - - let mut imported_wallet_id = [0u8; 32]; - let import_success = unsafe { - wallet_manager::wallet_manager_import_wallet_from_bytes( - manager2, - wallet_bytes_out, - wallet_bytes_len_out, - imported_wallet_id.as_mut_ptr(), - error, - ) - }; - - assert!(import_success, "Failed to import wallet"); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); - - // Wallet IDs should match - assert_eq!(original_wallet_id, imported_wallet_id, "Wallet IDs should match"); - - // Clean up - unsafe { - wallet_manager::wallet_manager_free_wallet_bytes( - wallet_bytes_out, - wallet_bytes_len_out, - ); - wallet_manager::wallet_manager_free(manager1); - wallet_manager::wallet_manager_free(manager2); - } - } - - #[test] - fn test_invalid_mnemonic() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - // Create a wallet manager - let manager = unsafe { wallet_manager::wallet_manager_create(FFINetwork::Testnet, error) }; - assert!(!manager.is_null()); - - let invalid_mnemonic = CString::new("invalid mnemonic phrase").unwrap(); - - let mut wallet_bytes_out: *mut u8 = ptr::null_mut(); - let mut wallet_bytes_len_out: usize = 0; - let mut wallet_id_out = [0u8; 32]; - - let success = unsafe { - wallet_manager::wallet_manager_add_wallet_from_mnemonic_return_serialized_bytes( - manager, - invalid_mnemonic.as_ptr(), - 0, - ptr::null(), - false, - false, - &mut wallet_bytes_out, - &mut wallet_bytes_len_out, - wallet_id_out.as_mut_ptr(), - error, - ) - }; - - assert!(!success, "Should fail with invalid mnemonic"); - assert_ne!(unsafe { (*error).code }, FFIErrorCode::Success); - assert!(wallet_bytes_out.is_null()); - assert_eq!(wallet_bytes_len_out, 0); - - // Clean up - unsafe { - wallet_manager::wallet_manager_free(manager); - } - } - - #[test] - fn test_null_mnemonic() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - // Create a wallet manager - let manager = unsafe { wallet_manager::wallet_manager_create(FFINetwork::Testnet, error) }; - assert!(!manager.is_null()); - - let mut wallet_bytes_out: *mut u8 = ptr::null_mut(); - let mut wallet_bytes_len_out: usize = 0; - let mut wallet_id_out = [0u8; 32]; - - let success = unsafe { - wallet_manager::wallet_manager_add_wallet_from_mnemonic_return_serialized_bytes( - manager, - ptr::null(), - 0, - ptr::null(), - false, - false, - &mut wallet_bytes_out, - &mut wallet_bytes_len_out, - wallet_id_out.as_mut_ptr(), - error, - ) - }; - - assert!(!success, "Should fail with null mnemonic"); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); - - // Clean up - unsafe { - wallet_manager::wallet_manager_free(manager); - } - } - - #[test] - fn test_create_wallet_with_custom_account_options() { - use crate::types::FFIAccountCreationOptionType; - - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - // Create a wallet manager - let manager = unsafe { wallet_manager::wallet_manager_create(FFINetwork::Testnet, error) }; - assert!(!manager.is_null()); - - let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); - - // Create custom account options (BIP44 accounts only) - let bip44_indices = [0u32, 1u32, 2u32]; - - let account_options = FFIWalletAccountCreationOptions { - option_type: FFIAccountCreationOptionType::BIP44AccountsOnly, - bip44_indices: bip44_indices.as_ptr(), - bip44_count: bip44_indices.len(), - bip32_indices: ptr::null(), - bip32_count: 0, - coinjoin_indices: ptr::null(), - coinjoin_count: 0, - topup_indices: ptr::null(), - topup_count: 0, - platform_payment_specs: ptr::null(), - platform_payment_count: 0, - special_account_types: ptr::null(), - special_account_types_count: 0, - }; - - let mut wallet_bytes_out: *mut u8 = ptr::null_mut(); - let mut wallet_bytes_len_out: usize = 0; - let mut wallet_id_out = [0u8; 32]; - - let success = unsafe { - wallet_manager::wallet_manager_add_wallet_from_mnemonic_return_serialized_bytes( - manager, - mnemonic.as_ptr(), - 0, - &account_options, - false, - false, - &mut wallet_bytes_out, - &mut wallet_bytes_len_out, - wallet_id_out.as_mut_ptr(), - error, - ) - }; - - assert!(success, "Failed to create wallet with custom options"); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); - assert!(!wallet_bytes_out.is_null()); - assert!(wallet_bytes_len_out > 0); - - // Clean up - unsafe { - wallet_manager::wallet_manager_free_wallet_bytes( - wallet_bytes_out, - wallet_bytes_len_out, - ); - wallet_manager::wallet_manager_free(manager); - } - } -} diff --git a/key-wallet-ffi/src/wallet_manager_tests.rs b/key-wallet-ffi/src/wallet_manager_tests.rs deleted file mode 100644 index 76abf7e48..000000000 --- a/key-wallet-ffi/src/wallet_manager_tests.rs +++ /dev/null @@ -1,1117 +0,0 @@ -//! Unit tests for wallet_manager FFI module - -#[cfg(test)] -#[allow(clippy::module_inception)] -mod tests { - use crate::error::{FFIError, FFIErrorCode}; - use crate::{wallet, wallet_manager}; - use dash_network::ffi::FFINetwork; - use key_wallet_manager::{WalletId, WalletInterface}; - use std::ffi::{CStr, CString}; - use std::ptr; - use std::slice; - - const TEST_MNEMONIC: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; - const TEST_MNEMONIC_2: &str = - "letter advice cage absurd amount doctor acoustic avoid letter advice cage above"; - const TEST_MNEMONIC_3: &str = "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong"; - - #[test] - fn test_wallet_manager_creation() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - // Create a wallet manager - let manager = unsafe { wallet_manager::wallet_manager_create(FFINetwork::Testnet, error) }; - assert_eq!(unsafe { (*manager).network() }, FFINetwork::Testnet); - - assert!(!manager.is_null()); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); - - // Verify initial state - let count = unsafe { wallet_manager::wallet_manager_wallet_count(manager, error) }; - assert_eq!(count, 0); - - // Clean up - unsafe { - wallet_manager::wallet_manager_free(manager); - } - } - - #[test] - fn test_add_wallet_from_mnemonic() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - let manager = unsafe { wallet_manager::wallet_manager_create(FFINetwork::Testnet, error) }; - assert!(!manager.is_null()); - - // Add a wallet from mnemonic - let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); - - let success = unsafe { - wallet_manager::wallet_manager_add_wallet_from_mnemonic( - manager, - mnemonic.as_ptr(), - error, - ) - }; - - assert!(success); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); - - // Verify wallet was added - let count = unsafe { wallet_manager::wallet_manager_wallet_count(manager, error) }; - assert_eq!(count, 1); - - // Clean up - unsafe { - wallet_manager::wallet_manager_free(manager); - } - } - - #[test] - fn test_get_wallet_ids() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - let manager = unsafe { wallet_manager::wallet_manager_create(FFINetwork::Testnet, error) }; - assert!(!manager.is_null()); - - // Use distinct mnemonics so each wallet has a unique ID. - let mnemonics = [TEST_MNEMONIC, TEST_MNEMONIC_2, TEST_MNEMONIC_3]; - unsafe { - for (i, mnemonic_str) in mnemonics.iter().enumerate() { - let mnemonic = CString::new(*mnemonic_str).unwrap(); - - let success = wallet_manager::wallet_manager_add_wallet_from_mnemonic( - manager, - mnemonic.as_ptr(), - error, - ); - if !success { - println!("Failed to add wallet {}! Error code: {:?}", i, (*error).code); - if !(*error).message.is_null() { - let msg = CStr::from_ptr((*error).message); - println!("Error message: {:?}", msg); - } - } - assert!(success, "Failed to add wallet {}", i); - } - } - - // Get wallet IDs - let mut wallet_ids: *mut u8 = ptr::null_mut(); - let mut count: usize = 0; - - let success = unsafe { - wallet_manager::wallet_manager_get_wallet_ids( - manager, - &mut wallet_ids, - &mut count, - error, - ) - }; - - assert!(success); - assert_eq!(count, 3); - assert!(!wallet_ids.is_null()); - - // Verify IDs are unique - let ids = unsafe { - let mut unique_ids = Vec::new(); - for i in 0..count { - let id_ptr = wallet_ids.add(i * 32); - let id = slice::from_raw_parts(id_ptr, 32); - unique_ids.push(id.to_vec()); - } - unique_ids - }; - - // Check all IDs are different - for i in 0..ids.len() { - for j in (i + 1)..ids.len() { - assert_ne!(ids[i], ids[j]); - } - } - - // Clean up - unsafe { - wallet_manager::wallet_manager_free_wallet_ids(wallet_ids, count); - wallet_manager::wallet_manager_free(manager); - } - } - - #[test] - fn test_wallet_balance() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - let manager = unsafe { wallet_manager::wallet_manager_create(FFINetwork::Testnet, error) }; - assert!(!manager.is_null()); - - // Add a wallet - let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); - let success = unsafe { - wallet_manager::wallet_manager_add_wallet_from_mnemonic( - manager, - mnemonic.as_ptr(), - error, - ) - }; - assert!(success); - - // Get wallet ID - let mut wallet_ids: *mut u8 = ptr::null_mut(); - let mut count: usize = 0; - - let success = unsafe { - wallet_manager::wallet_manager_get_wallet_ids( - manager, - &mut wallet_ids, - &mut count, - error, - ) - }; - assert!(success); - - // Get wallet balance - let mut confirmed: u64 = 0; - let mut unconfirmed: u64 = 0; - - let success = unsafe { - wallet_manager::wallet_manager_get_wallet_balance( - manager, - wallet_ids, - &mut confirmed, - &mut unconfirmed, - error, - ) - }; - - assert!(success); - assert_eq!(confirmed, 0); // New wallet has no balance - assert_eq!(unconfirmed, 0); - - // Clean up - unsafe { - wallet_manager::wallet_manager_free_wallet_ids(wallet_ids, count); - wallet_manager::wallet_manager_free(manager); - } - } - - #[test] - fn test_error_handling() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - // Test with null manager - let count = unsafe { wallet_manager::wallet_manager_wallet_count(ptr::null(), error) }; - assert_eq!(count, 0); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); - - // Test with invalid mnemonic - let manager = unsafe { wallet_manager::wallet_manager_create(FFINetwork::Testnet, error) }; - assert!(!manager.is_null()); - - let invalid_mnemonic = CString::new("invalid mnemonic").unwrap(); - let success = unsafe { - wallet_manager::wallet_manager_add_wallet_from_mnemonic( - manager, - invalid_mnemonic.as_ptr(), - error, - ) - }; - assert!(!success); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidMnemonic); - - // Clean up - unsafe { - wallet_manager::wallet_manager_free(manager); - } - } - - #[test] - fn test_wallet_manager_add_wallet_with_account_count() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - let manager = unsafe { wallet_manager::wallet_manager_create(FFINetwork::Testnet, error) }; - assert!(!manager.is_null()); - - // Add a wallet with account count - let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); - - let success = unsafe { - wallet_manager::wallet_manager_add_wallet_from_mnemonic( - manager, - mnemonic.as_ptr(), - error, - ) - }; - assert!(success); - - // Verify wallet was added - let count = unsafe { wallet_manager::wallet_manager_wallet_count(manager, error) }; - assert_eq!(count, 1); - - // Clean up - unsafe { - wallet_manager::wallet_manager_free(manager); - } - } - - #[test] - fn test_wallet_manager_get_wallet() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - let manager = unsafe { wallet_manager::wallet_manager_create(FFINetwork::Testnet, error) }; - assert!(!manager.is_null()); - - // Add a wallet - let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); - let success = unsafe { - wallet_manager::wallet_manager_add_wallet_from_mnemonic( - manager, - mnemonic.as_ptr(), - error, - ) - }; - assert!(success); - - // Get wallet ID - let mut wallet_ids: *mut u8 = ptr::null_mut(); - let mut id_count: usize = 0; - let success = unsafe { - wallet_manager::wallet_manager_get_wallet_ids( - manager, - &mut wallet_ids, - &mut id_count, - error, - ) - }; - assert!(success); - - // Get the wallet - now implemented, should return a valid wallet - let wallet = - unsafe { wallet_manager::wallet_manager_get_wallet(manager, wallet_ids, error) }; - assert!(!wallet.is_null()); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); - - // Clean up the wallet (cast from const to mut for free) - unsafe { - wallet::wallet_free(wallet as *mut _); - } - - // Clean up - unsafe { - wallet_manager::wallet_manager_free_wallet_ids(wallet_ids, id_count); - wallet_manager::wallet_manager_free(manager); - } - } - - #[test] - fn test_wallet_manager_get_wallet_balance() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - let manager = unsafe { wallet_manager::wallet_manager_create(FFINetwork::Testnet, error) }; - assert!(!manager.is_null()); - - // Add wallet - let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); - let success = unsafe { - wallet_manager::wallet_manager_add_wallet_from_mnemonic( - manager, - mnemonic.as_ptr(), - error, - ) - }; - assert!(success); - - // Get wallet ID - let mut wallet_ids: *mut u8 = ptr::null_mut(); - let mut id_count: usize = 0; - let success = unsafe { - wallet_manager::wallet_manager_get_wallet_ids( - manager, - &mut wallet_ids, - &mut id_count, - error, - ) - }; - assert!(success); - - // Get wallet balance - let mut confirmed_balance: u64 = 0; - let mut unconfirmed_balance: u64 = 0; - let success = unsafe { - wallet_manager::wallet_manager_get_wallet_balance( - manager, - wallet_ids, - &mut confirmed_balance, - &mut unconfirmed_balance, - error, - ) - }; - - // Should succeed and balance should be 0 for new wallet - assert!(success); - assert_eq!(confirmed_balance, 0); - assert_eq!(unconfirmed_balance, 0); - - // Clean up - unsafe { - wallet_manager::wallet_manager_free_wallet_ids(wallet_ids, id_count); - wallet_manager::wallet_manager_free(manager); - } - } - - // Removed old test_wallet_manager_process_transaction - see updated version below - - #[test] - fn test_wallet_manager_null_inputs() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - // Test null manager operations - let count = unsafe { wallet_manager::wallet_manager_wallet_count(ptr::null(), error) }; - assert_eq!(count, 0); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); - - // Test null manager with get_wallet_balance - let mut confirmed: u64 = 0; - let mut unconfirmed: u64 = 0; - let null_wallet_id = [0u8; 32]; - let success = unsafe { - wallet_manager::wallet_manager_get_wallet_balance( - ptr::null_mut(), - null_wallet_id.as_ptr(), - &mut confirmed, - &mut unconfirmed, - error, - ) - }; - assert!(!success); - - // Test adding wallet with null manager - let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); - let success = unsafe { - wallet_manager::wallet_manager_add_wallet_from_mnemonic( - ptr::null_mut(), - mnemonic.as_ptr(), - error, - ) - }; - assert!(!success); - } - - #[test] - fn test_wallet_manager_free_null() { - // Should handle null gracefully - unsafe { - wallet_manager::wallet_manager_free(ptr::null_mut()); - } - } - - #[test] - fn test_wallet_manager_synced_height() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - let manager = unsafe { wallet_manager::wallet_manager_create(FFINetwork::Testnet, error) }; - assert!(!manager.is_null()); - - // Get initial height - let height = unsafe { wallet_manager::wallet_manager_current_height(manager, error) }; - assert_eq!(height, 0); - - // Updating last-processed height for an unknown wallet is a no-op. - let unknown_wallet: WalletId = [0xff; 32]; - let new_height = 12345; - unsafe { - let manager_ref = &*manager; - manager_ref.runtime.block_on(async { - let mut manager_guard = manager_ref.manager.write().await; - manager_guard.update_wallet_last_processed_height(&unknown_wallet, new_height); - }); - } - - // Get updated height - let current_height = - unsafe { wallet_manager::wallet_manager_current_height(manager, error) }; - assert_eq!(current_height, 0); - - // Clean up - unsafe { - wallet_manager::wallet_manager_free(manager); - } - } - - #[test] - fn test_wallet_manager_get_wallet_balance_implementation() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - let manager = unsafe { wallet_manager::wallet_manager_create(FFINetwork::Testnet, error) }; - assert!(!manager.is_null()); - - // Add a wallet from mnemonic - let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); - - let success = unsafe { - wallet_manager::wallet_manager_add_wallet_from_mnemonic( - manager, - mnemonic.as_ptr(), - error, - ) - }; - assert!(success); - - // Get wallet IDs to test balance retrieval - let mut wallet_ids: *mut u8 = ptr::null_mut(); - let mut id_count: usize = 0; - - let success = unsafe { - wallet_manager::wallet_manager_get_wallet_ids( - manager, - &mut wallet_ids as *mut *mut u8, - &mut id_count as *mut usize, - error, - ) - }; - assert!(success); - assert_eq!(id_count, 1); - assert!(!wallet_ids.is_null()); - - // Get the wallet balance (should be 0 for a new wallet) - let mut confirmed: u64 = 0; - let mut unconfirmed: u64 = 0; - - let wallet_id_slice = unsafe { slice::from_raw_parts(wallet_ids, 32) }; - let success = unsafe { - wallet_manager::wallet_manager_get_wallet_balance( - manager, - wallet_id_slice.as_ptr(), - &mut confirmed, - &mut unconfirmed, - error, - ) - }; - assert!(success); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); - - // New wallet should have 0 balance - assert_eq!(confirmed, 0); - assert_eq!(unconfirmed, 0); - - // Test with null manager - let success = unsafe { - wallet_manager::wallet_manager_get_wallet_balance( - ptr::null(), - wallet_id_slice.as_ptr(), - &mut confirmed, - &mut unconfirmed, - error, - ) - }; - assert!(!success); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); - - // Test with null wallet_id - let success = unsafe { - wallet_manager::wallet_manager_get_wallet_balance( - manager, - ptr::null(), - &mut confirmed, - &mut unconfirmed, - error, - ) - }; - assert!(!success); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); - - // Test with null output pointers - let success = unsafe { - wallet_manager::wallet_manager_get_wallet_balance( - manager, - wallet_id_slice.as_ptr(), - ptr::null_mut(), - &mut unconfirmed, - error, - ) - }; - assert!(!success); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); - - // Test with invalid wallet ID (all zeros which won't match any wallet) - let invalid_wallet_id = [0u8; 32]; - let success = unsafe { - wallet_manager::wallet_manager_get_wallet_balance( - manager, - invalid_wallet_id.as_ptr(), - &mut confirmed, - &mut unconfirmed, - error, - ) - }; - assert!(!success); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::NotFound); - - // Clean up - unsafe { - wallet_manager::wallet_manager_free_wallet_ids(wallet_ids, id_count); - wallet_manager::wallet_manager_free(manager); - } - } - - #[test] - fn test_wallet_manager_process_transaction() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - let manager = unsafe { wallet_manager::wallet_manager_create(FFINetwork::Testnet, error) }; - assert!(!manager.is_null()); - - // Add a wallet from mnemonic - let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); - - let success = unsafe { - wallet_manager::wallet_manager_add_wallet_from_mnemonic( - manager, - mnemonic.as_ptr(), - error, - ) - }; - assert!(success); - - // Create a sample transaction bytes (this is a minimal valid transaction structure) - // This is a simplified transaction for testing - in real use you'd have actual transaction data - let tx_bytes = [ - 0x02, 0x00, 0x00, 0x00, // version - 0x00, // input count - 0x00, // output count - 0x00, 0x00, 0x00, 0x00, // locktime - ]; - - // Create transaction contexts for testing - let mempool_context = crate::types::FFITransactionContext::mempool(); - - let block_context = - crate::types::FFITransactionContext::in_block(crate::types::FFIBlockInfo { - height: 100000, - block_hash: [0u8; 32], - timestamp: 1234567890, - }); - - // Test processing a mempool transaction - let processed = unsafe { - wallet_manager::wallet_manager_process_transaction( - manager, - tx_bytes.as_ptr(), - tx_bytes.len(), - &mempool_context, - false, - error, - ) - }; - - // The transaction is invalid (simplified format), so deserialization will fail - assert!(!processed); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); - - // Test processing a block transaction - let processed = unsafe { - wallet_manager::wallet_manager_process_transaction( - manager, - tx_bytes.as_ptr(), - tx_bytes.len(), - &block_context, - false, - error, - ) - }; - assert!(!processed); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); - - // Test processing a chain-locked block transaction - let chain_locked_context = crate::types::FFITransactionContext::in_chain_locked_block( - crate::types::FFIBlockInfo { - height: 100000, - block_hash: [0u8; 32], - timestamp: 1234567890, - }, - ); - let processed = unsafe { - wallet_manager::wallet_manager_process_transaction( - manager, - tx_bytes.as_ptr(), - tx_bytes.len(), - &chain_locked_context, - true, - error, - ) - }; - assert!(!processed); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); - - // Test with null manager - let processed = unsafe { - wallet_manager::wallet_manager_process_transaction( - ptr::null_mut(), - tx_bytes.as_ptr(), - tx_bytes.len(), - &mempool_context, - false, - error, - ) - }; - assert!(!processed); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); - - // Test with null transaction bytes - let processed = unsafe { - wallet_manager::wallet_manager_process_transaction( - manager, - ptr::null(), - 10, - &mempool_context, - false, - error, - ) - }; - assert!(!processed); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); - - // Test with zero length - let processed = unsafe { - wallet_manager::wallet_manager_process_transaction( - manager, - tx_bytes.as_ptr(), - 0, - &mempool_context, - false, - error, - ) - }; - assert!(!processed); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); - - // Test with invalid transaction bytes - let invalid_tx = [0xFF, 0xFF, 0xFF]; - let processed = unsafe { - wallet_manager::wallet_manager_process_transaction( - manager, - invalid_tx.as_ptr(), - invalid_tx.len(), - &mempool_context, - false, - error, - ) - }; - assert!(!processed); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); - - // Clean up - unsafe { - wallet_manager::wallet_manager_free(manager); - } - } - - #[test] - fn test_wallet_manager_get_wallet_and_info() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - let manager = unsafe { wallet_manager::wallet_manager_create(FFINetwork::Testnet, error) }; - assert!(!manager.is_null()); - - // Add a wallet from mnemonic - let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); - - let success = unsafe { - wallet_manager::wallet_manager_add_wallet_from_mnemonic( - manager, - mnemonic.as_ptr(), - error, - ) - }; - assert!(success); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); - - // Get wallet IDs - let mut wallet_ids: *mut u8 = ptr::null_mut(); - let mut id_count: usize = 0; - - let success = unsafe { - wallet_manager::wallet_manager_get_wallet_ids( - manager, - &mut wallet_ids as *mut *mut u8, - &mut id_count as *mut usize, - error, - ) - }; - assert!(success); - assert_eq!(id_count, 1); - assert!(!wallet_ids.is_null()); - - let wallet_id_slice = unsafe { slice::from_raw_parts(wallet_ids, 32) }; - - // Test getting the wallet - let valid_wallet = unsafe { - wallet_manager::wallet_manager_get_wallet(manager, wallet_id_slice.as_ptr(), error) - }; - assert!(!valid_wallet.is_null()); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); - - // Test getting the managed wallet info - let valid_wallet_info = unsafe { - wallet_manager::wallet_manager_get_managed_wallet_info( - manager, - wallet_id_slice.as_ptr(), - error, - ) - }; - assert!(!valid_wallet_info.is_null()); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); - - // Test with invalid wallet ID (all zeros) - let invalid_wallet_id = [0u8; 32]; - - let invalid_wallet = unsafe { - wallet_manager::wallet_manager_get_wallet(manager, invalid_wallet_id.as_ptr(), error) - }; - assert!(invalid_wallet.is_null()); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::NotFound); - - let invalid_wallet_info = unsafe { - wallet_manager::wallet_manager_get_managed_wallet_info( - manager, - invalid_wallet_id.as_ptr(), - error, - ) - }; - assert!(invalid_wallet_info.is_null()); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::NotFound); - - // Test with null manager - let null_wallet = unsafe { - wallet_manager::wallet_manager_get_wallet(ptr::null(), wallet_id_slice.as_ptr(), error) - }; - assert!(null_wallet.is_null()); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); - - let null_wallet_info = unsafe { - wallet_manager::wallet_manager_get_managed_wallet_info( - ptr::null(), - wallet_id_slice.as_ptr(), - error, - ) - }; - assert!(null_wallet_info.is_null()); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); - - // Clean up - unsafe { - // Free the valid wallet (cast from const to mut for free) - wallet::wallet_free(valid_wallet as *mut _); - // Free the valid managed wallet info - crate::managed_wallet::managed_wallet_info_free(valid_wallet_info); - // Free the wallet IDs - wallet_manager::wallet_manager_free_wallet_ids(wallet_ids, id_count); - // Free the manager - wallet_manager::wallet_manager_free(manager); - } - } - - #[cfg(feature = "bincode")] - #[test] - fn test_create_wallet_from_mnemonic_return_serialized_bytes() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - // Create a wallet manager - let manager = unsafe { wallet_manager::wallet_manager_create(FFINetwork::Testnet, error) }; - assert!(!manager.is_null()); - - // Test basic wallet creation and serialization - let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); - - let mut wallet_bytes_out: *mut u8 = ptr::null_mut(); - let mut wallet_bytes_len_out: usize = 0; - let mut wallet_id_out = [0u8; 32]; - - let success = unsafe { - crate::wallet_manager::wallet_manager_add_wallet_from_mnemonic_return_serialized_bytes( - manager, - mnemonic.as_ptr(), - 0, // birth_height - ptr::null(), // default account options - false, // don't downgrade to pubkey wallet - false, // allow_external_signing - &mut wallet_bytes_out as *mut *mut u8, - &mut wallet_bytes_len_out as *mut usize, - wallet_id_out.as_mut_ptr(), - error, - ) - }; - - assert!(success); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); - assert!(!wallet_bytes_out.is_null()); - assert!(wallet_bytes_len_out > 0); - assert_ne!(wallet_id_out, [0u8; 32]); - - // Store the wallet ID for comparison - let original_wallet_id = wallet_id_out; - - // Clean up the serialized bytes - unsafe { - crate::wallet_manager::wallet_manager_free_wallet_bytes( - wallet_bytes_out, - wallet_bytes_len_out, - ); - } - - // Test with downgrade to watch-only wallet (create new manager to avoid duplicate wallet ID) - let manager2 = unsafe { wallet_manager::wallet_manager_create(FFINetwork::Testnet, error) }; - assert!(!manager2.is_null()); - - let mut wallet_bytes_out: *mut u8 = ptr::null_mut(); - let mut wallet_bytes_len_out: usize = 0; - let mut wallet_id_out = [0u8; 32]; - - let success = unsafe { - crate::wallet_manager::wallet_manager_add_wallet_from_mnemonic_return_serialized_bytes( - manager2, - mnemonic.as_ptr(), - 0, - ptr::null(), - true, // downgrade to pubkey wallet - false, // watch-only, not externally signable - &mut wallet_bytes_out as *mut *mut u8, - &mut wallet_bytes_len_out as *mut usize, - wallet_id_out.as_mut_ptr(), - error, - ) - }; - - if !success { - let error_msg = if unsafe { (*error).message.is_null() } { - "No error message".to_string() - } else { - unsafe { std::ffi::CStr::from_ptr((*error).message).to_string_lossy().to_string() } - }; - panic!("Function failed with error: {:?} - {}", unsafe { (*error).code }, error_msg); - } - assert!(success); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); - assert!(!wallet_bytes_out.is_null()); - assert!(wallet_bytes_len_out > 0); - // The wallet ID should be the same since it's derived from the same mnemonic - assert_eq!(wallet_id_out, original_wallet_id); - - // Import the watch-only wallet to verify it works (create third manager for import) - let manager3 = unsafe { wallet_manager::wallet_manager_create(FFINetwork::Testnet, error) }; - assert!(!manager3.is_null()); - - let wallet_bytes_slice = - unsafe { slice::from_raw_parts(wallet_bytes_out, wallet_bytes_len_out) }; - let mut import_wallet_id_out = [0u8; 32]; - - let import_success = unsafe { - crate::wallet_manager::wallet_manager_import_wallet_from_bytes( - manager3, - wallet_bytes_slice.as_ptr(), - wallet_bytes_slice.len(), - import_wallet_id_out.as_mut_ptr(), - error, - ) - }; - - assert!(import_success); - assert_eq!(import_wallet_id_out, original_wallet_id); - - // Clean up - unsafe { - crate::wallet_manager::wallet_manager_free_wallet_bytes( - wallet_bytes_out, - wallet_bytes_len_out, - ); - wallet_manager::wallet_manager_free(manager2); - wallet_manager::wallet_manager_free(manager3); - } - - // Test with externally signable wallet (create fourth manager) - let manager4 = unsafe { wallet_manager::wallet_manager_create(FFINetwork::Testnet, error) }; - assert!(!manager4.is_null()); - - let mut wallet_bytes_out: *mut u8 = ptr::null_mut(); - let mut wallet_bytes_len_out: usize = 0; - let mut wallet_id_out = [0u8; 32]; - - let success = unsafe { - crate::wallet_manager::wallet_manager_add_wallet_from_mnemonic_return_serialized_bytes( - manager4, - mnemonic.as_ptr(), - 0, - ptr::null(), - true, // downgrade to pubkey wallet - true, // externally signable - &mut wallet_bytes_out as *mut *mut u8, - &mut wallet_bytes_len_out as *mut usize, - wallet_id_out.as_mut_ptr(), - error, - ) - }; - - assert!(success); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); - assert!(!wallet_bytes_out.is_null()); - assert!(wallet_bytes_len_out > 0); - assert_eq!(wallet_id_out, original_wallet_id); - - // Clean up - unsafe { - crate::wallet_manager::wallet_manager_free_wallet_bytes( - wallet_bytes_out, - wallet_bytes_len_out, - ); - } - - // Test with invalid mnemonic (create fifth manager) - let manager5 = unsafe { wallet_manager::wallet_manager_create(FFINetwork::Testnet, error) }; - assert!(!manager5.is_null()); - - let invalid_mnemonic = CString::new("invalid mnemonic phrase").unwrap(); - let mut wallet_bytes_out: *mut u8 = ptr::null_mut(); - let mut wallet_bytes_len_out: usize = 0; - let mut wallet_id_out = [0u8; 32]; - - let success = unsafe { - crate::wallet_manager::wallet_manager_add_wallet_from_mnemonic_return_serialized_bytes( - manager5, - invalid_mnemonic.as_ptr(), - 0, - ptr::null(), - false, - false, - &mut wallet_bytes_out as *mut *mut u8, - &mut wallet_bytes_len_out as *mut usize, - wallet_id_out.as_mut_ptr(), - error, - ) - }; - - assert!(!success); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidMnemonic); - assert!(wallet_bytes_out.is_null()); - assert_eq!(wallet_bytes_len_out, 0); - - // Clean up all managers - unsafe { - crate::wallet_manager::wallet_manager_free(manager); - crate::wallet_manager::wallet_manager_free(manager4); - crate::wallet_manager::wallet_manager_free(manager5); - } - } - - #[cfg(feature = "bincode")] - #[test] - fn test_serialized_wallet_across_managers() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - // Create first wallet manager - let manager1 = unsafe { wallet_manager::wallet_manager_create(FFINetwork::Testnet, error) }; - assert!(!manager1.is_null()); - - let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); - - let mut wallet_bytes_out: *mut u8 = ptr::null_mut(); - let mut wallet_bytes_len_out: usize = 0; - let mut wallet_id_out = [0u8; 32]; - - // Create and serialize a wallet with the first manager - let success = unsafe { - crate::wallet_manager::wallet_manager_add_wallet_from_mnemonic_return_serialized_bytes( - manager1, - mnemonic.as_ptr(), - 100, // birth_height - ptr::null(), // default account options - false, // don't downgrade to pubkey wallet - false, // allow_external_signing - &mut wallet_bytes_out as *mut *mut u8, - &mut wallet_bytes_len_out as *mut usize, - wallet_id_out.as_mut_ptr(), - error, - ) - }; - - assert!(success); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); - assert!(!wallet_bytes_out.is_null()); - assert!(wallet_bytes_len_out > 0); - - // Store the wallet ID for comparison - let original_wallet_id = wallet_id_out; - - // Create a copy of the serialized bytes before freeing the manager - let wallet_bytes_copy = unsafe { - let mut copy = Vec::with_capacity(wallet_bytes_len_out); - ptr::copy_nonoverlapping(wallet_bytes_out, copy.as_mut_ptr(), wallet_bytes_len_out); - copy.set_len(wallet_bytes_len_out); - copy - }; - - // Clean up the first manager - unsafe { - crate::wallet_manager::wallet_manager_free(manager1); - } - - // Create a completely new wallet manager - let manager2 = unsafe { wallet_manager::wallet_manager_create(FFINetwork::Testnet, error) }; - assert!(!manager2.is_null()); - - // Import the wallet using the serialized bytes in the new manager - let mut import_wallet_id_out = [0u8; 32]; - let import_success = unsafe { - crate::wallet_manager::wallet_manager_import_wallet_from_bytes( - manager2, - wallet_bytes_copy.as_ptr(), - wallet_bytes_copy.len(), - import_wallet_id_out.as_mut_ptr(), - error, - ) - }; - - assert!(import_success); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); - assert_eq!( - import_wallet_id_out, original_wallet_id, - "Wallet ID should be the same after import" - ); - - // Verify we can get the wallet from the new manager - let wallet = unsafe { - crate::wallet_manager::wallet_manager_get_wallet( - manager2, - import_wallet_id_out.as_ptr(), - error, - ) - }; - assert!(!wallet.is_null()); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); - - // Clean up - unsafe { - wallet::wallet_free(wallet as *mut _); - crate::wallet_manager::wallet_manager_free_wallet_bytes( - wallet_bytes_out, - wallet_bytes_len_out, - ); - crate::wallet_manager::wallet_manager_free(manager2); - } - } -} diff --git a/key-wallet-ffi/src/wallet_tests.rs b/key-wallet-ffi/src/wallet_tests.rs deleted file mode 100644 index 922ec7ea3..000000000 --- a/key-wallet-ffi/src/wallet_tests.rs +++ /dev/null @@ -1,364 +0,0 @@ -//! Unit tests for wallet FFI module - -#[cfg(test)] -mod wallet_tests { - use crate::account::account_free; - use crate::error::{FFIError, FFIErrorCode}; - use crate::types::FFIAccountKind; - use crate::wallet; - use dash_network::ffi::FFINetwork; - use std::ffi::CString; - use std::ptr; - - const TEST_MNEMONIC: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; - - #[test] - fn test_wallet_creation_from_mnemonic() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); - - let wallet = unsafe { - wallet::wallet_create_from_mnemonic(mnemonic.as_ptr(), FFINetwork::Testnet, error) - }; - - assert!(!wallet.is_null()); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); - - // Clean up - unsafe { - wallet::wallet_free(wallet); - } - } - - #[test] - fn test_wallet_creation_from_seed() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - let seed = [0x01u8; 64]; - - let wallet = unsafe { - wallet::wallet_create_from_seed(seed.as_ptr(), seed.len(), FFINetwork::Testnet, error) - }; - - assert!(!wallet.is_null()); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); - - // Clean up - unsafe { - wallet::wallet_free(wallet); - } - } - - #[test] - fn test_wallet_creation_methods() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - // Test random wallet creation - let random_wallet = unsafe { wallet::wallet_create_random(FFINetwork::Testnet, error) }; - assert!(!random_wallet.is_null()); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); - - // Verify it's not watch-only - let is_watch_only = unsafe { wallet::wallet_is_watch_only(random_wallet, error) }; - assert!(!is_watch_only); - - // Clean up - unsafe { - wallet::wallet_free(random_wallet); - } - } - - #[test] - fn test_wallet_multiple_accounts() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - let seed = [0x03u8; 64]; - - // Create wallet with multiple accounts - unsafe { - for _account_index in 0..3 { - let wallet = wallet::wallet_create_from_seed( - seed.as_ptr(), - seed.len(), - FFINetwork::Testnet, - error, - ); - - assert!(!wallet.is_null()); - assert_eq!((*error).code, FFIErrorCode::Success); - - // Clean up - wallet::wallet_free(wallet); - } - } - } - - #[test] - fn test_wallet_error_cases() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - // Test with null mnemonic - let wallet = - unsafe { wallet::wallet_create_from_mnemonic(ptr::null(), FFINetwork::Testnet, error) }; - assert!(wallet.is_null()); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); - - // Test with invalid mnemonic - let invalid_mnemonic = CString::new("invalid mnemonic").unwrap(); - let wallet = unsafe { - wallet::wallet_create_from_mnemonic( - invalid_mnemonic.as_ptr(), - FFINetwork::Testnet, - error, - ) - }; - assert!(wallet.is_null()); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidMnemonic); - - // Test with null seed - let wallet = - unsafe { wallet::wallet_create_from_seed(ptr::null(), 64, FFINetwork::Testnet, error) }; - assert!(wallet.is_null()); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); - } - - #[test] - fn test_wallet_id_operations() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - let wallet = unsafe { wallet::wallet_create_random(FFINetwork::Testnet, error) }; - assert!(!wallet.is_null()); - - // Get wallet ID - let mut id = [0u8; 32]; - let success = unsafe { wallet::wallet_get_id(wallet, id.as_mut_ptr(), error) }; - assert!(success); - - // ID should not be all zeros - assert_ne!(id, [0u8; 32]); - - // Test with null buffer - let success = unsafe { wallet::wallet_get_id(wallet, ptr::null_mut(), error) }; - assert!(!success); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); - - // Clean up - unsafe { - wallet::wallet_free(wallet); - } - } - - #[test] - fn test_wallet_create_from_seed_bytes() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - // Create seed bytes directly - let seed_bytes = [0x05u8; 64]; - - let wallet = unsafe { - wallet::wallet_create_from_seed( - seed_bytes.as_ptr(), - seed_bytes.len(), - FFINetwork::Testnet, - error, - ) - }; - - assert!(!wallet.is_null()); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); - - // Clean up - unsafe { - wallet::wallet_free(wallet); - } - } - - #[test] - fn test_wallet_create_from_seed_bytes_null() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - // Test with null seed bytes - let wallet = - unsafe { wallet::wallet_create_from_seed(ptr::null(), 64, FFINetwork::Testnet, error) }; - - assert!(wallet.is_null()); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); - } - - #[test] - fn test_wallet_has_mnemonic() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - // Create wallet from mnemonic - let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); - - let wallet_with_mnemonic = unsafe { - wallet::wallet_create_from_mnemonic(mnemonic.as_ptr(), FFINetwork::Testnet, error) - }; - assert!(!wallet_with_mnemonic.is_null()); - - // Test has_mnemonic - should return true - let has_mnemonic = unsafe { wallet::wallet_has_mnemonic(wallet_with_mnemonic, error) }; - assert!(has_mnemonic); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); - - // Clean up - unsafe { - wallet::wallet_free(wallet_with_mnemonic); - } - } - - #[test] - fn test_wallet_has_mnemonic_null() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - // Test with null wallet - let has_mnemonic = unsafe { wallet::wallet_has_mnemonic(ptr::null(), error) }; - assert!(!has_mnemonic); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); - } - - #[test] - fn test_wallet_add_account() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - let wallet = unsafe { wallet::wallet_create_random(FFINetwork::Testnet, error) }; - assert!(!wallet.is_null()); - - // Test adding account - check if it succeeds or fails gracefully - let result = - unsafe { wallet::wallet_add_account(wallet, FFIAccountKind::StandardBIP44, 1) }; - // Some implementations may not support adding accounts, so just verify it doesn't crash - // and the error code is set appropriately - assert!(!result.account.is_null() || result.error_code != 0); - - // Clean up the account if it was created - if !result.account.is_null() { - unsafe { - account_free(result.account); - } - } - - // Free error message if present - if !result.error_message.is_null() { - unsafe { - let _ = CString::from_raw(result.error_message); - } - } - - // Clean up - unsafe { - wallet::wallet_free(wallet); - } - } - - #[test] - fn test_wallet_add_account_null() { - // Test with null wallet - let result = unsafe { - wallet::wallet_add_account(ptr::null_mut(), FFIAccountKind::StandardBIP44, 0) - }; - assert!(result.account.is_null()); - assert_ne!(result.error_code, 0); - - // Free error message if present - if !result.error_message.is_null() { - unsafe { - let _ = CString::from_raw(result.error_message); - } - } - } - - #[test] - fn test_wallet_create_edge_cases() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - // Test creating from normal seed size - let normal_seed = [0x07u8; 64]; // Standard seed size - let wallet = unsafe { - wallet::wallet_create_from_seed( - normal_seed.as_ptr(), - normal_seed.len(), - FFINetwork::Testnet, - error, - ) - }; - assert!(!wallet.is_null()); - unsafe { - wallet::wallet_free(wallet); - } - - // Test creating from larger seed - let large_seed = [0x08u8; 128]; - let wallet = unsafe { - wallet::wallet_create_from_seed( - large_seed.as_ptr(), - large_seed.len(), - FFINetwork::Testnet, - error, - ) - }; - // Large seeds may or may not be accepted - just test it doesn't crash - if !wallet.is_null() { - unsafe { - wallet::wallet_free(wallet); - } - } - } - - #[test] - fn test_wallet_xpub_operations() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - let wallet = unsafe { wallet::wallet_create_random(FFINetwork::Testnet, error) }; - assert!(!wallet.is_null()); - - // Get xpub for account 0 - let xpub = unsafe { wallet::wallet_get_xpub(wallet, 0, error) }; - assert!(!xpub.is_null()); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); - - // Verify xpub string format - let xpub_str = unsafe { std::ffi::CStr::from_ptr(xpub).to_str().unwrap() }; - assert!(xpub_str.starts_with("tpub")); // Testnet public key - - // Clean up - unsafe { - let _ = CString::from_raw(xpub); - wallet::wallet_free(wallet); - } - } - - #[test] - fn test_wallet_xpub_null_wallet() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - // Test with null wallet - let xpub = unsafe { wallet::wallet_get_xpub(ptr::null(), 0, error) }; - assert!(xpub.is_null()); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); - } - - #[test] - fn test_wallet_free_null() { - // Should handle null gracefully - unsafe { - wallet::wallet_free(ptr::null_mut()); - } - } -} diff --git a/key-wallet-ffi/tests/check_address.rs b/key-wallet-ffi/tests/check_address.rs deleted file mode 100644 index 6d20085a5..000000000 --- a/key-wallet-ffi/tests/check_address.rs +++ /dev/null @@ -1,31 +0,0 @@ -#[test] -fn test_check_address() { - use dashcore::address::NetworkUnchecked; - - // Use a known valid Dash testnet address - let addr_str = "yTw7Kn5CrQvpBQy5dNMT8A3PQnU3kEj7jJ"; - - // Parse the address (NetworkUnchecked is what implements FromStr) - match addr_str.parse::>() { - Ok(addr) => { - println!("Address parsed successfully"); - // Try to require testnet network - match addr.require_network(dashcore::Network::Testnet) { - Ok(addr_checked) => { - println!("Address is valid for testnet: {}", addr_checked); - println!("Address type: {:?}", addr_checked.address_type()); - } - Err(e) => { - println!("Warning: Address network check failed: {}", e); - // Don't panic - just warn, as this might be a version issue - } - } - } - Err(e) => { - // For now, just skip the test if address parsing fails - // This is likely due to version/format incompatibility - println!("Warning: Could not parse address '{}': {}", addr_str, e); - println!("This may be due to library version differences"); - } - } -} diff --git a/key-wallet-ffi/tests/debug_addr.rs b/key-wallet-ffi/tests/debug_addr.rs deleted file mode 100644 index f6d919f0f..000000000 --- a/key-wallet-ffi/tests/debug_addr.rs +++ /dev/null @@ -1,34 +0,0 @@ -#[test] -fn test_debug_address() { - use std::str::FromStr; - - let addr_str = "yTw7Kn5CrQvpBQy5dNMT8A3PQnU3kEj7jJ"; - - println!("Parsing address: {}", addr_str); - - match key_wallet::Address::from_str(addr_str) { - Ok(addr) => { - println!("Address parsed successfully!"); - - // Try different networks - for network in &[ - dashcore::Network::Mainnet, - dashcore::Network::Testnet, - dashcore::Network::Regtest, - dashcore::Network::Devnet, - ] { - match addr.clone().require_network(*network) { - Ok(checked) => { - println!("✓ Valid for {:?}: {}", network, checked); - } - Err(e) => { - println!("✗ Not valid for {:?}: {}", network, e); - } - } - } - } - Err(e) => { - println!("Failed to parse address: {}", e); - } - } -} diff --git a/key-wallet-ffi/tests/debug_wallet_add.rs b/key-wallet-ffi/tests/debug_wallet_add.rs deleted file mode 100644 index 901d9523b..000000000 --- a/key-wallet-ffi/tests/debug_wallet_add.rs +++ /dev/null @@ -1,41 +0,0 @@ -use dash_network::ffi::FFINetwork; - -#[test] -fn test_debug_wallet_add() { - use key_wallet_ffi::error::FFIError; - use key_wallet_ffi::wallet_manager; - use std::ffi::CString; - - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - let manager = unsafe { wallet_manager::wallet_manager_create(FFINetwork::Testnet, error) }; - assert!(!manager.is_null()); - println!("Manager created successfully"); - - let mnemonic = CString::new("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").unwrap(); - - println!("Adding wallet from mnemonic"); - let success = unsafe { - wallet_manager::wallet_manager_add_wallet_from_mnemonic(manager, mnemonic.as_ptr(), error) - }; - - if !success { - unsafe { - println!("Failed to add wallet! Error code: {:?}", (*error).code); - if !(*error).message.is_null() { - let msg = std::ffi::CStr::from_ptr((*error).message); - println!("Error message: {:?}", msg); - } - } - } else { - println!("Successfully added wallet from mnemonic"); - } - - assert!(success); - - // Clean up - unsafe { - wallet_manager::wallet_manager_free(manager); - } -} diff --git a/key-wallet-ffi/tests/ffi_tests.rs b/key-wallet-ffi/tests/ffi_tests.rs deleted file mode 100644 index ecd085e77..000000000 --- a/key-wallet-ffi/tests/ffi_tests.rs +++ /dev/null @@ -1,13 +0,0 @@ -//! FFI tests -//! -//! These tests verify the FFI implementation works correctly. -//! They test the Rust implementation directly, not through generated bindings. - -#[test] -fn test_ffi_types_exist() { - // This test just verifies the crate compiles with all the expected types - use key_wallet_ffi::key_wallet_ffi_initialize; - - // Verify we can call initialize - assert!(key_wallet_ffi_initialize()); -} diff --git a/key-wallet-ffi/tests/integration_test.rs b/key-wallet-ffi/tests/integration_test.rs deleted file mode 100644 index fedc05231..000000000 --- a/key-wallet-ffi/tests/integration_test.rs +++ /dev/null @@ -1,210 +0,0 @@ -//! Integration tests for key-wallet-ffi -//! -//! These tests verify the interaction between different FFI modules - -use dash_network::ffi::FFINetwork; -use key_wallet_ffi::error::{FFIError, FFIErrorCode}; -use std::ffi::CString; -use std::ptr; - -const TEST_MNEMONIC: &str = - "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; - -#[test] -fn test_full_wallet_workflow() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - // 1. Generate a mnemonic - let mnemonic = unsafe { key_wallet_ffi::mnemonic::mnemonic_generate(12, error) }; - assert!(!mnemonic.is_null()); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); - - // 2. Validate the mnemonic - let is_valid = unsafe { key_wallet_ffi::mnemonic::mnemonic_validate(mnemonic, error) }; - assert!(is_valid); - - // 3. Create wallet manager - let manager = unsafe { - key_wallet_ffi::wallet_manager::wallet_manager_create(FFINetwork::Testnet, error) - }; - assert!(!manager.is_null()); - - // 4. Add wallet to manager - let success = unsafe { - key_wallet_ffi::wallet_manager::wallet_manager_add_wallet_from_mnemonic( - manager, mnemonic, error, - ) - }; - assert!(success); - - // 5. Get wallet IDs - let mut wallet_ids: *mut u8 = ptr::null_mut(); - let mut count: usize = 0; - let success = unsafe { - key_wallet_ffi::wallet_manager::wallet_manager_get_wallet_ids( - manager, - &mut wallet_ids, - &mut count, - error, - ) - }; - assert!(success); - assert_eq!(count, 1); - - let wallet_id = wallet_ids; // First wallet ID starts at offset 0 - - // 6. Get balance - let mut confirmed: u64 = 0; - let mut unconfirmed: u64 = 0; - let success = unsafe { - key_wallet_ffi::wallet_manager::wallet_manager_get_wallet_balance( - manager, - wallet_id, - &mut confirmed, - &mut unconfirmed, - error, - ) - }; - assert!(success); - assert_eq!(confirmed, 0); - assert_eq!(unconfirmed, 0); - - // Clean up - unsafe { - key_wallet_ffi::wallet_manager::wallet_manager_free_wallet_ids(wallet_ids, count); - key_wallet_ffi::wallet_manager::wallet_manager_free(manager); - key_wallet_ffi::mnemonic::mnemonic_free(mnemonic); - } -} - -#[test] -fn test_seed_to_wallet_workflow() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - // 1. Convert mnemonic to seed - let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); - let passphrase = CString::new("").unwrap(); - - let mut seed = [0u8; 64]; - let mut seed_len: usize = 0; - - let success = unsafe { - key_wallet_ffi::mnemonic::mnemonic_to_seed( - mnemonic.as_ptr(), - passphrase.as_ptr(), - seed.as_mut_ptr(), - &mut seed_len, - error, - ) - }; - assert!(success); - assert_eq!(seed_len, 64); - - // 2. Create wallet from seed - let wallet = unsafe { - key_wallet_ffi::wallet::wallet_create_from_seed( - seed.as_ptr(), - seed_len, - FFINetwork::Testnet, - error, - ) - }; - assert!(!wallet.is_null()); - - // Clean up - unsafe { - key_wallet_ffi::wallet::wallet_free(wallet); - } -} - -#[test] -fn test_derivation_paths() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - // Test BIP44 paths - let mut path_buffer = vec![0u8; 256]; - - // Account path - let success = unsafe { - key_wallet_ffi::derivation::derivation_bip44_account_path( - FFINetwork::Mainnet, - 0, - path_buffer.as_mut_ptr() as *mut std::os::raw::c_char, - path_buffer.len(), - error, - ) - }; - assert!(success); - - let path_str = unsafe { - std::ffi::CStr::from_ptr(path_buffer.as_ptr() as *const std::os::raw::c_char) - .to_str() - .unwrap() - }; - assert_eq!(path_str, "m/44'/5'/0'"); - - // Payment path - path_buffer.fill(0); - let success = unsafe { - key_wallet_ffi::derivation::derivation_bip44_payment_path( - FFINetwork::Mainnet, - 0, - false, - 5, - path_buffer.as_mut_ptr() as *mut std::os::raw::c_char, - path_buffer.len(), - error, - ) - }; - assert!(success); - - let path_str = unsafe { - std::ffi::CStr::from_ptr(path_buffer.as_ptr() as *const std::os::raw::c_char) - .to_str() - .unwrap() - }; - assert_eq!(path_str, "m/44'/5'/0'/0/5"); -} - -#[test] -fn test_error_handling() { - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - // Test various error conditions - - // 1. Invalid mnemonic - let invalid_mnemonic = CString::new("invalid mnemonic phrase").unwrap(); - let wallet = unsafe { - key_wallet_ffi::wallet::wallet_create_from_mnemonic( - invalid_mnemonic.as_ptr(), - FFINetwork::Testnet, - error, - ) - }; - assert!(wallet.is_null()); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidMnemonic); - - // 2. Null pointer errors - let wallet = unsafe { - key_wallet_ffi::wallet::wallet_create_from_mnemonic(ptr::null(), FFINetwork::Testnet, error) - }; - assert!(wallet.is_null()); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); - - // 3. Invalid seed size - let invalid_seed = [0u8; 10]; // Too small - let wallet = unsafe { - key_wallet_ffi::wallet::wallet_create_from_seed( - invalid_seed.as_ptr(), - invalid_seed.len(), - FFINetwork::Testnet, - error, - ) - }; - assert!(wallet.is_null()); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); -} diff --git a/key-wallet-ffi/tests/test_account_collection.rs b/key-wallet-ffi/tests/test_account_collection.rs deleted file mode 100644 index 035cb900f..000000000 --- a/key-wallet-ffi/tests/test_account_collection.rs +++ /dev/null @@ -1,196 +0,0 @@ -//! Integration tests for account collection FFI functions - -use dash_network::ffi::FFINetwork; -use key_wallet_ffi::account::account_free; -use key_wallet_ffi::account_collection::*; -use key_wallet_ffi::types::{FFIAccountCreationOptionType, FFIWalletAccountCreationOptions}; -use key_wallet_ffi::wallet::{wallet_create_from_mnemonic_with_options, wallet_free}; -use key_wallet_ffi::FFIError; -use std::ffi::CString; -use std::ptr; - -#[test] -fn test_account_collection_comprehensive() { - unsafe { - // Create a test mnemonic - let mnemonic = CString::new( - "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" - ).unwrap(); - let error = &mut FFIError::default(); - - // Create wallet with various account types - let account_options = FFIWalletAccountCreationOptions { - option_type: FFIAccountCreationOptionType::AllAccounts, - bip44_indices: [0, 1, 2].as_ptr(), - bip44_count: 3, - bip32_indices: [0].as_ptr(), - bip32_count: 1, - coinjoin_indices: [0, 1].as_ptr(), - coinjoin_count: 2, - topup_indices: [0, 1, 2].as_ptr(), - topup_count: 3, - platform_payment_specs: ptr::null(), - platform_payment_count: 0, - special_account_types: ptr::null(), - special_account_types_count: 0, - }; - - let wallet = wallet_create_from_mnemonic_with_options( - mnemonic.as_ptr(), - FFINetwork::Testnet, - &account_options, - error, - ); - assert!(!wallet.is_null()); - - // Get account collection for testnet - let collection = wallet_get_account_collection(wallet, error); - assert!(!collection.is_null()); - - // Test account count - let count = account_collection_count(collection); - assert!(count > 0, "Should have at least some accounts"); - - // Test BIP44 accounts - let mut bip44_indices: *mut u32 = ptr::null_mut(); - let mut bip44_count: usize = 0; - let success = - account_collection_get_bip44_indices(collection, &mut bip44_indices, &mut bip44_count); - assert!(success); - assert_eq!(bip44_count, 3, "Should have 3 BIP44 accounts"); - - // Get each BIP44 account - for i in 0..3 { - let account = account_collection_get_bip44_account(collection, i); - assert!(!account.is_null(), "BIP44 account {} should exist", i); - account_free(account); - } - - // Test BIP32 accounts - let mut bip32_indices: *mut u32 = ptr::null_mut(); - let mut bip32_count: usize = 0; - let success = - account_collection_get_bip32_indices(collection, &mut bip32_indices, &mut bip32_count); - assert!(success); - assert_eq!(bip32_count, 1, "Should have 1 BIP32 account"); - - let bip32_account = account_collection_get_bip32_account(collection, 0); - assert!(!bip32_account.is_null()); - account_free(bip32_account); - - // Test CoinJoin accounts - let mut coinjoin_indices: *mut u32 = ptr::null_mut(); - let mut coinjoin_count: usize = 0; - let success = account_collection_get_coinjoin_indices( - collection, - &mut coinjoin_indices, - &mut coinjoin_count, - ); - assert!(success); - assert_eq!(coinjoin_count, 2, "Should have 2 CoinJoin accounts"); - - // Test special accounts existence - assert!(account_collection_has_identity_registration(collection)); - assert!(account_collection_has_identity_invitation(collection)); - assert!(account_collection_has_provider_voting_keys(collection)); - assert!(account_collection_has_provider_owner_keys(collection)); - - // Test getting special accounts - let identity_reg = account_collection_get_identity_registration(collection); - assert!(!identity_reg.is_null()); - account_free(identity_reg); - - let identity_inv = account_collection_get_identity_invitation(collection); - assert!(!identity_inv.is_null()); - account_free(identity_inv); - - // Test identity topup accounts - let mut topup_indices: *mut u32 = ptr::null_mut(); - let mut topup_count: usize = 0; - let success = account_collection_get_identity_topup_indices( - collection, - &mut topup_indices, - &mut topup_count, - ); - assert!(success); - assert_eq!(topup_count, 3, "Should have 3 identity topup accounts"); - - // Get each topup account - for i in 0..3 { - let topup = account_collection_get_identity_topup(collection, i); - assert!(!topup.is_null(), "Identity topup {} should exist", i); - account_free(topup); - } - - // Clean up arrays - if !bip44_indices.is_null() { - free_u32_array(bip44_indices, bip44_count); - } - if !bip32_indices.is_null() { - free_u32_array(bip32_indices, bip32_count); - } - if !coinjoin_indices.is_null() { - free_u32_array(coinjoin_indices, coinjoin_count); - } - if !topup_indices.is_null() { - free_u32_array(topup_indices, topup_count); - } - - // Clean up - account_collection_free(collection); - wallet_free(wallet); - } -} - -#[test] -fn test_account_collection_minimal() { - unsafe { - // Create a test mnemonic - let mnemonic = CString::new( - "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" - ).unwrap(); - let test = &mut FFIError::default(); - - // Create wallet with minimal accounts (default) - let wallet = wallet_create_from_mnemonic_with_options( - mnemonic.as_ptr(), - FFINetwork::Testnet, - ptr::null(), // Use default options - test, - ); - assert!(!wallet.is_null()); - - // Get account collection - let collection = wallet_get_account_collection(wallet, test); - assert!(!collection.is_null()); - - // Should have at least some default accounts - let count = account_collection_count(collection); - assert!(count > 0, "Default wallet should have some accounts"); - - // Check for BIP44 account 0 (should exist by default) - let account0 = account_collection_get_bip44_account(collection, 0); - assert!(!account0.is_null(), "Default wallet should have BIP44 account 0"); - account_free(account0); - - // Clean up - account_collection_free(collection); - wallet_free(wallet); - } -} - -#[test] -fn test_account_collection_null_safety() { - unsafe { - // Test null safety of various functions - assert_eq!(account_collection_count(ptr::null()), 0); - assert!(!account_collection_has_identity_registration(ptr::null())); - assert!(!account_collection_has_identity_invitation(ptr::null())); - assert!(account_collection_get_bip44_account(ptr::null(), 0).is_null()); - assert!(account_collection_get_identity_registration(ptr::null()).is_null()); - - // Test free with null (should not crash) - account_collection_free(ptr::null_mut()); - free_u32_array(ptr::null_mut(), 0); - } -} diff --git a/key-wallet-ffi/tests/test_addr_checksum.rs b/key-wallet-ffi/tests/test_addr_checksum.rs deleted file mode 100644 index 4b5c9fb85..000000000 --- a/key-wallet-ffi/tests/test_addr_checksum.rs +++ /dev/null @@ -1,31 +0,0 @@ -#[test] -fn test_address_checksum() { - // The test uses this address - let's see if it's valid - let test_addr = "yTw7Kn5CrQvpBQy5dNMT8A3PQnU3kEj7jJ"; - - // Try decoding with base58 - use dashcore::base58; - - match base58::decode_check(test_addr) { - Ok(data) => { - println!("Base58 decode successful, {} bytes", data.len()); - if !data.is_empty() { - println!("Version byte: 0x{:02x}", data[0]); - } - } - Err(e) => { - println!("Base58 decode failed: {:?}", e); - } - } - - // Compare with a known good address - let good_addr = "yRd4FhXfVGHXpsuZXPNkMrfD9GVj46pnjt"; - match base58::decode_check(good_addr) { - Ok(data) => { - println!("Good address decoded, {} bytes, version: 0x{:02x}", data.len(), data[0]); - } - Err(e) => { - println!("Good address decode failed: {:?}", e); - } - } -} diff --git a/key-wallet-ffi/tests/test_addr_simple.rs b/key-wallet-ffi/tests/test_addr_simple.rs deleted file mode 100644 index 6fae7e2a5..000000000 --- a/key-wallet-ffi/tests/test_addr_simple.rs +++ /dev/null @@ -1,42 +0,0 @@ -use dash_network::ffi::FFINetwork; - -#[test] -fn test_address_simple() { - use key_wallet_ffi::error::FFIError; - - let mut error = FFIError::default(); - let error = &mut error as *mut FFIError; - - // Create a wallet to get a valid address - let seed = [0x42u8; 64]; - let wallet = unsafe { - key_wallet_ffi::wallet::wallet_create_from_seed( - seed.as_ptr(), - seed.len(), - FFINetwork::Testnet, - error, - ) - }; - assert!(!wallet.is_null()); - - // Since we can't derive addresses directly from wallets anymore, - // we'll test wallet creation and basic properties - let is_watch_only = unsafe { key_wallet_ffi::wallet::wallet_is_watch_only(wallet, error) }; - assert!(!is_watch_only); - - // Get wallet ID to verify it was created - let mut wallet_id = [0u8; 32]; - let success = - unsafe { key_wallet_ffi::wallet::wallet_get_id(wallet, wallet_id.as_mut_ptr(), error) }; - assert!(success); - assert_ne!(wallet_id, [0u8; 32]); - - println!("Generated wallet with ID: {:?}", &wallet_id[..8]); - - // Clean up - unsafe { - key_wallet_ffi::wallet::wallet_free(wallet); - } - - println!("Test passed!"); -} diff --git a/key-wallet-ffi/tests/test_error_conversions.rs b/key-wallet-ffi/tests/test_error_conversions.rs deleted file mode 100644 index f1df4a5ec..000000000 --- a/key-wallet-ffi/tests/test_error_conversions.rs +++ /dev/null @@ -1,209 +0,0 @@ -//! Tests for error conversions between different crates - -use key_wallet_ffi::error::{FFIError, FFIErrorCode}; - -/// Helper to test an FFIError conversion and clean up the message -fn assert_ffi_error_code(ffi_err: FFIError, expected: FFIErrorCode) { - assert_eq!(ffi_err.code, expected); -} - -#[test] -fn test_key_wallet_error_to_ffi_error() { - use key_wallet::Error as KeyWalletError; - - // Test InvalidMnemonic conversion - let err = KeyWalletError::InvalidMnemonic("bad mnemonic".to_string()); - assert_ffi_error_code(err.into(), FFIErrorCode::InvalidMnemonic); - - // Test InvalidNetwork conversion - let err = KeyWalletError::InvalidNetwork; - assert_ffi_error_code(err.into(), FFIErrorCode::InvalidNetwork); - - // Test InvalidAddress conversion - let err = KeyWalletError::InvalidAddress("bad address".to_string()); - assert_ffi_error_code(err.into(), FFIErrorCode::InvalidAddress); - - // Test InvalidDerivationPath conversion - let err = KeyWalletError::InvalidDerivationPath("bad path".to_string()); - assert_ffi_error_code(err.into(), FFIErrorCode::InvalidDerivationPath); - - // Test InvalidParameter conversion - let err = KeyWalletError::InvalidParameter("bad param".to_string()); - assert_ffi_error_code(err.into(), FFIErrorCode::InvalidInput); - - // Test Serialization conversion - let err = KeyWalletError::Serialization("serialization failed".to_string()); - assert_ffi_error_code(err.into(), FFIErrorCode::SerializationError); - - // Test WatchOnly conversion - let err = KeyWalletError::WatchOnly; - assert_ffi_error_code(err.into(), FFIErrorCode::InvalidState); - - // Test CoinJoinNotEnabled conversion - let err = KeyWalletError::CoinJoinNotEnabled; - assert_ffi_error_code(err.into(), FFIErrorCode::InvalidState); - - // Test KeyError conversion (should map to WalletError) - let err = KeyWalletError::KeyError("key error".to_string()); - assert_ffi_error_code(err.into(), FFIErrorCode::WalletError); - - // Test Base58 conversion (should map to WalletError) - let err = KeyWalletError::Base58; - assert_ffi_error_code(err.into(), FFIErrorCode::WalletError); -} - -#[test] -fn test_wallet_manager_error_to_ffi_error() { - use key_wallet_manager::WalletError; - - // Test WalletNotFound conversion - let wallet_id = [0u8; 32]; - let err = WalletError::WalletNotFound(wallet_id); - assert_ffi_error_code(err.into(), FFIErrorCode::NotFound); - - // Test InvalidMnemonic conversion - let err = WalletError::InvalidMnemonic("bad mnemonic".to_string()); - assert_ffi_error_code(err.into(), FFIErrorCode::InvalidMnemonic); - - // Test InvalidNetwork conversion - let err = WalletError::InvalidNetwork; - assert_ffi_error_code(err.into(), FFIErrorCode::InvalidNetwork); - - // Test AccountNotFound conversion - let err = WalletError::AccountNotFound(0); - assert_ffi_error_code(err.into(), FFIErrorCode::NotFound); - - // Test AddressGeneration conversion - let err = WalletError::AddressGeneration("failed to generate".to_string()); - assert_ffi_error_code(err.into(), FFIErrorCode::InvalidAddress); - - // Test InvalidParameter conversion - let err = WalletError::InvalidParameter("bad param".to_string()); - assert_ffi_error_code(err.into(), FFIErrorCode::InvalidInput); - - // Test TransactionBuild conversion - let err = WalletError::TransactionBuild("tx build failed".to_string()); - assert_ffi_error_code(err.into(), FFIErrorCode::InvalidTransaction); - - // Test InsufficientFunds conversion - let err = WalletError::InsufficientFunds; - assert_ffi_error_code(err.into(), FFIErrorCode::InvalidState); - - // Test WalletCreation conversion - let err = WalletError::WalletCreation("creation failed".to_string()); - assert_ffi_error_code(err.into(), FFIErrorCode::WalletError); - - // Test WalletExists conversion - let err = WalletError::WalletExists(wallet_id); - assert_ffi_error_code(err.into(), FFIErrorCode::InvalidState); - - // Test AccountCreation conversion - let err = WalletError::AccountCreation("account creation failed".to_string()); - assert_ffi_error_code(err.into(), FFIErrorCode::WalletError); -} - -#[test] -fn test_key_wallet_error_to_wallet_manager_error() { - use key_wallet::Error as KeyWalletError; - use key_wallet_manager::WalletError; - - // Test InvalidMnemonic conversion - let err = KeyWalletError::InvalidMnemonic("bad mnemonic".to_string()); - let wallet_err: WalletError = err.into(); - match wallet_err { - WalletError::InvalidMnemonic(msg) => assert_eq!(msg, "bad mnemonic"), - _ => panic!("Wrong error type"), - } - - // Test InvalidNetwork conversion - let err = KeyWalletError::InvalidNetwork; - let wallet_err: WalletError = err.into(); - assert!(matches!(wallet_err, WalletError::InvalidNetwork)); - - // Test InvalidAddress conversion - let err = KeyWalletError::InvalidAddress("bad address".to_string()); - let wallet_err: WalletError = err.into(); - match wallet_err { - WalletError::AddressGeneration(msg) => assert!(msg.contains("bad address")), - _ => panic!("Wrong error type"), - } - - // Test InvalidParameter conversion - let err = KeyWalletError::InvalidParameter("bad param".to_string()); - let wallet_err: WalletError = err.into(); - match wallet_err { - WalletError::InvalidParameter(msg) => assert_eq!(msg, "bad param"), - _ => panic!("Wrong error type"), - } - - // Test WatchOnly conversion - let err = KeyWalletError::WatchOnly; - let wallet_err: WalletError = err.into(); - match wallet_err { - WalletError::InvalidParameter(msg) => assert!(msg.contains("watch-only")), - _ => panic!("Wrong error type"), - } - - // Test CoinJoinNotEnabled conversion - let err = KeyWalletError::CoinJoinNotEnabled; - let wallet_err: WalletError = err.into(); - match wallet_err { - WalletError::InvalidParameter(msg) => assert!(msg.contains("CoinJoin")), - _ => panic!("Wrong error type"), - } - - // Test KeyError conversion - let err = KeyWalletError::KeyError("key issue".to_string()); - let wallet_err: WalletError = err.into(); - match wallet_err { - WalletError::AccountCreation(msg) => assert!(msg.contains("key issue")), - _ => panic!("Wrong error type"), - } - - // Test Serialization conversion - let err = KeyWalletError::Serialization("serialize failed".to_string()); - let wallet_err: WalletError = err.into(); - match wallet_err { - WalletError::InvalidParameter(msg) => assert!(msg.contains("serialize failed")), - _ => panic!("Wrong error type"), - } -} - -#[test] -fn test_error_message_consistency() { - use key_wallet::Error as KeyWalletError; - use key_wallet_manager::WalletError; - - // Test that error messages are preserved through conversions - let original_msg = "This is a test error message"; - let key_err = KeyWalletError::InvalidMnemonic(original_msg.to_string()); - - // Convert to WalletError - let wallet_err: WalletError = key_err.clone().into(); - let wallet_msg = wallet_err.to_string(); - assert!(wallet_msg.contains(original_msg)); - - // Convert to FFIError - let ffi_err: FFIError = key_err.into(); - assert_ffi_error_code(ffi_err, FFIErrorCode::InvalidMnemonic); -} - -#[test] -fn test_ffi_error_success() { - // Test creating a success FFIError - let err = FFIError::default(); - assert_eq!(err.code, FFIErrorCode::Success); - assert!(err.message.is_null()); -} - -#[test] -fn test_ffi_error_with_message() { - // Test creating an error with a message - let mut err = FFIError::default(); - unsafe { - err.set(FFIErrorCode::InvalidInput, "Test error"); - } - - assert_eq!(err.code, FFIErrorCode::InvalidInput); - assert!(!err.message.is_null()); -} diff --git a/key-wallet-ffi/tests/test_import_wallet.rs b/key-wallet-ffi/tests/test_import_wallet.rs deleted file mode 100644 index 33a4c2103..000000000 --- a/key-wallet-ffi/tests/test_import_wallet.rs +++ /dev/null @@ -1,74 +0,0 @@ -//! Test for wallet import from bytes via FFI - -#[cfg(feature = "bincode")] -#[cfg(test)] -mod tests { - use dash_network::ffi::FFINetwork; - use key_wallet_ffi::error::{FFIError, FFIErrorCode}; - use key_wallet_ffi::wallet::wallet_free_const; - use key_wallet_ffi::wallet_manager::*; - use std::os::raw::c_char; - use std::ptr; - - #[test] - fn test_import_wallet_from_bytes() { - unsafe { - // Create a wallet manager - let mut error = FFIError::default(); - let manager = wallet_manager_create(FFINetwork::Testnet, &mut error); - assert_eq!(error.code, FFIErrorCode::Success); - assert!(!manager.is_null()); - - // First, create a wallet from mnemonic - let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about\0"; - - let success = wallet_manager_add_wallet_from_mnemonic( - manager, - mnemonic.as_ptr() as *const c_char, - &mut error, - ); - assert!(success); - assert_eq!(error.code, FFIErrorCode::Success); - - // Get the wallet for serialization - let mut wallet_ids_ptr: *mut u8 = ptr::null_mut(); - let mut count: usize = 0; - let success = - wallet_manager_get_wallet_ids(manager, &mut wallet_ids_ptr, &mut count, &mut error); - assert!(success); - assert_eq!(count, 1); - assert!(!wallet_ids_ptr.is_null()); - - // Get the wallet - let wallet_ptr = wallet_manager_get_wallet(manager, wallet_ids_ptr, &mut error); - assert!(!wallet_ptr.is_null()); - assert_eq!(error.code, FFIErrorCode::Success); - - // Now we would serialize the wallet to bytes here if we had that functionality exposed - // For now, we'll just test that the import function exists and compiles - - // Create a second manager to test import - let manager2 = wallet_manager_create(FFINetwork::Testnet, &mut error); - assert_eq!(error.code, FFIErrorCode::Success); - assert!(!manager2.is_null()); - - // Test with invalid input (null bytes) - let mut imported_wallet_id = [0u8; 32]; - let success = wallet_manager_import_wallet_from_bytes( - manager2, - ptr::null(), - 0, - imported_wallet_id.as_mut_ptr(), - &mut error, - ); - assert!(!success); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - - // Clean up - wallet_free_const(wallet_ptr); - wallet_manager_free_wallet_ids(wallet_ids_ptr, count); - wallet_manager_free(manager); - wallet_manager_free(manager2); - } - } -} diff --git a/key-wallet-ffi/tests/test_managed_account_collection.rs b/key-wallet-ffi/tests/test_managed_account_collection.rs deleted file mode 100644 index 9a49aaa65..000000000 --- a/key-wallet-ffi/tests/test_managed_account_collection.rs +++ /dev/null @@ -1,463 +0,0 @@ -//! Tests for managed account collection FFI bindings - -use dash_network::ffi::FFINetwork; -use key_wallet_ffi::error::{FFIError, FFIErrorCode}; -use key_wallet_ffi::managed_account_collection::*; -use key_wallet_ffi::types::{FFIAccountCreationOptionType, FFIWalletAccountCreationOptions}; -use key_wallet_ffi::wallet_manager::{ - wallet_manager_add_wallet_from_mnemonic_with_options, wallet_manager_create, - wallet_manager_free, wallet_manager_free_wallet_ids, wallet_manager_get_wallet_ids, -}; -use std::ffi::CString; -use std::ptr; - -const TEST_MNEMONIC: &str = - "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; - -#[test] -fn test_managed_account_collection_basic() { - unsafe { - let mut error = FFIError::default(); - - // Create wallet manager - let manager = wallet_manager_create(FFINetwork::Testnet, &mut error); - assert!(!manager.is_null()); - assert_eq!(error.code, FFIErrorCode::Success); - - // Add a wallet with default accounts - let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); - - let success = wallet_manager_add_wallet_from_mnemonic_with_options( - manager, - mnemonic.as_ptr(), - ptr::null(), // Use default options - &mut error, - ); - assert!(success); - assert_eq!(error.code, FFIErrorCode::Success); - - // Get wallet IDs - let mut wallet_ids_out: *mut u8 = ptr::null_mut(); - let mut count_out: usize = 0; - - let success = - wallet_manager_get_wallet_ids(manager, &mut wallet_ids_out, &mut count_out, &mut error); - assert!(success); - assert_eq!(count_out, 1); - assert!(!wallet_ids_out.is_null()); - - // Get the managed account collection - let collection = managed_wallet_get_account_collection(manager, wallet_ids_out, &mut error); - assert!(!collection.is_null()); - assert_eq!(error.code, FFIErrorCode::Success); - - // Check that we have some accounts - let count = managed_account_collection_count(collection); - assert!(count > 0); - - // Check BIP44 accounts - let mut indices: *mut std::os::raw::c_uint = ptr::null_mut(); - let mut indices_count: usize = 0; - let success = managed_account_collection_get_bip44_indices( - collection, - &mut indices, - &mut indices_count, - ); - assert!(success); - assert!(indices_count > 0); - - // Get first BIP44 account - let account = managed_account_collection_get_bip44_account(collection, 0); - assert!(!account.is_null()); - - // Clean up - key_wallet_ffi::managed_account::managed_core_account_free(account); - if !indices.is_null() { - key_wallet_ffi::account_collection::free_u32_array(indices, indices_count); - } - managed_account_collection_free(collection); - wallet_manager_free_wallet_ids(wallet_ids_out, count_out); - wallet_manager_free(manager); - } -} - -#[test] -fn test_managed_account_collection_with_special_accounts() { - unsafe { - let mut error = FFIError::default(); - - // Create wallet manager - let manager = wallet_manager_create(FFINetwork::Testnet, &mut error); - assert!(!manager.is_null()); - - // Create wallet with special accounts - let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); - - let mut options = FFIWalletAccountCreationOptions::default_options(); - options.option_type = FFIAccountCreationOptionType::AllAccounts; - - // Add various special accounts - let special_types = [ - key_wallet_ffi::types::FFIAccountKind::ProviderVotingKeys, - key_wallet_ffi::types::FFIAccountKind::ProviderOwnerKeys, - key_wallet_ffi::types::FFIAccountKind::IdentityRegistration, - key_wallet_ffi::types::FFIAccountKind::IdentityInvitation, - ]; - options.special_account_types = special_types.as_ptr(); - options.special_account_types_count = special_types.len(); - - // Configure standard accounts - let bip44_indices = [0, 4, 5, 8]; - let bip32_indices = [0]; - let coinjoin_indices = [0, 1]; - let topup_indices = [0, 1, 2]; - - options.bip44_indices = bip44_indices.as_ptr(); - options.bip44_count = bip44_indices.len(); - - options.bip32_indices = bip32_indices.as_ptr(); - options.bip32_count = bip32_indices.len(); - - options.coinjoin_indices = coinjoin_indices.as_ptr(); - options.coinjoin_count = coinjoin_indices.len(); - - options.topup_indices = topup_indices.as_ptr(); - options.topup_count = topup_indices.len(); - - let success = wallet_manager_add_wallet_from_mnemonic_with_options( - manager, - mnemonic.as_ptr(), - &options, - &mut error, - ); - assert!(success); - - // Get wallet IDs - let mut wallet_ids_out: *mut u8 = ptr::null_mut(); - let mut count_out: usize = 0; - - let success = - wallet_manager_get_wallet_ids(manager, &mut wallet_ids_out, &mut count_out, &mut error); - assert!(success); - assert_eq!(count_out, 1); - - // Get the managed account collection - let collection = managed_wallet_get_account_collection(manager, wallet_ids_out, &mut error); - assert!(!collection.is_null()); - - // Verify BIP44 accounts - let mut indices: *mut std::os::raw::c_uint = ptr::null_mut(); - let mut indices_count: usize = 0; - let success = managed_account_collection_get_bip44_indices( - collection, - &mut indices, - &mut indices_count, - ); - assert!(success); - assert_eq!(indices_count, 4); - if !indices.is_null() { - key_wallet_ffi::account_collection::free_u32_array(indices, indices_count); - } - - // Verify BIP32 accounts - let success = managed_account_collection_get_bip32_indices( - collection, - &mut indices, - &mut indices_count, - ); - assert!(success); - assert_eq!(indices_count, 1); - if !indices.is_null() { - key_wallet_ffi::account_collection::free_u32_array(indices, indices_count); - } - - // Verify CoinJoin accounts - let success = managed_account_collection_get_coinjoin_indices( - collection, - &mut indices, - &mut indices_count, - ); - assert!(success); - assert_eq!(indices_count, 2); - if !indices.is_null() { - key_wallet_ffi::account_collection::free_u32_array(indices, indices_count); - } - - // Check special accounts existence - assert!(managed_account_collection_has_identity_registration(collection)); - assert!(managed_account_collection_has_identity_invitation(collection)); - assert!(managed_account_collection_has_provider_voting_keys(collection)); - assert!(managed_account_collection_has_provider_owner_keys(collection)); - - // Get specific accounts - let identity_reg = managed_account_collection_get_identity_registration(collection); - assert!(!identity_reg.is_null()); - key_wallet_ffi::managed_account::managed_core_account_free(identity_reg); - - let voting_keys = managed_account_collection_get_provider_voting_keys(collection); - assert!(!voting_keys.is_null()); - key_wallet_ffi::managed_account::managed_core_account_free(voting_keys); - - // Clean up - managed_account_collection_free(collection); - wallet_manager_free_wallet_ids(wallet_ids_out, count_out); - wallet_manager_free(manager); - } -} - -#[test] -fn test_managed_account_collection_summary() { - unsafe { - use std::ffi::CStr; - - let mut error = FFIError::default(); - - // Create wallet manager - let manager = wallet_manager_create(FFINetwork::Testnet, &mut error); - assert!(!manager.is_null()); - - // Create wallet with multiple account types - let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); - - let mut options = FFIWalletAccountCreationOptions::default_options(); - options.option_type = FFIAccountCreationOptionType::AllAccounts; - - // Add various special accounts - let special_types = [ - key_wallet_ffi::types::FFIAccountKind::ProviderVotingKeys, - key_wallet_ffi::types::FFIAccountKind::ProviderOwnerKeys, - key_wallet_ffi::types::FFIAccountKind::IdentityRegistration, - ]; - options.special_account_types = special_types.as_ptr(); - options.special_account_types_count = special_types.len(); - - // Configure standard accounts - let bip44_indices = [0, 1, 2]; - let bip32_indices = [0]; - - options.bip44_indices = bip44_indices.as_ptr(); - options.bip44_count = bip44_indices.len(); - - options.bip32_indices = bip32_indices.as_ptr(); - options.bip32_count = bip32_indices.len(); - - let success = wallet_manager_add_wallet_from_mnemonic_with_options( - manager, - mnemonic.as_ptr(), - &options, - &mut error, - ); - assert!(success); - - // Get wallet IDs - let mut wallet_ids_out: *mut u8 = ptr::null_mut(); - let mut count_out: usize = 0; - - let success = - wallet_manager_get_wallet_ids(manager, &mut wallet_ids_out, &mut count_out, &mut error); - assert!(success); - assert_eq!(count_out, 1); - - // Get the managed account collection - let collection = managed_wallet_get_account_collection(manager, wallet_ids_out, &mut error); - assert!(!collection.is_null()); - - // Get the summary - let summary_ptr = managed_account_collection_summary(collection); - assert!(!summary_ptr.is_null()); - - // Convert to Rust string to verify content - let summary_cstr = CStr::from_ptr(summary_ptr); - let summary = summary_cstr.to_str().unwrap(); - - // Verify the summary contains expected content - assert!(summary.contains("Managed Account Summary:")); - assert!(summary.contains("BIP44 Accounts")); - assert!(summary.contains("BIP32 Accounts")); - assert!(summary.contains("Identity Registration Account")); - assert!(summary.contains("Provider Voting Keys Account")); - assert!(summary.contains("Provider Owner Keys Account")); - - // Clean up - key_wallet_ffi::utils::string_free(summary_ptr); - managed_account_collection_free(collection); - wallet_manager_free_wallet_ids(wallet_ids_out, count_out); - wallet_manager_free(manager); - } -} - -#[test] -fn test_managed_account_collection_summary_data() { - unsafe { - let mut error = FFIError::default(); - - // Create wallet manager - let manager = wallet_manager_create(FFINetwork::Testnet, &mut error); - assert!(!manager.is_null()); - - // Create wallet with various account types - let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); - - let mut options = FFIWalletAccountCreationOptions::default_options(); - options.option_type = FFIAccountCreationOptionType::AllAccounts; - - // Add various special accounts - let special_types = [ - key_wallet_ffi::types::FFIAccountKind::IdentityRegistration, - key_wallet_ffi::types::FFIAccountKind::IdentityInvitation, - ]; - options.special_account_types = special_types.as_ptr(); - options.special_account_types_count = special_types.len(); - - // Configure standard accounts - let bip44_indices = [0, 1, 2, 5]; - let bip32_indices = [0]; - let coinjoin_indices = [0, 1]; - let topup_indices = [0, 1, 2]; - - options.bip44_indices = bip44_indices.as_ptr(); - options.bip44_count = bip44_indices.len(); - - options.bip32_indices = bip32_indices.as_ptr(); - options.bip32_count = bip32_indices.len(); - - options.coinjoin_indices = coinjoin_indices.as_ptr(); - options.coinjoin_count = coinjoin_indices.len(); - - options.topup_indices = topup_indices.as_ptr(); - options.topup_count = topup_indices.len(); - - let success = wallet_manager_add_wallet_from_mnemonic_with_options( - manager, - mnemonic.as_ptr(), - &options, - &mut error, - ); - assert!(success); - - // Get wallet IDs - let mut wallet_ids_out: *mut u8 = ptr::null_mut(); - let mut count_out: usize = 0; - - let success = - wallet_manager_get_wallet_ids(manager, &mut wallet_ids_out, &mut count_out, &mut error); - assert!(success); - assert_eq!(count_out, 1); - - // Get the managed account collection - let collection = managed_wallet_get_account_collection(manager, wallet_ids_out, &mut error); - assert!(!collection.is_null()); - - // Get the summary data - let summary = managed_account_collection_summary_data(collection); - assert!(!summary.is_null()); - - let summary_ref = &*summary; - - // Verify BIP44 indices - assert_eq!(summary_ref.bip44_count, 4); - assert!(!summary_ref.bip44_indices.is_null()); - let bip44_slice = - std::slice::from_raw_parts(summary_ref.bip44_indices, summary_ref.bip44_count); - assert_eq!(bip44_slice, &[0, 1, 2, 5]); - - // Verify BIP32 indices - assert_eq!(summary_ref.bip32_count, 1); - assert!(!summary_ref.bip32_indices.is_null()); - - // Verify CoinJoin indices - assert_eq!(summary_ref.coinjoin_count, 2); - assert!(!summary_ref.coinjoin_indices.is_null()); - - // Verify identity topup indices - assert_eq!(summary_ref.identity_topup_count, 3); - assert!(!summary_ref.identity_topup_indices.is_null()); - - // Verify boolean flags - assert!(summary_ref.has_identity_registration); - assert!(summary_ref.has_identity_invitation); - - // Clean up - managed_account_collection_summary_free(summary); - managed_account_collection_free(collection); - wallet_manager_free_wallet_ids(wallet_ids_out, count_out); - wallet_manager_free(manager); - } -} - -#[test] -fn test_managed_account_collection_null_safety() { - unsafe { - let mut error = FFIError::default(); - - // Test with null manager - let collection = - managed_wallet_get_account_collection(ptr::null(), ptr::null(), &mut error); - assert!(collection.is_null()); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - - // Test with null collection for various functions - assert_eq!(managed_account_collection_count(ptr::null()), 0); - assert!(!managed_account_collection_has_identity_registration(ptr::null())); - assert!(managed_account_collection_get_bip44_account(ptr::null(), 0).is_null()); - assert!(managed_account_collection_summary(ptr::null()).is_null()); - assert!(managed_account_collection_summary_data(ptr::null()).is_null()); - - // Test free with null (should not crash) - managed_account_collection_free(ptr::null_mut()); - managed_account_collection_summary_free(ptr::null_mut()); - } -} - -#[test] -fn test_managed_account_collection_nonexistent_accounts() { - unsafe { - let mut error = FFIError::default(); - - // Create wallet manager - let manager = wallet_manager_create(FFINetwork::Testnet, &mut error); - assert!(!manager.is_null()); - - // Create wallet with minimal accounts - let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); - - let success = wallet_manager_add_wallet_from_mnemonic_with_options( - manager, - mnemonic.as_ptr(), - ptr::null(), // Default options - &mut error, - ); - assert!(success); - - // Get wallet IDs - let mut wallet_ids_out: *mut u8 = ptr::null_mut(); - let mut count_out: usize = 0; - - let success = - wallet_manager_get_wallet_ids(manager, &mut wallet_ids_out, &mut count_out, &mut error); - assert!(success); - assert_eq!(count_out, 1); - - // Get the managed account collection - let collection = managed_wallet_get_account_collection(manager, wallet_ids_out, &mut error); - assert!(!collection.is_null()); - - // Try to get non-existent accounts - let account = managed_account_collection_get_bip44_account(collection, 999); - assert!(account.is_null()); - - let account = managed_account_collection_get_bip32_account(collection, 999); - assert!(account.is_null()); - - let account = managed_account_collection_get_coinjoin_account(collection, 999); - assert!(account.is_null()); - - let account = managed_account_collection_get_identity_topup(collection, 999); - assert!(account.is_null()); - - // Clean up - managed_account_collection_free(collection); - wallet_manager_free_wallet_ids(wallet_ids_out, count_out); - wallet_manager_free(manager); - } -} diff --git a/key-wallet-ffi/tests/test_valid_addr.rs b/key-wallet-ffi/tests/test_valid_addr.rs deleted file mode 100644 index 2c34a054b..000000000 --- a/key-wallet-ffi/tests/test_valid_addr.rs +++ /dev/null @@ -1,48 +0,0 @@ -#[test] -fn test_valid_testnet_address() { - use std::str::FromStr; - - // Generate a valid testnet address - use key_wallet::wallet::initialization::WalletAccountCreationOptions; - use key_wallet::{Mnemonic, Network, Wallet}; - - let mnemonic_str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; - let mnemonic = - Mnemonic::from_phrase(mnemonic_str, key_wallet::mnemonic::Language::English).unwrap(); - - let wallet = - Wallet::from_mnemonic(mnemonic, Network::Testnet, WalletAccountCreationOptions::Default) - .unwrap(); - - if let Some(account) = wallet.get_bip44_account(0) { - use key_wallet::ChildNumber; - use secp256k1::Secp256k1; - let secp = Secp256k1::new(); - - let child_external = ChildNumber::from_normal_idx(0).unwrap(); - let child_index = ChildNumber::from_normal_idx(0).unwrap(); - - let derived_key = - account.account_xpub.derive_pub(&secp, &[child_external, child_index]).unwrap(); - let public_key = derived_key.public_key; - let dash_pubkey = dashcore::PublicKey::new(public_key); - let address = key_wallet::Address::p2pkh(&dash_pubkey, dashcore::Network::Testnet); - - println!("Generated testnet address: {}", address); - - // Now try to validate it - let addr_str = address.to_string(); - match key_wallet::Address::from_str(&addr_str) { - Ok(parsed) => { - println!("Successfully parsed generated address"); - match parsed.require_network(dashcore::Network::Testnet) { - Ok(_) => println!("✓ Address is valid for testnet"), - Err(e) => println!("✗ Address not valid for testnet: {}", e), - } - } - Err(e) => { - println!("Failed to parse generated address: {}", e); - } - } - } -} diff --git a/key-wallet/CLAUDE.md b/key-wallet/CLAUDE.md index 662825e88..5bd27f94a 100644 --- a/key-wallet/CLAUDE.md +++ b/key-wallet/CLAUDE.md @@ -367,23 +367,7 @@ managed_account.mark_addresses_used(&tx); ## Common Integration Points -### 1. FFI Bindings (key-wallet-ffi) -```rust -// Expose safe C interfaces -#[no_mangle] -pub extern "C" fn wallet_from_mnemonic( - mnemonic: *const c_char, - network: u8 -) -> *mut Wallet -``` - -### 2. Swift Integration -```swift -// Swift SDK uses the FFI bindings -let wallet = DashWallet(mnemonic: "...", network: .mainnet) -``` - -### 3. RPC Integration +### 1. RPC Integration ```rust // Sync with Dash Core node let client = RpcClient::new(url)?; diff --git a/key-wallet/README.md b/key-wallet/README.md index 8e0d3a880..b1a512d47 100644 --- a/key-wallet/README.md +++ b/key-wallet/README.md @@ -11,7 +11,6 @@ The key-wallet crate is a core component of the rust-dashcore ecosystem, offerin - Multiple cryptographic schemes (ECDSA, BLS, EdDSA) - Advanced account and address management with gap limit tracking - Transaction checking and UTXO management -- FFI bindings for cross-platform integration - No-std support for embedded systems ## Architecture