diff --git a/.github/workflows/swift-sdk-build.yml b/.github/workflows/swift-sdk-build.yml new file mode 100644 index 0000000000..c93efb56ca --- /dev/null +++ b/.github/workflows/swift-sdk-build.yml @@ -0,0 +1,263 @@ +name: Build Swift SDK and Example (no warnings) + +on: + pull_request: + paths: + - 'packages/swift-sdk/**' + - 'packages/rs-*/**' + - '.github/workflows/swift-sdk-build.yml' + push: + branches: [ main, master ] + paths: + - 'packages/swift-sdk/**' + - 'packages/rs-*/**' + - '.github/workflows/swift-sdk-build.yml' + +jobs: + swift-sdk-build: + name: Swift SDK and Example build (warnings as errors) + runs-on: macos-15 + timeout-minutes: 60 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Select Xcode 16 + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: '16.*' + + - name: Show Xcode and Swift versions + run: | + xcodebuild -version + swift --version + + # Rust + Cargo cache to speed up FFI build + - name: Set up Rust toolchain (stable) + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + key: cargo-registry-${{ hashFiles('**/Cargo.lock') }} + + - name: Add iOS Rust targets + run: | + rustup target add aarch64-apple-ios aarch64-apple-ios-sim + + - name: Install cbindgen (for header generation) + run: | + brew install cbindgen || true + + - name: Restore cached Protobuf (protoc) + id: cache-protoc + uses: actions/cache@v4 + with: + path: | + ${{ env.HOME }}/.local/protoc-32.0/bin + ${{ env.HOME }}/.local/protoc-32.0/include + key: protoc/32.0/${{ runner.os }}/universal + + - name: Install Protobuf (protoc) if cache miss + if: steps.cache-protoc.outputs.cache-hit != 'true' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euxo pipefail + VERSION=32.0 + OS=osx-universal_binary + PROTOC_DIR="$HOME/.local/protoc-${VERSION}" + mkdir -p "$PROTOC_DIR" + curl -fsSL -H "Authorization: token ${GITHUB_TOKEN}" \ + -o /tmp/protoc.zip \ + "https://github.com/protocolbuffers/protobuf/releases/download/v${VERSION}/protoc-${VERSION}-${OS}.zip" + unzip -o /tmp/protoc.zip -d "$PROTOC_DIR" + + - name: Save cached Protobuf (protoc) + if: steps.cache-protoc.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: | + ${{ env.HOME }}/.local/protoc-32.0/bin + ${{ env.HOME }}/.local/protoc-32.0/include + key: protoc/32.0/${{ runner.os }}/universal + + - name: Verify protoc and export env + run: | + set -euxo pipefail + echo "$HOME/.local/protoc-32.0/bin" >> $GITHUB_PATH + echo "PROTOC=$HOME/.local/protoc-32.0/bin/protoc" >> "$GITHUB_ENV" + "$HOME/.local/protoc-32.0/bin/protoc" --version + + - name: Determine rust-dashcore revision (from rs-dpp) + id: dashcore_rev + shell: bash + run: | + set -euo pipefail + # Use the same rust-dashcore revision as rs-dpp (parse single-line dep) + REV=$(grep -E '^[[:space:]]*dashcore[[:space:]]*=[[:space:]]*\{.*rev[[:space:]]*=' packages/rs-dpp/Cargo.toml \ + | sed -E 's/.*rev[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/' \ + | head -n1 || true) + if [ -z "${REV:-}" ]; then + echo "Failed to determine rust-dashcore revision from Cargo.toml" >&2 + exit 1 + fi + echo "rev=$REV" >> "$GITHUB_OUTPUT" + + - name: Checkout rust-dashcore at required revision + shell: bash + run: | + set -euxo pipefail + BASE_DIR="$(dirname "$GITHUB_WORKSPACE")" + cd "$BASE_DIR" + if [ -d rust-dashcore/.git ]; then + echo "Updating existing rust-dashcore checkout" + cd rust-dashcore + git fetch --all --tags --prune + else + git clone --no-tags --filter=blob:none https://github.com/dashpay/rust-dashcore.git rust-dashcore + cd rust-dashcore + fi + git checkout "${{ steps.dashcore_rev.outputs.rev }}" + + - name: Determine rust-dashcore toolchain channel + id: dashcore_toolchain + shell: bash + run: | + set -euo pipefail + FILE="$(dirname "$GITHUB_WORKSPACE")/rust-dashcore/rust-toolchain.toml" + if [ -f "$FILE" ]; then + CHANNEL=$(grep -E '^[[:space:]]*channel[[:space:]]*=' "$FILE" | sed -E 's/.*"([^"]+)".*/\1/' | head -n1 || true) + fi + CHANNEL=${CHANNEL:-stable} + echo "channel=$CHANNEL" >> "$GITHUB_OUTPUT" + echo "RUST_DASHCORE_TOOLCHAIN=$CHANNEL" >> "$GITHUB_ENV" + + - name: Ensure rust-dashcore toolchain has iOS targets + shell: bash + run: | + set -euxo pipefail + RUST_DASHCORE_DIR="$(dirname "$GITHUB_WORKSPACE")/rust-dashcore" + cd "$RUST_DASHCORE_DIR" + TOOLCHAIN="${{ steps.dashcore_toolchain.outputs.channel }}" + rustup toolchain install "$TOOLCHAIN" --profile minimal --no-self-update + rustup target add --toolchain "$TOOLCHAIN" aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios + + - name: Build DashSDKFFI.xcframework and install into Swift package + run: | + bash packages/swift-sdk/build_ios.sh + + - name: Zip XCFramework and compute checksum + run: | + cd packages/swift-sdk + ditto -c -k --sequesterRsrc --keepParent DashSDKFFI.xcframework DashSDKFFI.xcframework.zip + swift package compute-checksum DashSDKFFI.xcframework.zip > xc_checksum.txt + + - name: Upload DashSDKFFI artifacts + uses: actions/upload-artifact@v4 + with: + name: DashSDKFFI.xcframework + path: | + packages/swift-sdk/DashSDKFFI.xcframework + packages/swift-sdk/DashSDKFFI.xcframework.zip + packages/swift-sdk/xc_checksum.txt + retention-days: 14 + + - name: Comment/update PR with artifact link and usage guide (skip if unchanged) + if: github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + const fs = require('fs'); + const marker = ''; + let checksum = ''; + try { checksum = fs.readFileSync('packages/swift-sdk/xc_checksum.txt', 'utf8').trim(); } catch (e) {} + + // Find existing marker comment on this PR + const issue_number = context.payload.pull_request.number; + const comments = await github.paginate(github.rest.issues.listComments, { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number, + per_page: 100 + }); + const existing = comments.find(c => (c.body || '').includes(marker)); + + // Extract old checksum if present + const extractChecksum = (text) => { + if (!text) return ''; + const m = text.match(/checksum:\s*"?([a-f0-9]{64})"?/i); + return m ? m[1] : ''; + }; + const oldChecksum = existing ? extractChecksum(existing.body) : ''; + + if (oldChecksum && checksum && oldChecksum === checksum) { + core.info(`Checksum unchanged (${checksum}); skipping PR comment update.`); + return; + } + + const body = `${marker}\n` + + `✅ DashSDKFFI.xcframework built for this PR.\n\n` + + `- Workflow run: ${runUrl}\n` + + `- Artifacts: DashSDKFFI.xcframework (folder), DashSDKFFI.xcframework.zip, xc_checksum.txt\n\n` + + `SwiftPM (host the zip at a stable URL, then use):\n` + + '```swift\n' + + '.binaryTarget(\n' + + ' name: "DashSDKFFI",\n' + + ' url: "https://your.cdn.example/DashSDKFFI.xcframework.zip",\n' + + ` checksum: "${checksum || ''}"\n` + + ')\n' + + '```\n\n' + + `Xcode manual integration:\n` + + `- Download 'DashSDKFFI.xcframework' artifact from the run link above.\n` + + `- Drag it into your app target (Frameworks, Libraries & Embedded Content) and set Embed & Sign.\n` + + `- If using the Swift wrapper package, point its binaryTarget to the xcframework location or add the package and place the xcframework at the expected path.\n`; + + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number, + body + }); + } + + - name: Build SwiftDashSDK package (warnings as errors) + working-directory: packages/swift-sdk + run: | + swift build -c debug -Xswiftc -warnings-as-errors + + - name: Resolve ExampleApp dependencies + working-directory: packages/swift-sdk + run: | + xcodebuild -project SwiftExampleApp/SwiftExampleApp.xcodeproj -resolvePackageDependencies + + - name: Build SwiftExampleApp (warnings as errors) + working-directory: packages/swift-sdk + env: + # Treat Swift warnings as errors during xcodebuild + OTHER_SWIFT_FLAGS: -warnings-as-errors + SWIFT_TREAT_WARNINGS_AS_ERRORS: YES + run: | + xcodebuild \ + -project SwiftExampleApp/SwiftExampleApp.xcodeproj \ + -scheme SwiftExampleApp \ + -sdk iphonesimulator \ + -destination 'generic/platform=iOS Simulator' \ + -configuration Debug \ + ONLY_ACTIVE_ARCH=YES \ + OTHER_SWIFT_FLAGS="$OTHER_SWIFT_FLAGS" \ + SWIFT_TREAT_WARNINGS_AS_ERRORS=$SWIFT_TREAT_WARNINGS_AS_ERRORS \ + build diff --git a/.github/workflows/swift-sdk-release.yml b/.github/workflows/swift-sdk-release.yml new file mode 100644 index 0000000000..4f705c2cff --- /dev/null +++ b/.github/workflows/swift-sdk-release.yml @@ -0,0 +1,135 @@ +name: Release DashSDKFFI XCFramework + +on: + push: + tags: + - 'ffi-*' + workflow_dispatch: + inputs: + tag: + description: 'Tag name to release (optional; defaults to current ref_name on tag push)' + required: false + +permissions: + contents: write + +jobs: + build-and-release: + name: Build and release DashSDKFFI + runs-on: macos-15 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Select Xcode 16 + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: '16.*' + + - name: Show Xcode and Swift versions + run: | + xcodebuild -version + swift --version + + - name: Set up Rust toolchain (stable) + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + key: cargo-registry-${{ hashFiles('**/Cargo.lock') }} + + - name: Add iOS Rust targets + run: | + rustup target add aarch64-apple-ios aarch64-apple-ios-sim + + - name: Install cbindgen (for header generation) + run: | + brew install cbindgen || true + + - name: Install protoc (Protocol Buffers compiler) + uses: arduino/setup-protoc@v3 + with: + version: '32.x' + + - name: Build DashSDKFFI.xcframework and install into Swift package + run: | + bash packages/swift-sdk/build_ios.sh + + - name: Zip XCFramework and compute checksum + id: zip + run: | + cd packages/swift-sdk + ditto -c -k --sequesterRsrc --keepParent DashSDKFFI.xcframework DashSDKFFI.xcframework.zip + swift package compute-checksum DashSDKFFI.xcframework.zip > xc_checksum.txt + echo "checksum=$(cat xc_checksum.txt)" >> $GITHUB_OUTPUT + + - name: Determine tag + id: tag + run: | + if [ -n "${{ github.event.inputs.tag }}" ]; then + echo "name=${{ github.event.inputs.tag }}" >> $GITHUB_OUTPUT + else + # on tag push, ref_name is the tag + echo "name=${{ github.ref_name }}" >> $GITHUB_OUTPUT + fi + + - name: Check existing release for changes + id: check + uses: actions/github-script@v7 + env: + NAME: ${{ steps.tag.outputs.name }} + with: + script: | + const tag = process.env.NAME || '${{ steps.tag.outputs.name }}'; + const checksumNew = '${{ steps.zip.outputs.checksum }}'.trim(); + try { + const rel = await github.rest.repos.getReleaseByTag({ owner: context.repo.owner, repo: context.repo.repo, tag }); + const assets = await github.rest.repos.listReleaseAssets({ owner: context.repo.owner, repo: context.repo.repo, release_id: rel.data.id, per_page: 100 }); + const checksumAsset = assets.data.find(a => a.name === 'xc_checksum.txt'); + if (!checksumAsset) { + core.setOutput('changed', 'true'); + return; + } + const res = await github.request('GET {url}', { url: checksumAsset.url, headers: { Accept: 'application/octet-stream' } }); + const checksumOld = (res.data || '').toString().trim(); + core.info(`Old checksum: ${checksumOld}, New checksum: ${checksumNew}`); + core.setOutput('changed', checksumOld === checksumNew ? 'false' : 'true'); + } catch (e) { + // No release found -> treat as changed + core.info(`No existing release for tag ${tag}. Creating new.`); + core.setOutput('changed', 'true'); + } + result-encoding: string + + - name: Create/Update release (only if changed) + if: steps.check.outputs.changed == 'true' + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.tag.outputs.name }} + name: DashSDKFFI ${{ steps.tag.outputs.name }} + draft: false + prerelease: false + files: | + packages/swift-sdk/DashSDKFFI.xcframework.zip + packages/swift-sdk/xc_checksum.txt + body: | + DashSDKFFI.xcframework built for tag ${{ steps.tag.outputs.name }}. + + SwiftPM usage: + ```swift + .binaryTarget( + name: "DashSDKFFI", + url: "https://github.com/${{ github.repository }}/releases/download/${{ steps.tag.outputs.name }}/DashSDKFFI.xcframework.zip", + checksum: "${{ steps.zip.outputs.checksum }}" + ) + ``` + + - name: Skip release (no changes) + if: steps.check.outputs.changed != 'true' + run: | + echo "No changes detected in XCFramework; skipping release upload." diff --git a/.github/workflows/tests-rs-sdk-ffi-build.yml b/.github/workflows/tests-rs-sdk-ffi-build.yml index 5306af6ad7..14f4fb9578 100644 --- a/.github/workflows/tests-rs-sdk-ffi-build.yml +++ b/.github/workflows/tests-rs-sdk-ffi-build.yml @@ -92,10 +92,15 @@ jobs: BLST_PORTABLE: "1" IPHONEOS_DEPLOYMENT_TARGET: "18.0" IPHONESIMULATOR_DEPLOYMENT_TARGET: "18.0" - RUSTFLAGS: "-C link-arg=-mios-version-min=18.0" + CARGO_TARGET_AARCH64_APPLE_IOS_RUSTFLAGS: "-C link-arg=-mios-version-min=18.0" + CARGO_TARGET_AARCH64_APPLE_IOS_SIM_RUSTFLAGS: "-C link-arg=-mios-version-min=18.0" + CARGO_TARGET_X86_64_APPLE_IOS_RUSTFLAGS: "-C link-arg=-mios-version-min=18.0" run: | echo "Using BLST_PORTABLE=${BLST_PORTABLE} to avoid iOS linker issues" - echo "Minimum iOS deployment target: ${IPHONEOS_DEPLOYMENT_TARGET} (RUSTFLAGS=${RUSTFLAGS})" + echo "Minimum iOS deployment target: ${IPHONEOS_DEPLOYMENT_TARGET}" + echo "AARCH64 iOS rustflags: ${CARGO_TARGET_AARCH64_APPLE_IOS_RUSTFLAGS}" + echo "AARCH64 iOS-sim rustflags: ${CARGO_TARGET_AARCH64_APPLE_IOS_SIM_RUSTFLAGS}" + echo "x86_64 iOS rustflags: ${CARGO_TARGET_X86_64_APPLE_IOS_RUSTFLAGS}" cargo build --release --target ${{ matrix.target }} - name: Verify build output diff --git a/AGENTS.md b/AGENTS.md index b352391044..6fe43877e2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -43,3 +43,4 @@ - iOS/FFI artifacts: `packages/rs-sdk-ffi` and Swift app in `packages/swift-sdk`. - Example: build iOS framework - `cd packages/rs-sdk-ffi && ./build_ios.sh` + - iOS Simulator MCP server: see `packages/swift-sdk/IOS_SIMULATOR_MCP.md` for Codex config, tools, and usage. Default output dir set via `IOS_SIMULATOR_MCP_DEFAULT_OUTPUT_DIR`. diff --git a/Cargo.lock b/Cargo.lock index 8381a970d9..e142a72220 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -228,17 +228,6 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi 0.1.19", - "libc", - "winapi", -] - [[package]] name = "autocfg" version = "1.5.0" @@ -812,32 +801,13 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" -[[package]] -name = "cbindgen" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da6bc11b07529f16944307272d5bd9b22530bc7d05751717c9d416586cedab49" -dependencies = [ - "clap 3.2.25", - "heck 0.4.1", - "indexmap 1.9.3", - "log", - "proc-macro2", - "quote", - "serde", - "serde_json", - "syn 1.0.109", - "tempfile", - "toml 0.5.11", -] - [[package]] name = "cbindgen" version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fce8dd7fcfcbf3a0a87d8f515194b49d6135acab73e18bd380d1d93bb1a15eb" dependencies = [ - "clap 4.5.45", + "clap", "heck 0.4.1", "indexmap 2.10.0", "log", @@ -847,7 +817,7 @@ dependencies = [ "serde_json", "syn 2.0.106", "tempfile", - "toml 0.8.23", + "toml", ] [[package]] @@ -856,7 +826,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "975982cdb7ad6a142be15bdf84aea7ec6a9e5d4d797c004d43185b24cfe4e684" dependencies = [ - "clap 4.5.45", + "clap", "heck 0.5.0", "indexmap 2.10.0", "log", @@ -866,7 +836,7 @@ dependencies = [ "serde_json", "syn 2.0.106", "tempfile", - "toml 0.8.23", + "toml", ] [[package]] @@ -905,7 +875,7 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" name = "check-features" version = "2.0.0" dependencies = [ - "toml 0.8.23", + "toml", ] [[package]] @@ -993,21 +963,6 @@ dependencies = [ "libloading", ] -[[package]] -name = "clap" -version = "3.2.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" -dependencies = [ - "atty", - "bitflags 1.3.2", - "clap_lex 0.2.4", - "indexmap 1.9.3", - "strsim 0.10.0", - "termcolor", - "textwrap", -] - [[package]] name = "clap" version = "4.5.45" @@ -1026,8 +981,8 @@ checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" dependencies = [ "anstream", "anstyle", - "clap_lex 0.7.5", - "strsim 0.11.1", + "clap_lex", + "strsim", ] [[package]] @@ -1042,15 +997,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "clap_lex" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" -dependencies = [ - "os_str_bytes", -] - [[package]] name = "clap_lex" version = "0.7.5" @@ -1210,7 +1156,7 @@ dependencies = [ "anes", "cast", "ciborium", - "clap 4.5.45", + "clap", "criterion-plot", "is-terminal", "itertools 0.10.5", @@ -1406,7 +1352,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim 0.11.1", + "strsim", "syn 2.0.106", ] @@ -1436,7 +1382,7 @@ dependencies = [ [[package]] name = "dash-network" version = "0.40.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=02d902c9845d5ed9e5cb88fd32a8c254742f20fd#02d902c9845d5ed9e5cb88fd32a8c254742f20fd" +source = "git+https://github.com/dashpay/rust-dashcore?rev=befd0356bebfcd0d06d1028d8a03bfa4c78bd219#befd0356bebfcd0d06d1028d8a03bfa4c78bd219" dependencies = [ "bincode 2.0.0-rc.3", "bincode_derive", @@ -1449,7 +1395,7 @@ name = "dash-platform-balance-checker" version = "2.0.0" dependencies = [ "anyhow", - "clap 4.5.45", + "clap", "dapi-grpc", "dash-sdk", "dpp", @@ -1472,7 +1418,7 @@ dependencies = [ "bip37-bloom-filter", "chrono", "ciborium", - "clap 4.5.45", + "clap", "dapi-grpc", "dapi-grpc-macros", "dash-context-provider", @@ -1507,13 +1453,13 @@ dependencies = [ [[package]] name = "dash-spv" version = "0.40.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=02d902c9845d5ed9e5cb88fd32a8c254742f20fd#02d902c9845d5ed9e5cb88fd32a8c254742f20fd" +source = "git+https://github.com/dashpay/rust-dashcore?rev=befd0356bebfcd0d06d1028d8a03bfa4c78bd219#befd0356bebfcd0d06d1028d8a03bfa4c78bd219" dependencies = [ "anyhow", "async-trait", "bincode 1.3.3", "blsful", - "clap 4.5.45", + "clap", "crossterm", "dashcore", "dashcore_hashes", @@ -1535,9 +1481,9 @@ dependencies = [ [[package]] name = "dash-spv-ffi" version = "0.40.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=02d902c9845d5ed9e5cb88fd32a8c254742f20fd#02d902c9845d5ed9e5cb88fd32a8c254742f20fd" +source = "git+https://github.com/dashpay/rust-dashcore?rev=befd0356bebfcd0d06d1028d8a03bfa4c78bd219#befd0356bebfcd0d06d1028d8a03bfa4c78bd219" dependencies = [ - "cbindgen 0.26.0", + "cbindgen 0.29.0", "dash-spv", "dashcore", "env_logger 0.10.2", @@ -1558,7 +1504,7 @@ dependencies = [ [[package]] name = "dashcore" version = "0.40.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=02d902c9845d5ed9e5cb88fd32a8c254742f20fd#02d902c9845d5ed9e5cb88fd32a8c254742f20fd" +source = "git+https://github.com/dashpay/rust-dashcore?rev=befd0356bebfcd0d06d1028d8a03bfa4c78bd219#befd0356bebfcd0d06d1028d8a03bfa4c78bd219" dependencies = [ "anyhow", "base64-compat", @@ -1584,12 +1530,12 @@ dependencies = [ [[package]] name = "dashcore-private" version = "0.40.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=02d902c9845d5ed9e5cb88fd32a8c254742f20fd#02d902c9845d5ed9e5cb88fd32a8c254742f20fd" +source = "git+https://github.com/dashpay/rust-dashcore?rev=befd0356bebfcd0d06d1028d8a03bfa4c78bd219#befd0356bebfcd0d06d1028d8a03bfa4c78bd219" [[package]] name = "dashcore-rpc" version = "0.40.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=02d902c9845d5ed9e5cb88fd32a8c254742f20fd#02d902c9845d5ed9e5cb88fd32a8c254742f20fd" +source = "git+https://github.com/dashpay/rust-dashcore?rev=befd0356bebfcd0d06d1028d8a03bfa4c78bd219#befd0356bebfcd0d06d1028d8a03bfa4c78bd219" dependencies = [ "dashcore-rpc-json", "hex", @@ -1602,7 +1548,7 @@ dependencies = [ [[package]] name = "dashcore-rpc-json" version = "0.40.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=02d902c9845d5ed9e5cb88fd32a8c254742f20fd#02d902c9845d5ed9e5cb88fd32a8c254742f20fd" +source = "git+https://github.com/dashpay/rust-dashcore?rev=befd0356bebfcd0d06d1028d8a03bfa4c78bd219#befd0356bebfcd0d06d1028d8a03bfa4c78bd219" dependencies = [ "bincode 2.0.0-rc.3", "dashcore", @@ -1617,7 +1563,7 @@ dependencies = [ [[package]] name = "dashcore_hashes" version = "0.40.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=02d902c9845d5ed9e5cb88fd32a8c254742f20fd#02d902c9845d5ed9e5cb88fd32a8c254742f20fd" +source = "git+https://github.com/dashpay/rust-dashcore?rev=befd0356bebfcd0d06d1028d8a03bfa4c78bd219#befd0356bebfcd0d06d1028d8a03bfa4c78bd219" dependencies = [ "bincode 2.0.0-rc.3", "dashcore-private", @@ -1903,7 +1849,7 @@ dependencies = [ "bs58", "chrono", "ciborium", - "clap 4.5.45", + "clap", "console-subscriber", "dapi-grpc", "delegate", @@ -2738,15 +2684,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - [[package]] name = "hermit-abi" version = "0.5.2" @@ -3254,7 +3191,7 @@ version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ - "hermit-abi 0.5.2", + "hermit-abi", "libc", "windows-sys 0.59.0", ] @@ -3424,7 +3361,7 @@ dependencies = [ [[package]] name = "key-wallet" version = "0.40.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=02d902c9845d5ed9e5cb88fd32a8c254742f20fd#02d902c9845d5ed9e5cb88fd32a8c254742f20fd" +source = "git+https://github.com/dashpay/rust-dashcore?rev=befd0356bebfcd0d06d1028d8a03bfa4c78bd219#befd0356bebfcd0d06d1028d8a03bfa4c78bd219" dependencies = [ "aes", "base58ck", @@ -3452,7 +3389,7 @@ dependencies = [ [[package]] name = "key-wallet-ffi" version = "0.40.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=02d902c9845d5ed9e5cb88fd32a8c254742f20fd#02d902c9845d5ed9e5cb88fd32a8c254742f20fd" +source = "git+https://github.com/dashpay/rust-dashcore?rev=befd0356bebfcd0d06d1028d8a03bfa4c78bd219#befd0356bebfcd0d06d1028d8a03bfa4c78bd219" dependencies = [ "cbindgen 0.29.0", "dash-network", @@ -3468,7 +3405,7 @@ dependencies = [ [[package]] name = "key-wallet-manager" version = "0.40.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=02d902c9845d5ed9e5cb88fd32a8c254742f20fd#02d902c9845d5ed9e5cb88fd32a8c254742f20fd" +source = "git+https://github.com/dashpay/rust-dashcore?rev=befd0356bebfcd0d06d1028d8a03bfa4c78bd219#befd0356bebfcd0d06d1028d8a03bfa4c78bd219" dependencies = [ "async-trait", "bincode 2.0.0-rc.3", @@ -4021,7 +3958,7 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" dependencies = [ - "hermit-abi 0.5.2", + "hermit-abi", "libc", ] @@ -4143,12 +4080,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "os_str_bytes" -version = "6.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" - [[package]] name = "overload" version = "0.1.1" @@ -5145,7 +5076,6 @@ dependencies = [ "cbindgen 0.27.0", "dash-sdk", "dash-spv-ffi", - "dashcore", "dotenvy", "drive-proof-verifier", "env_logger 0.11.8", @@ -5919,12 +5849,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - [[package]] name = "strsim" version = "0.11.1" @@ -6180,12 +6104,6 @@ dependencies = [ "test-case-core", ] -[[package]] -name = "textwrap" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" - [[package]] name = "thiserror" version = "1.0.69" @@ -6409,15 +6327,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "toml" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" -dependencies = [ - "serde", -] - [[package]] name = "toml" version = "0.8.23" diff --git a/packages/dash-platform-balance-checker/src/main_trusted.rs b/packages/dash-platform-balance-checker/src/main_trusted.rs index 1c30d714c3..e622555f16 100644 --- a/packages/dash-platform-balance-checker/src/main_trusted.rs +++ b/packages/dash-platform-balance-checker/src/main_trusted.rs @@ -3,7 +3,7 @@ use dash_sdk::dapi_client::{Address, AddressList}; use dash_sdk::dpp::identity::accessors::IdentityGettersV0; use dash_sdk::dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; use dash_sdk::platform::{Fetch, Identifier, Identity}; -use dash_sdk::{Sdk, SdkBuilder}; +use dash_sdk::SdkBuilder; use dpp::dashcore::Network; use rs_sdk_trusted_context_provider::TrustedHttpContextProvider; use std::num::NonZeroUsize; diff --git a/packages/rs-dpp/Cargo.toml b/packages/rs-dpp/Cargo.toml index 4821a7c007..905f2a5c52 100644 --- a/packages/rs-dpp/Cargo.toml +++ b/packages/rs-dpp/Cargo.toml @@ -23,17 +23,17 @@ chrono = { version = "0.4.35", default-features = false, features = [ ] } chrono-tz = { version = "0.8", optional = true } ciborium = { version = "0.2.2", optional = true } -dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "02d902c9845d5ed9e5cb88fd32a8c254742f20fd", features = [ +dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "befd0356bebfcd0d06d1028d8a03bfa4c78bd219", features = [ "std", "secp-recovery", "rand", "signer", "serde", ], default-features = false } -key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "02d902c9845d5ed9e5cb88fd32a8c254742f20fd", optional = true } -key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "02d902c9845d5ed9e5cb88fd32a8c254742f20fd", optional = true } -dash-spv = { git = "https://github.com/dashpay/rust-dashcore", rev = "02d902c9845d5ed9e5cb88fd32a8c254742f20fd", optional = true } -dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "02d902c9845d5ed9e5cb88fd32a8c254742f20fd", optional = true } +key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "befd0356bebfcd0d06d1028d8a03bfa4c78bd219", optional = true } +key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "befd0356bebfcd0d06d1028d8a03bfa4c78bd219", optional = true } +dash-spv = { git = "https://github.com/dashpay/rust-dashcore", rev = "befd0356bebfcd0d06d1028d8a03bfa4c78bd219", optional = true } +dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "befd0356bebfcd0d06d1028d8a03bfa4c78bd219", optional = true } env_logger = { version = "0.11" } getrandom = { version = "0.2", features = ["js"] } diff --git a/packages/rs-dpp/src/fee/fee_result/mod.rs b/packages/rs-dpp/src/fee/fee_result/mod.rs index b87c9f7785..0be1214e84 100644 --- a/packages/rs-dpp/src/fee/fee_result/mod.rs +++ b/packages/rs-dpp/src/fee/fee_result/mod.rs @@ -149,7 +149,7 @@ impl BalanceChangeForIdentity { match self.change { AddToBalance { .. } => { // when we add balance we are sure that all the storage fee and processing fee has - // been payed + // been paid Ok(self.into_fee_result()) } RemoveFromBalance { diff --git a/packages/rs-platform-wallet/Cargo.toml b/packages/rs-platform-wallet/Cargo.toml index b73c3151ea..25e1879265 100644 --- a/packages/rs-platform-wallet/Cargo.toml +++ b/packages/rs-platform-wallet/Cargo.toml @@ -11,11 +11,11 @@ description = "Platform wallet with identity management support" dpp = { path = "../rs-dpp" } # Key wallet dependencies (from rust-dashcore) -key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "02d902c9845d5ed9e5cb88fd32a8c254742f20fd" } -key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "02d902c9845d5ed9e5cb88fd32a8c254742f20fd", optional = true } +key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "befd0356bebfcd0d06d1028d8a03bfa4c78bd219" } +key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "befd0356bebfcd0d06d1028d8a03bfa4c78bd219", optional = true } # Core dependencies -dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "02d902c9845d5ed9e5cb88fd32a8c254742f20fd" } +dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "befd0356bebfcd0d06d1028d8a03bfa4c78bd219" } # Standard dependencies serde = { version = "1.0", features = ["derive"] } diff --git a/packages/rs-sdk-ffi/Cargo.toml b/packages/rs-sdk-ffi/Cargo.toml index a6df912e0c..d6f52da57e 100644 --- a/packages/rs-sdk-ffi/Cargo.toml +++ b/packages/rs-sdk-ffi/Cargo.toml @@ -9,6 +9,8 @@ description = "FFI bindings for Dash Platform SDK - C-compatible interface for c [lib] crate-type = ["staticlib", "cdylib", "rlib"] + + [dependencies] dash-sdk = { path = "../rs-sdk", features = ["dpns-contract", "dashpay-contract"] } drive-proof-verifier = { path = "../rs-drive-proof-verifier" } @@ -16,8 +18,7 @@ rs-sdk-trusted-context-provider = { path = "../rs-sdk-trusted-context-provider", simple-signer = { path = "../simple-signer" } # Core SDK integration (always included for unified SDK) -dash-spv-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "02d902c9845d5ed9e5cb88fd32a8c254742f20fd", optional = true } -dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "02d902c9845d5ed9e5cb88fd32a8c254742f20fd" } +dash-spv-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "befd0356bebfcd0d06d1028d8a03bfa4c78bd219", optional = true } # FFI and serialization serde = { version = "1.0", features = ["derive"] } diff --git a/packages/rs-sdk-ffi/build_ios.sh b/packages/rs-sdk-ffi/build_ios.sh index 434b49c1f3..d8cac72f17 100755 --- a/packages/rs-sdk-ffi/build_ios.sh +++ b/packages/rs-sdk-ffi/build_ios.sh @@ -34,6 +34,20 @@ NC='\033[0m' # No Color CARGO_FEATURES="" FRAMEWORK_NAME="DashSDKFFI" +# Set iOS deployment targets to match Xcode 16 SDKs and avoid 10.0 default +export IPHONEOS_DEPLOYMENT_TARGET="18.0" +export IPHONESIMULATOR_DEPLOYMENT_TARGET="18.0" +# Ensure linker uses the same min version when rustc links, but only for iOS targets +export CARGO_TARGET_AARCH64_APPLE_IOS_RUSTFLAGS="${CARGO_TARGET_AARCH64_APPLE_IOS_RUSTFLAGS:-} -C link-arg=-mios-version-min=${IPHONEOS_DEPLOYMENT_TARGET}" +export CARGO_TARGET_AARCH64_APPLE_IOS_SIM_RUSTFLAGS="${CARGO_TARGET_AARCH64_APPLE_IOS_SIM_RUSTFLAGS:-} -C link-arg=-mios-version-min=${IPHONESIMULATOR_DEPLOYMENT_TARGET}" +export CARGO_TARGET_X86_64_APPLE_IOS_RUSTFLAGS="${CARGO_TARGET_X86_64_APPLE_IOS_RUSTFLAGS:-} -C link-arg=-mios-version-min=${IPHONESIMULATOR_DEPLOYMENT_TARGET}" + +echo "Using IPHONEOS_DEPLOYMENT_TARGET=${IPHONEOS_DEPLOYMENT_TARGET}" +echo "Using IPHONESIMULATOR_DEPLOYMENT_TARGET=${IPHONESIMULATOR_DEPLOYMENT_TARGET}" +echo "CARGO_TARGET_AARCH64_APPLE_IOS_RUSTFLAGS=${CARGO_TARGET_AARCH64_APPLE_IOS_RUSTFLAGS}" +echo "CARGO_TARGET_AARCH64_APPLE_IOS_SIM_RUSTFLAGS=${CARGO_TARGET_AARCH64_APPLE_IOS_SIM_RUSTFLAGS}" +echo "CARGO_TARGET_X86_64_APPLE_IOS_RUSTFLAGS=${CARGO_TARGET_X86_64_APPLE_IOS_RUSTFLAGS}" + echo -e "${GREEN}Building Dash SDK FFI for iOS ($BUILD_ARCH)${NC}" # Check if we have the required iOS targets installed @@ -163,6 +177,27 @@ if [ -f "$KEY_WALLET_HEADER_PATH" ] && [ -f "$SPV_HEADER_PATH" ]; then extern "C" { #endif +// Forward declarations to ensure cross-refs compile regardless of merge order +typedef struct FFIClientConfig FFIClientConfig; +// Provide explicit opaque definitions so Swift can import the type names +typedef struct FFIDashSpvClient { unsigned char _private[0]; } FFIDashSpvClient; +typedef struct FFIWallet { unsigned char _private[0]; } FFIWallet; +typedef struct FFIAccount { unsigned char _private[0]; } FFIAccount; +typedef struct FFIAccountCollection { unsigned char _private[0]; } FFIAccountCollection; +typedef struct FFIBLSAccount { unsigned char _private[0]; } FFIBLSAccount; +typedef struct FFIEdDSAAccount { unsigned char _private[0]; } FFIEdDSAAccount; +typedef struct FFIAddressPool { unsigned char _private[0]; } FFIAddressPool; +typedef struct FFIManagedAccountCollection { unsigned char _private[0]; } FFIManagedAccountCollection; +typedef struct FFIWalletManager { unsigned char _private[0]; } FFIWalletManager; +typedef struct FFIManagedAccount { unsigned char _private[0]; } FFIManagedAccount; +// Platform SDK opaque handles +typedef struct SDKHandle { unsigned char _private[0]; } SDKHandle; +typedef struct DataContractHandle { unsigned char _private[0]; } DataContractHandle; +typedef struct DocumentHandle { unsigned char _private[0]; } DocumentHandle; +typedef struct IdentityHandle { unsigned char _private[0]; } IdentityHandle; +typedef struct IdentityPublicKeyHandle { unsigned char _private[0]; } IdentityPublicKeyHandle; +typedef struct SignerHandle { unsigned char _private[0]; } SignerHandle; + // ============================================================================ // Key Wallet FFI Functions and Types // ============================================================================ @@ -198,13 +233,7 @@ EOF /^} \/\/ extern "C"$/ { next } # Skip extern "C" closing /^#endif.*__cplusplus/ { next } # Skip any endif with __cplusplus /^#endif \/\* KEY_WALLET_FFI_H \*\/$/ { exit } - in_content { - # Fix the ManagedWalletInfo reference in FFIManagedWallet struct - if (/ManagedWalletInfo \*inner;/) { - gsub(/ManagedWalletInfo \*inner;/, "FFIManagedWalletInfo *inner;") - } - print - } + in_content { print } ' "$KEY_WALLET_HEADER_PATH" >> "$MERGED_HEADER" # Add separator for SPV FFI @@ -214,9 +243,6 @@ EOF // Dash SPV FFI Functions and Types // ============================================================================ -// Forward declaration for FFIClientConfig (opaque type) -typedef struct FFIClientConfig FFIClientConfig; - EOF # Extract SPV FFI content @@ -229,9 +255,10 @@ EOF /^#pragma once/ { next } /^typedef struct CoreSDKHandle \{/ { skip = 1 } /^\} CoreSDKHandle;/ && skip { skip = 0; next } - /^typedef ClientConfig FFIClientConfig;/ { next } # Skip broken typedef /^#ifdef __cplusplus$/ { next } + /^namespace dash_spv_ffi \{/ { next } /^extern "C" \{$/ { next } + /^\} \/\/ namespace dash_spv_ffi$/ { next } /^} \/\/ extern "C"$/ { next } /^#endif.*__cplusplus/ { next } /^#endif.*DASH_SPV_FFI_H/ { next } @@ -294,6 +321,42 @@ else echo -e "${YELLOW} cd ../../../rust-dashcore/dash-spv-ffi && cargo build --release${NC}" fi +# Build dash-spv-ffi from local rust-dashcore for device and simulator +RUST_DASHCORE_PATH="$PROJECT_ROOT/../rust-dashcore" +SPV_CRATE_PATH="$RUST_DASHCORE_PATH/dash-spv-ffi" +if [ -d "$SPV_CRATE_PATH" ]; then + echo -e "${GREEN}Building dash-spv-ffi (local rust-dashcore)${NC}" + pushd "$SPV_CRATE_PATH" >/dev/null + if [ "$BUILD_ARCH" != "x86" ]; then + if [ -n "${RUST_DASHCORE_TOOLCHAIN:-}" ]; then + echo -e "${GREEN}Using toolchain '+${RUST_DASHCORE_TOOLCHAIN}' for device build${NC}" + cargo +"${RUST_DASHCORE_TOOLCHAIN}" build --lib --target aarch64-apple-ios --release > /tmp/cargo_build_spv_device.log 2>&1 || { echo -e "${RED}✗ dash-spv-ffi device build failed${NC}"; cat /tmp/cargo_build_spv_device.log; exit 1; } + else + cargo build --lib --target aarch64-apple-ios --release > /tmp/cargo_build_spv_device.log 2>&1 || { echo -e "${RED}✗ dash-spv-ffi device build failed${NC}"; cat /tmp/cargo_build_spv_device.log; exit 1; } + fi + fi + if [ "$BUILD_ARCH" = "universal" ]; then + if [ -n "${RUST_DASHCORE_TOOLCHAIN:-}" ]; then + echo -e "${GREEN}Using toolchain '+${RUST_DASHCORE_TOOLCHAIN}' for sim builds${NC}" + cargo +"${RUST_DASHCORE_TOOLCHAIN}" build --lib --target aarch64-apple-ios-sim --release > /tmp/cargo_build_spv_sim_arm.log 2>&1 || { echo -e "${RED}✗ dash-spv-ffi sim (arm64) build failed${NC}"; cat /tmp/cargo_build_spv_sim_arm.log; exit 1; } + cargo +"${RUST_DASHCORE_TOOLCHAIN}" build --lib --target x86_64-apple-ios --release > /tmp/cargo_build_spv_sim_x86.log 2>&1 || { echo -e "${RED}✗ dash-spv-ffi sim (x86_64) build failed${NC}"; cat /tmp/cargo_build_spv_sim_x86.log; exit 1; } + else + cargo build --lib --target aarch64-apple-ios-sim --release > /tmp/cargo_build_spv_sim_arm.log 2>&1 || { echo -e "${RED}✗ dash-spv-ffi sim (arm64) build failed${NC}"; cat /tmp/cargo_build_spv_sim_arm.log; exit 1; } + cargo build --lib --target x86_64-apple-ios --release > /tmp/cargo_build_spv_sim_x86.log 2>&1 || { echo -e "${RED}✗ dash-spv-ffi sim (x86_64) build failed${NC}"; cat /tmp/cargo_build_spv_sim_x86.log; exit 1; } + fi + else + if [ -n "${RUST_DASHCORE_TOOLCHAIN:-}" ]; then + echo -e "${GREEN}Using toolchain '+${RUST_DASHCORE_TOOLCHAIN}' for sim build${NC}" + cargo +"${RUST_DASHCORE_TOOLCHAIN}" build --lib --target aarch64-apple-ios-sim --release > /tmp/cargo_build_spv_sim_arm.log 2>&1 || { echo -e "${RED}✗ dash-spv-ffi sim (arm64) build failed${NC}"; cat /tmp/cargo_build_spv_sim_arm.log; exit 1; } + else + cargo build --lib --target aarch64-apple-ios-sim --release > /tmp/cargo_build_spv_sim_arm.log 2>&1 || { echo -e "${RED}✗ dash-spv-ffi sim (arm64) build failed${NC}"; cat /tmp/cargo_build_spv_sim_arm.log; exit 1; } + fi + fi + popd >/dev/null +else + echo -e "${YELLOW}⚠ Local rust-dashcore not found at $SPV_CRATE_PATH; SPV symbols must be provided by rs-sdk-ffi${NC}" +fi + # Create simulator library based on architecture mkdir -p "$OUTPUT_DIR/simulator" @@ -314,6 +377,14 @@ fi if [ "$BUILD_ARCH" != "x86" ]; then mkdir -p "$OUTPUT_DIR/device" cp "$PROJECT_ROOT/target/aarch64-apple-ios/release/librs_sdk_ffi.a" "$OUTPUT_DIR/device/" + # Merge with dash-spv-ffi device lib if available + if [ -f "$SPV_CRATE_PATH/target/aarch64-apple-ios/release/libdash_spv_ffi.a" ]; then + echo -e "${GREEN}Merging device libs (rs-sdk-ffi + dash-spv-ffi)${NC}" + libtool -static -o "$OUTPUT_DIR/device/libDashSDKFFI_combined.a" \ + "$OUTPUT_DIR/device/librs_sdk_ffi.a" \ + "$SPV_CRATE_PATH/target/aarch64-apple-ios/release/libdash_spv_ffi.a" + COMBINED_DEVICE_LIB=1 + fi fi # Create module map; include SDK, SPV, and KeyWallet headers @@ -365,11 +436,30 @@ rm -rf "$OUTPUT_DIR/$FRAMEWORK_NAME.xcframework" XCFRAMEWORK_CMD="xcodebuild -create-xcframework" if [ "$BUILD_ARCH" != "x86" ] && [ -f "$OUTPUT_DIR/device/librs_sdk_ffi.a" ]; then - XCFRAMEWORK_CMD="$XCFRAMEWORK_CMD -library $OUTPUT_DIR/device/librs_sdk_ffi.a -headers $HEADERS_DIR" + if [ -n "${COMBINED_DEVICE_LIB:-}" ] && [ -f "$OUTPUT_DIR/device/libDashSDKFFI_combined.a" ]; then + XCFRAMEWORK_CMD="$XCFRAMEWORK_CMD -library $OUTPUT_DIR/device/libDashSDKFFI_combined.a -headers $HEADERS_DIR" + else + XCFRAMEWORK_CMD="$XCFRAMEWORK_CMD -library $OUTPUT_DIR/device/librs_sdk_ffi.a -headers $HEADERS_DIR" + fi fi if [ -f "$OUTPUT_DIR/simulator/librs_sdk_ffi.a" ]; then - XCFRAMEWORK_CMD="$XCFRAMEWORK_CMD -library $OUTPUT_DIR/simulator/librs_sdk_ffi.a -headers $HEADERS_DIR" + # Try to merge with SPV sim lib if it exists + SIM_SPV_LIB="" + if [ -f "$SPV_CRATE_PATH/target/aarch64-apple-ios-sim/release/libdash_spv_ffi.a" ]; then + SIM_SPV_LIB="$SPV_CRATE_PATH/target/aarch64-apple-ios-sim/release/libdash_spv_ffi.a" + elif [ -f "$SPV_CRATE_PATH/target/x86_64-apple-ios/release/libdash_spv_ffi.a" ]; then + SIM_SPV_LIB="$SPV_CRATE_PATH/target/x86_64-apple-ios/release/libdash_spv_ffi.a" + fi + if [ -n "$SIM_SPV_LIB" ]; then + echo -e "${GREEN}Merging simulator libs (rs-sdk-ffi + dash-spv-ffi)${NC}" + libtool -static -o "$OUTPUT_DIR/simulator/libDashSDKFFI_combined.a" \ + "$OUTPUT_DIR/simulator/librs_sdk_ffi.a" \ + "$SIM_SPV_LIB" + XCFRAMEWORK_CMD="$XCFRAMEWORK_CMD -library $OUTPUT_DIR/simulator/libDashSDKFFI_combined.a -headers $HEADERS_DIR" + else + XCFRAMEWORK_CMD="$XCFRAMEWORK_CMD -library $OUTPUT_DIR/simulator/librs_sdk_ffi.a -headers $HEADERS_DIR" + fi fi XCFRAMEWORK_CMD="$XCFRAMEWORK_CMD -output $OUTPUT_DIR/$FRAMEWORK_NAME.xcframework" diff --git a/packages/swift-sdk/IOS_SIMULATOR_MCP.md b/packages/swift-sdk/IOS_SIMULATOR_MCP.md new file mode 100644 index 0000000000..aa53bc87f1 --- /dev/null +++ b/packages/swift-sdk/IOS_SIMULATOR_MCP.md @@ -0,0 +1,432 @@ +# iOS Simulator MCP — Codex Guide (local snapshot) + +This file documents how Codex CLI in this repository is configured to use the iOS Simulator MCP server and includes an upstream README snapshot for quick reference. + +## Codex CLI configuration + +Add the following to `~/.codex/config.toml` and restart Codex CLI: + +```toml +[mcp_servers."ios-simulator"] +command = "/usr/bin/env" +args = ["npx", "-y", "ios-simulator-mcp@latest"] +cwd = "/Users/samuelw" +auto_start = true +env = { IOS_SIMULATOR_MCP_DEFAULT_OUTPUT_DIR = "/Users/samuelw/MCP" } +# Optional: pin a specific simulator +# env = { IOS_SIMULATOR_MCP_DEFAULT_OUTPUT_DIR = "/Users/samuelw/MCP", IDB_UDID = "YOUR-UDID" } +``` + +Tips +- Create the folder first: `mkdir -p /Users/samuelw/MCP` +- Relative `output_path` values write into `IOS_SIMULATOR_MCP_DEFAULT_OUTPUT_DIR`. +- You may still pass absolute paths per-call to tools. + +## Frequently used tools +- `screenshot { output_path: "sim.png" }` — saves to `/Users/samuelw/MCP/sim.png`. +- `record_video { output_path: "run.mp4" }` then `stop_recording`. +- `ui_describe_all`, `ui_describe_point`, `ui_tap`, `ui_swipe`, `ui_type`, `ui_view`. + +--- + +## Upstream README snapshot + +Source: https://github.com/joshuayoes/ios-simulator-mcp/blob/main/README.md +Snapshot date: 2025-09-09 + +> Note: If anything here drifts from upstream, prefer upstream docs. + +# iOS Simulator MCP Server + +[![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/install-mcp?name=ios-simulator&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsImlvcy1zaW11bGF0b3ItbWNwIl19) [![NPM Version](https://img.shields.io/npm/v/ios-simulator-mcp)](https://www.npmjs.com/package/ios-simulator-mcp) + +A Model Context Protocol (MCP) server for interacting with iOS simulators. This server allows you to interact with iOS simulators by getting information about them, controlling UI interactions, and inspecting UI elements. + +> **Security Notice**: Command injection vulnerabilities present in versions < 1.3.3 have been fixed. Please update to v1.3.3 or later. See [SECURITY.md](SECURITY.md) for details. + +https://github.com/user-attachments/assets/453ebe7b-cc93-4ac2-b08d-0f8ac8339ad3 + +## 🌟 Featured In + +This project has been featured and mentioned in various publications and resources: + +- [Claude Code Best Practices article](https://www.anthropic.com/engineering/claude-code-best-practices#:~:text=Write%20code%2C%20screenshot%20result%2C%20iterate) - Anthropic's engineering blog showcasing best practices +- [React Native Newsletter Issue 187](https://us3.campaign-archive.com/?u=78d9e37a94fa0b522939163d4&id=656ed2c2cf#:~:text=iOS%20Simulator%20MCP%20Server) - Featured in the most popular React Native community newsletter +- [Mobile Automation Newsletter - #56](https://testableapple.com/newsletter/56/#:~:text=iOS-,iOS%20Simulator%20MCP,-%F0%9F%8E%99%EF%B8%8F%20Joshua%20Yoes) - Featured a long running newsletter about mobile testing and automation resources +- [punkeye/awesome-mcp-server listing](https://github.com/punkpeye/awesome-mcp-servers) - Listed in one of the most popular curated awesome MCP servers collection + +## Tools + +### `get_booted_sim_id` + +**Description:** Get the ID of the currently booted iOS simulator + +**Parameters:** No Parameters + +### `ui_describe_all` + +**Description:** Describes accessibility information for the entire screen in the iOS Simulator + +**Parameters:** + +``` +{ + /** + * Udid of target, can also be set with the IDB_UDID env var + * Format: UUID (8-4-4-4-12 hexadecimal characters) + */ + udid?: string; +} +``` + +### `ui_tap` + +**Description:** Tap on the screen in the iOS Simulator + +**Parameters:** + +``` +{ + /** + * Press duration in seconds (decimal numbers allowed) + */ + duration?: string; + /** + * Udid of target, can also be set with the IDB_UDID env var + * Format: UUID (8-4-4-4-12 hexadecimal characters) + */ + udid?: string; + /** The x-coordinate */ + x: number; + /** The y-coordinate */ + y: number; +} +``` + +### `ui_type` + +**Description:** Input text into the iOS Simulator + +**Parameters:** + +``` +{ + /** + * Udid of target, can also be set with the IDB_UDID env var + * Format: UUID (8-4-4-4-12 hexadecimal characters) + */ + udid?: string; + /** + * Text to input + * Format: ASCII printable characters only + */ + text: string; +} +``` + +### `ui_swipe` + +**Description:** Swipe on the screen in the iOS Simulator + +**Parameters:** + +``` +{ + /** + * Udid of target, can also be set with the IDB_UDID env var + * Format: UUID (8-4-4-4-12 hexadecimal characters) + */ + udid?: string; + /** The starting x-coordinate */ + x_start: number; + /** The starting y-coordinate */ + y_start: number; + /** The ending x-coordinate */ + x_end: number; + /** The ending y-coordinate */ + y_end: number; + /** The size of each step in the swipe (default is 1) */ + delta?: number; +} +``` + +### `ui_describe_point` + +**Description:** Returns the accessibility element at given co-ordinates on the iOS Simulator's screen + +**Parameters:** + +``` +{ + /** + * Udid of target, can also be set with the IDB_UDID env var + * Format: UUID (8-4-4-4-12 hexadecimal characters) + */ + udid?: string; + /** The x-coordinate */ + x: number; + /** The y-coordinate */ + y: number; +} +``` + +### `ui_view` + +**Description:** Get the image content of a compressed screenshot of the current simulator view + +**Parameters:** + +``` +{ + /** + * Udid of target, can also be set with the IDB_UDID env var + * Format: UUID (8-4-4-4-12 hexadecimal characters) + */ + udid?: string; +} +``` + +### `screenshot` + +**Description:** Takes a screenshot of the iOS Simulator + +**Parameters:** + +``` +{ + /** + * Udid of target, can also be set with the IDB_UDID env var + * Format: UUID (8-4-4-4-12 hexadecimal characters) + */ + udid?: string; + /** File path where the screenshot will be saved. If relative, it uses the directory specified by the `IOS_SIMULATOR_MCP_DEFAULT_OUTPUT_DIR` env var, or `~/Downloads` if not set. */ + output_path: string; + /** Image format (png, tiff, bmp, gif, or jpeg). Default is png. */ + type?: "png" | "tiff" | "bmp" | "gif" | "jpeg"; + /** Display to capture (internal or external). Default depends on device type. */ + display?: "internal" | "external"; + /** For non-rectangular displays, handle the mask by policy (ignored, alpha, or black) */ + mask?: "ignored" | "alpha" | "black"; +} +``` + +### `record_video` + +**Description:** Records a video of the iOS Simulator using simctl directly + +**Parameters:** + +``` +{ + /** Optional output path. If not provided, a default name will be used. The file will be saved in the directory specified by `IOS_SIMULATOR_MCP_DEFAULT_OUTPUT_DIR` or in `~/Downloads` if the environment variable is not set. */ + output_path?: string; + /** Specifies the codec type: "h264" or "hevc". Default is "hevc". */ + codec?: "h264" | "hevc"; + /** Display to capture: "internal" or "external". Default depends on device type. */ + display?: "internal" | "external"; + /** For non-rectangular displays, handle the mask by policy: "ignored", "alpha", or "black". */ + mask?: "ignored" | "alpha" | "black"; + /** Force the output file to be written to, even if the file already exists. */ + force?: boolean; +} +``` + +### `stop_recording` + +**Description:** Stops the simulator video recording using killall + +**Parameters:** No Parameters + +## 💡 Use Case: QA Step via MCP Tool Calls + +This MCP server allows AI assistants integrated with a Model Context Protocol (MCP) client to perform Quality Assurance tasks by making tool calls. This is useful immediately after implementing features to help ensure UI consistency and correct behavior. + +### How to Use + +After a feature implementation, instruct your AI assistant within its MCP client environment to use the available tools. For example, in Cursor's agent mode, you could use the prompts below to quickly validate and document UI interactions. + +### Example Prompts + +- **Verify UI Elements:** + + ``` + Verify all accessibility elements on the current screen + ``` + +- **Confirm Text Input:** + + ``` + Enter "QA Test" into the text input field and confirm the input is correct + ``` + +- **Check Tap Response:** + + ``` + Tap on coordinates x=250, y=400 and verify the expected element is triggered + ``` + +- **Validate Swipe Action:** + + ``` + Swipe from x=150, y=600 to x=150, y=100 and confirm correct behavior + ``` + +- **Detailed Element Check:** + + ``` + Describe the UI element at position x=300, y=350 to ensure proper labeling and functionality + ``` + +- **Show Your AI Agent the Simulator Screen:** + + ``` + View the current simulator screen + ``` + +- **Take Screenshot:** + + ``` + Take a screenshot of the current simulator screen and save it to my_screenshot.png + ``` + +- **Record Video:** + + ``` + Start recording a video of the simulator screen (saves to the default output directory, which is `~/Downloads` unless overridden by `IOS_SIMULATOR_MCP_DEFAULT_OUTPUT_DIR`) + ``` + +- **Stop Recording:** + ``` + Stop the current simulator screen recording + ``` + +## Prerequisites + +- Node.js +- macOS (as iOS simulators are only available on macOS) +- [Xcode](https://developer.apple.com/xcode/resources/) and iOS simulators installed +- Facebook [IDB](https://fbidb.io/) tool [(see install guide)](https://fbidb.io/docs/installation) + +## Installation + +This section provides instructions for integrating the iOS Simulator MCP server with different Model Context Protocol (MCP) clients. + +### Installation with Cursor + +Cursor manages MCP servers through its configuration file located at `~/.cursor/mcp.json`. + +#### Option 1: Using NPX (Recommended) + +1. Edit your Cursor MCP configuration file. You can often open it directly from Cursor or use a command like: + ```bash + # Open with your default editor (or use 'code', 'vim', etc.) + open ~/.cursor/mcp.json + # Or use Cursor's command if available + # cursor ~/.cursor/mcp.json + ``` +2. Add or update the `mcpServers` section with the iOS simulator server configuration: + ```json + { + "mcpServers": { + // ... other servers might be listed here ... + "ios-simulator": { + "command": "npx", + "args": ["-y", "ios-simulator-mcp"] + } + } + } + ``` + Ensure the JSON structure is valid, especially if `mcpServers` already exists. +3. Restart Cursor for the changes to take effect. + +#### Option 2: Local Development + +1. Clone this repository: + ```bash + git clone https://github.com/joshuayoes/ios-simulator-mcp + cd ios-simulator-mcp + ``` +2. Install dependencies: + ```bash + npm install + ``` +3. Build the project: + ```bash + npm run build + ``` +4. Edit your Cursor MCP configuration file (as shown in Option 1). +5. Add or update the `mcpServers` section, pointing to your local build: + ```json + { + "mcpServers": { + // ... other servers might be listed here ... + "ios-simulator": { + "command": "node", + "args": ["/full/path/to/your/ios-simulator-mcp/build/index.js"] + } + } + } + ``` + **Important:** Replace `/full/path/to/your/` with the absolute path to where you cloned the `ios-simulator-mcp` repository. +6. Restart Cursor for the changes to take effect. + +### Installation with Claude Code + +Claude Code CLI can manage MCP servers using the `claude mcp` commands or by editing its configuration files directly. For more details on Claude Code MCP configuration, refer to the [official documentation](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/tutorials#set-up-model-context-protocol-mcp). + +#### Option 1: Using NPX (Recommended) + +1. Add the server using the `claude mcp add` command: + ```bash + claude mcp add ios-simulator npx ios-simulator-mcp + ``` +2. Restart any running Claude Code sessions if necessary. + +#### Option 2: Local Development + +1. Clone this repository, install dependencies, and build the project as described in the Cursor "Local Development" steps 1-3. +2. Add the server using the `claude mcp add` command, pointing to your local build: + ```bash + claude mcp add ios-simulator --command node --args "/full/path/to/your/ios-simulator-mcp/build/index.js" + ``` + **Important:** Replace `/full/path/to/your/` with the absolute path to where you cloned the `ios-simulator-mcp` repository. +3. Restart any running Claude Code sessions if necessary. + +## Configuration + +### Environment Variables + +| Variable | Description | Example | +| -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | +| `IOS_SIMULATOR_MCP_FILTERED_TOOLS` | A comma-separated list of tool names to filter out from being registered. | `screenshot,record_video,stop_recording` | +| `IOS_SIMULATOR_MCP_DEFAULT_OUTPUT_DIR` | Specifies a default directory for output files like screenshots and video recordings. If not set, `~/Downloads` will be used. This can be handy if your agent has limited access to the file system. | `~/Code/awesome-project/tmp` | + +#### Configuration Example + +``` +{ + "mcpServers": { + "ios-simulator": { + "command": "npx", + "args": ["-y", "ios-simulator-mcp"], + "env": { + "IOS_SIMULATOR_MCP_FILTERED_TOOLS": "screenshot,record_video,stop_recording", + "IOS_SIMULATOR_MCP_DEFAULT_OUTPUT_DIR": "~/Code/awesome-project/tmp" + } + } + } +} +``` + +## MCP Registry Server Listings + + + iOS Simulator MCP server + + +[![MseeP.ai Security Assessment Badge](https://mseep.net/pr/joshuayoes-ios-simulator-mcp-badge.png)](https://mseep.ai/app/joshuayoes-ios-simulator-mcp) + +## License + +MIT + diff --git a/packages/swift-sdk/Package.swift b/packages/swift-sdk/Package.swift index 6e30c9ca9c..be085e73ab 100644 --- a/packages/swift-sdk/Package.swift +++ b/packages/swift-sdk/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.8 +// swift-tools-version: 6.0 import PackageDescription @@ -23,7 +23,9 @@ let package = Package( .target( name: "SwiftDashSDK", dependencies: ["DashSDKFFI"], - path: "Sources/SwiftDashSDK" + path: "Sources/SwiftDashSDK", + exclude: ["KeyWallet/README.md"] ), - ] + ], + swiftLanguageModes: [.v6] ) diff --git a/packages/swift-sdk/README.md b/packages/swift-sdk/README.md index 1ce9adee6a..6286e76503 100644 --- a/packages/swift-sdk/README.md +++ b/packages/swift-sdk/README.md @@ -2,6 +2,8 @@ This Swift SDK provides iOS-friendly bindings for the Dash Platform, wrapping the `rs-sdk-ffi` crate with idiomatic Swift interfaces. +See also: iOS Simulator MCP usage and Codex config in [IOS_SIMULATOR_MCP.md](./IOS_SIMULATOR_MCP.md). + ## Features - **Identity Management**: Create, fetch, and manage Dash Platform identities @@ -441,4 +443,4 @@ The underlying FFI is thread-safe, but individual handles should not be shared a ## License -This SDK follows the same license as the Dash Platform project. \ No newline at end of file +This SDK follows the same license as the Dash Platform project. diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/ConcurrencyCompat.swift b/packages/swift-sdk/Sources/SwiftDashSDK/ConcurrencyCompat.swift new file mode 100644 index 0000000000..aacb0393a6 --- /dev/null +++ b/packages/swift-sdk/Sources/SwiftDashSDK/ConcurrencyCompat.swift @@ -0,0 +1,11 @@ +import Foundation + +// Swift 6 sendability adjustments for FFI pointers and wrappers. +// These are safe under our usage patterns where FFI pointers are thread-confined +// or explicitly synchronized at the Rust boundary. + +extension OpaquePointer: @retroactive @unchecked Sendable {} + +// FFI value types from DashSDKFFI headers used across actor boundaries +// These are plain C structs and treated as inert data blobs. +extension FFIDetailedSyncProgress: @retroactive @unchecked Sendable {} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/DataContract.swift b/packages/swift-sdk/Sources/SwiftDashSDK/DataContract.swift index 4e1242488c..60b7de1cbb 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/DataContract.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/DataContract.swift @@ -1,4 +1,5 @@ import Foundation +import DashSDKFFI /// Swift wrapper for Dash Platform Data Contract public class DataContract { @@ -13,11 +14,11 @@ public class DataContract { } /// Create a DataContract from a C handle - public init?(handle: OpaquePointer) { + public init?(handle: UnsafeMutablePointer) { // In a real implementation, this would extract data from the C handle // For now, create a placeholder self.id = "placeholder" self.ownerId = "placeholder" self.schema = [:] } -} \ No newline at end of file +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Identity.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Identity.swift index be695a3eae..e87ec71a76 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Identity.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Identity.swift @@ -1,4 +1,5 @@ import Foundation +import DashSDKFFI /// Swift wrapper for Dash Platform Identity public class Identity { @@ -13,7 +14,7 @@ public class Identity { } /// Create an Identity from a C handle - public init?(handle: OpaquePointer) { + public init?(handle: UnsafeMutablePointer) { // In a real implementation, this would extract data from the C handle // For now, create a placeholder self.id = "placeholder" @@ -22,4 +23,4 @@ public class Identity { } /// Get the balance (already accessible as property) -} \ No newline at end of file +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Account.swift b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Account.swift index 8b89ae907e..40fa890a22 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Account.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Account.swift @@ -3,10 +3,10 @@ import DashSDKFFI /// Swift wrapper for a wallet account public class Account { - private let handle: OpaquePointer + private let handle: UnsafeMutablePointer private weak var wallet: Wallet? - internal init(handle: OpaquePointer, wallet: Wallet) { + internal init(handle: UnsafeMutablePointer, wallet: Wallet) { self.handle = handle self.wallet = wallet } diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/AccountCollection.swift b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/AccountCollection.swift index ccea0789d6..2134032b63 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/AccountCollection.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/AccountCollection.swift @@ -3,10 +3,10 @@ import DashSDKFFI /// Swift wrapper for a collection of accounts public class AccountCollection { - private let handle: OpaquePointer + private let handle: UnsafeMutablePointer private weak var wallet: Wallet? - internal init(handle: OpaquePointer, wallet: Wallet) { + internal init(handle: UnsafeMutablePointer, wallet: Wallet) { self.handle = handle self.wallet = wallet } @@ -22,7 +22,7 @@ public class AccountCollection { guard let rawPointer = account_collection_get_provider_operator_keys(handle) else { return nil } - let accountHandle = OpaquePointer(rawPointer) + let accountHandle = rawPointer.assumingMemoryBound(to: FFIBLSAccount.self) return BLSAccount(handle: accountHandle, wallet: wallet) } @@ -33,7 +33,7 @@ public class AccountCollection { guard let rawPointer = account_collection_get_provider_platform_keys(handle) else { return nil } - let accountHandle = OpaquePointer(rawPointer) + let accountHandle = rawPointer.assumingMemoryBound(to: FFIEdDSAAccount.self) return EdDSAAccount(handle: accountHandle, wallet: wallet) } @@ -51,4 +51,4 @@ public class AccountCollection { return AccountCollectionSummary(ffiSummary: summaryPtr.pointee) } -} \ No newline at end of file +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/AddressPool.swift b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/AddressPool.swift index a0032bde44..0db5861bdb 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/AddressPool.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/AddressPool.swift @@ -3,9 +3,9 @@ import DashSDKFFI /// Swift wrapper for an address pool from a managed account public class AddressPool { - private let handle: OpaquePointer + private let handle: UnsafeMutablePointer - internal init(handle: OpaquePointer) { + internal init(handle: UnsafeMutablePointer) { self.handle = handle } @@ -116,4 +116,4 @@ public struct AddressInfo { self.used = ffiInfo.used self.generatedAt = Date(timeIntervalSince1970: TimeInterval(ffiInfo.generated_at)) } -} \ No newline at end of file +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/BLSAccount.swift b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/BLSAccount.swift index c0a4458f96..b8860e38a7 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/BLSAccount.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/BLSAccount.swift @@ -3,10 +3,10 @@ import DashSDKFFI /// Swift wrapper for a BLS account (used for provider keys) public class BLSAccount { - internal let handle: OpaquePointer + internal let handle: UnsafeMutablePointer private weak var wallet: Wallet? - internal init(handle: OpaquePointer, wallet: Wallet?) { + internal init(handle: UnsafeMutablePointer, wallet: Wallet?) { self.handle = handle self.wallet = wallet } @@ -17,4 +17,4 @@ public class BLSAccount { // BLS account specific functionality can be added here // This class manages the lifecycle of BLS provider key accounts -} \ No newline at end of file +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/EdDSAAccount.swift b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/EdDSAAccount.swift index 0260257a52..2abd295c61 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/EdDSAAccount.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/EdDSAAccount.swift @@ -3,10 +3,10 @@ import DashSDKFFI /// Swift wrapper for an EdDSA account (used for platform P2P keys) public class EdDSAAccount { - internal let handle: OpaquePointer + internal let handle: UnsafeMutablePointer private weak var wallet: Wallet? - internal init(handle: OpaquePointer, wallet: Wallet?) { + internal init(handle: UnsafeMutablePointer, wallet: Wallet?) { self.handle = handle self.wallet = wallet } @@ -17,4 +17,4 @@ public class EdDSAAccount { // EdDSA account specific functionality can be added here // This class manages the lifecycle of EdDSA platform P2P key accounts -} \ No newline at end of file +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/ManagedAccount.swift b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/ManagedAccount.swift index 3e3bc2b591..c1f4e62094 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/ManagedAccount.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/ManagedAccount.swift @@ -3,10 +3,10 @@ import DashSDKFFI /// Swift wrapper for a managed account with address pool management public class ManagedAccount { - internal let handle: OpaquePointer + internal let handle: UnsafeMutablePointer private let manager: WalletManager - internal init(handle: OpaquePointer, manager: WalletManager) { + internal init(handle: UnsafeMutablePointer, manager: WalletManager) { self.handle = handle self.manager = manager } diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/ManagedAccountCollection.swift b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/ManagedAccountCollection.swift index 56a25e52f5..b2cf02286f 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/ManagedAccountCollection.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/ManagedAccountCollection.swift @@ -3,10 +3,10 @@ import DashSDKFFI /// Swift wrapper for a collection of managed accounts public class ManagedAccountCollection { - private let handle: OpaquePointer + private let handle: UnsafeMutablePointer private let manager: WalletManager - internal init(handle: OpaquePointer, manager: WalletManager) { + internal init(handle: UnsafeMutablePointer, manager: WalletManager) { self.handle = handle self.manager = manager } @@ -211,7 +211,7 @@ public class ManagedAccountCollection { guard let rawPointer = managed_account_collection_get_provider_operator_keys(handle) else { return nil } - let accountHandle = OpaquePointer(rawPointer) + let accountHandle = rawPointer.assumingMemoryBound(to: FFIManagedAccount.self) return ManagedAccount(handle: accountHandle, manager: manager) } @@ -225,7 +225,7 @@ public class ManagedAccountCollection { guard let rawPointer = managed_account_collection_get_provider_platform_keys(handle) else { return nil } - let accountHandle = OpaquePointer(rawPointer) + let accountHandle = rawPointer.assumingMemoryBound(to: FFIManagedAccount.self) return ManagedAccount(handle: accountHandle, manager: manager) } @@ -248,4 +248,4 @@ public class ManagedAccountCollection { return ManagedAccountCollectionSummary(ffiSummary: summaryPtr.pointee) } -} \ No newline at end of file +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/ManagedWallet.swift b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/ManagedWallet.swift index 8be3a575c3..6598c01b8f 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/ManagedWallet.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/ManagedWallet.swift @@ -3,7 +3,7 @@ import DashSDKFFI /// Swift wrapper for managed wallet with address pool management and transaction checking public class ManagedWallet { - private let handle: UnsafeMutablePointer + private let handle: UnsafeMutablePointer private let network: KeyWalletNetwork /// Create a managed wallet wrapper from a regular wallet @@ -421,10 +421,8 @@ public class ManagedWallet { // MARK: - Private Helpers - private func getInfoHandle() -> OpaquePointer? { - // The handle is an FFIManagedWallet*, which contains an FFIManagedWalletInfo* as inner - // We treat it as opaque in Swift - return OpaquePointer(handle) + private func getInfoHandle() -> UnsafeMutablePointer? { + // The handle is an FFIManagedWalletInfo* (opaque C handle) + return handle } } - diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Wallet.swift b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Wallet.swift index d5054310f3..b79500dbd0 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Wallet.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Wallet.swift @@ -3,7 +3,7 @@ import DashSDKFFI /// Swift wrapper for a Dash wallet with HD key derivation public class Wallet { - private let handle: OpaquePointer + private let handle: UnsafeMutablePointer internal let network: KeyWalletNetwork private let ownsHandle: Bool @@ -36,7 +36,7 @@ public class Wallet { self.network = network var error = FFIError() - let walletPtr: OpaquePointer? + let walletPtr: UnsafeMutablePointer? if case .specificAccounts = accountOptions { // Use the with_options variant for specific accounts @@ -113,7 +113,7 @@ public class Wallet { self.ownsHandle = true var error = FFIError() - let walletPtr: OpaquePointer? = seed.withUnsafeBytes { seedBytes in + let walletPtr: UnsafeMutablePointer? = seed.withUnsafeBytes { seedBytes in let seedPtr = seedBytes.bindMemory(to: UInt8.self).baseAddress if case .specificAccounts = accountOptions { @@ -192,7 +192,7 @@ public class Wallet { public static func createRandom(network: KeyWalletNetwork = .mainnet, accountOptions: AccountCreationOption = .default) throws -> Wallet { var error = FFIError() - let walletPtr: OpaquePointer? + let walletPtr: UnsafeMutablePointer? if case .specificAccounts = accountOptions { var options = accountOptions.toFFIOptions() @@ -217,7 +217,7 @@ public class Wallet { } /// Private initializer for internal use (takes ownership) - private init(handle: OpaquePointer, network: KeyWalletNetwork) { + private init(handle: UnsafeMutablePointer, network: KeyWalletNetwork) { self.handle = handle self.network = network self.ownsHandle = true @@ -531,13 +531,11 @@ public class Wallet { return AccountCollection(handle: collectionHandle, wallet: self) } - internal var ffiHandle: OpaquePointer { - return handle - } + internal var ffiHandle: UnsafeMutablePointer { handle } // Non-owning initializer for wallets obtained from WalletManager public init(nonOwningHandle handle: UnsafeRawPointer, network: KeyWalletNetwork) { - self.handle = OpaquePointer(handle) + self.handle = UnsafeMutablePointer(mutating: handle.bindMemory(to: FFIWallet.self, capacity: 1)) self.network = network self.ownsHandle = false } diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift index b86a861beb..430edb9837 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift @@ -3,7 +3,7 @@ import DashSDKFFI /// Swift wrapper for wallet manager that manages multiple wallets public class WalletManager { - private let handle: OpaquePointer + private let handle: UnsafeMutablePointer private let ownsHandle: Bool /// Create a new standalone wallet manager @@ -25,21 +25,21 @@ public class WalletManager { /// Create a wallet manager from an SPV client /// - Parameter spvClient: The FFI SPV client handle to get the wallet manager from - public init(fromSPVClient spvClient: OpaquePointer) throws { + public init(fromSPVClient spvClient: UnsafeMutablePointer) throws { // Note: dash_spv_ffi_client_get_wallet_manager returns a pointer to FFIWalletManager // but Swift can't see that type, so we treat it as OpaquePointer let managerPtr = dash_spv_ffi_client_get_wallet_manager(spvClient) - guard let managerHandle = managerPtr else { + guard let managerHandle = managerPtr?.assumingMemoryBound(to: FFIWalletManager.self) else { throw KeyWalletError.walletError("Failed to get wallet manager from SPV client") } - self.handle = OpaquePointer(managerHandle) + self.handle = managerHandle self.ownsHandle = true } /// Create a wallet manager wrapper from an existing handle (does not own the handle) - /// - Parameter handle: The FFI wallet manager handle (OpaquePointer) - internal init(handle: OpaquePointer) { + /// - Parameter handle: The FFI wallet manager handle + internal init(handle: UnsafeMutablePointer) { self.handle = handle self.ownsHandle = false } @@ -589,9 +589,7 @@ public class WalletManager { return ManagedAccountCollection(handle: collection, manager: self) } - internal var ffiHandle: OpaquePointer { - return handle - } + internal var ffiHandle: UnsafeMutablePointer { handle } // MARK: - Serialization diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/SDK.swift b/packages/swift-sdk/Sources/SwiftDashSDK/SDK.swift index d4bdcb70c8..4cf4576513 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/SDK.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/SDK.swift @@ -53,8 +53,8 @@ extension Data { } /// Swift wrapper for the Dash Platform SDK -public class SDK { - public private(set) var handle: OpaquePointer? +public final class SDK: @unchecked Sendable { + public private(set) var handle: UnsafeMutablePointer? /// Identities operations public lazy var identities = Identities(sdk: self) @@ -83,17 +83,13 @@ public class SDK { print("🔵 SDK: Logging enabled at level: \(level)") } - /// Testnet DAPI addresses from WASM SDK (verified working) - private static let testnetDAPIAddresses = [ - "http://35.92.255.144:1443", - "https://52.12.176.90:1443", - "https://35.82.197.197:1443", - "https://44.240.98.102:1443", - "https://52.34.144.50:1443", - "https://44.239.39.153:1443", - "https://35.164.23.245:1443", - "https://54.149.33.167:1443" - ].joined(separator: ",") + /// Local Platform DAPI addresses; override via UserDefaults key "platformDAPIAddresses" + private static var platformDAPIAddresses: String { + if let override = UserDefaults.standard.string(forKey: "platformDAPIAddresses"), !override.isEmpty { + return override + } + return "http://127.0.0.1:1443" + } /// Create a new SDK instance with trusted setup /// @@ -108,20 +104,8 @@ public class SDK { config.network = network print("🔵 SDK.init: Network config set to: \(config.network)") - // Set DAPI addresses based on network - switch network { - case DashSDKNetwork(rawValue: 0): // Mainnet - config.dapi_addresses = nil // Use default mainnet addresses - case DashSDKNetwork(rawValue: 1): // Testnet - // Use the testnet addresses provided by the user - config.dapi_addresses = nil // Will be set below - case DashSDKNetwork(rawValue: 2): // Devnet - config.dapi_addresses = nil // Use default devnet addresses - case DashSDKNetwork(rawValue: 3): // Local - config.dapi_addresses = nil // Use default local addresses - default: - config.dapi_addresses = nil - } + // Default to SDK-provided addresses; may override below + config.dapi_addresses = nil config.skip_asset_lock_proof_verification = false config.request_retry_count = 1 @@ -130,9 +114,12 @@ public class SDK { // Create SDK with trusted setup print("🔵 SDK.init: Creating SDK with trusted setup...") let result: DashSDKResult - if network == DashSDKNetwork(rawValue: 1) { // Testnet - print("🔵 SDK.init: Using testnet DAPI addresses") - result = Self.testnetDAPIAddresses.withCString { addressesCStr -> DashSDKResult in + // Force local DAPI regardless of selected network when enabled + let forceLocal = UserDefaults.standard.bool(forKey: "useLocalhostPlatform") + if forceLocal { + let localAddresses = Self.platformDAPIAddresses + print("🔵 SDK.init: Using local DAPI addresses: \(localAddresses)") + result = localAddresses.withCString { addressesCStr -> DashSDKResult in var mutableConfig = config mutableConfig.dapi_addresses = addressesCStr print("🔵 SDK.init: Calling dash_sdk_create_trusted...") @@ -160,7 +147,7 @@ public class SDK { } // Store the handle - handle = OpaquePointer(result.data) + handle = result.data?.assumingMemoryBound(to: SDKHandle.self) } /// Load known contracts into the trusted context provider @@ -217,7 +204,6 @@ public class SDK { deinit { if let handle = handle { - // The handle is already the correct type for the C function dash_sdk_destroy(handle) } } @@ -251,7 +237,7 @@ public class SDK { dash_sdk_string_free(jsonCStr) } - guard let data = jsonStr.data(using: .utf8) else { + guard let data = jsonStr.data(using: String.Encoding.utf8) else { throw SDKError.serializationError("Invalid JSON data") } @@ -280,6 +266,7 @@ public class SDK { // } /// Get an identity by ID + @MainActor public func getIdentity(id: String) async throws -> Identity? { // This would call the C function to get identity // For now, return nil as placeholder @@ -287,6 +274,7 @@ public class SDK { } /// Get a data contract by ID + @MainActor public func getDataContract(id: String) async throws -> DataContract? { // This would call the C function to get data contract // For now, return nil as placeholder @@ -581,4 +569,3 @@ public class Contracts { return nil } } - diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/SPV/SPVClient.swift b/packages/swift-sdk/Sources/SwiftDashSDK/SPV/SPVClient.swift index ba61a2985e..7eb49929bf 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/SPV/SPVClient.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/SPV/SPVClient.swift @@ -10,9 +10,11 @@ private func spvProgressCallback( ) { guard let progressPtr = progressPtr, let userData = userData else { return } - + let snapshot = progressPtr.pointee let context = Unmanaged.fromOpaque(userData).takeUnretainedValue() - context.handleProgressUpdate(progressPtr) + DispatchQueue.main.async { + context.handleProgressUpdate(snapshot) + } } private func spvCompletionCallback( @@ -21,9 +23,11 @@ private func spvCompletionCallback( userData: UnsafeMutableRawPointer? ) { guard let userData = userData else { return } - + let errorString: String? = errorMsg.map { String(cString: $0) } let context = Unmanaged.fromOpaque(userData).takeUnretainedValue() - context.handleSyncCompletion(success: success, errorMsg: errorMsg) + DispatchQueue.main.async { + context.handleSyncCompletion(success: success, error: errorString) + } } // MARK: - SPV Sync Progress @@ -35,6 +39,8 @@ public struct SPVSyncProgress { public let transactionProgress: Double public let currentHeight: UInt32 public let targetHeight: UInt32 + // Checkpoint height we started from (0 if none) + public let startHeight: UInt32 public let rate: Double // blocks per second public let estimatedTimeRemaining: TimeInterval? @@ -86,6 +92,7 @@ public protocol SPVClientDelegate: AnyObject { // MARK: - SPV Client +@MainActor public class SPVClient: ObservableObject { // Published properties for SwiftUI @Published public var isConnected = false @@ -98,9 +105,8 @@ public class SPVClient: ObservableObject { public weak var delegate: SPVClientDelegate? // FFI handles - // Treat SPV client as an opaque handle to avoid relying on the C struct name - private var client: OpaquePointer? - private var config: OpaquePointer? + private var client: UnsafeMutablePointer? + private var config: UnsafeMutablePointer? // Callback context private var callbackContext: CallbackContext? @@ -108,8 +114,12 @@ public class SPVClient: ObservableObject { // Network private let network: Network private var masternodeSyncEnabled: Bool = true + // If true, SPV will only connect to peers explicitly configured via FFI + public var restrictToConfiguredPeers: Bool = false // Sync tracking + // Height we start syncing from (checkpoint); used to render absolute heights + fileprivate var startFromHeight: UInt32 = 0 private var syncStartTime: Date? private var lastBlockHeight: UInt32 = 0 internal var syncCancelled = false @@ -127,10 +137,7 @@ public class SPVClient: ObservableObject { } deinit { - Task { @MainActor in - stop() - destroyClient() - } + // Minimal teardown; prefer explicit stop() by callers. } // MARK: - Client Lifecycle @@ -141,39 +148,66 @@ public class SPVClient: ObservableObject { } // Initialize SPV logging (one-time). Default to off unless SPV_LOG is provided. - struct SPVLogInit { static var done = false } - if !SPVLogInit.done { + enum SPVLogInit { + static let once: Void = { + let level = (ProcessInfo.processInfo.environment["SPV_LOG"] ?? "off") + _ = level.withCString { cstr in + dash_spv_ffi_init_logging(cstr) + } + }() + } + _ = SPVLogInit.once + if swiftLoggingEnabled { let level = (ProcessInfo.processInfo.environment["SPV_LOG"] ?? "off") - level.withCString { cstr in - dash_spv_ffi_init_logging(cstr) - } - SPVLogInit.done = true - if swiftLoggingEnabled { - print("[SPV][Log] Initialized SPV logging level=\(level)") - } + print("[SPV][Log] Initialized SPV logging level=\(level)") } // Create configuration based on network raw value - let rawConfigPtr: UnsafeMutableRawPointer? = { + let configPtr: UnsafeMutablePointer? = { switch network { case DashSDKNetwork(rawValue: 0): - return UnsafeMutableRawPointer(dash_spv_ffi_config_mainnet()) + return dash_spv_ffi_config_mainnet() case DashSDKNetwork(rawValue: 1): - return UnsafeMutableRawPointer(dash_spv_ffi_config_testnet()) + return dash_spv_ffi_config_testnet() case DashSDKNetwork(rawValue: 2): // Map devnet to custom FFINetwork value 3 - return UnsafeMutableRawPointer(dash_spv_ffi_config_new(FFINetwork(rawValue: 3))) + return dash_spv_ffi_config_new(FFINetwork(rawValue: 3)) default: - return UnsafeMutableRawPointer(dash_spv_ffi_config_testnet()) + return dash_spv_ffi_config_testnet() } }() - guard let rawConfigPtr = rawConfigPtr else { + guard let configPtr = configPtr else { throw SPVError.configurationFailed } - - let configPtr = OpaquePointer(rawConfigPtr) - + + // If requested, prefer local core peers (defaults to 127.0.0.1 with network default port) + let useLocalCore = UserDefaults.standard.bool(forKey: "useLocalhostCore") + if useLocalCore { + let peers = SPVClient.readLocalCorePeers() + if swiftLoggingEnabled { + print("[SPV][Config] Use Local Core enabled; peers=\(peers.joined(separator: ", "))") + } + // Add peers via FFI (supports "ip:port" or bare IP for network-default port) + for addr in peers { + addr.withCString { cstr in + let rc = dash_spv_ffi_config_add_peer(configPtr, cstr) + if rc != 0, let err = dash_spv_ffi_get_last_error() { + let msg = String(cString: err) + print("[SPV][Config] add_peer failed for \(addr): \(msg)") + } + } + } + // Enforce restrict mode when using local core by default + restrictToConfiguredPeers = true + } + + // Apply restrict-to-configured-peers if requested + if restrictToConfiguredPeers { + if swiftLoggingEnabled { print("[SPV][Config] Enabling restrict-to-configured-peers mode") } + _ = dash_spv_ffi_config_set_restrict_to_configured_peers(configPtr, true) + } + // Set data directory if provided if let dataDir = dataDir { let result = dash_spv_ffi_config_set_data_dir(configPtr, dataDir) @@ -225,6 +259,8 @@ public class SPVClient: ObservableObject { } let finalHeight: UInt32 = (rc == 0 && cpOutHeight > 0) ? cpOutHeight : h _ = dash_spv_ffi_config_set_start_from_height(configPtr, finalHeight) + // Remember checkpoint for UI normalization + self.startFromHeight = finalHeight } // Create client @@ -240,6 +276,16 @@ public class SPVClient: ObservableObject { setupEventCallbacks() } + private static func readLocalCorePeers() -> [String] { + // If no override is set, default to 127.0.0.1 and let FFI pick port by network + let raw = UserDefaults.standard.string(forKey: "corePeerAddresses")?.trimmingCharacters(in: .whitespacesAndNewlines) + let list = (raw?.isEmpty == false ? raw! : "127.0.0.1") + return list + .split(separator: ",") + .map { $0.trimmingCharacters(in: .whitespaces) } + .filter { !$0.isEmpty } + } + /// Enable/disable masternode sync. If the client is running, apply the update immediately. public func setMasternodeSyncEnabled(_ enabled: Bool) throws { self.masternodeSyncEnabled = enabled @@ -254,7 +300,7 @@ public class SPVClient: ObservableObject { } public func start() throws { - guard let client = client else { + guard self.client != nil else { throw SPVError.notInitialized } @@ -262,24 +308,22 @@ public class SPVClient: ObservableObject { if result != 0 { if let errorMsg = dash_spv_ffi_get_last_error() { let error = String(cString: errorMsg) - Task { @MainActor in self.lastError = error } + self.lastError = error throw SPVError.startFailed(error) } throw SPVError.startFailed("Unknown error") } - Task { @MainActor in self.isConnected = true } + self.isConnected = true } public func stop() { guard let client = client else { return } dash_spv_ffi_client_stop(client) - Task { @MainActor in - self.isConnected = false - self.isSyncing = false - self.syncProgress = nil - } + self.isConnected = false + self.isSyncing = false + self.syncProgress = nil } private func destroyClient() { @@ -299,7 +343,7 @@ public class SPVClient: ObservableObject { // MARK: - Synchronization public func startSync() async throws { - guard let client = client else { + guard self.client != nil else { throw SPVError.notInitialized } @@ -307,9 +351,7 @@ public class SPVClient: ObservableObject { throw SPVError.alreadySyncing } - await MainActor.run { - self.isSyncing = true - } + self.isSyncing = true syncCancelled = false syncStartTime = Date() @@ -374,8 +416,9 @@ public class SPVClient: ObservableObject { hash = Data(bytes: hashPtr, count: 32) } - Task { @MainActor in - context.client?.handleBlockEvent(height: height, hash: hash) + let clientRef = context.client + Task { @MainActor [weak clientRef] in + clientRef?.handleBlockEvent(height: height, hash: hash) } } @@ -395,8 +438,9 @@ public class SPVClient: ObservableObject { addresses = addressesStr.components(separatedBy: ",") } - Task { @MainActor in - context.client?.handleTransactionEvent( + let clientRef = context.client + Task { @MainActor [weak clientRef] in + clientRef?.handleTransactionEvent( txid: txid, confirmed: confirmed, amount: amount, @@ -441,6 +485,7 @@ public class SPVClient: ObservableObject { transactionProgress: progress.transactionProgress, currentHeight: height, targetHeight: progress.targetHeight, + startHeight: self.startFromHeight, rate: rate, estimatedTimeRemaining: progress.estimatedTimeRemaining ) @@ -474,11 +519,11 @@ public class SPVClient: ObservableObject { // MARK: - Wallet Manager Access - public func getWalletManager() -> OpaquePointer? { + public func getWalletManager() -> UnsafeMutablePointer? { guard let client = client else { return nil } let managerPtr = dash_spv_ffi_client_get_wallet_manager(client) - return OpaquePointer(managerPtr) + return managerPtr?.assumingMemoryBound(to: FFIWalletManager.self) } // MARK: - Statistics @@ -568,6 +613,7 @@ public class SPVClient: ObservableObject { // MARK: - Callback Context +@MainActor private class CallbackContext { weak var client: SPVClient? @@ -575,63 +621,53 @@ private class CallbackContext { self.client = client } - func handleProgressUpdate(_ progressPtr: UnsafePointer) { - let ffiProgress = progressPtr.pointee - - // Determine sync stage based on percentage - let stage: SPVSyncStage - if ffiProgress.percentage < 0.3 { - stage = .headers - } else if ffiProgress.percentage < 0.7 { - stage = .masternodes - } else if ffiProgress.percentage < 1.0 { - stage = .transactions - } else { - stage = .complete - } + func handleProgressUpdate(_ ffiProgress: FFIDetailedSyncProgress) { - // Calculate estimated time remaining - var estimatedTime: TimeInterval? = nil - if ffiProgress.estimated_seconds_remaining > 0 { - estimatedTime = Double(ffiProgress.estimated_seconds_remaining) - } + // Compute stage and ETA outside the actor + let stage: SPVSyncStage = { + if ffiProgress.percentage < 0.3 { return .headers } + if ffiProgress.percentage < 0.7 { return .masternodes } + if ffiProgress.percentage < 1.0 { return .transactions } + return .complete + }() + let estimatedTime: TimeInterval? = (ffiProgress.estimated_seconds_remaining > 0) + ? TimeInterval(ffiProgress.estimated_seconds_remaining) + : nil - if client?.swiftLoggingEnabled == true { - let pct = max(0.0, min(ffiProgress.percentage, 1.0)) * 100.0 - let cur = ffiProgress.current_height - let tot = ffiProgress.total_height - let rate = ffiProgress.headers_per_second - let eta = ffiProgress.estimated_seconds_remaining - print("[SPV][Progress] stage=\(stage.rawValue) pct=\(String(format: "%.2f", pct))% height=\(cur)/\(tot) rate=\(String(format: "%.2f", rate)) hdr/s eta=\(eta)s") - } - - let progress = SPVSyncProgress( - stage: stage, - headerProgress: min(ffiProgress.percentage / 0.3, 1.0), - masternodeProgress: min(max((ffiProgress.percentage - 0.3) / 0.4, 0), 1.0), - transactionProgress: min(max((ffiProgress.percentage - 0.7) / 0.3, 0), 1.0), - currentHeight: ffiProgress.current_height, - targetHeight: ffiProgress.total_height, - rate: ffiProgress.headers_per_second, - estimatedTimeRemaining: estimatedTime - ) - - let now = Date().timeIntervalSince1970 - if let client = self.client, now - client.lastProgressUIUpdate >= client.progressUICoalesceInterval { - client.lastProgressUIUpdate = now - Task { @MainActor in - guard let clientStrong = self.client else { return } - clientStrong.syncProgress = progress - clientStrong.delegate?.spvClient(clientStrong, didUpdateSyncProgress: progress) + // Update UI/state on main actor + guard let client = self.client else { return } + + if client.swiftLoggingEnabled { + let pct = max(0.0, min(ffiProgress.percentage, 1.0)) * 100.0 + let cur = ffiProgress.current_height + let tot = ffiProgress.total_height + let rate = ffiProgress.headers_per_second + let eta = ffiProgress.estimated_seconds_remaining + print("[SPV][Progress] stage=\(stage.rawValue) pct=\(String(format: "%.2f", pct))% height=\(cur)/\(tot) rate=\(String(format: "%.2f", rate)) hdr/s eta=\(eta)s") + } + + let absoluteCurrent: UInt32 = client.startFromHeight &+ ffiProgress.current_height + let progress = SPVSyncProgress( + stage: stage, + headerProgress: min(ffiProgress.percentage / 0.3, 1.0), + masternodeProgress: min(max((ffiProgress.percentage - 0.3) / 0.4, 0), 1.0), + transactionProgress: min(max((ffiProgress.percentage - 0.7) / 0.3, 0), 1.0), + currentHeight: absoluteCurrent, + targetHeight: ffiProgress.total_height, + startHeight: client.startFromHeight, + rate: ffiProgress.headers_per_second, + estimatedTimeRemaining: estimatedTime + ) + + let now = Date().timeIntervalSince1970 + if now - client.lastProgressUIUpdate >= client.progressUICoalesceInterval { + client.lastProgressUIUpdate = now + client.syncProgress = progress + client.delegate?.spvClient(client, didUpdateSyncProgress: progress) } - } } - func handleSyncCompletion(success: Bool, errorMsg: UnsafePointer?) { - var error: String? = nil - if let errorMsg = errorMsg { - error = String(cString: errorMsg) - } + func handleSyncCompletion(success: Bool, error: String?) { if client?.swiftLoggingEnabled == true { if success { @@ -641,8 +677,16 @@ private class CallbackContext { } } - Task { @MainActor in - guard let client = self.client else { return } + Task { @MainActor [weak self] in + guard let client = self?.client else { return } + if client.swiftLoggingEnabled { + if success { + print("[SPV][Complete] Sync finished successfully") + } else { + let errMsg = error ?? "unknown error" + print("[SPV][Complete] Sync failed: \(errMsg)") + } + } client.isSyncing = false client.lastError = error @@ -654,6 +698,7 @@ private class CallbackContext { transactionProgress: 1.0, currentHeight: client.syncProgress?.targetHeight ?? 0, targetHeight: client.syncProgress?.targetHeight ?? 0, + startHeight: client.startFromHeight, rate: 0, estimatedTimeRemaining: nil ) @@ -668,7 +713,7 @@ private class CallbackContext { // MARK: - Supporting Types -public struct SPVStats { +public struct SPVStats: Sendable { public let connectedPeers: Int public let headerHeight: Int public let filterHeight: Int @@ -676,7 +721,7 @@ public struct SPVStats { } // A lightweight snapshot of sync progress from FFISyncProgress -public struct SPVSyncSnapshot { +public struct SPVSyncSnapshot: Sendable { public let headerHeight: UInt32 public let filterHeaderHeight: UInt32 public let masternodeHeight: UInt32 diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp.xcodeproj/project.pbxproj b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp.xcodeproj/project.pbxproj index 9ab267ae42..af7e9b7fc9 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp.xcodeproj/project.pbxproj +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp.xcodeproj/project.pbxproj @@ -444,7 +444,7 @@ PRODUCT_BUNDLE_IDENTIFIER = org.dashfoundation.SwiftExampleApp; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -472,7 +472,7 @@ PRODUCT_BUNDLE_IDENTIFIER = org.dashfoundation.SwiftExampleApp; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; @@ -490,7 +490,7 @@ PRODUCT_BUNDLE_IDENTIFIER = org.dashfoundation.SwiftExampleAppTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftExampleApp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/SwiftExampleApp"; }; @@ -509,7 +509,7 @@ PRODUCT_BUNDLE_IDENTIFIER = org.dashfoundation.SwiftExampleAppTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftExampleApp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/SwiftExampleApp"; }; @@ -526,7 +526,7 @@ PRODUCT_BUNDLE_IDENTIFIER = org.dashfoundation.SwiftExampleAppUITests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_TARGET_NAME = SwiftExampleApp; }; @@ -543,7 +543,7 @@ PRODUCT_BUNDLE_IDENTIFIER = org.dashfoundation.SwiftExampleAppUITests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_TARGET_NAME = SwiftExampleApp; }; diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/AppState.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/AppState.swift index 2c033f683f..745c7b616e 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/AppState.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/AppState.swift @@ -26,6 +26,22 @@ class AppState: ObservableObject { @Published var dataStatistics: (identities: Int, documents: Int, contracts: Int, tokenBalances: Int)? + @Published var useLocalPlatform: Bool { + didSet { + UserDefaults.standard.set(useLocalPlatform, forKey: "useLocalhostPlatform") + // Maintain backward-compat key for older SDK builds + UserDefaults.standard.set(useLocalPlatform, forKey: "useLocalhost") + Task { await switchNetwork(to: currentNetwork) } + } + } + + @Published var useLocalCore: Bool { + didSet { + UserDefaults.standard.set(useLocalCore, forKey: "useLocalhostCore") + // TODO: Reconfigure SPV client peers when supported + } + } + private let testSigner = TestSigner() private var dataManager: DataManager? private var modelContext: ModelContext? @@ -38,6 +54,12 @@ class AppState: ObservableObject { } else { self.currentNetwork = .testnet } + // Migration: if legacy key set and new keys absent, propagate + let legacyLocal = UserDefaults.standard.bool(forKey: "useLocalhost") + let hasPlatformKey = UserDefaults.standard.object(forKey: "useLocalhostPlatform") != nil + let hasCoreKey = UserDefaults.standard.object(forKey: "useLocalhostCore") != nil + self.useLocalPlatform = hasPlatformKey ? UserDefaults.standard.bool(forKey: "useLocalhostPlatform") : legacyLocal + self.useLocalCore = hasCoreKey ? UserDefaults.standard.bool(forKey: "useLocalhostCore") : legacyLocal } func initializeSDK(modelContext: ModelContext) { @@ -61,7 +83,7 @@ class AppState: ObservableObject { NSLog("🔵 AppState: Creating SDK instance for network: \(currentNetwork)") // Create SDK instance for current network - let sdkNetwork = currentNetwork.sdkNetwork + let sdkNetwork: DashSDKNetwork = currentNetwork.sdkNetwork NSLog("🔵 AppState: SDK network value: \(sdkNetwork)") let newSDK = try SDK(network: sdkNetwork) @@ -166,7 +188,7 @@ class AppState: ObservableObject { isLoading = true // Create new SDK instance for the network - let sdkNetwork = network.sdkNetwork + let sdkNetwork: DashSDKNetwork = network.sdkNetwork let newSDK = try SDK(network: sdkNetwork) sdk = newSDK @@ -505,6 +527,7 @@ class AppState: ObservableObject { // MARK: - Startup Diagnostics + @MainActor private func runStartupDiagnostics(sdk: SDK) async { NSLog("====== PLATFORM QUERY DIAGNOSTICS (STARTUP) ======") @@ -523,7 +546,7 @@ class AppState: ObservableObject { } // Run a few key queries to test connectivity - let diagnosticQueries: [(name: String, test: () async throws -> Any)] = [ + let diagnosticQueries: [(name: String, test: @MainActor () async throws -> Any)] = [ ("Get Platform Status", { try await sdk.getStatus() }), @@ -590,6 +613,7 @@ class AppState: ObservableObject { NSLog("================================\n") } + @MainActor private func runSimpleDiagnostic(sdk: SDK) async { var diagnosticReport = "====== SIMPLE DIAGNOSTIC TEST ======\n" diagnosticReport += "Date: \(Date())\n\n" @@ -599,11 +623,10 @@ class AppState: ObservableObject { diagnosticReport += "Testing: Get Platform Status...\n" let status = try await sdk.getStatus() diagnosticReport += "✅ Platform Status Success\n" - if let dict = status as? [String: Any] { - diagnosticReport += " Version: \(dict["version"] ?? "unknown")\n" - diagnosticReport += " Mode: \(dict["mode"] ?? "unknown")\n" - diagnosticReport += " QuorumCount: \(dict["quorumCount"] ?? "unknown")\n" - } + let dict = status + diagnosticReport += " Version: \(dict["version"] ?? "unknown")\n" + diagnosticReport += " Mode: \(dict["mode"] ?? "unknown")\n" + diagnosticReport += " QuorumCount: \(dict["quorumCount"] ?? "unknown")\n" } catch { diagnosticReport += "❌ Platform Status Failed: \(error)\n" } @@ -647,4 +670,4 @@ class AppState: ObservableObject { // Also log to console NSLog(diagnosticReport) } -} \ No newline at end of file +} diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Services/WalletService.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Services/WalletService.swift index c8d7bcdf9f..90ebfd9b5c 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Services/WalletService.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Services/WalletService.swift @@ -1,10 +1,12 @@ import Foundation import SwiftData import Combine -import SwiftDashSDK +@preconcurrency import SwiftDashSDK @MainActor public class WalletService: ObservableObject { + // Sendable wrapper to move non-Sendable references across actor boundaries when safe + private final class SendableBox: @unchecked Sendable { let value: T; init(_ v: T) { self.value = v } } public static let shared = WalletService() // Published properties @@ -44,9 +46,10 @@ public class WalletService: ObservableObject { private init() {} deinit { - // SPVClient handles its own cleanup + // Avoid capturing self across an async boundary; capture the client locally + let client = spvClient Task { @MainActor in - spvClient?.stop() + client?.stop() } } @@ -64,22 +67,25 @@ public class WalletService: ObservableObject { // Capture current references on the main actor to avoid cross-actor hops later guard let client = spvClient, let mc = self.modelContainer else { return } + let clientBox = SendableBox(client) let net = currentNetwork let mnEnabled = shouldSyncMasternodes Task.detached(priority: .userInitiated) { + let clientLocal = clientBox.value do { // Initialize the SPV client with proper configuration let dataDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appendingPathComponent("SPV").path // Determine a start height based on checkpoint before the oldest (non-imported) wallet var startHeight: UInt32? = nil do { - // Fetch wallets on main actor - let wallets: [HDWallet] = try await MainActor.run { + // Fetch only the fields we need on the main actor, avoid moving PersistentModels across actors + let walletInfos: [(createdAt: Date, isImported: Bool, networks: Int)] = try await MainActor.run { let descriptor = FetchDescriptor() - return try self.modelContainer?.mainContext.fetch(descriptor) ?? [] + let wallets = try self.modelContainer?.mainContext.fetch(descriptor) ?? [] + return wallets.map { ($0.createdAt, $0.isImported, Int($0.networks)) } } // Filter to current network - let filtered = wallets.filter { w in + let filtered = walletInfos.filter { w in switch net { case .mainnet: return (w.networks & 1) != 0 case .testnet: return (w.networks & 2) != 0 @@ -90,7 +96,7 @@ public class WalletService: ObservableObject { let candidate = filtered.filter { !$0.isImported }.sorted { $0.createdAt < $1.createdAt }.first if let cand = candidate { let ts = UInt32(cand.createdAt.timeIntervalSince1970) - if let h = client.getCheckpointHeight(beforeTimestamp: ts) { + if let h = await client.getCheckpointHeight(beforeTimestamp: ts) { startHeight = h } } else { @@ -110,14 +116,14 @@ public class WalletService: ObservableObject { } } - try client.initialize(dataDir: dataDir, masternodesEnabled: mnEnabled, startHeight: startHeight) + try await clientLocal.initialize(dataDir: dataDir, masternodesEnabled: mnEnabled, startHeight: startHeight) // Start the SPV client - try client.start() + try await clientLocal.start() print("✅ SPV Client initialized and started successfully for \(net.rawValue)") // Seed UI with latest checkpoint height if we don't have a header yet - let seedHeight = client.getLatestCheckpointHeight() + let seedHeight = await clientLocal.getLatestCheckpointHeight() await MainActor.run { if WalletService.shared.latestHeaderHeight == 0, let cp = seedHeight { WalletService.shared.latestHeaderHeight = Int(cp) @@ -136,7 +142,7 @@ public class WalletService: ObservableObject { WalletService.shared.walletManager?.transactionService = TransactionService( walletManager: wrapper, modelContainer: mc, - spvClient: client + spvClient: clientLocal ) print("✅ WalletManager wrapper initialized successfully") } @@ -215,7 +221,7 @@ public class WalletService: ObservableObject { } private func loadCurrentWallet() { - guard let modelContainer = modelContainer else { return } + guard modelContainer != nil else { return } // The WalletManager will handle loading and restoring wallets from persistence // It will restore the serialized wallet bytes to the FFI wallet manager @@ -263,13 +269,13 @@ public class WalletService: ObservableObject { lastSyncError = nil // Kick off sync without blocking the main thread - Task.detached(priority: .userInitiated) { [weak self] in + Task.detached(priority: .userInitiated) { do { try await spvClient.startSync() } catch { await MainActor.run { - self?.lastSyncError = error - self?.isSyncing = false + WalletService.shared.lastSyncError = error + WalletService.shared.isSyncing = false } print("❌ Sync failed: \(error)") } @@ -294,7 +300,7 @@ public class WalletService: ObservableObject { print("Switching from \(currentNetwork.rawValue) to \(network.rawValue)") // Stop any ongoing sync - await stopSync() + stopSync() // Clean up current SPV client spvClient?.stop() @@ -456,20 +462,19 @@ public class WalletService: ObservableObject { extension WalletService { private func beginSPVStatsPolling() { spvStatsTimer?.invalidate() - spvStatsTimer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { [weak self] _ in - guard let self = self else { return } + spvStatsTimer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { _ in // Call FFI off the main actor to avoid UI stalls - Task.detached(priority: .utility) { [weak self] in - let client = await self?.spvClient - guard let client = client else { return } - guard let stats = client.getStats() else { return } + Task.detached(priority: .utility) { + let clientBox = await MainActor.run { WalletService.shared[keyPath: \WalletService.spvClient].map(SendableBox.init) } + guard let client = clientBox?.value else { return } + guard let stats = await client.getStats() else { return } await MainActor.run { // Only overwrite with positive values; keep seeded values otherwise if stats.headerHeight > 0 { - self?.latestHeaderHeight = max(self?.latestHeaderHeight ?? 0, stats.headerHeight) + WalletService.shared.latestHeaderHeight = max(WalletService.shared.latestHeaderHeight, stats.headerHeight) } if stats.filterHeight > 0 { - self?.latestFilterHeight = max(self?.latestFilterHeight ?? 0, stats.filterHeight) + WalletService.shared.latestFilterHeight = max(WalletService.shared.latestFilterHeight, stats.filterHeight) } // Keep latestMasternodeListHeight as 0 until available } @@ -483,57 +488,68 @@ extension WalletService { extension WalletService: SPVClientDelegate { nonisolated public func spvClient(_ client: SPVClient, didUpdateSyncProgress progress: SPVSyncProgress) { + // Copy needed values to Sendable primitives to avoid capturing 'progress' + let startHeight = progress.startHeight + let currentHeight = progress.currentHeight + let targetHeight = progress.targetHeight + let rate = progress.rate + let stage = progress.stage + let mappedStage = WalletService.mapSyncStage(stage) + let overall = progress.overallProgress + Task { @MainActor in - // Prefer a deterministic percentage from heights, not FFI's percentage - let headerPct = min(1.0, max(0.0, Double(progress.currentHeight) / Double(max(1, progress.targetHeight)))) + let base = Double(startHeight) + let numer = max(0.0, Double(currentHeight) - base) + let denom = max(1.0, Double(targetHeight) - base) + let headerPct = min(1.0, max(0.0, numer / denom)) - // Update published properties (top overlay + headers row) - self.syncProgress = headerPct - self.headerProgress = headerPct + WalletService.shared.syncProgress = headerPct + WalletService.shared.headerProgress = headerPct - // Convert to detailed progress for UI (top overlay) - self.detailedSyncProgress = SyncProgress( - current: UInt64(progress.currentHeight), - total: UInt64(progress.targetHeight), - rate: progress.rate, + WalletService.shared.detailedSyncProgress = SyncProgress( + current: UInt64(currentHeight), + total: UInt64(targetHeight), + rate: rate, progress: headerPct, - stage: mapSyncStage(progress.stage) + stage: mappedStage ) - + if ProcessInfo.processInfo.environment["SPV_SWIFT_LOG"] == "1" { - print("📊 Sync progress: \(progress.stage.rawValue) - \(Int(progress.overallProgress * 100))%") + print("📊 Sync progress: \(stage.rawValue) - \(Int(overall * 100))%") } } - // Update per-section progress using best available data without blocking UI - Task.detached(priority: .utility) { [weak self] in - guard let self = self else { return } - // Capture actor-isolated values we might need - let (client, prevTx, prevMn): (SPVClient?, Double, Double) = await MainActor.run { - (self.spvClient, self.transactionProgress, self.masternodeProgress) + Task.detached(priority: .utility) { + let (clientBox, prevTx, prevMn): (SendableBox?, Double, Double) = await MainActor.run { + (WalletService.shared[keyPath: \WalletService.spvClient].map(SendableBox.init), WalletService.shared.transactionProgress, WalletService.shared.masternodeProgress) } + let client = clientBox?.value - // 1) Headers: use detailed current/total from progress callback - let headerPct = min(1.0, max(0.0, Double(progress.currentHeight) / Double(max(1, progress.targetHeight)))) + let base = Double(startHeight) + let numer = max(0.0, Double(currentHeight) - base) + let denom = max(1.0, Double(targetHeight) - base) + let headerPct = min(1.0, max(0.0, numer / denom)) - // 2) Filters: prefer snapshot lastSyncedFilterHeight / headerHeight; fallback to stats ratio - var txPct = prevTx - if let snap = client?.getSyncSnapshot(), snap.headerHeight > 0 { - txPct = min(1.0, max(0.0, Double(snap.lastSyncedFilterHeight) / Double(snap.headerHeight))) - } else if let stats = client?.getStats(), stats.headerHeight > 0 { - txPct = min(1.0, max(0.0, Double(stats.filterHeight) / Double(stats.headerHeight))) + let txPctFinal: Double + if let snap = await client?.getSyncSnapshot(), snap.headerHeight > 0 { + txPctFinal = min(1.0, max(0.0, Double(snap.lastSyncedFilterHeight) / Double(snap.headerHeight))) + } else if let stats = await client?.getStats(), stats.headerHeight > 0 { + txPctFinal = min(1.0, max(0.0, Double(stats.filterHeight) / Double(stats.headerHeight))) + } else { + txPctFinal = prevTx } - // 3) Masternodes: show only synced/unsynced (no misleading ratio) - var mnPct = prevMn - if let snap = client?.getSyncSnapshot() { - mnPct = snap.masternodesSynced ? 1.0 : 0.0 + let mnPctFinal: Double + if let snap = await client?.getSyncSnapshot() { + mnPctFinal = snap.masternodesSynced ? 1.0 : 0.0 + } else { + mnPctFinal = prevMn } await MainActor.run { - self.headerProgress = headerPct - self.transactionProgress = txPct - self.masternodeProgress = mnPct + WalletService.shared.headerProgress = headerPct + WalletService.shared.transactionProgress = txPctFinal + WalletService.shared.masternodeProgress = mnPctFinal } } } @@ -579,7 +595,7 @@ extension WalletService: SPVClientDelegate { } } - private func mapSyncStage(_ stage: SPVSyncStage) -> SyncStage { + nonisolated private static func mapSyncStage(_ stage: SPVSyncStage) -> SyncStage { switch stage { case .idle: return .idle diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift index ed1c24db07..de4a37b747 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift @@ -200,20 +200,15 @@ struct CreateWalletView: View { .onAppear { setupInitialNetworkSelection() } - // Hidden navigation link to push backup screen - .overlay( - NavigationLink( - destination: SeedBackupView( - mnemonic: generatedMnemonic, - onConfirm: { - createWallet(using: generatedMnemonic) - } - ), - isActive: $showBackupScreen, - label: { EmptyView() } + // Navigate to backup screen when requested (iOS 16+ API) + .navigationDestination(isPresented: $showBackupScreen) { + SeedBackupView( + mnemonic: generatedMnemonic, + onConfirm: { + createWallet(using: generatedMnemonic) + } ) - .opacity(0) - ) + } } private var canCreateWallet: Bool { diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Wallet/TransactionService.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Wallet/TransactionService.swift index 29398890a6..070596c5a0 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Wallet/TransactionService.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Wallet/TransactionService.swift @@ -60,22 +60,6 @@ class TransactionService: ObservableObject { // TODO: Implement broadcast with new SPV client // try await spvClient.broadcastTransaction(transaction.rawTransaction) throw TransactionError.broadcastFailed("SPV broadcast not yet implemented") - - // Create transaction record - let hdTransaction = HDTransaction(txHash: transaction.txid) - hdTransaction.rawTransaction = transaction.rawTransaction - hdTransaction.fee = transaction.fee - hdTransaction.type = "sent" - hdTransaction.amount = -Int64(transaction.fee) // Will be updated when we process outputs - hdTransaction.isPending = true - hdTransaction.wallet = walletManager.currentWallet - - // TODO: update UTXO state via SDK once available - - modelContainer.mainContext.insert(hdTransaction) - try modelContainer.mainContext.save() - - await loadTransactions() } catch { lastError = error throw TransactionError.broadcastFailed(error.localizedDescription) @@ -153,9 +137,10 @@ class TransactionService: ObservableObject { } } - // Start sync without blocking UI - Task.detached(priority: .userInitiated) { - try? await spvClient.startSync() + // Start sync without blocking UI; inherit MainActor to avoid sending non-Sendable captures + let client = spvClient + Task(priority: .userInitiated) { + try? await client.startSync() } } diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Wallet/WalletManager.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Wallet/WalletManager.swift index 2a8c4108f5..27af4fcec5 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Wallet/WalletManager.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Wallet/WalletManager.swift @@ -129,7 +129,7 @@ class WalletManager: ObservableObject { modelContainer.mainContext.insert(wallet) // Create default account model - let account = wallet.createAccount(at: 0) + _ = wallet.createAccount(at: 0) // Sync complete wallet state from Rust managed info try await syncWalletFromManagedInfo(for: wallet) diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Wallet/WalletStorage.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Wallet/WalletStorage.swift index 861ee3ee26..f86b0899e1 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Wallet/WalletStorage.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Wallet/WalletStorage.swift @@ -1,4 +1,5 @@ import Foundation +import LocalAuthentication import Security import CryptoKit @@ -171,12 +172,14 @@ public class WalletStorage { } public func retrieveSeedWithBiometric() throws -> Data { + let context = LAContext() + context.localizedReason = "Authenticate to access your wallet" let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: keychainService, kSecAttrAccount as String: biometricKeychainAccount, kSecReturnData as String: true, - kSecUseOperationPrompt as String: "Authenticate to access your wallet" + kSecUseAuthenticationContext as String: context ] var result: AnyObject? @@ -309,4 +312,4 @@ extension WalletStorage { ) ) } -} \ No newline at end of file +} diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Wallet/WalletViewModel.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Wallet/WalletViewModel.swift index d78188cfd2..0bb4d57c54 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Wallet/WalletViewModel.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Wallet/WalletViewModel.swift @@ -239,47 +239,35 @@ public class WalletViewModel: ObservableObject { isSyncing = true - do { - // Watch all addresses - for account in wallet.accounts { - let allAddresses = account.externalAddresses + account.internalAddresses - - for address in allAddresses { - // TODO: Implement watch address with new SPV client - // try await spvClient.watchAddress(address.address) - print("Would watch address: \(address.address)") - } + // Watch all addresses + for account in wallet.accounts { + let allAddresses = account.externalAddresses + account.internalAddresses + for address in allAddresses { + // TODO: Implement watch address with new SPV client + // try await spvClient.watchAddress(address.address) + print("Would watch address: \(address.address)") } - - // Set up callbacks for new transactions - // TODO: Set up transaction callbacks with new SPV client - // await spvClient.onTransaction { [weak self] txInfo in - // Task { @MainActor in - // await self?.processIncomingTransaction(txInfo) - // } - // } - - // Start sync - // TODO: Implement start sync with new SPV client - // try await spvClient.startSync() - print("Would start sync") - } catch { - self.error = error - showError = true - isSyncing = false } + + // Set up callbacks for new transactions (placeholder) + // TODO: Set up transaction callbacks with new SPV client + // await spvClient.onTransaction { [weak self] txInfo in + // Task { @MainActor in + // await self?.processIncomingTransaction(txInfo) + // } + // } + + // Start sync (placeholder) + // TODO: Implement start sync with new SPV client + // try await spvClient.startSync() + print("Would start sync") } public func stopSync() async { - do { - // TODO: Implement stop sync with new SPV client - // try await spvClient.stopSync() - print("Would stop sync") - isSyncing = false - } catch { - self.error = error - showError = true - } + // TODO: Implement stop sync with new SPV client + // try await spvClient.stopSync() + print("Would stop sync") + isSyncing = false } // MARK: - Transaction Processing diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Helpers/WIFParser.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Helpers/WIFParser.swift index 5b52ae79c9..bb866debfa 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Helpers/WIFParser.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Helpers/WIFParser.swift @@ -82,7 +82,7 @@ enum WIFParser { let zeroCount = data.prefix(while: { $0 == 0 }).count // Convert data to big integer - var num = data.reduce(into: [UInt8]()) { result, byte in + let num = data.reduce(into: [UInt8]()) { result, byte in var carry = UInt(byte) for i in 0.. [String: Any] { - var json: [String: Any] = [ + let json: [String: Any] = [ "name": token.name, "symbol": token.symbol, "description": token.description as Any, @@ -412,4 +412,4 @@ extension PersistentDataContract { return json } -} \ No newline at end of file +} diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Models/SwiftData/PersistentIdentity.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Models/SwiftData/PersistentIdentity.swift index b12dd2035f..3b3e482374 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Models/SwiftData/PersistentIdentity.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Models/SwiftData/PersistentIdentity.swift @@ -125,6 +125,7 @@ final class PersistentIdentity { extension PersistentIdentity { /// Convert to app's IdentityModel + @MainActor func toIdentityModel() -> IdentityModel { let publicKeyModels = publicKeys.compactMap { $0.toIdentityPublicKey() } @@ -159,6 +160,7 @@ extension PersistentIdentity { } /// Create from IdentityModel + @MainActor static func from(_ model: IdentityModel, network: String = "testnet") -> PersistentIdentity { // Store special keys in keychain first var votingKeyId: String? = nil @@ -286,4 +288,4 @@ extension PersistentIdentity { identity.isLocal == true && identity.network == network } } -} \ No newline at end of file +} diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Models/SwiftData/PersistentPublicKey.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Models/SwiftData/PersistentPublicKey.swift index 874dc6557f..1c4ef445eb 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Models/SwiftData/PersistentPublicKey.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Models/SwiftData/PersistentPublicKey.swift @@ -61,17 +61,20 @@ final class PersistentPublicKey { // MARK: - Private Key Methods /// Check if this public key has an associated private key + @MainActor var hasPrivateKey: Bool { privateKeyKeychainIdentifier != nil && isPrivateKeyAvailable } /// Check if the private key is still available in keychain + @MainActor var isPrivateKeyAvailable: Bool { - guard let keychainId = privateKeyKeychainIdentifier else { return false } + guard privateKeyKeychainIdentifier != nil else { return false } return KeychainManager.shared.hasPrivateKey(identityId: Data.identifier(fromBase58: identityId) ?? Data(), keyIndex: keyId) } /// Retrieve the private key data from keychain + @MainActor func getPrivateKeyData() -> Data? { guard let identityData = Data.identifier(fromBase58: identityId) else { return nil } lastAccessed = Date() @@ -79,6 +82,7 @@ final class PersistentPublicKey { } /// Store a private key for this public key + @MainActor func setPrivateKey(_ privateKeyData: Data) { guard let identityData = Data.identifier(fromBase58: identityId) else { return } if let keychainId = KeychainManager.shared.storePrivateKey(privateKeyData, identityId: identityData, keyIndex: keyId) { @@ -88,9 +92,10 @@ final class PersistentPublicKey { } /// Remove the private key from keychain + @MainActor func removePrivateKey() { guard let identityData = Data.identifier(fromBase58: identityId) else { return } - KeychainManager.shared.deletePrivateKey(identityId: identityData, keyIndex: keyId) + _ = KeychainManager.shared.deletePrivateKey(identityId: identityData, keyIndex: keyId) self.privateKeyKeychainIdentifier = nil } @@ -170,4 +175,4 @@ extension PersistentPublicKey { identityId: identityId ) } -} \ No newline at end of file +} diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/SDK/PlatformQueryExtensions.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/SDK/PlatformQueryExtensions.swift index 5f193d382b..ad3b76c048 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/SDK/PlatformQueryExtensions.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/SDK/PlatformQueryExtensions.swift @@ -3,7 +3,13 @@ import SwiftDashSDK import DashSDKFFI // MARK: - Platform Query Extensions for SDK +@MainActor extension SDK { + // Helper to pass non-Sendable pointers across @Sendable closures when safe + private final class SendablePtr: @unchecked Sendable { + let ptr: UnsafeMutablePointer + init(_ p: UnsafeMutablePointer) { self.ptr = p } + } // MARK: - Helper Functions @@ -96,66 +102,21 @@ extension SDK { // MARK: - Identity Queries /// Get an identity by ID + @MainActor public func identityGet(identityId: String) async throws -> [String: Any] { print("🔵 SDK.identityGet: Called with ID: \(identityId)") - guard let handle = handle else { + guard let sdkHandle = handle else { print("❌ SDK.identityGet: SDK handle is nil") throw SDKError.invalidState("SDK not initialized") } - print("🔵 SDK.identityGet: SDK handle exists: \(handle)") - print("🔵 SDK.identityGet: About to call dash_sdk_identity_fetch with handle: \(handle) and ID: \(identityId)") - - // Call the FFI function on a background queue with timeout - return try await withCheckedThrowingContinuation { continuation in - // Use a flag to ensure continuation is only resumed once - let continuationResumed = NSLock() - var isResumed = false - - DispatchQueue.global(qos: .userInitiated).async { - print("🔵 SDK.identityGet: On background queue, calling FFI...") - - // Create a timeout - let timeoutWorkItem = DispatchWorkItem { - continuationResumed.lock() - defer { continuationResumed.unlock() } - - if !isResumed { - isResumed = true - print("❌ SDK.identityGet: FFI call timed out after 30 seconds") - continuation.resume(throwing: SDKError.timeout("Identity fetch timed out")) - } - } - DispatchQueue.global().asyncAfter(deadline: .now() + 30, execute: timeoutWorkItem) - - // Make the FFI call - let result = dash_sdk_identity_fetch(handle, identityId) - - // Cancel timeout if we got a result - timeoutWorkItem.cancel() - - print("🔵 SDK.identityGet: FFI call returned, processing result...") - - // Try to resume with the result - continuationResumed.lock() - defer { continuationResumed.unlock() } - - if !isResumed { - isResumed = true - do { - let jsonResult = try self.processJSONResult(result) - print("✅ SDK.identityGet: Successfully processed result") - continuation.resume(returning: jsonResult) - } catch { - print("❌ SDK.identityGet: Error processing result: \(error)") - continuation.resume(throwing: error) - } - } else { - print("⚠️ SDK.identityGet: Continuation already resumed (likely from timeout), ignoring FFI result") - } - } - } + print("🔵 SDK.identityGet: SDK handle exists: \(sdkHandle)") + print("🔵 SDK.identityGet: About to call dash_sdk_identity_fetch with handle and ID: \(identityId)") + + // Make the FFI call directly and return the parsed JSON + let result = dash_sdk_identity_fetch(sdkHandle, identityId) + return try processJSONResult(result) } /// Get identity keys @@ -288,7 +249,7 @@ extension SDK { /// Add a data contract to the trusted context provider cache /// This allows the SDK to use the contract without fetching it from the network public func addContractToContext(contractId: String, binaryData: Data) throws { - guard let handle = handle else { + guard self.handle != nil else { throw SDKError.invalidState("SDK not initialized") } @@ -408,7 +369,8 @@ extension SDK { defer { // Clean up contract handle when done - dash_sdk_data_contract_destroy(OpaquePointer(contractHandle)) + let contractPtr = contractHandle.assumingMemoryBound(to: DataContractHandle.self) + dash_sdk_data_contract_destroy(contractPtr) } // Create search parameters struct with proper string handling @@ -422,7 +384,7 @@ extension SDK { if let orderByClause = orderByClauseCString { return orderByClause.withUnsafeBufferPointer { orderByPtr in var searchParams = DashSDKDocumentSearchParams() - searchParams.data_contract_handle = OpaquePointer(contractHandle) + searchParams.data_contract_handle = UnsafePointer(contractHandle.assumingMemoryBound(to: DataContractHandle.self)) searchParams.document_type = documentTypePtr.baseAddress searchParams.where_json = wherePtr.baseAddress searchParams.order_by_json = orderByPtr.baseAddress @@ -442,7 +404,7 @@ extension SDK { } } else { var searchParams = DashSDKDocumentSearchParams() - searchParams.data_contract_handle = OpaquePointer(contractHandle) + searchParams.data_contract_handle = UnsafePointer(contractHandle.assumingMemoryBound(to: DataContractHandle.self)) searchParams.document_type = documentTypePtr.baseAddress searchParams.where_json = wherePtr.baseAddress searchParams.order_by_json = nil @@ -463,7 +425,7 @@ extension SDK { } } else { var searchParams = DashSDKDocumentSearchParams() - searchParams.data_contract_handle = OpaquePointer(contractHandle) + searchParams.data_contract_handle = UnsafePointer(contractHandle.assumingMemoryBound(to: DataContractHandle.self)) searchParams.document_type = documentTypePtr.baseAddress searchParams.where_json = nil searchParams.order_by_json = nil @@ -478,6 +440,7 @@ extension SDK { } /// Get a specific document + @MainActor public func documentGet(dataContractId: String, documentType: String, documentId: String) async throws -> [String: Any] { guard let handle = handle else { throw SDKError.invalidState("SDK not initialized") @@ -497,11 +460,12 @@ extension SDK { defer { // Clean up contract handle when done - dash_sdk_data_contract_destroy(OpaquePointer(contractHandle)) + let contractPtr = contractHandle.assumingMemoryBound(to: DataContractHandle.self) + dash_sdk_data_contract_destroy(contractPtr) } // Now fetch the document - let documentResult = dash_sdk_document_fetch(handle, OpaquePointer(contractHandle), documentType, documentId) + let documentResult = dash_sdk_document_fetch(handle, contractHandle.assumingMemoryBound(to: DataContractHandle.self), documentType, documentId) if let error = documentResult.error { let errorMessage = error.pointee.message != nil ? String(cString: error.pointee.message!) : "Unknown error" @@ -515,11 +479,11 @@ extension SDK { defer { // Clean up document handle - dash_sdk_document_destroy(handle, OpaquePointer(documentHandle)) + dash_sdk_document_destroy(handle, documentHandle.assumingMemoryBound(to: DocumentHandle.self)) } // Get document info to convert to JSON - let info = dash_sdk_document_get_info(OpaquePointer(documentHandle)) + let info = dash_sdk_document_get_info(documentHandle.assumingMemoryBound(to: DocumentHandle.self)) defer { if let info = info { dash_sdk_document_info_free(info) @@ -588,6 +552,7 @@ extension SDK { // MARK: - DPNS Queries /// Get DPNS usernames for identity + @MainActor public func dpnsGetUsername(identityId: String, limit: UInt32?) async throws -> [[String: Any]] { guard let handle = handle else { throw SDKError.invalidState("SDK not initialized") @@ -620,6 +585,7 @@ extension SDK { } /// Get non-resolved DPNS contests for a specific identity + @MainActor public func dpnsGetNonResolvedContestsForIdentity(identityId: String, limit: UInt32?) async throws -> [String: Any] { guard let handle = handle else { throw SDKError.invalidState("SDK not initialized") @@ -684,6 +650,7 @@ extension SDK { } /// Get current DPNS contests (active vote polls) + @MainActor public func dpnsGetCurrentContests(startTime: UInt64 = 0, endTime: UInt64 = 0, limit: UInt16 = 100) async throws -> [String: UInt64] { guard let handle = handle else { throw SDKError.invalidState("SDK not initialized") @@ -717,18 +684,13 @@ extension SDK { } /// Get the vote state for a contested DPNS username + @MainActor public func dpnsGetContestedVoteState(name: String, limit: UInt32 = 100) async throws -> [String: Any] { guard let handle = self.handle else { throw SDKError.invalidState("SDK not initialized") } - - let result = await withCheckedContinuation { continuation in - DispatchQueue.global(qos: .userInitiated).async { - let result = name.withCString { namePtr in - dash_sdk_dpns_get_contested_vote_state(handle, namePtr, limit) - } - continuation.resume(returning: result) - } + let result = name.withCString { namePtr in + dash_sdk_dpns_get_contested_vote_state(handle, namePtr, limit) } // Check for error @@ -850,6 +812,7 @@ extension SDK { } /// Search DPNS names by prefix + @MainActor public func dpnsSearch(prefix: String, limit: UInt32? = nil) async throws -> [[String: Any]] { guard let handle = handle else { throw SDKError.invalidState("SDK not initialized") @@ -1030,7 +993,7 @@ extension SDK { } // If no data, return empty result - guard let dataPtr = result.data else { + if result.data == nil { return ["upgrades": []] } @@ -1136,11 +1099,9 @@ extension SDK { // Convert JSON object to [String: UInt64] var balances: [String: UInt64] = [:] - if let dict = json as? [String: Any] { - for (tokenId, balance) in dict { - if let balanceNum = balance as? NSNumber { - balances[tokenId] = balanceNum.uint64Value - } + for (tokenId, balance) in json { + if let balanceNum = balance as? NSNumber { + balances[tokenId] = balanceNum.uint64Value } } @@ -1406,4 +1367,4 @@ extension SDK { let result = dash_sdk_system_get_prefunded_specialized_balance(handle, id) return try processUInt64Result(result) } -} \ No newline at end of file +} diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/SDK/SDKExtensions.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/SDK/SDKExtensions.swift index 4ee561c9ec..91d7973de5 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/SDK/SDKExtensions.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/SDK/SDKExtensions.swift @@ -19,17 +19,5 @@ protocol Signer { func canSign(identityPublicKey: Data) -> Bool } -// Global signer storage for C callbacks -private var globalSignerStorage: Signer? - // MARK: - SDK Extensions for the example app -extension SDK { - /// Initialize SDK with a custom signer for the example app - convenience init(network: SwiftDashSDK.Network, signer: Signer) throws { - // Store the signer globally for C callbacks - globalSignerStorage = signer - - // Initialize the SDK normally - try self.init(network: network) - } -} +// No global signer storage is kept; signers are created and used at call sites. diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/SDK/StateTransitionExtensions.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/SDK/StateTransitionExtensions.swift index 4682bf94ed..4efca835dd 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/SDK/StateTransitionExtensions.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/SDK/StateTransitionExtensions.swift @@ -2,64 +2,57 @@ import Foundation import SwiftDashSDK import DashSDKFFI +// MARK: - OpaquePointer -> typed FFI helpers +@inline(__always) private func idConst(_ p: OpaquePointer) -> UnsafePointer { UnsafePointer(p) } +@inline(__always) private func idMut(_ p: OpaquePointer) -> UnsafeMutablePointer { UnsafeMutablePointer(p) } +@inline(__always) private func signerConst(_ p: OpaquePointer) -> UnsafePointer { UnsafePointer(p) } +@inline(__always) private func signerMut(_ p: OpaquePointer) -> UnsafeMutablePointer { UnsafeMutablePointer(p) } +@inline(__always) private func idPubKeyConst(_ p: OpaquePointer) -> UnsafePointer { UnsafePointer(p) } +@inline(__always) private func dataContractConst(_ p: OpaquePointer) -> UnsafePointer { UnsafePointer(p) } +@inline(__always) private func dataContractMut(_ p: OpaquePointer) -> UnsafeMutablePointer { UnsafeMutablePointer(p) } +@inline(__always) private func documentConst(_ p: OpaquePointer) -> UnsafePointer { UnsafePointer(p) } +@inline(__always) private func documentMut(_ p: OpaquePointer) -> UnsafeMutablePointer { UnsafeMutablePointer(p) } + +// MARK: - Sendable wrappers +private final class SendableOpaque: @unchecked Sendable { let p: OpaquePointer; init(_ p: OpaquePointer) { self.p = p } } + // MARK: - Key Selection Helpers /// Helper to select the appropriate key for signing operations /// Returns the key we most likely have the private key for private func selectSigningKey(from identity: DPPIdentity, operation: String) -> IdentityPublicKey? { - // IMPORTANT: We need to use the key that we actually have the private key for - // First, check which keys we have private keys for - print("🔑 [\(operation)] Checking available private keys for identity \(identity.id.toBase58String())") - - var keysWithPrivateKeys: [IdentityPublicKey] = [] - for key in identity.publicKeys.values { - let privateKey = KeychainManager.shared.retrievePrivateKey( - identityId: identity.id, - keyIndex: Int32(key.id) - ) - if privateKey != nil { - keysWithPrivateKeys.append(key) - print("✅ [\(operation)] Found private key for key ID \(key.id) (purpose: \(key.purpose), security: \(key.securityLevel))") - } else { - print("❌ [\(operation)] No private key for key ID \(key.id)") + // Select a suitable public key based on policy only. Availability of a + // matching private key is enforced by the caller when creating the signer. + print("🔑 [\(operation)] Selecting public key for identity \(identity.id.toBase58String())") + + let keys = Array(identity.publicKeys.values) + + // For contract create/update, require CRITICAL + AUTHENTICATION + if operation == "CONTRACT CREATE" || operation == "CONTRACT UPDATE" { + if let k = keys.first(where: { $0.securityLevel == .critical && $0.purpose == .authentication }) { + print("📝 [\(operation)] Selected CRITICAL AUTHENTICATION key #\(k.id)") + return k } - } - - guard !keysWithPrivateKeys.isEmpty else { - print("❌ [\(operation)] No keys with available private keys found!") + print("❌ [\(operation)] No CRITICAL AUTHENTICATION key found") return nil } - - // For contract creation and updates, ONLY critical AUTHENTICATION key is allowed - if operation == "CONTRACT CREATE" || operation == "CONTRACT UPDATE" { - let criticalAuthKey = keysWithPrivateKeys.first { - $0.securityLevel == .critical && $0.purpose == .authentication - } - if criticalAuthKey == nil { - print("❌ [\(operation)] Data contract operations require a critical AUTHENTICATION key, but none found with private key!") - } - return criticalAuthKey + + // Otherwise prefer CRITICAL, then AUTHENTICATION, then any + if let k = keys.first(where: { $0.securityLevel == .critical }) { + print("📝 [\(operation)] Selected CRITICAL key #\(k.id)") + return k } - - // For other operations, prefer critical key if we have its private key - let criticalKey = keysWithPrivateKeys.first { $0.securityLevel == .critical } - - // Fall back to authentication key, then any key - let keyToUse = criticalKey ?? keysWithPrivateKeys.first { key in - key.purpose == .authentication - } ?? keysWithPrivateKeys.first - - if let key = keyToUse { - print("📝 [\(operation)] Selected key ID \(key.id) - purpose: \(key.purpose), type: \(key.keyType), security: \(key.securityLevel)") - } else { - print("❌ [\(operation)] No public key found for identity") + if let k = keys.first(where: { $0.purpose == .authentication }) { + print("📝 [\(operation)] Selected AUTHENTICATION key #\(k.id)") + return k } - - return keyToUse + let k = keys.first + if let k = k { print("📝 [\(operation)] Selected fallback key #\(k.id)") } + return k } /// Helper to create a public key handle from an IdentityPublicKey -private func createPublicKeyHandle(from key: IdentityPublicKey, operation: String) -> OpaquePointer? { +private func createPublicKeyHandle(from key: IdentityPublicKey, operation: String) -> UnsafeMutablePointer? { let keyData = key.data let keyType = key.keyType.ffiValue let purpose = key.purpose.ffiValue @@ -92,18 +85,19 @@ private func createPublicKeyHandle(from key: IdentityPublicKey, operation: Strin } print("✅ [\(operation)] Public key handle created from local data") - return OpaquePointer(keyHandle) + return keyHandle.assumingMemoryBound(to: IdentityPublicKeyHandle.self) } // MARK: - State Transition Extensions +@MainActor extension SDK { // MARK: - Identity Handle Management /// Convert a DPPIdentity to an identity handle /// The returned handle must be freed with dash_sdk_identity_destroy when done - public func identityToHandle(_ identity: DPPIdentity) throws -> OpaquePointer { + nonisolated public func identityToHandle(_ identity: DPPIdentity) throws -> OpaquePointer { // Convert identity ID to 32-byte array let idBytes = identity.id // identity.id is already Data guard idBytes.count == 32 else { @@ -174,7 +168,8 @@ extension SDK { if result.data_type.rawValue == 3, // ResultIdentityHandle let identityHandle = result.data { // Get identity info from the handle - let infoPtr = dash_sdk_identity_get_info(OpaquePointer(identityHandle)!) + let idPtr = identityHandle.assumingMemoryBound(to: IdentityHandle.self) + let infoPtr = dash_sdk_identity_get_info(UnsafePointer(idPtr)) if let info = infoPtr { // Convert the C struct to a Swift dictionary @@ -194,12 +189,12 @@ extension SDK { dash_sdk_identity_info_free(info) // Destroy the identity handle - dash_sdk_identity_destroy(OpaquePointer(identityHandle)!) + dash_sdk_identity_destroy(idPtr) continuation.resume(returning: identityDict) } else { // Destroy the identity handle - dash_sdk_identity_destroy(OpaquePointer(identityHandle)!) + dash_sdk_identity_destroy(idPtr) continuation.resume(throwing: SDKError.internalError("Failed to get identity info")) } } else { @@ -222,6 +217,7 @@ extension SDK { outputIndex: UInt32, privateKey: Data ) async throws -> UInt64 { + let idBox = SendableOpaque(identity) return try await withCheckedThrowingContinuation { continuation in DispatchQueue.global().async { [weak self] in guard let self = self, let handle = self.handle else { @@ -239,7 +235,7 @@ extension SDK { privateKey.withUnsafeBytes { keyBytes in dash_sdk_identity_topup_with_instant_lock( handle, - identity, + idConst(idBox.p), instantLockBytes.bindMemory(to: UInt8.self).baseAddress!, UInt(instantLock.count), txBytes.bindMemory(to: UInt8.self).baseAddress!, @@ -257,7 +253,8 @@ extension SDK { if result.data_type.rawValue == 3, // ResultIdentityHandle let toppedUpIdentityHandle = result.data { // Get identity info from the handle to retrieve the new balance - let infoPtr = dash_sdk_identity_get_info(OpaquePointer(toppedUpIdentityHandle)!) + let idPtr = toppedUpIdentityHandle.assumingMemoryBound(to: IdentityHandle.self) + let infoPtr = dash_sdk_identity_get_info(UnsafePointer(idPtr)) if let info = infoPtr { let balance = info.pointee.balance @@ -266,12 +263,12 @@ extension SDK { dash_sdk_identity_info_free(info) // Destroy the topped up identity handle - dash_sdk_identity_destroy(OpaquePointer(toppedUpIdentityHandle)!) + dash_sdk_identity_destroy(idPtr) continuation.resume(returning: balance) } else { // Destroy the identity handle - dash_sdk_identity_destroy(OpaquePointer(toppedUpIdentityHandle)!) + dash_sdk_identity_destroy(idPtr) continuation.resume(throwing: SDKError.internalError("Failed to get identity info after topup")) } } else { @@ -294,6 +291,8 @@ extension SDK { publicKeyId: UInt32 = 0, signer: OpaquePointer ) async throws -> (senderBalance: UInt64, receiverBalance: UInt64) { + let fromBox = SendableOpaque(fromIdentity) + let signerBox = SendableOpaque(signer) return try await withCheckedThrowingContinuation { continuation in DispatchQueue.global().async { [weak self] in guard let self = self, let handle = self.handle else { @@ -305,11 +304,11 @@ extension SDK { let result = toIdentityId.withCString { toIdCStr in dash_sdk_identity_transfer_credits( handle, - fromIdentity, + idConst(fromBox.p), toIdCStr, amount, publicKeyId, - signer, + signerConst(signerBox.p), nil // Default put settings ) } @@ -345,6 +344,8 @@ extension SDK { publicKeyId: UInt32 = 0, signer: OpaquePointer ) async throws -> UInt64 { + let idBox = SendableOpaque(identity) + let signerBox = SendableOpaque(signer) return try await withCheckedThrowingContinuation { continuation in DispatchQueue.global().async { [weak self] in guard let self = self, let handle = self.handle else { @@ -356,12 +357,12 @@ extension SDK { let result = toAddress.withCString { addressCStr in dash_sdk_identity_withdraw( handle, - identity, + idConst(idBox.p), addressCStr, amount, coreFeePerByte, publicKeyId, - signer, + signerConst(signerBox.p), nil // Default put settings ) } @@ -401,12 +402,18 @@ extension SDK { properties: [String: Any], signer: OpaquePointer ) async throws -> [String: Any] { + let signerBox = SendableOpaque(signer) let startTime = Date() print("📝 [DOCUMENT CREATE] Starting at \(startTime)") print("📝 [DOCUMENT CREATE] Contract ID: \(contractId)") print("📝 [DOCUMENT CREATE] Document Type: \(documentType)") print("📝 [DOCUMENT CREATE] Owner ID: \(ownerIdentity.idString)") - + // Pre-serialize properties to avoid capturing non-Sendable dictionary in concurrent closure + guard let propertiesDataPre = try? JSONSerialization.data(withJSONObject: properties), + let propertiesJsonPre = String(data: propertiesDataPre, encoding: .utf8) else { + throw SDKError.invalidParameter("Failed to serialize properties to JSON") + } + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<[String: Any], Error>) in DispatchQueue.global().async { [weak self] in guard let self = self, let handle = self.handle else { @@ -415,14 +422,8 @@ extension SDK { return } - // Convert properties to JSON print("📝 [DOCUMENT CREATE] Converting properties to JSON...") - guard let propertiesData = try? JSONSerialization.data(withJSONObject: properties), - let propertiesJson = String(data: propertiesData, encoding: .utf8) else { - print("❌ [DOCUMENT CREATE] Failed to serialize properties") - continuation.resume(throwing: SDKError.invalidParameter("Failed to serialize properties to JSON")) - return - } + let propertiesJson = propertiesJsonPre print("✅ [DOCUMENT CREATE] Properties JSON created: \(propertiesJson.prefix(100))...") // 1. Create document using contract from trusted context (no network fetches needed) @@ -469,7 +470,7 @@ extension SDK { } // Cast the result data to DashSDKDocumentCreateResult pointer - let createResultPtr = UnsafeMutablePointer(OpaquePointer(resultData)) + let createResultPtr = resultData.assumingMemoryBound(to: DashSDKDocumentCreateResult.self) let createResultStruct = createResultPtr.pointee let documentHandle = createResultStruct.document_handle let entropy = createResultStruct.entropy @@ -527,7 +528,7 @@ extension SDK { docTypeCStr, entropyPtr, keyHandle, - signer, + signerConst(signerBox.p), tokenPaymentInfo, putSettings, stateTransitionOptions @@ -549,8 +550,9 @@ extension SDK { } else if putResult.data_type == DashSDKFFI.String, let jsonData = putResult.data { // Parse the returned JSON - let jsonString = String(cString: UnsafePointer(OpaquePointer(jsonData))) - dash_sdk_string_free(UnsafeMutablePointer(mutating: UnsafePointer(OpaquePointer(jsonData)))) + let jsonPtr = jsonData.assumingMemoryBound(to: CChar.self) + let jsonString = String(cString: jsonPtr) + dash_sdk_string_free(jsonPtr) print("✅ [DOCUMENT CREATE] Success! Total operation time: \(Date().timeIntervalSince(startTime)) seconds") print("📝 [DOCUMENT CREATE] Response: \(jsonString.prefix(200))...") @@ -578,6 +580,12 @@ extension SDK { properties: [String: Any], signer: OpaquePointer ) async throws -> [String: Any] { + let signerBox = SendableOpaque(signer) + // Pre-serialize properties to avoid capturing non-Sendable dictionary in concurrent closure + guard let propertiesDataPre = try? JSONSerialization.data(withJSONObject: properties), + let propertiesJsonPre = String(data: propertiesDataPre, encoding: .utf8) else { + throw SDKError.invalidParameter("Failed to serialize properties to JSON") + } let startTime = Date() print("📝 [DOCUMENT REPLACE] Starting at \(startTime)") print("📝 [DOCUMENT REPLACE] Contract: \(contractId), Type: \(documentType), Doc: \(documentId)") @@ -615,7 +623,8 @@ extension SDK { } defer { - dash_sdk_data_contract_destroy(OpaquePointer(contractHandle)!) + let dcPtr = contractHandle.assumingMemoryBound(to: DataContractHandle.self) + dash_sdk_data_contract_destroy(dcPtr) } // Now fetch the document using the contract handle @@ -623,7 +632,7 @@ extension SDK { documentId.withCString { docIdCStr in dash_sdk_document_fetch( handle, - OpaquePointer(contractHandle), + UnsafePointer(contractHandle.assumingMemoryBound(to: DataContractHandle.self)), docTypeCStr, docIdCStr ) @@ -649,21 +658,16 @@ extension SDK { } defer { - dash_sdk_document_free(OpaquePointer(documentHandle)) + dash_sdk_document_free(documentHandle.assumingMemoryBound(to: DocumentHandle.self)) } print("✅ [DOCUMENT REPLACE] Document fetched successfully") // 2. Update the document properties - // Convert properties to JSON and set on the document - guard let propertiesData = try? JSONSerialization.data(withJSONObject: properties), - let propertiesJson = String(data: propertiesData, encoding: .utf8) else { - continuation.resume(throwing: SDKError.invalidParameter("Failed to serialize properties to JSON")) - return - } - - propertiesJson.withCString { propsCStr in - dash_sdk_document_set_properties(OpaquePointer(documentHandle), propsCStr) + // Use pre-serialized JSON to avoid capturing non-Sendable value types + let propertiesJson = propertiesJsonPre + _ = propertiesJson.withCString { propsCStr in + dash_sdk_document_set_properties(documentHandle.assumingMemoryBound(to: DocumentHandle.self), propsCStr) } // 3. Get appropriate key for signing @@ -691,17 +695,17 @@ extension SDK { let replaceResult = contractId.withCString { contractIdCStr in documentType.withCString { docTypeCStr in - dash_sdk_document_replace_on_platform_and_wait( - handle, - OpaquePointer(documentHandle), - contractIdCStr, - docTypeCStr, - keyHandle, - signer, - nil, // token payment info - nil, // put settings - nil // state transition options - ) + dash_sdk_document_replace_on_platform_and_wait( + handle, + UnsafePointer(documentHandle.assumingMemoryBound(to: DocumentHandle.self)), + contractIdCStr, + docTypeCStr, + keyHandle, + signerConst(signerBox.p), + nil, // token payment info + nil, // put settings + nil // state transition options + ) } } @@ -716,7 +720,7 @@ extension SDK { } else if replaceResult.data_type == DashSDKFFI.ResultDocumentHandle, let resultHandle = replaceResult.data { // Document was successfully replaced - dash_sdk_document_free(OpaquePointer(resultHandle)) + dash_sdk_document_free(resultHandle.assumingMemoryBound(to: DocumentHandle.self)) let totalTime = Date().timeIntervalSince(startTime) print("✅ [DOCUMENT REPLACE] Document replaced successfully") @@ -740,6 +744,7 @@ extension SDK { ownerIdentity: DPPIdentity, signer: OpaquePointer ) async throws { + let signerBox = SendableOpaque(signer) let startTime = Date() print("🗑️ [DOCUMENT DELETE] Starting at \(startTime)") print("🗑️ [DOCUMENT DELETE] Contract: \(contractId), Type: \(documentType), Doc: \(documentId)") @@ -787,7 +792,7 @@ extension SDK { contractIdCString, documentTypeCString, keyHandle, - signer, + signerConst(signerBox.p), nil, // token_payment_info nil, // put_settings nil // state_transition_creation_options @@ -824,6 +829,7 @@ extension SDK { toIdentityId: String, signer: OpaquePointer ) async throws -> [String: Any] { + let signerBox = SendableOpaque(signer) let startTime = Date() print("🔁 [DOCUMENT TRANSFER] Starting at \(startTime)") print("🔁 [DOCUMENT TRANSFER] Contract: \(contractId), Type: \(documentType), Doc: \(documentId)") @@ -880,7 +886,8 @@ extension SDK { } defer { - dash_sdk_data_contract_destroy(OpaquePointer(contractHandle)!) + let dcPtr2 = contractHandle.assumingMemoryBound(to: DataContractHandle.self) + dash_sdk_data_contract_destroy(dcPtr2) } let contractFetchTime = Date().timeIntervalSince(contractFetchStartTime) @@ -892,7 +899,7 @@ extension SDK { // Now fetch the document using the contract handle let fetchResult = dash_sdk_document_fetch( handle, - OpaquePointer(contractHandle), + UnsafePointer(contractHandle.assumingMemoryBound(to: DataContractHandle.self)), documentTypeCString, documentIdCString ) @@ -910,7 +917,7 @@ extension SDK { } defer { - dash_sdk_document_destroy(handle, OpaquePointer(documentHandle)!) + dash_sdk_document_destroy(handle, documentHandle.assumingMemoryBound(to: DocumentHandle.self)) } print("✅ [DOCUMENT TRANSFER] Document fetched successfully") @@ -922,12 +929,12 @@ extension SDK { print("🔄 [DOCUMENT TRANSFER] Creating state transition...") let transitionResult = dash_sdk_document_transfer_to_identity( handle, - OpaquePointer(documentHandle), + UnsafePointer(documentHandle.assumingMemoryBound(to: DocumentHandle.self)), toIdentityCString, contractIdCString, documentTypeCString, keyHandle, - signer, + signerConst(signerBox.p), nil, // token_payment_info nil, // put_settings nil // state_transition_creation_options @@ -946,12 +953,12 @@ extension SDK { print("🔄 [DOCUMENT TRANSFER] Broadcasting and waiting for confirmation...") let result = dash_sdk_document_transfer_to_identity_and_wait( handle, - OpaquePointer(documentHandle), + UnsafePointer(documentHandle.assumingMemoryBound(to: DocumentHandle.self)), toIdentityCString, contractIdCString, documentTypeCString, keyHandle, - signer, + signerConst(signerBox.p), nil, // token_payment_info nil, // put_settings nil // state_transition_creation_options @@ -1008,6 +1015,7 @@ extension SDK { ownerIdentity: DPPIdentity, signer: OpaquePointer ) async throws -> [String: Any] { + let signerBox = SendableOpaque(signer) let startTime = Date() print("💰 [DOCUMENT UPDATE PRICE] Starting...") print("💰 [DOCUMENT UPDATE PRICE] Contract: \(contractId), Type: \(documentType)") @@ -1041,7 +1049,8 @@ extension SDK { } defer { - dash_sdk_data_contract_destroy(OpaquePointer(contractHandle)!) + let dcPtr3 = contractHandle.assumingMemoryBound(to: DataContractHandle.self) + dash_sdk_data_contract_destroy(dcPtr3) } // Step 2: Fetch the document @@ -1050,7 +1059,7 @@ extension SDK { documentId.withCString { docIdCStr in dash_sdk_document_fetch( handle, - OpaquePointer(contractHandle), + UnsafePointer(contractHandle.assumingMemoryBound(to: DataContractHandle.self)), docTypeCStr, docIdCStr ) @@ -1072,7 +1081,7 @@ extension SDK { } defer { - dash_sdk_document_destroy(handle, OpaquePointer(documentHandle)!) + dash_sdk_document_destroy(handle, documentHandle.assumingMemoryBound(to: DocumentHandle.self)) } print("✅ [DOCUMENT UPDATE PRICE] Document fetched successfully") @@ -1099,12 +1108,12 @@ extension SDK { documentType.withCString { documentTypeCStr in dash_sdk_document_update_price_of_document_and_wait( handle, - OpaquePointer(documentHandle), + UnsafePointer(documentHandle.assumingMemoryBound(to: DocumentHandle.self)), contractIdCStr, documentTypeCStr, newPrice, keyHandle, - signer, + signerConst(signerBox.p), nil, // token_payment_info nil, // put_settings nil // state_transition_creation_options @@ -1142,6 +1151,7 @@ extension SDK { price: UInt64, signer: OpaquePointer ) async throws -> [String: Any] { + let signerBox = SendableOpaque(signer) let startTime = Date() print("🛍️ [DOCUMENT PURCHASE] Starting at \(startTime)") print("🛍️ [DOCUMENT PURCHASE] Contract: \(contractId), Type: \(documentType), Doc: \(documentId)") @@ -1152,7 +1162,6 @@ extension SDK { } return try await withCheckedThrowingContinuation { continuation in - Task { // Convert strings to C strings guard let contractIdCString = contractId.cString(using: .utf8), let documentTypeCString = documentType.cString(using: .utf8), @@ -1197,7 +1206,7 @@ extension SDK { } defer { - dash_sdk_data_contract_destroy(OpaquePointer(contractHandle)) + dash_sdk_data_contract_destroy(contractHandle.assumingMemoryBound(to: DataContractHandle.self)) } print("📝 [DOCUMENT PURCHASE] Contract fetched in \(Date().timeIntervalSince(contractFetchStartTime)) seconds") @@ -1206,7 +1215,7 @@ extension SDK { print("📝 [DOCUMENT PURCHASE] Step 2: Fetching document...") let documentFetchStart = Date() - let documentResult = dash_sdk_document_fetch(handle, OpaquePointer(contractHandle), documentTypeCString, documentIdCString) + let documentResult = dash_sdk_document_fetch(handle, UnsafePointer(contractHandle.assumingMemoryBound(to: DataContractHandle.self)), documentTypeCString, documentIdCString) if let error = documentResult.error { let errorMessage = error.pointee.message != nil ? String(cString: error.pointee.message!) : "Unknown error" @@ -1221,7 +1230,7 @@ extension SDK { } defer { - dash_sdk_document_destroy(handle, OpaquePointer(documentHandle)) + dash_sdk_document_destroy(handle, documentHandle.assumingMemoryBound(to: DocumentHandle.self)) } print("📝 [DOCUMENT PURCHASE] Document fetched in \(Date().timeIntervalSince(documentFetchStart)) seconds") @@ -1233,13 +1242,13 @@ extension SDK { let result = dash_sdk_document_purchase_and_wait( handle, - OpaquePointer(documentHandle), + UnsafePointer(documentHandle.assumingMemoryBound(to: DocumentHandle.self)), contractIdCString, documentTypeCString, price, purchaserIdCString, keyHandle, - signer, + signerConst(signerBox.p), nil, // token_payment_info - null for now nil, // put_settings - null for now nil // state_transition_creation_options - null for now @@ -1263,15 +1272,17 @@ extension SDK { // The result should contain the purchased document if let documentData = result.data { // We received the purchased document back - let purchasedDocHandle = OpaquePointer(documentData) + let purchasedDocHandle = documentData.assumingMemoryBound(to: DocumentHandle.self) - // Get info about the purchased document - var purchasedDocInfo: [String: Any] = [:] + // Get info about the purchased document (extract Sendable primitives) + var purchasedId: String? = nil + var purchasedOwner: String? = nil + var purchasedRevision: UInt64 = 0 if let info = dash_sdk_document_get_info(purchasedDocHandle) { let docInfo = info.pointee - purchasedDocInfo["id"] = String(cString: docInfo.id) - purchasedDocInfo["owner_id"] = String(cString: docInfo.owner_id) - purchasedDocInfo["revision"] = docInfo.revision + if let idPtr = docInfo.id { purchasedId = String(cString: idPtr) } + if let ownerPtr = docInfo.owner_id { purchasedOwner = String(cString: ownerPtr) } + purchasedRevision = docInfo.revision dash_sdk_document_info_free(info) } @@ -1281,25 +1292,31 @@ extension SDK { let totalTime = Date().timeIntervalSince(startTime) print("✅ [DOCUMENT PURCHASE] Purchase completed and confirmed in \(totalTime) seconds") print("📦 [DOCUMENT PURCHASE] Document successfully purchased and ownership transferred") - print("📄 [DOCUMENT PURCHASE] New owner: \(purchasedDocInfo["owner_id"] ?? "unknown")") + print("📄 [DOCUMENT PURCHASE] New owner: \(purchasedOwner ?? "unknown")") - // Return success with the purchased document info - continuation.resume(returning: [ - "success": true, - "message": "Document purchased successfully", - "transitionType": "documentPurchase", - "contractId": contractId, - "documentType": documentType, - "documentId": documentId, - "price": price, - "purchasedDocument": purchasedDocInfo - ]) + // Return success with the purchased document info on main actor to avoid sending non-Sendable values + DispatchQueue.main.async { + let purchasedDocInfo: [String: Any] = [ + "id": purchasedId as Any, + "owner_id": purchasedOwner as Any, + "revision": purchasedRevision + ] + continuation.resume(returning: [ + "success": true, + "message": "Document purchased successfully", + "transitionType": "documentPurchase", + "contractId": contractId, + "documentType": documentType, + "documentId": documentId, + "price": price, + "purchasedDocument": purchasedDocInfo + ]) + } } else { print("❌ [DOCUMENT PURCHASE] No data returned from purchase") continuation.resume(throwing: SDKError.internalError("No data returned from document purchase")) return } - } } } @@ -1326,6 +1343,7 @@ extension SDK { signer: OpaquePointer, note: String? = nil ) async throws -> [String: Any] { + let signerBox = SendableOpaque(signer) print("🟦 TOKEN MINT: Starting token mint operation") print("🟦 TOKEN MINT: Contract ID: \(contractId)") print("🟦 TOKEN MINT: Recipient ID: \(recipientId ?? "owner (default)")") @@ -1356,7 +1374,7 @@ extension SDK { defer { print("🟦 TOKEN MINT: Cleaning up identity handle") // Clean up the identity handle when done - dash_sdk_identity_destroy(ownerIdentityHandle) + dash_sdk_identity_destroy(idMut(ownerIdentityHandle)) } // Get the owner ID from the identity @@ -1392,7 +1410,7 @@ extension SDK { // Get the public key handle for the minting key print("🟦 TOKEN MINT: Getting public key handle for key ID: \(keyId)") let keyHandleResult = dash_sdk_identity_get_public_key_by_id( - ownerIdentityHandle, + idConst(ownerIdentityHandle), UInt8(keyId) ) @@ -1406,7 +1424,7 @@ extension SDK { return } - let publicKeyHandle = OpaquePointer(keyHandleData)! + let publicKeyHandle = keyHandleData.assumingMemoryBound(to: IdentityPublicKeyHandle.self) print("✅ TOKEN MINT: Successfully got public key handle") defer { print("🟦 TOKEN MINT: Cleaning up public key handle") @@ -1448,7 +1466,7 @@ extension SDK { ownerIdBytes.bindMemory(to: UInt8.self).baseAddress!, ¶ms, publicKeyHandle, - signer, + signerConst(signerBox.p), nil, // Default put settings nil // Default state transition options ) @@ -1466,7 +1484,7 @@ extension SDK { ownerIdBytes.bindMemory(to: UInt8.self).baseAddress!, ¶ms, publicKeyHandle, - signer, + signerConst(signerBox.p), nil, // Default put settings nil // Default state transition options ) @@ -1505,6 +1523,7 @@ extension SDK { signer: OpaquePointer, note: String? = nil ) async throws -> [String: Any] { + let signerBox = SendableOpaque(signer) return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<[String: Any], Error>) in DispatchQueue.global().async { [weak self] in guard let self = self, let handle = self.handle else { @@ -1523,7 +1542,7 @@ extension SDK { defer { // Clean up the identity handle when done - dash_sdk_identity_destroy(ownerIdentityHandle) + dash_sdk_identity_destroy(idMut(ownerIdentityHandle)) } // Get the owner ID from the identity @@ -1548,7 +1567,7 @@ extension SDK { // Get the public key handle for the freezing key let keyHandleResult = dash_sdk_identity_get_public_key_by_id( - ownerIdentityHandle, + idConst(ownerIdentityHandle), UInt8(freezingKey.id) ) @@ -1561,7 +1580,7 @@ extension SDK { return } - let publicKeyHandle = OpaquePointer(keyHandleData)! + let publicKeyHandle = keyHandleData.assumingMemoryBound(to: IdentityPublicKeyHandle.self) defer { // Clean up the public key handle when done dash_sdk_identity_public_key_destroy(publicKeyHandle) @@ -1588,7 +1607,7 @@ extension SDK { ownerIdBytes.bindMemory(to: UInt8.self).baseAddress!, ¶ms, publicKeyHandle, - signer, + signerConst(signerBox.p), nil, // Default put settings nil // Default state transition options ) @@ -1601,7 +1620,7 @@ extension SDK { ownerIdBytes.bindMemory(to: UInt8.self).baseAddress!, ¶ms, publicKeyHandle, - signer, + signerConst(signerBox.p), nil, // Default put settings nil // Default state transition options ) @@ -1636,6 +1655,7 @@ extension SDK { signer: OpaquePointer, note: String? = nil ) async throws -> [String: Any] { + let signerBox = SendableOpaque(signer) return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<[String: Any], Error>) in DispatchQueue.global().async { [weak self] in guard let self = self, let handle = self.handle else { @@ -1654,7 +1674,7 @@ extension SDK { defer { // Clean up the identity handle when done - dash_sdk_identity_destroy(ownerIdentityHandle) + dash_sdk_identity_destroy(idMut(ownerIdentityHandle)) } // Get the owner ID from the identity @@ -1679,7 +1699,7 @@ extension SDK { // Get the public key handle for the unfreezing key let keyHandleResult = dash_sdk_identity_get_public_key_by_id( - ownerIdentityHandle, + idConst(ownerIdentityHandle), UInt8(unfreezingKey.id) ) @@ -1692,7 +1712,7 @@ extension SDK { return } - let publicKeyHandle = OpaquePointer(keyHandleData)! + let publicKeyHandle = keyHandleData.assumingMemoryBound(to: IdentityPublicKeyHandle.self) defer { // Clean up the public key handle when done dash_sdk_identity_public_key_destroy(publicKeyHandle) @@ -1719,7 +1739,7 @@ extension SDK { ownerIdBytes.bindMemory(to: UInt8.self).baseAddress!, ¶ms, publicKeyHandle, - signer, + signerConst(signerBox.p), nil, // Default put settings nil // Default state transition options ) @@ -1732,7 +1752,7 @@ extension SDK { ownerIdBytes.bindMemory(to: UInt8.self).baseAddress!, ¶ms, publicKeyHandle, - signer, + signerConst(signerBox.p), nil, // Default put settings nil // Default state transition options ) @@ -1767,6 +1787,7 @@ extension SDK { signer: OpaquePointer, note: String? = nil ) async throws -> [String: Any] { + let signerBox = SendableOpaque(signer) return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<[String: Any], Error>) in DispatchQueue.global().async { [weak self] in guard let self = self, let handle = self.handle else { @@ -1785,7 +1806,7 @@ extension SDK { defer { // Clean up the identity handle when done - dash_sdk_identity_destroy(ownerIdentityHandle) + dash_sdk_identity_destroy(idMut(ownerIdentityHandle)) } // Get the owner ID from the identity @@ -1800,7 +1821,7 @@ extension SDK { // Get the public key handle for the burning key let keyHandleResult = dash_sdk_identity_get_public_key_by_id( - ownerIdentityHandle, + idConst(ownerIdentityHandle), UInt8(burningKey.id) ) @@ -1813,7 +1834,7 @@ extension SDK { return } - let publicKeyHandle = OpaquePointer(keyHandleData)! + let publicKeyHandle = keyHandleData.assumingMemoryBound(to: IdentityPublicKeyHandle.self) defer { // Clean up the public key handle when done dash_sdk_identity_public_key_destroy(publicKeyHandle) @@ -1839,7 +1860,7 @@ extension SDK { ownerIdBytes.bindMemory(to: UInt8.self).baseAddress!, ¶ms, publicKeyHandle, - signer, + signerConst(signerBox.p), nil, // Default put settings nil // Default state transition options ) @@ -1852,7 +1873,7 @@ extension SDK { ownerIdBytes.bindMemory(to: UInt8.self).baseAddress!, ¶ms, publicKeyHandle, - signer, + signerConst(signerBox.p), nil, // Default put settings nil // Default state transition options ) @@ -1886,6 +1907,7 @@ extension SDK { signer: OpaquePointer, note: String? = nil ) async throws -> [String: Any] { + let signerBox = SendableOpaque(signer) return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<[String: Any], Error>) in DispatchQueue.global().async { [weak self] in guard let self = self, let handle = self.handle else { @@ -1904,7 +1926,7 @@ extension SDK { defer { // Clean up the identity handle when done - dash_sdk_identity_destroy(ownerIdentityHandle) + dash_sdk_identity_destroy(idMut(ownerIdentityHandle)) } // Get the owner ID from the identity @@ -1929,7 +1951,7 @@ extension SDK { // Get the public key handle for the destroy key let keyHandleResult = dash_sdk_identity_get_public_key_by_id( - ownerIdentityHandle, + idConst(ownerIdentityHandle), UInt8(destroyKey.id) ) @@ -1942,7 +1964,7 @@ extension SDK { return } - let publicKeyHandle = OpaquePointer(keyHandleData)! + let publicKeyHandle = keyHandleData.assumingMemoryBound(to: IdentityPublicKeyHandle.self) defer { // Clean up the public key handle when done dash_sdk_identity_public_key_destroy(publicKeyHandle) @@ -1969,7 +1991,7 @@ extension SDK { ownerIdBytes.bindMemory(to: UInt8.self).baseAddress!, ¶ms, publicKeyHandle, - signer, + signerConst(signerBox.p), nil, // Default put settings nil // Default state transition options ) @@ -1982,7 +2004,7 @@ extension SDK { ownerIdBytes.bindMemory(to: UInt8.self).baseAddress!, ¶ms, publicKeyHandle, - signer, + signerConst(signerBox.p), nil, // Default put settings nil // Default state transition options ) @@ -2017,6 +2039,7 @@ extension SDK { signer: OpaquePointer, note: String? = nil ) async throws -> [String: Any] { + let signerBox = SendableOpaque(signer) return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<[String: Any], Error>) in DispatchQueue.global().async { [weak self] in guard let self = self, let handle = self.handle else { @@ -2035,7 +2058,7 @@ extension SDK { defer { // Clean up the identity handle when done - dash_sdk_identity_destroy(ownerIdentityHandle) + dash_sdk_identity_destroy(idMut(ownerIdentityHandle)) } // Get the owner ID from the identity @@ -2043,7 +2066,7 @@ extension SDK { // Get the public key handle for the claiming key let keyHandleResult = dash_sdk_identity_get_public_key_by_id( - ownerIdentityHandle, + idConst(ownerIdentityHandle), UInt8(keyId) ) @@ -2056,7 +2079,7 @@ extension SDK { return } - let publicKeyHandle = OpaquePointer(keyHandleData)! + let publicKeyHandle = keyHandleData.assumingMemoryBound(to: IdentityPublicKeyHandle.self) defer { // Clean up the public key handle when done dash_sdk_identity_public_key_destroy(publicKeyHandle) @@ -2088,13 +2111,13 @@ extension SDK { if let note = note { return note.withCString { noteCStr in params.public_note = noteCStr - + return dash_sdk_token_claim( handle, ownerIdBytes.bindMemory(to: UInt8.self).baseAddress!, ¶ms, publicKeyHandle, - signer, + signerConst(signerBox.p), nil, // Default put settings nil // Default state transition options ) @@ -2107,7 +2130,7 @@ extension SDK { ownerIdBytes.bindMemory(to: UInt8.self).baseAddress!, ¶ms, publicKeyHandle, - signer, + signerConst(signerBox.p), nil, // Default put settings nil // Default state transition options ) @@ -2142,6 +2165,7 @@ extension SDK { signer: OpaquePointer, note: String? = nil ) async throws -> [String: Any] { + let signerBox = SendableOpaque(signer) return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<[String: Any], Error>) in DispatchQueue.global().async { [weak self] in guard let self = self, let handle = self.handle else { @@ -2160,7 +2184,7 @@ extension SDK { defer { // Clean up the identity handle when done - dash_sdk_identity_destroy(ownerIdentityHandle) + dash_sdk_identity_destroy(idMut(ownerIdentityHandle)) } // Get the owner ID from the identity @@ -2178,7 +2202,7 @@ extension SDK { // Get the public key handle for the transfer key let keyHandleResult = dash_sdk_identity_get_public_key_by_id( - ownerIdentityHandle, + idConst(ownerIdentityHandle), UInt8(keyId) ) @@ -2191,7 +2215,7 @@ extension SDK { return } - let publicKeyHandle = OpaquePointer(keyHandleData)! + let publicKeyHandle = keyHandleData.assumingMemoryBound(to: IdentityPublicKeyHandle.self) defer { // Clean up the public key handle when done dash_sdk_identity_public_key_destroy(publicKeyHandle) @@ -2221,7 +2245,7 @@ extension SDK { ownerIdBytes.bindMemory(to: UInt8.self).baseAddress!, ¶ms, publicKeyHandle, - signer, + signerConst(signerBox.p), nil, // Default put settings nil // Default state transition options ) @@ -2234,13 +2258,13 @@ extension SDK { ownerIdBytes.bindMemory(to: UInt8.self).baseAddress!, ¶ms, publicKeyHandle, - signer, + signerConst(signerBox.p), nil, // Default put settings nil // Default state transition options ) - } - } - } + } + } + } } if result.error == nil { @@ -2270,6 +2294,7 @@ extension SDK { signer: OpaquePointer, note: String? = nil ) async throws -> [String: Any] { + let signerBox = SendableOpaque(signer) return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<[String: Any], Error>) in DispatchQueue.global().async { [weak self] in guard let self = self, let handle = self.handle else { @@ -2288,7 +2313,7 @@ extension SDK { defer { // Clean up the identity handle when done - dash_sdk_identity_destroy(ownerIdentityHandle) + dash_sdk_identity_destroy(idMut(ownerIdentityHandle)) } // Get the owner ID from the identity @@ -2296,7 +2321,7 @@ extension SDK { // Get the public key handle for the pricing key let keyHandleResult = dash_sdk_identity_get_public_key_by_id( - ownerIdentityHandle, + idConst(ownerIdentityHandle), UInt8(keyId) ) @@ -2309,7 +2334,7 @@ extension SDK { return } - let publicKeyHandle = OpaquePointer(keyHandleData)! + let publicKeyHandle = keyHandleData.assumingMemoryBound(to: IdentityPublicKeyHandle.self) defer { // Clean up the public key handle when done dash_sdk_identity_public_key_destroy(publicKeyHandle) @@ -2361,7 +2386,7 @@ extension SDK { ownerIdBytes.bindMemory(to: UInt8.self).baseAddress!, ¶ms, publicKeyHandle, - signer, + signerConst(signerBox.p), nil, // Default put settings nil // Default state transition options ) @@ -2374,7 +2399,7 @@ extension SDK { ownerIdBytes.bindMemory(to: UInt8.self).baseAddress!, ¶ms, publicKeyHandle, - signer, + signerConst(signerBox.p), nil, // Default put settings nil // Default state transition options ) @@ -2410,6 +2435,13 @@ extension SDK { contractConfig: [String: Any], signer: OpaquePointer ) async throws -> [String: Any] { + let signerBox = SendableOpaque(signer) + // Pre-serialize schemas to avoid capturing non-Sendable values + let schemasToUsePre = documentSchemas ?? [:] + guard let jsonDataPre = try? JSONSerialization.data(withJSONObject: schemasToUsePre), + let jsonStringPre = String(data: jsonDataPre, encoding: .utf8) else { + throw SDKError.serializationError("Failed to serialize contract schema") + } return try await withCheckedThrowingContinuation { continuation in DispatchQueue.global().async { [weak self] in guard let self = self, let handle = self.handle else { @@ -2417,16 +2449,8 @@ extension SDK { return } - // The FFI function expects just the document schemas directly - // Token schemas, groups, and other config are not supported yet - let schemasToUse = documentSchemas ?? [:] - - // Convert to JSON string - guard let jsonData = try? JSONSerialization.data(withJSONObject: schemasToUse), - let jsonString = String(data: jsonData, encoding: .utf8) else { - continuation.resume(throwing: SDKError.serializationError("Failed to serialize contract schema")) - return - } + // Use pre-serialized schema JSON + let jsonString = jsonStringPre print("📄 [CONTRACT CREATE] Sending document schemas: \(jsonString)") @@ -2437,14 +2461,14 @@ extension SDK { } defer { - dash_sdk_identity_destroy(identityHandle) + dash_sdk_identity_destroy(idMut(identityHandle)) } // Step 1: Create the contract locally let createResult = jsonString.withCString { jsonCStr in dash_sdk_data_contract_create( handle, - identityHandle, + idConst(identityHandle), jsonCStr ) } @@ -2462,7 +2486,7 @@ extension SDK { } defer { - dash_sdk_data_contract_destroy(OpaquePointer(contractHandle)) + dash_sdk_data_contract_destroy(contractHandle.assumingMemoryBound(to: DataContractHandle.self)) } // Step 2: Select signing key (must be critical authentication key for contract creation) @@ -2484,9 +2508,9 @@ extension SDK { // Step 3: Broadcast the contract to the network let putResult = dash_sdk_data_contract_put_to_platform_and_wait( handle, - OpaquePointer(contractHandle), + UnsafePointer(contractHandle.assumingMemoryBound(to: DataContractHandle.self)), keyHandle, - signer + signerConst(signerBox.p) ) if let error = putResult.error { @@ -2583,14 +2607,14 @@ extension SDK { } defer { - dash_sdk_identity_destroy(identityHandle) + dash_sdk_identity_destroy(idMut(identityHandle)) } // Create the updated contract let createResult = jsonString.withCString { jsonCStr in dash_sdk_data_contract_create( handle, - identityHandle, + idConst(identityHandle), jsonCStr ) } @@ -2608,7 +2632,7 @@ extension SDK { } defer { - dash_sdk_data_contract_destroy(OpaquePointer(updatedContractHandle)) + dash_sdk_data_contract_destroy(updatedContractHandle.assumingMemoryBound(to: DataContractHandle.self)) } // Select signing key (must be critical authentication key for contract update) @@ -2630,7 +2654,7 @@ extension SDK { // Broadcast the updated contract to the network let putResult = dash_sdk_data_contract_put_to_platform_and_wait( handle, - OpaquePointer(updatedContractHandle), + UnsafePointer(updatedContractHandle.assumingMemoryBound(to: DataContractHandle.self)), keyHandle, signer ) @@ -2661,6 +2685,7 @@ extension SDK { // MARK: - Convenience Methods with DPPIdentity +@MainActor extension SDK { /// Transfer credits between identities (convenience method with DPPIdentity) public func transferCredits( @@ -2673,7 +2698,7 @@ extension SDK { let identityHandle = try identityToHandle(identity) defer { // Clean up the handle when done - dash_sdk_identity_destroy(identityHandle) + dash_sdk_identity_destroy(idMut(identityHandle)) } // Call the lower-level method @@ -2698,7 +2723,7 @@ extension SDK { let identityHandle = try identityToHandle(identity) defer { // Clean up the handle when done - dash_sdk_identity_destroy(identityHandle) + dash_sdk_identity_destroy(idMut(identityHandle)) } // Call the lower-level method @@ -2723,7 +2748,7 @@ extension SDK { let identityHandle = try identityToHandle(identity) defer { // Clean up the handle when done - dash_sdk_identity_destroy(identityHandle) + dash_sdk_identity_destroy(idMut(identityHandle)) } // Call the lower-level method @@ -2739,7 +2764,7 @@ extension SDK { // MARK: - Helper Methods - private func normalizeIdentityId(_ identityId: String) -> String { + nonisolated private func normalizeIdentityId(_ identityId: String) -> String { // Remove any prefix let cleanId = identityId .replacingOccurrences(of: "id:", with: "") diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Services/KeychainManager.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Services/KeychainManager.swift index 6eeebf700c..7be7e0a8ef 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Services/KeychainManager.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Services/KeychainManager.swift @@ -2,6 +2,7 @@ import Foundation import Security /// Manages secure storage of private keys in the iOS Keychain +@MainActor final class KeychainManager { static let shared = KeychainManager() @@ -33,7 +34,7 @@ final class KeychainManager { ] // Add metadata - var metadata: [String: Any] = [ + let metadata: [String: Any] = [ "identityId": identityId.toHexString(), "keyIndex": keyIndex, "createdAt": Date().timeIntervalSince1970 @@ -298,4 +299,4 @@ enum KeychainError: LocalizedError { return "Invalid key data" } } -} \ No newline at end of file +} diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/UnifiedAppState.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/UnifiedAppState.swift index 5c3c9dab35..a4e806a17e 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/UnifiedAppState.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/UnifiedAppState.swift @@ -63,30 +63,26 @@ class UnifiedAppState: ObservableObject { } func initialize() async { - do { - // Initialize Platform SDK - await MainActor.run { - platformState.initializeSDK(modelContext: modelContainer.mainContext) - } - - // Wait for Platform SDK to be ready - try? await Task.sleep(nanoseconds: 500_000_000) // 0.5 second + // Initialize Platform SDK + await MainActor.run { + platformState.initializeSDK(modelContext: modelContainer.mainContext) + } + + // Wait for Platform SDK to be ready + try? await Task.sleep(nanoseconds: 500_000_000) // 0.5 second - // If SDK reports trusted mode, disable masternode SPV sync - if let sdk = platformState.sdk { - do { - let status: SwiftDashSDK.SDKStatus = try sdk.getStatus() - let isTrusted = status.mode.lowercased() == "trusted" - await MainActor.run { self.walletService.setMasternodesEnabled(!isTrusted) } - } catch { - // Ignore status errors; keep default (false) until known - } + // If SDK reports trusted mode, disable masternode SPV sync + if let sdk = platformState.sdk { + do { + let status: SwiftDashSDK.SDKStatus = try sdk.getStatus() + let isTrusted = status.mode.lowercased() == "trusted" + await MainActor.run { self.walletService.setMasternodesEnabled(!isTrusted) } + } catch { + // Ignore status errors; keep default (false) until known } - - isInitialized = true - } catch { - self.error = error } + + isInitialized = true } func reset() async { @@ -94,7 +90,7 @@ class UnifiedAppState: ObservableObject { error = nil // Reset services - await walletService.stopSync() + walletService.stopSync() // Reset platform state platformState.sdk = nil diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Utils/EnvLoader.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Utils/EnvLoader.swift index 7b5eac88a9..5cc3374f4f 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Utils/EnvLoader.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Utils/EnvLoader.swift @@ -1,6 +1,7 @@ import Foundation /// Environment variable loader for test configuration +@MainActor struct EnvLoader { private static var envVars: [String: String] = [:] @@ -128,4 +129,4 @@ enum EnvError: LocalizedError { return "Missing required environment variable: \(key)" } } -} \ No newline at end of file +} diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/ContractsView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/ContractsView.swift index d502a428f0..4726b77023 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/ContractsView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/ContractsView.swift @@ -289,6 +289,7 @@ struct FetchContractView: View { } } + @MainActor private func fetchContract() async { guard let sdk = appState.sdk else { appState.showError(message: "SDK not initialized") @@ -299,7 +300,7 @@ struct FetchContractView: View { isLoading = true // In a real app, we would use the SDK's contract fetching functionality - if let contract = try await sdk.getDataContract(id: contractIdToFetch) { + if (try await sdk.getDataContract(id: contractIdToFetch)) != nil { // Convert SDK contract to our model // For now, we'll show a success message appState.showError(message: "Contract fetched successfully") @@ -313,4 +314,4 @@ struct FetchContractView: View { isLoading = false } } -} \ No newline at end of file +} diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DiagnosticsView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DiagnosticsView.swift index 79dd76e0b4..2f773d9c6b 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DiagnosticsView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DiagnosticsView.swift @@ -157,6 +157,7 @@ struct DiagnosticsView: View { } } + @MainActor private func runAllQueries() { guard let sdk = appState.platformState.sdk else { return } @@ -169,7 +170,7 @@ struct DiagnosticsView: View { var testResults: [QueryTestResult] = [] // Define all queries to test with categories - let queriesToTest: [(name: String, label: String, category: String, test: () async throws -> Any)] = [ + let queriesToTest: [(name: String, label: String, category: String, test: @MainActor () async throws -> Any)] = [ // Identity Queries (10 queries) ("getIdentity", "Get Identity", "Identity", { try await sdk.identityGet(identityId: TestData.testIdentityId) @@ -728,4 +729,4 @@ struct DiagnosticsView_Previews: PreviewProvider { .environmentObject(UnifiedAppState()) } } -} \ No newline at end of file +} diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DocumentFieldsView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DocumentFieldsView.swift index 05ff9fc896..840057ba5e 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DocumentFieldsView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DocumentFieldsView.swift @@ -267,7 +267,7 @@ extension DocumentFieldsView { .textFieldStyle(RoundedBorderTextFieldStyle()) .autocapitalization(.none) .disableAutocorrection(true) - .onChange(of: currentValue) { newValue in + .onChange(of: currentValue) { _, newValue in // Remove any non-hex characters and convert to lowercase let cleaned = newValue.lowercased().filter { "0123456789abcdef".contains($0) } if cleaned != newValue { diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DocumentWithPriceView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DocumentWithPriceView.swift index b7ee732965..807989e3e2 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DocumentWithPriceView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DocumentWithPriceView.swift @@ -22,9 +22,9 @@ struct DocumentWithPriceView: View { HStack { TextField("Enter document ID", text: $documentId) .textFieldStyle(RoundedBorderTextFieldStyle()) - .onChange(of: documentId) { newValue in - handleDocumentIdChange(newValue) - } + .modifier(DocumentIdChangeHandler(documentId: $documentId) { + handleDocumentIdChange($0) + }) if isLoading { ProgressView() @@ -330,9 +330,22 @@ struct DocumentWithPriceView: View { } } +// Cross-version onChange helper for documentId +private struct DocumentIdChangeHandler: ViewModifier { + @Binding var documentId: String + let onChange: (String) -> Void + func body(content: Content) -> some View { + if #available(iOS 17.0, *) { + content.onChange(of: documentId) { _, newValue in onChange(newValue) } + } else { + content.onChange(of: documentId) { newValue in onChange(newValue) } + } + } +} + // Extension to check if character is hex digit extension Character { var isHexDigit: Bool { return "0123456789abcdefABCDEF".contains(self) } -} \ No newline at end of file +} diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DocumentsView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DocumentsView.swift index 8da64f2842..0def855f11 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DocumentsView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DocumentsView.swift @@ -283,35 +283,30 @@ struct CreateDocumentView: View { } private func createDocument() async { - guard let sdk = appState.sdk, + guard appState.sdk != nil, let contract = selectedContract, !selectedDocumentType.isEmpty else { appState.showError(message: "Please select a contract and document type") return } - do { - isLoading = true - - // In a real app, we would use the SDK's document creation functionality - let document = DocumentModel( - id: UUID().uuidString, - contractId: contract.id, - documentType: selectedDocumentType, - ownerId: Data(hexString: selectedOwnerId) ?? Data(), - data: documentData, - createdAt: Date(), - updatedAt: Date() - ) - - appState.documents.append(document) - appState.showError(message: "Document created successfully") - - isLoading = false - } catch { - appState.showError(message: "Failed to create document: \(error.localizedDescription)") - isLoading = false - } + isLoading = true + + // In a real app, we would use the SDK's document creation functionality + let document = DocumentModel( + id: UUID().uuidString, + contractId: contract.id, + documentType: selectedDocumentType, + ownerId: Data(hexString: selectedOwnerId) ?? Data(), + data: documentData, + createdAt: Date(), + updatedAt: Date() + ) + + appState.documents.append(document) + appState.showError(message: "Document created successfully") + + isLoading = false } private func loadSampleContracts() { @@ -376,4 +371,4 @@ private let dateFormatter: DateFormatter = { formatter.dateStyle = .medium formatter.timeStyle = .short return formatter -}() \ No newline at end of file +}() diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DynamicDocumentFormView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DynamicDocumentFormView.swift index 2a13d0d5a9..7fa3da45b2 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DynamicDocumentFormView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DynamicDocumentFormView.swift @@ -33,11 +33,36 @@ struct DynamicDocumentFormView: View { .onAppear { parseSchema() } - .onChange(of: stringValues) { _ in updateDocumentData() } - .onChange(of: numberValues) { _ in updateDocumentData() } - .onChange(of: boolValues) { _ in updateDocumentData() } - .onChange(of: arrayValues) { _ in updateDocumentData() } + .modifier(DocumentFormChangeHandler(stringValues: $stringValues, + numberValues: $numberValues, + boolValues: $boolValues, + arrayValues: $arrayValues, + onChange: updateDocumentData)) } + +// Helper ViewModifier to use new onChange signatures on iOS 17+ while keeping compatibility +private struct DocumentFormChangeHandler: ViewModifier { + @Binding var stringValues: [String: String] + @Binding var numberValues: [String: Double] + @Binding var boolValues: [String: Bool] + @Binding var arrayValues: [String: [String]] + let onChange: () -> Void + func body(content: Content) -> some View { + if #available(iOS 17.0, *) { + content + .onChange(of: stringValues) { _, _ in onChange() } + .onChange(of: numberValues) { _, _ in onChange() } + .onChange(of: boolValues) { _, _ in onChange() } + .onChange(of: arrayValues) { _, _ in onChange() } + } else { + content + .onChange(of: stringValues) { _ in onChange() } + .onChange(of: numberValues) { _ in onChange() } + .onChange(of: boolValues) { _ in onChange() } + .onChange(of: arrayValues) { _ in onChange() } + } + } +} @ViewBuilder private func fieldView(for fieldName: String, schema: [String: Any]) -> some View { @@ -87,8 +112,6 @@ struct DynamicDocumentFormView: View { @ViewBuilder private func stringField(for fieldName: String, schema: [String: Any]) -> some View { let maxLength = schema["maxLength"] as? Int - let minLength = schema["minLength"] as? Int - let pattern = schema["pattern"] as? String let format = schema["format"] as? String let enumValues = schema["enum"] as? [String] @@ -189,7 +212,7 @@ struct DynamicDocumentFormView: View { .textFieldStyle(RoundedBorderTextFieldStyle()) .autocapitalization(.none) .disableAutocorrection(true) - .onChange(of: stringValues[fieldName] ?? "") { newValue in + .onChange(of: stringValues[fieldName] ?? "") { _, newValue in // Remove any non-hex characters and convert to lowercase let cleaned = newValue.lowercased().filter { "0123456789abcdef".contains($0) } if cleaned != newValue { @@ -245,7 +268,7 @@ struct DynamicDocumentFormView: View { if let properties = schema["properties"] as? [String: Any] { ForEach(Array(properties.keys.sorted()), id: \.self) { subFieldName in - if let subFieldSchema = properties[subFieldName] as? [String: Any] { + if properties[subFieldName] is [String: Any] { HStack { Text("• \(subFieldName)") .font(.caption) @@ -516,4 +539,4 @@ struct DynamicDocumentFormView_Previews: PreviewProvider { ) .padding() } -} \ No newline at end of file +} diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/IdentitiesView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/IdentitiesView.swift index 8a872d5c24..7359730917 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/IdentitiesView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/IdentitiesView.swift @@ -74,7 +74,7 @@ struct IdentitiesView: View { } private func refreshAllBalances() async { - guard let sdk = appState.sdk else { return } + guard appState.sdk != nil else { return } // Get all non-local identities let nonLocalIdentities = appState.identities.filter { !$0.isLocal } @@ -84,47 +84,47 @@ struct IdentitiesView: View { // Fetch each identity's balance and DPNS name await withTaskGroup(of: Void.self) { group in for identity in nonLocalIdentities { + // Capture only Sendable primitives for the concurrent task + let identityId = identity.id + let identityIdString = identity.idString + let needsDPNS = (identity.dpnsName == nil && identity.mainDpnsName == nil) + group.addTask { do { - // Fetch identity data - let fetchedIdentity = try await sdk.identityGet(identityId: identity.idString) - - // Update balance - if let balanceValue = fetchedIdentity["balance"] { - var newBalance: UInt64 = 0 - if let balanceNum = balanceValue as? NSNumber { - newBalance = balanceNum.uint64Value - } else if let balanceString = balanceValue as? String, - let balanceUInt = UInt64(balanceString) { - newBalance = balanceUInt - } - - await MainActor.run { - appState.updateIdentityBalance(id: identity.id, newBalance: newBalance) + // Perform SDK calls and state updates on the main actor + try await Task { @MainActor in + guard let sdk = appState.sdk else { return } + + let fetchedIdentity = try await sdk.identityGet(identityId: identityIdString) + + if let balanceValue = fetchedIdentity["balance"] { + let newBalanceLocal: UInt64 = { + if let balanceNum = balanceValue as? NSNumber { + return balanceNum.uint64Value + } else if let balanceString = balanceValue as? String, + let balanceUInt = UInt64(balanceString) { + return balanceUInt + } else { + return 0 + } + }() + appState.updateIdentityBalance(id: identityId, newBalance: newBalanceLocal) } - } - - // Also try to fetch DPNS name if we don't have one - if identity.dpnsName == nil && identity.mainDpnsName == nil { - do { - let usernames = try await sdk.dpnsGetUsername( - identityId: identity.idString, - limit: 1 - ) - - if let firstUsername = usernames.first, - let label = firstUsername["label"] as? String { - await MainActor.run { - appState.updateIdentityDPNSName(id: identity.id, dpnsName: label) + + if needsDPNS { + do { + let usernames = try await sdk.dpnsGetUsername(identityId: identityIdString, limit: 1) + if let firstUsername = usernames.first, + let label = firstUsername["label"] as? String { + appState.updateIdentityDPNSName(id: identityId, dpnsName: label) } + } catch { + // ignore } - } catch { - // Silently fail - not all identities have DPNS names } - } + }.value } catch { - // Log error but continue with other identities - print("Failed to refresh identity \(identity.idString): \(error)") + print("Failed to refresh identity \(identityIdString): \(error)") } } } @@ -285,4 +285,4 @@ struct IdentityRow: View { } } } -} \ No newline at end of file +} diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/IdentityDetailView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/IdentityDetailView.swift index 986686c761..d8a8da0db5 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/IdentityDetailView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/IdentityDetailView.swift @@ -350,13 +350,11 @@ struct IdentityDetailView: View { isLoadingDPNS = true defer { isLoadingDPNS = false } - guard let sdk = appState.sdk else { return } + guard appState.sdk != nil else { return } - // Fetch both regular and contested names in parallel - async let regularNamesTask = fetchRegularDPNSNames(identity: identity) - async let contestedNamesTask = fetchContestedDPNSNames(identity: identity) - - let (regular, contested) = await (regularNamesTask, contestedNamesTask) + // Fetch regular and contested names sequentially to avoid sending non-Sendable results across tasks + let regular = await fetchRegularDPNSNames(identity: identity) + let contested = await fetchContestedDPNSNames(identity: identity) await MainActor.run { let regularNames = regular.0 @@ -375,6 +373,7 @@ struct IdentityDetailView: View { } } + @MainActor private func fetchRegularDPNSNames(identity: IdentityModel) async -> ([String], [String: Any]) { guard let sdk = appState.sdk else { return ([], [:]) } @@ -393,6 +392,7 @@ struct IdentityDetailView: View { } } + @MainActor private func fetchContestedDPNSNames(identity: IdentityModel) async -> ([String], [String: Any]) { guard let sdk = appState.sdk else { return ([], [:]) } @@ -490,4 +490,4 @@ struct EditAliasView: View { dismiss() } -} \ No newline at end of file +} diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/KeyDetailView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/KeyDetailView.swift index fac366ceaa..4a47fdb373 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/KeyDetailView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/KeyDetailView.swift @@ -139,7 +139,6 @@ struct KeyDetailView: View { validationError = nil Task { - do { // Parse the private key input let trimmedInput = privateKeyInput.trimmingCharacters(in: .whitespacesAndNewlines) @@ -152,8 +151,8 @@ struct KeyDetailView: View { return } - // Get SDK instance - guard let sdk = appState.sdk else { + // Ensure SDK exists + guard appState.sdk != nil else { await MainActor.run { validationError = "SDK not initialized" isValidating = false @@ -198,12 +197,6 @@ struct KeyDetailView: View { isValidating = false } } - } catch { - await MainActor.run { - validationError = error.localizedDescription - isValidating = false - } - } } } @@ -251,4 +244,4 @@ struct KeyDetailView: View { dismiss() } } -} \ No newline at end of file +} diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/KeysListView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/KeysListView.swift index 30660d721f..9f3909ba25 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/KeysListView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/KeysListView.swift @@ -1,10 +1,10 @@ import SwiftUI import SwiftDashSDK -import SwiftDashSDK struct KeysListView: View { + struct IdentifiableInt: Identifiable { let id: Int } let identity: IdentityModel - @State private var showingPrivateKey: Int? = nil + @State private var showingPrivateKey: IdentifiableInt? = nil @State private var copiedKeyId: Int? = nil private var privateKeysAvailableCount: Int { @@ -22,7 +22,7 @@ struct KeysListView: View { // For keys with private keys, use a button instead of NavigationLink Button(action: { print("🔑 View Private button pressed for key \(publicKey.id)") - showingPrivateKey = Int(publicKey.id) + showingPrivateKey = IdentifiableInt(id: Int(publicKey.id)) }) { KeyRowView( publicKey: publicKey, @@ -58,7 +58,7 @@ struct KeysListView: View { .foregroundColor(.green) } - if let votingKey = identity.votingPrivateKey { + if identity.votingPrivateKey != nil { HStack { Label("Voting Key", systemImage: "hand.raised.fill") Spacer() @@ -67,7 +67,7 @@ struct KeysListView: View { } } - if let ownerKey = identity.ownerPrivateKey { + if identity.ownerPrivateKey != nil { HStack { Label("Owner Key", systemImage: "person.badge.key.fill") Spacer() @@ -80,10 +80,10 @@ struct KeysListView: View { .navigationTitle("Identity Keys") .navigationBarTitleDisplayMode(.inline) .sheet(item: $showingPrivateKey) { keyId in - let _ = print("🔑 Sheet presenting for keyId: \(keyId)") + let _ = print("🔑 Sheet presenting for keyId: \(keyId.id)") PrivateKeyView( identity: identity, - keyId: UInt32(keyId), + keyId: UInt32(keyId.id), onCopy: { keyId in copiedKeyId = keyId DispatchQueue.main.asyncAfter(deadline: .now() + 2) { @@ -413,7 +413,4 @@ struct CopiedToast: View { } -// Extension to make Int identifiable for sheet presentation -extension Int: Identifiable { - public var id: Int { self } -} \ No newline at end of file +// Int Identifiable workaround removed; using wrapper type instead diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/OptionsView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/OptionsView.swift index 0d547db1ce..4e74765df1 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/OptionsView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/OptionsView.swift @@ -38,6 +38,22 @@ struct OptionsView: View { .pickerStyle(SegmentedPickerStyle()) .disabled(isSwitchingNetwork) + Toggle("Use Local DAPI (Platform)", isOn: $appState.useLocalPlatform) + .onChange(of: appState.useLocalPlatform) { _, _ in + isSwitchingNetwork = true + Task { + await appState.switchNetwork(to: appState.currentNetwork) + await MainActor.run { isSwitchingNetwork = false } + } + } + .help("When enabled, Platform requests use local DAPI at 127.0.0.1:1443 (override via 'platformDAPIAddresses').") + + Toggle("Use Local Core (SPV)", isOn: $appState.useLocalCore) + .onChange(of: appState.useLocalCore) { _, _ in + // Core override will be applied when SPV peer overrides are supported + } + .help("When enabled, Core (SPV) connects only to configured peers (default 127.0.0.1 with network port). Override via 'corePeerAddresses'.") + HStack { Text("Network Status") Spacer() @@ -321,4 +337,3 @@ struct FeatureRow: View { } } } - diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/QueryDetailView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/QueryDetailView.swift index 51de9f197e..cbc268c80d 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/QueryDetailView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/QueryDetailView.swift @@ -459,24 +459,20 @@ struct QueryDetailView: View { // Query all contested resource votes by this identity, filtered for DPNS let dpnsContractId = "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec" - let result = try await sdk.getContestedResourceIdentityVotes( + let votes = try await sdk.getContestedResourceIdentityVotes( identityId: identityId, limit: limit, offset: 0, orderAscending: orderAscending ) - // Filter results to only show DPNS-related votes - if let votes = result as? [[String: Any]] { - let dpnsVotes = votes.filter { vote in - if let contractId = vote["contractId"] as? String { - return contractId == dpnsContractId - } - return false + let dpnsVotes = votes.filter { vote in + if let contractId = vote["contractId"] as? String { + return contractId == dpnsContractId } - return dpnsVotes + return false } - return result + return dpnsVotes case "getDpnsVotePollsByEndDate": let startDateStr = queryInputs["startDate"] ?? "" @@ -491,7 +487,7 @@ struct QueryDetailView: View { let endTimestamp: UInt64? = endDateStr.isEmpty ? nil : (dateFormatter.date(from: endDateStr)?.timeIntervalSince1970).map { UInt64($0 * 1000) } - let result = try await sdk.getVotePollsByEndDate( + let polls = try await sdk.getVotePollsByEndDate( startTimeMs: startTimestamp, endTimeMs: endTimestamp, limit: limit, @@ -501,16 +497,13 @@ struct QueryDetailView: View { // Filter to only DPNS-related polls let dpnsContractId = "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec" - if let polls = result as? [[String: Any]] { - let dpnsPolls = polls.filter { poll in - if let contractId = poll["contractId"] as? String { - return contractId == dpnsContractId - } - return false + let dpnsPolls = polls.filter { poll in + if let contractId = poll["contractId"] as? String { + return contractId == dpnsContractId } - return dpnsPolls + return false } - return result + return dpnsPolls // Voting & Contested Resources Queries case "getContestedResources": @@ -1165,4 +1158,3 @@ struct QueryInput { self.placeholder = placeholder } } - diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/RegisterNameView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/RegisterNameView.swift index 732d3d4081..a54c60c998 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/RegisterNameView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/RegisterNameView.swift @@ -151,7 +151,7 @@ struct RegisterNameView: View { .textContentType(.username) .autocapitalization(.none) .autocorrectionDisabled(true) - .onChange(of: username) { _ in + .modifier(UsernameChangeHandler(username: $username) { // Cancel any existing timer checkTimer?.invalidate() @@ -173,7 +173,7 @@ struct RegisterNameView: View { } } } - } + }) if !normalizedUsername.isEmpty { VStack(alignment: .leading, spacing: 4) { @@ -480,9 +480,9 @@ struct RegisterNameView: View { throw SDKError.internalError("Failed to create public key from data") } - let publicKeyOpaquePtr = OpaquePointer(publicKeyPtr) + let publicKeyTypedPtr = publicKeyPtr.assumingMemoryBound(to: IdentityPublicKeyHandle.self) defer { - dash_sdk_identity_public_key_destroy(publicKeyOpaquePtr) + dash_sdk_identity_public_key_destroy(publicKeyTypedPtr) } // Create signer from private key @@ -503,7 +503,7 @@ struct RegisterNameView: View { throw SDKError.internalError("Failed to create signer") } - let signerHandle = OpaquePointer(signerData) + let signerHandle = signerData.assumingMemoryBound(to: SignerHandle.self) defer { dash_sdk_signer_destroy(signerHandle) } @@ -514,7 +514,7 @@ struct RegisterNameView: View { handle, namePtr, UnsafeRawPointer(identityOpaquePtr), - UnsafeRawPointer(publicKeyOpaquePtr), + UnsafeRawPointer(publicKeyTypedPtr), UnsafeRawPointer(signerHandle) ) } @@ -632,6 +632,19 @@ struct RegisterNameView: View { } } } + +} + +private struct UsernameChangeHandler: ViewModifier { + @Binding var username: String + let onChange: () -> Void + func body(content: Content) -> some View { + if #available(iOS 17.0, *) { + content.onChange(of: username) { _, _ in onChange() } + } else { + content.onChange(of: username) { _ in onChange() } + } + } } // Preview diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/TokensView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/TokensView.swift index 2c2faf0b01..24e9d5d04d 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/TokensView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/TokensView.swift @@ -466,8 +466,8 @@ struct TokenActionDetailView: View { } private func performTokenAction() async { - guard let sdk = appState.sdk, - let identity = selectedIdentity else { + guard appState.sdk != nil, + selectedIdentity != nil else { appState.showError(message: "Please select an identity") return } @@ -590,4 +590,4 @@ struct EmptyStateView: View { } .padding() } -} \ No newline at end of file +} diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/TransitionDetailView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/TransitionDetailView.swift index 33ff0694a1..0e4141e5eb 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/TransitionDetailView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/TransitionDetailView.swift @@ -557,7 +557,7 @@ struct TransitionDetailView: View { private func executeIdentityTopUp(sdk: SDK) async throws -> Any { guard !selectedIdentityId.isEmpty, - let identity = appState.platformState.identities.first(where: { $0.idString == selectedIdentityId }) else { + appState.platformState.identities.contains(where: { $0.idString == selectedIdentityId }) else { throw SDKError.invalidParameter("No identity selected") } @@ -614,7 +614,7 @@ struct TransitionDetailView: View { } defer { - dash_sdk_signer_destroy(OpaquePointer(signer)!) + dash_sdk_signer_destroy(signer.assumingMemoryBound(to: SignerHandle.self)) } // Use the convenience method with DPPIdentity @@ -629,7 +629,7 @@ struct TransitionDetailView: View { from: dppIdentity, toIdentityId: normalizedToIdentityId, amount: amount, - signer: OpaquePointer(signer)! + signer: OpaquePointer(signer) ) // Update sender's balance in our local state @@ -695,7 +695,7 @@ struct TransitionDetailView: View { } defer { - dash_sdk_signer_destroy(OpaquePointer(signer)!) + dash_sdk_signer_destroy(signer.assumingMemoryBound(to: SignerHandle.self)) } // Use the DPPIdentity for withdrawal @@ -711,7 +711,7 @@ struct TransitionDetailView: View { amount: amount, toAddress: toAddress, coreFeePerByte: coreFeePerByte, - signer: OpaquePointer(signer)! + signer: OpaquePointer(signer) ) // Update identity's balance in our local state @@ -871,7 +871,7 @@ struct TransitionDetailView: View { } defer { - dash_sdk_signer_destroy(OpaquePointer(signer)!) + dash_sdk_signer_destroy(signer.assumingMemoryBound(to: SignerHandle.self)) } // Use the DPPIdentity for document creation @@ -887,7 +887,7 @@ struct TransitionDetailView: View { documentType: documentType, ownerIdentity: dppIdentity, properties: properties, - signer: OpaquePointer(signer)! + signer: OpaquePointer(signer) ) return result @@ -949,7 +949,7 @@ struct TransitionDetailView: View { } } - guard let signingKey = selectedKey, let keyData = privateKeyData else { + guard selectedKey != nil, let keyData = privateKeyData else { throw SDKError.invalidParameter("No suitable key with available private key found for signing") } @@ -967,7 +967,7 @@ struct TransitionDetailView: View { } defer { - dash_sdk_signer_destroy(OpaquePointer(signer)!) + dash_sdk_signer_destroy(signer.assumingMemoryBound(to: SignerHandle.self)) } // Call the document delete function @@ -976,7 +976,7 @@ struct TransitionDetailView: View { documentType: documentType, documentId: documentId, ownerIdentity: dppIdentity, - signer: OpaquePointer(signer)! + signer: OpaquePointer(signer) ) return ["message": "Document deleted successfully"] @@ -1068,7 +1068,7 @@ struct TransitionDetailView: View { } defer { - dash_sdk_signer_destroy(OpaquePointer(signer)!) + dash_sdk_signer_destroy(signer.assumingMemoryBound(to: SignerHandle.self)) } // Call the document transfer function @@ -1078,7 +1078,7 @@ struct TransitionDetailView: View { documentId: documentId, fromIdentity: fromIdentity, toIdentityId: recipientId, - signer: OpaquePointer(signer)! + signer: OpaquePointer(signer) ) return result @@ -1169,7 +1169,7 @@ struct TransitionDetailView: View { } defer { - dash_sdk_signer_destroy(OpaquePointer(signer)!) + dash_sdk_signer_destroy(signer.assumingMemoryBound(to: SignerHandle.self)) } // Call the document update price function @@ -1179,7 +1179,7 @@ struct TransitionDetailView: View { documentId: documentId, newPrice: newPrice, ownerIdentity: ownerDPPIdentity, - signer: OpaquePointer(signer)! + signer: OpaquePointer(signer) ) return result @@ -1254,7 +1254,7 @@ struct TransitionDetailView: View { } defer { - dash_sdk_signer_destroy(OpaquePointer(signer)!) + dash_sdk_signer_destroy(signer.assumingMemoryBound(to: SignerHandle.self)) } // Call the document purchase function @@ -1264,7 +1264,7 @@ struct TransitionDetailView: View { documentId: documentId, purchaserIdentity: fromIdentity, price: price, - signer: OpaquePointer(signer)! + signer: OpaquePointer(signer) ) return result @@ -1401,7 +1401,7 @@ struct TransitionDetailView: View { } defer { - dash_sdk_signer_destroy(OpaquePointer(signer)!) + dash_sdk_signer_destroy(signer.assumingMemoryBound(to: SignerHandle.self)) } // Use the DPPIdentity for document replacement @@ -1418,7 +1418,7 @@ struct TransitionDetailView: View { documentId: documentId, ownerIdentity: dppIdentity, properties: properties, - signer: OpaquePointer(signer)! + signer: OpaquePointer(signer) ) return result @@ -1507,7 +1507,7 @@ struct TransitionDetailView: View { } defer { - dash_sdk_signer_destroy(OpaquePointer(signer)!) + dash_sdk_signer_destroy(signer.assumingMemoryBound(to: SignerHandle.self)) } // Use the DPPIdentity for minting @@ -1526,7 +1526,7 @@ struct TransitionDetailView: View { amount: amount, ownerIdentity: dppIdentity, keyId: mintingKey.id, - signer: OpaquePointer(signer)!, + signer: OpaquePointer(signer), note: note ) @@ -1604,7 +1604,7 @@ struct TransitionDetailView: View { } defer { - dash_sdk_signer_destroy(OpaquePointer(signer)!) + dash_sdk_signer_destroy(signer.assumingMemoryBound(to: SignerHandle.self)) } // Use the DPPIdentity for burning @@ -1622,7 +1622,7 @@ struct TransitionDetailView: View { amount: amount, ownerIdentity: dppIdentity, keyId: burningKey.id, - signer: OpaquePointer(signer)!, + signer: OpaquePointer(signer), note: note ) @@ -1683,7 +1683,7 @@ struct TransitionDetailView: View { } defer { - dash_sdk_signer_destroy(OpaquePointer(signer)!) + dash_sdk_signer_destroy(signer.assumingMemoryBound(to: SignerHandle.self)) } // Use the DPPIdentity for freezing @@ -1701,7 +1701,7 @@ struct TransitionDetailView: View { targetIdentityId: targetIdentityId, ownerIdentity: dppIdentity, keyId: freezingKey.id, - signer: OpaquePointer(signer)!, + signer: OpaquePointer(signer), note: note ) @@ -1765,7 +1765,7 @@ struct TransitionDetailView: View { } defer { - dash_sdk_signer_destroy(OpaquePointer(signerHandle)) + dash_sdk_signer_destroy(signerHandle.assumingMemoryBound(to: SignerHandle.self)) } // Use the DPPIdentity for unfreezing @@ -1781,7 +1781,7 @@ struct TransitionDetailView: View { targetIdentityId: targetIdentityId, ownerIdentity: dppIdentity, keyId: unfreezingKey.id, - signer: OpaquePointer(signerHandle)!, + signer: OpaquePointer(signerHandle), note: formInputs["note"] ) @@ -1845,7 +1845,7 @@ struct TransitionDetailView: View { } defer { - dash_sdk_signer_destroy(OpaquePointer(signerHandle)) + dash_sdk_signer_destroy(signerHandle.assumingMemoryBound(to: SignerHandle.self)) } // Use the DPPIdentity for destroying frozen funds @@ -1861,7 +1861,7 @@ struct TransitionDetailView: View { frozenIdentityId: frozenIdentityId, ownerIdentity: dppIdentity, keyId: destroyKey.id, - signer: OpaquePointer(signerHandle)!, + signer: OpaquePointer(signerHandle), note: formInputs["note"] ) @@ -1921,7 +1921,7 @@ struct TransitionDetailView: View { } defer { - dash_sdk_signer_destroy(OpaquePointer(signer)!) + dash_sdk_signer_destroy(signer.assumingMemoryBound(to: SignerHandle.self)) } // Use the DPPIdentity for claiming @@ -1939,7 +1939,7 @@ struct TransitionDetailView: View { distributionType: distributionType, ownerIdentity: dppIdentity, keyId: claimingKey.id, - signer: OpaquePointer(signer)!, + signer: OpaquePointer(signer), note: note ) @@ -2020,7 +2020,7 @@ struct TransitionDetailView: View { } defer { - dash_sdk_signer_destroy(OpaquePointer(signer)!) + dash_sdk_signer_destroy(signer.assumingMemoryBound(to: SignerHandle.self)) } // Use the DPPIdentity for transfer @@ -2039,7 +2039,7 @@ struct TransitionDetailView: View { amount: amount, ownerIdentity: dppIdentity, keyId: transferKey.id, - signer: OpaquePointer(signer)!, + signer: OpaquePointer(signer), note: note ) @@ -2102,7 +2102,7 @@ struct TransitionDetailView: View { } defer { - dash_sdk_signer_destroy(OpaquePointer(signer)!) + dash_sdk_signer_destroy(signer.assumingMemoryBound(to: SignerHandle.self)) } // Use the DPPIdentity for setting price @@ -2121,7 +2121,7 @@ struct TransitionDetailView: View { priceData: priceData, ownerIdentity: dppIdentity, keyId: pricingKey.id, - signer: OpaquePointer(signer)!, + signer: OpaquePointer(signer), note: note ) @@ -2237,7 +2237,7 @@ struct TransitionDetailView: View { } defer { - dash_sdk_signer_destroy(OpaquePointer(signer)!) + dash_sdk_signer_destroy(signer.assumingMemoryBound(to: SignerHandle.self)) } // Use the DPPIdentity for contract creation @@ -2254,7 +2254,7 @@ struct TransitionDetailView: View { tokenSchemas: tokenSchemas, groups: groups, contractConfig: contractConfig, - signer: OpaquePointer(signer)! + signer: OpaquePointer(signer) ) return result @@ -2336,7 +2336,7 @@ struct TransitionDetailView: View { } defer { - dash_sdk_signer_destroy(OpaquePointer(signer)!) + dash_sdk_signer_destroy(signer.assumingMemoryBound(to: SignerHandle.self)) } // Use the DPPIdentity for contract update @@ -2353,7 +2353,7 @@ struct TransitionDetailView: View { newDocumentSchemas: newDocumentSchemas, newTokenSchemas: newTokenSchemas, newGroups: newGroups, - signer: OpaquePointer(signer)! + signer: OpaquePointer(signer) ) return result diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/TransitionInputView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/TransitionInputView.swift index 070f0cac34..12c4044e10 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/TransitionInputView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/TransitionInputView.swift @@ -27,8 +27,8 @@ struct TransitionInputView: View { } } } - } - +} + return results.sorted(by: { $0.token.displayName < $1.token.displayName }) } @@ -336,7 +336,7 @@ struct TransitionInputView: View { .frame(maxWidth: .infinity, alignment: .leading) .background(Color.gray.opacity(0.1)) .cornerRadius(8) - .onChange(of: value) { newValue in + .onChange(of: value) { _, newValue in selectedContractId = newValue // Notify parent to update related fields onSpecialAction("contractSelected:\(newValue)") @@ -425,7 +425,7 @@ struct TransitionInputView: View { .frame(maxWidth: .infinity, alignment: .leading) .background(Color.gray.opacity(0.1)) .cornerRadius(8) - .onChange(of: value) { newValue in + .onChange(of: value) { _, newValue in selectedDocumentType = newValue // Notify parent to update schema onSpecialAction("documentTypeSelected:\(newValue)") @@ -552,7 +552,7 @@ struct TransitionInputView: View { .frame(maxWidth: .infinity, alignment: .leading) .background(Color.gray.opacity(0.1)) .cornerRadius(8) - .onChange(of: value) { newValue in + .onChange(of: value) { _, newValue in if newValue == "__manual__" { value = "" useManualEntry = true @@ -601,4 +601,4 @@ struct TransitionInputView: View { ) .environmentObject(appState) } -} \ No newline at end of file +} diff --git a/packages/swift-sdk/SwiftTests/Package.swift b/packages/swift-sdk/SwiftTests/Package.swift index ac6689adf7..2500f1b1d8 100644 --- a/packages/swift-sdk/SwiftTests/Package.swift +++ b/packages/swift-sdk/SwiftTests/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.5 +// swift-tools-version: 6.0 import PackageDescription let package = Package( @@ -26,5 +26,6 @@ let package = Package( path: "Tests/SwiftDashSDKTests", exclude: ["*.o", "*.d", "*.swiftdeps"] ), - ] -) \ No newline at end of file + ], + swiftLanguageVersions: [.v6] +) diff --git a/packages/swift-sdk/build_ios.sh b/packages/swift-sdk/build_ios.sh new file mode 100755 index 0000000000..84454aa823 --- /dev/null +++ b/packages/swift-sdk/build_ios.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +REPO_ROOT="$( cd "$SCRIPT_DIR/../.." && pwd )" + +echo "=== SwiftDashSDK iOS Build (Unified) ===" + +echo "1) Building Rust FFI (rs-sdk-ffi)" +pushd "$REPO_ROOT/packages/rs-sdk-ffi" >/dev/null +if [[ ! -x ./build_ios.sh ]]; then + echo "❌ Missing rs-sdk-ffi/build_ios.sh" + exit 1 +fi +./build_ios.sh +popd >/dev/null + +# Expected output from rs-sdk-ffi +UNIFIED_DIR="$REPO_ROOT/packages/rs-sdk-ffi/build/DashUnifiedSDK.xcframework" +SDKFFI_DIR="$REPO_ROOT/packages/rs-sdk-ffi/build/DashSDKFFI.xcframework" +if [[ -d "$UNIFIED_DIR" ]]; then + SRC_XCFRAMEWORK_DIR="$UNIFIED_DIR" +elif [[ -d "$SDKFFI_DIR" ]]; then + SRC_XCFRAMEWORK_DIR="$SDKFFI_DIR" +else + echo "❌ rs-sdk-ffi build did not produce an XCFramework (expected DashUnifiedSDK.xcframework or DashSDKFFI.xcframework)" + exit 1 +fi + +echo "2) Installing XCFramework into Swift package" +DEST_XCFRAMEWORK_DIR="$SCRIPT_DIR/DashSDKFFI.xcframework" +rm -rf "$DEST_XCFRAMEWORK_DIR" +cp -R "$SRC_XCFRAMEWORK_DIR" "$DEST_XCFRAMEWORK_DIR" + +# Verify required SPV symbols are present in the binary +LIB_SIM_MAIN="$DEST_XCFRAMEWORK_DIR/ios-arm64-simulator/librs_sdk_ffi.a" +LIB_SIM_SPV="$DEST_XCFRAMEWORK_DIR/ios-arm64-simulator/libdash_spv_ffi.a" +if [[ ! -f "$LIB_SIM_MAIN" ]]; then + echo "❌ Missing simulator library at $LIB_SIM_MAIN" + exit 1 +fi +echo " - Verifying required SPV symbols are present in XCFramework libs" +# Prefer ripgrep if available; fall back to grep for portability +# Avoid -q with pipefail, which can cause nm to SIGPIPE and fail the check. +if command -v rg >/dev/null 2>&1; then + SEARCH_CMD=(rg -F) # fixed-string match +else + SEARCH_CMD=(grep -F) # fixed-string match +fi + +CHECK_OK=1 +if nm -gU "$LIB_SIM_MAIN" 2>/dev/null | "${SEARCH_CMD[@]}" "_dash_spv_ffi_config_add_peer" >/dev/null; then + : +elif [[ -f "$LIB_SIM_SPV" ]] && nm -gU "$LIB_SIM_SPV" 2>/dev/null | "${SEARCH_CMD[@]}" "_dash_spv_ffi_config_add_peer" >/dev/null; then + : +else + echo "❌ Missing symbol: dash_spv_ffi_config_add_peer (in both main and spv libs)" + CHECK_OK=0 +fi + +if nm -gU "$LIB_SIM_MAIN" 2>/dev/null | "${SEARCH_CMD[@]}" "_dash_spv_ffi_config_set_restrict_to_configured_peers" >/dev/null; then + : +elif [[ -f "$LIB_SIM_SPV" ]] && nm -gU "$LIB_SIM_SPV" 2>/dev/null | "${SEARCH_CMD[@]}" "_dash_spv_ffi_config_set_restrict_to_configured_peers" >/dev/null; then + : +else + echo "❌ Missing symbol: dash_spv_ffi_config_set_restrict_to_configured_peers (in both main and spv libs)" + CHECK_OK=0 +fi + +if [[ $CHECK_OK -ne 1 ]]; then + echo " Please ensure dash-spv-ffi exports these symbols and is included in the XCFramework." + exit 1 +fi + +echo "3) Verifying Swift builds (if Xcode available)" +if command -v xcodebuild >/dev/null 2>&1; then + set +e + xcodebuild -project "$SCRIPT_DIR/SwiftExampleApp/SwiftExampleApp.xcodeproj" \ + -scheme SwiftExampleApp \ + -sdk iphonesimulator \ + -destination 'platform=iOS Simulator,name=iPhone 16' \ + -quiet build + XC_STATUS=$? + set -e + if [[ $XC_STATUS -ne 0 ]]; then + echo "❌ Xcode build failed" + exit $XC_STATUS + fi + echo "✅ Xcode build succeeded" +else + echo "⚠️ xcodebuild not found; skipping local build. Run this script on a macOS host with Xcode to fully verify." +fi + +echo "✅ Done. XCFramework installed at $DEST_XCFRAMEWORK_DIR" diff --git a/packages/swift-sdk/verify_build.sh b/packages/swift-sdk/verify_build.sh index 322eee5ee5..b33ba59c79 100755 --- a/packages/swift-sdk/verify_build.sh +++ b/packages/swift-sdk/verify_build.sh @@ -1,59 +1,32 @@ -#!/bin/bash +#!/usr/bin/env bash +set -euo pipefail -# Build verification script for Swift SDK +echo "=== SwiftDashSDK Build Verification (iOS) ===" -echo "=== Swift SDK Build Verification ===" -echo - -# Step 1: Try to build the crate -echo "Step 1: Building Swift SDK..." -if cargo build -p swift-sdk 2>/dev/null; then - echo "✅ Build successful" -else - echo "❌ Build failed" - exit 1 -fi +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -# Step 2: Check if library was created -echo -echo "Step 2: Checking library output..." -if [ -f "../../target/debug/libswift_sdk.a" ] || [ -f "../../target/debug/libswift_sdk.dylib" ]; then - echo "✅ Library file created" -else - echo "❌ Library file not found" - exit 1 -fi +echo "Step 1: Build/Install XCFramework via unified script" +"$SCRIPT_DIR/build_ios.sh" -# Step 3: List exported symbols (on macOS/Linux) echo -echo "Step 3: Checking exported symbols..." -if command -v nm >/dev/null 2>&1; then - echo "Exported swift_dash_* functions:" - nm -g ../../target/debug/libswift_sdk.* 2>/dev/null | grep "swift_dash_" | head -10 - echo "... and more" +echo "Step 2: Verify Xcode build (if available)" +if command -v xcodebuild >/dev/null 2>&1; then + set +e + xcodebuild -project "$SCRIPT_DIR/SwiftExampleApp/SwiftExampleApp.xcodeproj" \ + -scheme SwiftExampleApp \ + -sdk iphonesimulator \ + -destination 'platform=iOS Simulator,name=iPhone 16' \ + -quiet build + XC_STATUS=$? + set -e + if [[ $XC_STATUS -ne 0 ]]; then + echo "❌ Xcode build failed" + exit $XC_STATUS + fi + echo "✅ Example app builds for iOS Simulator" else - echo "⚠️ 'nm' command not found, skipping symbol check" + echo "⚠️ xcodebuild not found; skipping local build. Run this on macOS with Xcode to verify." fi -# Step 4: Check header generation readiness -echo -echo "Step 4: Header generation readiness..." -if [ -f "cbindgen.toml" ]; then - echo "✅ cbindgen configuration found" -else - echo "❌ cbindgen.toml not found" -fi - -echo -echo "=== Verification Summary ===" -echo "The Swift SDK is ready for use in iOS projects!" -echo -echo "To generate C headers for Swift:" -echo " cargo install cbindgen" -echo " cbindgen -c cbindgen.toml -o SwiftDashSDK.h" echo -echo "To use in iOS project:" -echo " 1. Build with: cargo build --release -p swift-sdk" -echo " 2. Add the .a file to your Xcode project" -echo " 3. Import the generated header in your Swift bridging header" -echo " 4. Call functions from Swift!" \ No newline at end of file +echo "✅ Verification complete" diff --git a/scripts/dash_core_version_switcher.py b/scripts/dash_core_version_switcher.py index 07af316d67..3880604d82 100644 --- a/scripts/dash_core_version_switcher.py +++ b/scripts/dash_core_version_switcher.py @@ -239,7 +239,11 @@ def main(): val = sha resolved = (branch, sha) - repo_root = os.getcwd() + # Prefer git repo root to avoid depending on CWD + try: + repo_root = subprocess.check_output(["git", "rev-parse", "--show-toplevel"], text=True).strip() + except Exception: + repo_root = os.getcwd() edited = [] for cargo in find_cargo_tomls(repo_root): if process_file(cargo, mode, val):