diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9e2203dae..24699114d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,10 @@ name: CI +# Unless we are on the main branch, the workflow should stop and yield to a new run if new code is pushed. +concurrency: + group: ${{ github.workflow }}-${{ github.ref == 'refs/heads/main' && github.sha || github.ref }} + cancel-in-progress: ${{ !contains(github.ref, 'main')}} + on: push: branches: [main] @@ -56,6 +61,41 @@ jobs: - name: Run Swift foreign binding tests run: ./swift/test_swift.sh + - name: Install SwiftLint + run: | + brew install swiftlint + + - name: Lint Swift Tests + run: swiftlint swift/tests + + kotlin-build-and-test: + name: Kotlin Build & Foreign Binding Tests + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Rust + uses: dtolnay/rust-toolchain@master + with: + toolchain: 1.92.0 + + - name: Build and test Kotlin bindings + run: ./kotlin/test_kotlin.sh + + - name: Install ktlint + run: | + curl -sSLO https://github.com/pinterest/ktlint/releases/latest/download/ktlint && + chmod a+x ktlint && + sudo mv ktlint /usr/local/bin/ + + - name: Lint Kotlin Tests + run: | + ktlint kotlin/walletkit-tests/src/test/kotlin + test: name: Tests runs-on: ubuntu-latest @@ -112,7 +152,7 @@ jobs: - uses: EmbarkStudios/cargo-deny-action@v2 with: command: check ${{ matrix.checks }} - rust-version: stable + rust-version: 1.92.0 docs: name: Check docs diff --git a/.github/workflows/initiate-release.yml b/.github/workflows/initiate-release.yml index 57ac4a5d9..70dcc3f26 100644 --- a/.github/workflows/initiate-release.yml +++ b/.github/workflows/initiate-release.yml @@ -29,8 +29,8 @@ jobs: env: BUMP_TYPE: ${{ github.event.inputs.bump_type }} run: | - # Get current version from Cargo.toml - CURRENT_VERSION=$(grep -m 1 'version = ' Cargo.toml | cut -d '"' -f 2) + # Get current version from workspace package in Cargo.toml + CURRENT_VERSION=$(cargo metadata --no-deps --format-version 1 | jq -r '.workspace_members[0]' | cut -d '#' -f2) # Ensure CURRENT_VERSION is in semantic versioning format if [[ ! "$CURRENT_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then @@ -38,6 +38,8 @@ jobs: exit 1 fi + cargo metadata --no-deps --format-version 1 | jq -r '.workspace_members' + # Split version into components IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fab6c1a84..ccd43e9dc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -68,11 +68,11 @@ jobs: components: rustfmt - name: Build the project (iOS) - run: ./build_swift.sh + run: ./swift/build_swift.sh - name: Compress XCFramework binary run: | - zip -r WalletKit.xcframework.zip WalletKit.xcframework + zip -r WalletKit.xcframework.zip swift/WalletKit.xcframework - name: Checkout swift repo uses: actions/checkout@v4 @@ -112,11 +112,11 @@ jobs: run: | # Copy non-binary source files - cp -r Sources/ target-repo/Sources + cp -r swift/Sources/ target-repo/Sources # Prepare Package.swift brew install swiftlint - ./archive_swift.sh --asset-url "$ASSET_URL" --checksum "$CHECKSUM" --release-version "$NEW_VERSION" + ./swift/archive_swift.sh --asset-url "$ASSET_URL" --checksum "$CHECKSUM" --release-version "$NEW_VERSION" cp Package.swift target-repo/ # Commit changes @@ -210,7 +210,7 @@ jobs: - name: Move artifacts run: | - mkdir -p kotlin/lib/src/main/jniLibs && cd kotlin/lib/src/main/jniLibs + mkdir -p kotlin/walletkit/src/main/jniLibs && cd kotlin/walletkit/src/main/jniLibs mkdir armeabi-v7a arm64-v8a x86 x86_64 mv /home/runner/work/walletkit/walletkit/android-armv7-linux-androideabi/libwalletkit.so ./armeabi-v7a/libwalletkit.so mv /home/runner/work/walletkit/walletkit/android-aarch64-linux-android/libwalletkit.so ./arm64-v8a/libwalletkit.so @@ -219,11 +219,11 @@ jobs: - name: Generate bindings working-directory: kotlin - run: cargo run -p uniffi-bindgen generate ./lib/src/main/jniLibs/arm64-v8a/libwalletkit.so --library --language kotlin --no-format --out-dir lib/src/main/java + run: cargo run -p uniffi-bindgen generate ./walletkit/src/main/jniLibs/arm64-v8a/libwalletkit.so --library --language kotlin --no-format --out-dir walletkit/src/main/java - name: Publish working-directory: kotlin - run: ./gradlew lib:publish -PversionName=${{ needs.pre-release-checks.outputs.new_version }} + run: ./gradlew walletkit:publish env: GITHUB_ACTOR: wld-walletkit-bot GITHUB_TOKEN: ${{ github.token }} @@ -243,16 +243,13 @@ jobs: make_latest: true - name: Create Release in swift repo - uses: softprops/action-gh-release@v2 - with: - repository: worldcoin/walletkit-swift - token: ${{ secrets.WALLETKIT_BOT_TOKEN }} - name: ${{ needs.pre-release-checks.outputs.new_version }} - tag_name: ${{ needs.pre-release-checks.outputs.new_version }} - body: | - ## Version ${{ needs.pre-release-checks.outputs.new_version }} - For full release notes, see the [main repo release](https://github.com/worldcoin/walletkit/releases/tag/${{ needs.pre-release-checks.outputs.new_version }}). - make_latest: true + env: + GH_TOKEN: ${{ secrets.WALLETKIT_BOT_TOKEN }} + run: | + gh release edit ${{ needs.pre-release-checks.outputs.new_version }} \ + --repo worldcoin/walletkit-swift \ + --draft=false \ + --latest publish-to-crates-io: needs: [pre-release-checks, create-github-release] diff --git a/.gitignore b/.gitignore index a59439236..e00b85c21 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,17 @@ target/ # Swift build outputs are not committed to this repo. WalletKit.xcframework/ Sources/ +swift/WalletKit.xcframework/ +swift/Sources/ +swift/ios_build/ +swift/local_build/ +swift/tests/Sources/ +swift/tests/.build/ +# Kotlin bindings and native libs +kotlin/libs/ +kotlin/walletkit/src/main/java/uniffi/ +kotlin/walletkit-tests/build/ .build/ .env @@ -21,4 +31,8 @@ Sources/ cache/ **/out/build-info +# Allow storage cache module sources. +!walletkit-core/src/storage/cache/ +!walletkit-core/src/storage/cache/** + # NOTE: Cargo.lock is not ignored because it is used for FFI builds (Swift & Kotlin) diff --git a/README.md b/README.md index b137f4d6a..9751b242e 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ WalletKit's bindings for Kotlin are distributed through GitHub packages. ```kotlin dependencies { /// ... - implementation "org.world:walletkit-android:VERSION" + implementation "org.world:walletkit:VERSION" } ``` @@ -70,7 +70,7 @@ To test local changes before publishing a release, use the build script to compi This will: 1. Build the Rust library for all Android architectures (arm64-v8a, armeabi-v7a, x86_64, x86) 2. Generate Kotlin UniFFI bindings -3. Publish to `~/.m2/repository/org/world/walletkit-android/` +3. Publish to `~/.m2/repository/org/world/walletkit/` In your consuming project, ensure `mavenLocal()` is included in your repositories and update your dependency version to the SNAPSHOT version (e.g., `0.3.1-SNAPSHOT`). diff --git a/build_android_local.sh b/build_android_local.sh index 7398387fc..7eb4cdbc4 100755 --- a/build_android_local.sh +++ b/build_android_local.sh @@ -25,11 +25,11 @@ cd kotlin # Publish to Maven Local echo "Publishing to Maven Local..." -./gradlew :lib:publishToMavenLocal -PversionName="$VERSION" +./gradlew :walletkit:publishToMavenLocal -PversionName="$VERSION" echo "" echo "โœ… Successfully published $VERSION to Maven Local!" -echo "Published to: ~/.m2/repository/org/world/walletkit-android/$VERSION/" +echo "Published to: ~/.m2/repository/org/world/walletkit/$VERSION/" echo "" echo "To use in your project:" -echo " implementation 'org.world:walletkit-android:$VERSION'" +echo " implementation 'org.world:walletkit:$VERSION'" diff --git a/kotlin/README.md b/kotlin/README.md new file mode 100644 index 000000000..9585b3158 --- /dev/null +++ b/kotlin/README.md @@ -0,0 +1,26 @@ +# Kotlin for WalletKit + +This folder contains support files for WalletKit to work in Kotlin: + +1. Script to build Kotlin/JNA bindings. +2. Foreign tests (JUnit) for Kotlin in the `walletkit-tests` module. + +## Building the Kotlin project + +```bash + # run from the walletkit directory + ./kotlin/build_kotlin.sh +``` + +## Running foreign tests for Kotlin + +```bash + # run from the walletkit directory + ./kotlin/test_kotlin.sh +``` + +## Kotlin project structure + +The Kotlin project has two members: +- `walletkit`: The main WalletKit library with UniFFI bindings for Kotlin. +- `walletkit-tests`: Unit tests to assert the Kotlin bindings behave as intended (foreign tests). diff --git a/kotlin/build.sh b/kotlin/build.sh index ecbc71b37..779431c94 100755 --- a/kotlin/build.sh +++ b/kotlin/build.sh @@ -4,7 +4,7 @@ set -e echo "Building WalletKit Android SDK..." # Create jniLibs directories -mkdir -p ./lib/src/main/jniLibs/{arm64-v8a,armeabi-v7a,x86_64,x86} +mkdir -p ./walletkit/src/main/jniLibs/{arm64-v8a,armeabi-v7a,x86_64,x86} # Build for all Android architectures echo "Building for aarch64-linux-android..." @@ -21,18 +21,18 @@ cross build -p walletkit --release --target=i686-linux-android --features v4 # Move .so files to jniLibs echo "Moving native libraries..." -mv ../target/aarch64-linux-android/release/libwalletkit.so ./lib/src/main/jniLibs/arm64-v8a/libwalletkit.so -mv ../target/armv7-linux-androideabi/release/libwalletkit.so ./lib/src/main/jniLibs/armeabi-v7a/libwalletkit.so -mv ../target/x86_64-linux-android/release/libwalletkit.so ./lib/src/main/jniLibs/x86_64/libwalletkit.so -mv ../target/i686-linux-android/release/libwalletkit.so ./lib/src/main/jniLibs/x86/libwalletkit.so +mv ../target/aarch64-linux-android/release/libwalletkit.so ./walletkit/src/main/jniLibs/arm64-v8a/libwalletkit.so +mv ../target/armv7-linux-androideabi/release/libwalletkit.so ./walletkit/src/main/jniLibs/armeabi-v7a/libwalletkit.so +mv ../target/x86_64-linux-android/release/libwalletkit.so ./walletkit/src/main/jniLibs/x86_64/libwalletkit.so +mv ../target/i686-linux-android/release/libwalletkit.so ./walletkit/src/main/jniLibs/x86/libwalletkit.so # Generate Kotlin bindings echo "Generating Kotlin bindings..." cargo run -p uniffi-bindgen generate \ - ./lib/src/main/jniLibs/arm64-v8a/libwalletkit.so \ + ./walletkit/src/main/jniLibs/arm64-v8a/libwalletkit.so \ --library \ --language kotlin \ --no-format \ - --out-dir lib/src/main/java + --out-dir walletkit/src/main/java echo "โœ… Build complete!" diff --git a/kotlin/build_kotlin.sh b/kotlin/build_kotlin.sh new file mode 100755 index 000000000..bd517cfff --- /dev/null +++ b/kotlin/build_kotlin.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Creates Kotlin/JNA bindings for the `walletkit` library. +# This mirrors the Bedrock Kotlin build flow. + +PROJECT_ROOT_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +KOTLIN_DIR="$PROJECT_ROOT_PATH/kotlin" +JAVA_SRC_DIR="$KOTLIN_DIR/walletkit/src/main/java" +LIBS_DIR="$KOTLIN_DIR/libs" + +# Clean previous artifacts +rm -rf "$JAVA_SRC_DIR" "$LIBS_DIR" +mkdir -p "$JAVA_SRC_DIR" "$LIBS_DIR" + +echo "๐ŸŸข Building Rust cdylib for host platform" +cargo build --package walletkit --release + +# Determine the correct library file extension and copy it +if [[ "$OSTYPE" == "darwin"* ]]; then + LIB_FILE="$PROJECT_ROOT_PATH/target/release/libwalletkit.dylib" + cp "$LIB_FILE" "$LIBS_DIR/" + echo "๐Ÿ“ฆ Copied libwalletkit.dylib for macOS" +elif [[ "$OSTYPE" == "linux-gnu"* ]]; then + LIB_FILE="$PROJECT_ROOT_PATH/target/release/libwalletkit.so" + cp "$LIB_FILE" "$LIBS_DIR/" + echo "๐Ÿ“ฆ Copied libwalletkit.so for Linux" +else + echo "โŒ Unsupported OS: $OSTYPE" + exit 1 +fi + +echo "๐ŸŸก Generating Kotlin bindings via uniffi-bindgen" +cargo run -p uniffi-bindgen -- generate \ + "$LIB_FILE" \ + --language kotlin \ + --library \ + --crate walletkit_core \ + --out-dir "$JAVA_SRC_DIR" + +echo "โœ… Kotlin bindings written to $JAVA_SRC_DIR" diff --git a/kotlin/lib/build.gradle.kts b/kotlin/lib/build.gradle.kts index eddfce6f9..734522be3 100644 --- a/kotlin/lib/build.gradle.kts +++ b/kotlin/lib/build.gradle.kts @@ -46,7 +46,7 @@ afterEvaluate { publications { create("maven") { groupId = "org.world" - artifactId = "walletkit-android" + artifactId = "walletkit" version = if (project.hasProperty("versionName")) { project.property("versionName") as String diff --git a/kotlin/settings.gradle.kts b/kotlin/settings.gradle.kts index 73668b1b0..e3860c3bb 100644 --- a/kotlin/settings.gradle.kts +++ b/kotlin/settings.gradle.kts @@ -14,6 +14,7 @@ pluginManagement { plugins { id("com.android.library") version "8.3.0" id("org.jetbrains.kotlin.android") version "1.9.22" + id("org.jetbrains.kotlin.jvm") version "1.9.22" } } @@ -25,4 +26,5 @@ dependencyResolutionManagement { } rootProject.name = "walletkit" -include("lib") +include("walletkit") +include("walletkit-tests") diff --git a/kotlin/test_kotlin.sh b/kotlin/test_kotlin.sh new file mode 100755 index 000000000..cfa043579 --- /dev/null +++ b/kotlin/test_kotlin.sh @@ -0,0 +1,113 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "=========================================" +echo "Running Kotlin/JVM Tests" +echo "=========================================" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +BLUE='\033[0;34m' +YELLOW='\033[0;33m' +NC='\033[0m' # No Color + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +TEST_RESULTS_DIR="$ROOT_DIR/kotlin/walletkit-tests/build/test-results/test" +rm -rf "$TEST_RESULTS_DIR" + +cd "$ROOT_DIR" + +# Set JAVA_HOME if not already set (for CI environments) +if [ -z "${JAVA_HOME:-}" ]; then + if [ -d "/opt/homebrew/Cellar/openjdk@17" ]; then + # macOS with Homebrew - find latest 17.x version + LATEST_JDK=$(ls -v /opt/homebrew/Cellar/openjdk@17 | grep "^17\." | tail -n 1) + if [ -n "$LATEST_JDK" ]; then + export JAVA_HOME="/opt/homebrew/Cellar/openjdk@17/$LATEST_JDK/libexec/openjdk.jdk/Contents/Home" + echo -e "${BLUE}๐Ÿ”ง Set JAVA_HOME to: $JAVA_HOME${NC}" + else + echo -e "${YELLOW}โš ๏ธ No OpenJDK 17.x found in Homebrew${NC}" + fi + elif command -v java >/dev/null 2>&1; then + JAVA_PATH=$(which java) + export JAVA_HOME=$(dirname $(dirname $(readlink -f $JAVA_PATH))) + echo -e "${BLUE}๐Ÿ”ง Detected JAVA_HOME: $JAVA_HOME${NC}" + else + echo -e "${YELLOW}โš ๏ธ JAVA_HOME not set and Java not found in PATH${NC}" + fi +fi + +echo -e "${BLUE}๐Ÿ”จ Step 1: Building Kotlin bindings with build_kotlin.sh${NC}" +"$ROOT_DIR/kotlin/build_kotlin.sh" + +echo -e "${GREEN}โœ… Kotlin bindings built${NC}" + +echo -e "${BLUE}๐Ÿ“ฆ Step 2: Setting up Gradle test environment${NC}" +cd "$ROOT_DIR/kotlin" + +# Generate Gradle wrapper if missing +if [ ! -f "gradlew" ]; then + echo "Gradle wrapper missing, generating..." + GRADLE_VERSION="${GRADLE_VERSION:-8.14.3}" + DIST_URL="https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip" + TMP_DIR="$(mktemp -d)" + ZIP_PATH="$TMP_DIR/gradle-${GRADLE_VERSION}.zip" + UNZIP_DIR="$TMP_DIR/unzip" + + echo "Downloading Gradle ${GRADLE_VERSION}..." + curl -sSL "$DIST_URL" -o "$ZIP_PATH" + mkdir -p "$UNZIP_DIR" + if command -v unzip >/dev/null 2>&1; then + unzip -q "$ZIP_PATH" -d "$UNZIP_DIR" + else + (cd "$UNZIP_DIR" && jar xvf "$ZIP_PATH" >/dev/null) + fi + + echo "Bootstrapping wrapper with Gradle ${GRADLE_VERSION}..." + "$UNZIP_DIR/gradle-${GRADLE_VERSION}/bin/gradle" wrapper --gradle-version "$GRADLE_VERSION" + + rm -rf "$TMP_DIR" +fi +echo -e "${GREEN}โœ… Gradle test environment ready${NC}" + +echo "" +echo -e "${BLUE}๐Ÿงช Step 3: Running Kotlin tests with verbose output...${NC}" +echo "" + +./gradlew --no-daemon walletkit-tests:test --info --continue + +echo "" +echo "๐Ÿ“Š Test Results Summary:" +echo "========================" + +if [ -d "$TEST_RESULTS_DIR" ]; then + echo "โœ… Test results found in: $TEST_RESULTS_DIR" + TOTAL_TESTS=$(find "$TEST_RESULTS_DIR" -name "*.xml" -exec grep -l "testcase" {} \; | wc -l | tr -d ' ') + if [ "$TOTAL_TESTS" -gt 0 ]; then + echo "๐Ÿ“‹ Total test files: $TOTAL_TESTS" + PASSED=$(find "$TEST_RESULTS_DIR" -name "*.xml" -exec grep -o "tests=\"[0-9]*\"" {} \; | cut -d'"' -f2 | awk '{sum+=$1} END {print sum+0}') + FAILURES=$(find "$TEST_RESULTS_DIR" -name "*.xml" -exec grep -o "failures=\"[0-9]*\"" {} \; | cut -d'"' -f2 | awk '{sum+=$1} END {print sum+0}') + ERRORS=$(find "$TEST_RESULTS_DIR" -name "*.xml" -exec grep -o "errors=\"[0-9]*\"" {} \; | cut -d'"' -f2 | awk '{sum+=$1} END {print sum+0}') + + echo "โœ… Tests passed: $PASSED" + echo "โŒ Tests failed: $FAILURES" + echo "โš ๏ธ Test errors: $ERRORS" + + if [ "$FAILURES" -gt 0 ] || [ "$ERRORS" -gt 0 ]; then + echo "" + echo -e "${YELLOW}โš ๏ธ Some tests failed${NC}" + exit 1 + else + echo "" + echo -e "${GREEN}๐ŸŽ‰ All tests passed!${NC}" + exit 0 + fi + fi +else + echo "โš ๏ธ No test results found" + echo "" + echo -e "${RED}โœ— Could not determine test results${NC}" + exit 1 +fi diff --git a/kotlin/walletkit-tests/build.gradle.kts b/kotlin/walletkit-tests/build.gradle.kts new file mode 100644 index 000000000..06c9fce69 --- /dev/null +++ b/kotlin/walletkit-tests/build.gradle.kts @@ -0,0 +1,31 @@ +// This build.gradle uses a JVM-only testing engine for unit testing. +// Note this is separate from the build.gradle used for building and publishing the actual library. + +plugins { + kotlin("jvm") +} + +repositories { + mavenCentral() +} + +dependencies { + implementation("net.java.dev.jna:jna:5.13.0") + testImplementation("org.jetbrains.kotlin:kotlin-test") + testImplementation("junit:junit:4.13.2") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3") +} + +sourceSets { + test { + kotlin.srcDirs( + "$rootDir/walletkit/src/main/java/uniffi/walletkit_core" + ) + } +} + +tasks.test { + useJUnit() + systemProperty("jna.library.path", "${rootDir}/libs") + reports.html.required.set(false) +} diff --git a/kotlin/walletkit-tests/src/test/kotlin/org/world/walletkit/SimpleTest.kt b/kotlin/walletkit-tests/src/test/kotlin/org/world/walletkit/SimpleTest.kt new file mode 100644 index 000000000..735329300 --- /dev/null +++ b/kotlin/walletkit-tests/src/test/kotlin/org/world/walletkit/SimpleTest.kt @@ -0,0 +1,13 @@ +package org.world.walletkit + +import java.io.File +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class SimpleTest { + @Test + fun simpleTest() { + assertEquals(1, 1) + } +} diff --git a/kotlin/walletkit/build.gradle.kts b/kotlin/walletkit/build.gradle.kts new file mode 100644 index 000000000..0df6f72ce --- /dev/null +++ b/kotlin/walletkit/build.gradle.kts @@ -0,0 +1,80 @@ +import java.io.ByteArrayOutputStream + +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") + id("maven-publish") +} + +kotlin { + jvmToolchain(17) +} + +android { + namespace = "org.world.walletkit" + compileSdk = 33 + + defaultConfig { + minSdk = 23 + @Suppress("deprecation") + targetSdk = 33 + consumerProguardFiles("consumer-rules.pro") + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = "17" + } + + publishing { + singleVariant("release") { + withSourcesJar() + } + } +} + +afterEvaluate { + publishing { + publications { + create("maven") { + groupId = "org.world" + artifactId = "walletkit" + + // Read version from Cargo.toml + val cargoToml = file("../../Cargo.toml") + val versionRegex = """version\s*=\s*"([^"]+)"""".toRegex() + val cargoContent = cargoToml.readText() + version = versionRegex.find(cargoContent)?.groupValues?.get(1) + ?: throw GradleException("Could not find version in Cargo.toml") + + afterEvaluate { + from(components["release"]) + } + } + } + + repositories { + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/worldcoin/walletkit") + credentials { + username = System.getenv("GITHUB_ACTOR") + password = System.getenv("GITHUB_TOKEN") + } + } + } + } +} + +dependencies { + // UniFFI requires JNA for native calls + implementation("net.java.dev.jna:jna:5.13.0") + implementation("androidx.core:core-ktx:1.8.0") + implementation("androidx.appcompat:appcompat:1.4.1") + implementation("com.google.android.material:material:1.5.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") +} diff --git a/kotlin/walletkit/consumer-rules.pro b/kotlin/walletkit/consumer-rules.pro new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/kotlin/walletkit/consumer-rules.pro @@ -0,0 +1 @@ + diff --git a/swift/README.md b/swift/README.md new file mode 100644 index 000000000..8f0e979b9 --- /dev/null +++ b/swift/README.md @@ -0,0 +1,57 @@ +# Swift for WalletKit + +This folder contains Swift support files for WalletKit: + +1. Script to cross-compile and build Swift bindings. +2. Script to build a Swift package for local development. +3. Foreign tests (XCTest suite) for Swift under `tests/`. + +## Building the Swift bindings + +To build the Swift project for release/distribution: + +```bash + # run from the walletkit directory + ./swift/build_swift.sh +``` + +## Testing WalletKit locally + +To build a Swift package that can be imported locally via Swift Package Manager: + +```bash + # run from the walletkit directory + ./swift/local_swift.sh +``` + +This creates a complete Swift package in `swift/local_build/` that you can import in your iOS project. + +## Integration via Package.swift + +Add the local package to your Package.swift dependencies: + +```swift +dependencies: [ + .package(name: "WalletKit", path: "../../../walletkit/swift/local_build"), + // ... other dependencies +], +``` + +Then add it to specific targets that need WalletKit functionality: + +```swift +.target( + name: "YourTarget", + dependencies: [ + .product(name: "WalletKit", package: "WalletKit"), + // ... other dependencies + ] +), +``` + +## Running foreign tests for Swift + +```bash + # run from the walletkit directory + ./swift/test_swift.sh +``` diff --git a/swift/archive_swift.sh b/swift/archive_swift.sh new file mode 100755 index 000000000..6a1351577 --- /dev/null +++ b/swift/archive_swift.sh @@ -0,0 +1,95 @@ +#!/bin/bash +set -e + +# Creates the dynamic Package.swift file for release. +# Usage: ./archive_swift.sh --asset-url --checksum --release-version + +# Initialize variables +ASSET_URL="" +CHECKSUM="" +RELEASE_VERSION="" + +# Function to show usage +show_usage() { + echo "โŒ Error: Missing required arguments" + echo "Usage: $0 --asset-url --checksum --release-version " + echo "" + echo "Example:" + echo " $0 --asset-url 'https://github.com/user/repo/releases/download/v1.0.0/WalletKit.xcframework.zip' --checksum 'abc123def456...' --release-version '1.0.0'" + exit 1 +} + +# Parse command line arguments +while [[ $# -gt 0 ]]; do + case $1 in + --asset-url) + ASSET_URL="$2" + shift 2 + ;; + --checksum) + CHECKSUM="$2" + shift 2 + ;; + --release-version) + RELEASE_VERSION="$2" + shift 2 + ;; + -h|--help) + show_usage + ;; + *) + echo "โŒ Unknown argument: $1" + show_usage + ;; + esac +done + +# Check if all required arguments are provided +if [ -z "$ASSET_URL" ] || [ -z "$CHECKSUM" ] || [ -z "$RELEASE_VERSION" ]; then + echo "โŒ Error: All arguments are required" + show_usage +fi + +echo "๐Ÿ”ง Creating Package.swift with:" +echo " Asset URL: $ASSET_URL" +echo " Checksum: $CHECKSUM" +echo " Release Version: $RELEASE_VERSION" +echo "" + +cat > Package.swift << EOF +// swift-tools-version: 5.7 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +// Release version: $RELEASE_VERSION + +import PackageDescription + +let package = Package( + name: "WalletKit", + platforms: [ + .iOS(.v13) + ], + products: [ + .library( + name: "WalletKit", + targets: ["WalletKit"]), + ], + targets: [ + .target( + name: "WalletKit", + dependencies: ["walletkit_coreFFI"], + path: "Sources/WalletKit" + ), + .binaryTarget( + name: "walletkit_coreFFI", + url: "$ASSET_URL", + checksum: "$CHECKSUM" + ) + ] +) +EOF + +swiftlint lint --autocorrect Package.swift + +echo "" +echo "โœ… Package.swift built successfully for version $RELEASE_VERSION!" diff --git a/swift/build_swift.sh b/swift/build_swift.sh new file mode 100755 index 000000000..90d5c32ad --- /dev/null +++ b/swift/build_swift.sh @@ -0,0 +1,127 @@ +#!/bin/bash +set -e + +# Creates a Swift build of the `WalletKit` library. +# This script can be used directly or called by other scripts. +# +# Usage: build_swift.sh [OUTPUT_DIR] +# OUTPUT_DIR: Directory where the XCFramework should be placed (default: swift/) + +PROJECT_ROOT_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +BASE_PATH="$PROJECT_ROOT_PATH/swift" # The base path for the Swift build +PACKAGE_NAME="walletkit" +TARGET_DIR="$PROJECT_ROOT_PATH/target" +FEATURES="v4" +SUPPORT_SOURCES_DIR="$BASE_PATH/support" + +# Default values +OUTPUT_DIR="$BASE_PATH" # Default to BASE_PATH if not provided +FRAMEWORK="WalletKit.xcframework" + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + --help|-h) + echo "Usage: $0 [OUTPUT_DIR]" + echo "" + echo "Arguments:" + echo " OUTPUT_DIR Directory where the XCFramework should be placed (default: swift/)" + echo "" + exit 0 + ;; + *) + # Assume it's the output directory if it doesn't start with -- + if [[ ! "$1" =~ ^-- ]]; then + OUTPUT_DIR="$1" + else + echo "Unknown option: $1" + echo "Use --help for usage information" + exit 1 + fi + shift + ;; + esac +done + +# Resolve OUTPUT_DIR to absolute path if it's relative +if [[ "$OUTPUT_DIR" != /* ]]; then + OUTPUT_DIR="$BASE_PATH/$OUTPUT_DIR" +fi + +SWIFT_SOURCES_DIR="$OUTPUT_DIR/Sources/WalletKit" +SWIFT_HEADERS_DIR="$BASE_PATH/ios_build/Headers/WalletKit" +FRAMEWORK_OUTPUT="$OUTPUT_DIR/$FRAMEWORK" + +echo "Building $FRAMEWORK to $FRAMEWORK_OUTPUT" + +# Clean up previous builds +rm -rf "$BASE_PATH/ios_build" +rm -rf "$FRAMEWORK_OUTPUT" + +# Create necessary directories +mkdir -p "$BASE_PATH/ios_build/bindings" +mkdir -p "$BASE_PATH/ios_build/target/universal-ios-sim/release" +mkdir -p "$SWIFT_SOURCES_DIR" +mkdir -p "$SWIFT_HEADERS_DIR" + +echo "Building Rust packages for iOS targets..." + +export IPHONEOS_DEPLOYMENT_TARGET="13.0" +export RUSTFLAGS="-C link-arg=-Wl,-application_extension" + +# Build for all iOS targets +cargo build --package $PACKAGE_NAME --target aarch64-apple-ios-sim --release \ + --manifest-path "$PROJECT_ROOT_PATH/Cargo.toml" --target-dir "$TARGET_DIR" \ + --features "$FEATURES" +cargo build --package $PACKAGE_NAME --target aarch64-apple-ios --release \ + --manifest-path "$PROJECT_ROOT_PATH/Cargo.toml" --target-dir "$TARGET_DIR" \ + --features "$FEATURES" +cargo build --package $PACKAGE_NAME --target x86_64-apple-ios --release \ + --manifest-path "$PROJECT_ROOT_PATH/Cargo.toml" --target-dir "$TARGET_DIR" \ + --features "$FEATURES" + +echo "Rust packages built. Combining simulator targets into universal binary..." + +# Create universal binary for simulators +lipo -create "$TARGET_DIR/aarch64-apple-ios-sim/release/lib${PACKAGE_NAME}.a" \ + "$TARGET_DIR/x86_64-apple-ios/release/lib${PACKAGE_NAME}.a" \ + -output $BASE_PATH/ios_build/target/universal-ios-sim/release/lib${PACKAGE_NAME}.a + +lipo -info $BASE_PATH/ios_build/target/universal-ios-sim/release/lib${PACKAGE_NAME}.a + +echo "Generating Swift bindings..." + +# Generate Swift bindings using uniffi +cargo run -p uniffi-bindgen --manifest-path "$PROJECT_ROOT_PATH/Cargo.toml" \ + --target-dir "$TARGET_DIR" -- generate \ + "$TARGET_DIR/aarch64-apple-ios-sim/release/lib${PACKAGE_NAME}.dylib" \ + --library \ + --crate walletkit_core \ + --language swift \ + --no-format \ + --out-dir $BASE_PATH/ios_build/bindings + +# Move generated Swift file to Sources directory +mv $BASE_PATH/ios_build/bindings/walletkit_core.swift ${SWIFT_SOURCES_DIR}/walletkit.swift + +# Copy support Swift sources for the WalletKit module. +if [ -d "$SUPPORT_SOURCES_DIR" ]; then + rsync -a "$SUPPORT_SOURCES_DIR"/ "$SWIFT_SOURCES_DIR"/ +fi + +# Move headers +mv $BASE_PATH/ios_build/bindings/walletkit_coreFFI.h $SWIFT_HEADERS_DIR/ +cat $BASE_PATH/ios_build/bindings/walletkit_coreFFI.modulemap > $SWIFT_HEADERS_DIR/module.modulemap + +echo "Creating XCFramework..." + +# Create XCFramework +xcodebuild -create-xcframework \ + -library "$TARGET_DIR/aarch64-apple-ios/release/lib${PACKAGE_NAME}.a" -headers $BASE_PATH/ios_build/Headers \ + -library $BASE_PATH/ios_build/target/universal-ios-sim/release/lib${PACKAGE_NAME}.a -headers $BASE_PATH/ios_build/Headers \ + -output $FRAMEWORK_OUTPUT + +# Clean up intermediate build files +rm -rf $BASE_PATH/ios_build + +echo "โœ… Swift framework built successfully at: $FRAMEWORK_OUTPUT" diff --git a/swift/local_swift.sh b/swift/local_swift.sh new file mode 100755 index 000000000..11b74dcab --- /dev/null +++ b/swift/local_swift.sh @@ -0,0 +1,71 @@ +#!/bin/bash +set -e + +# Creates a Swift package of the `WalletKit` library for local development. +# This script builds the library and sets up the proper structure for importing +# via Swift Package Manager using a local file:// URL. +# All artifacts are placed in swift/local_build to keep the repo clean. + +PROJECT_ROOT_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +BASE_PATH="$PROJECT_ROOT_PATH/swift" # The base path for the Swift build +LOCAL_BUILD_PATH="$BASE_PATH/local_build" # Local build artifacts directory +FRAMEWORK="WalletKit.xcframework" + +echo "Building $FRAMEWORK for local iOS development" + +# Clean up previous builds +rm -rf "$LOCAL_BUILD_PATH" + +# Create the local build directory +mkdir -p "$LOCAL_BUILD_PATH" + +echo "Running core Swift build..." + +# Call the main build script with local build directory +bash "$BASE_PATH/build_swift.sh" "$LOCAL_BUILD_PATH" + +echo "Creating Package.swift for local development..." + +# Create Package.swift for local development +cat > $LOCAL_BUILD_PATH/Package.swift << EOF +// swift-tools-version: 5.7 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "WalletKit", + platforms: [ + .iOS(.v13) + ], + products: [ + .library( + name: "WalletKit", + targets: ["WalletKit"]), + ], + targets: [ + .target( + name: "WalletKit", + dependencies: ["walletkit_coreFFI"], + path: "Sources/WalletKit" + ), + .binaryTarget( + name: "walletkit_coreFFI", + path: "WalletKit.xcframework" + ) + ] +) +EOF + +echo "" +echo "โœ… Swift package built successfully!" +echo "" +echo "๐Ÿ“ฆ Package location: $LOCAL_BUILD_PATH" +echo "" +echo "To use this package in your iOS app:" +echo "1. In Xcode, go to File โ†’ Add Package Dependencies..." +echo "2. Click 'Add Local...' and select the local_build directory: $LOCAL_BUILD_PATH" +echo "3. Or add it to your Package.swift dependencies:" +echo " .package(path: \"$LOCAL_BUILD_PATH\")" +echo "" +echo "The package exports the 'WalletKit' library that you can import in your Swift code." diff --git a/swift/tests/Package.swift b/swift/tests/Package.swift index 419c6b7ec..b042303ff 100644 --- a/swift/tests/Package.swift +++ b/swift/tests/Package.swift @@ -6,7 +6,7 @@ let package = Package( name: "WalletKitForeignTestPackage", platforms: [ .iOS(.v13), - .macOS(.v12), + .macOS(.v12) ], products: [ .library( @@ -27,6 +27,6 @@ let package = Package( name: "WalletKitTests", dependencies: ["WalletKit"], path: "WalletKitTests" - ), + ) ] ) diff --git a/swift/tests/WalletKitTests/AuthenticatorTests.swift b/swift/tests/WalletKitTests/AuthenticatorTests.swift index 411e42e63..c370cdb05 100644 --- a/swift/tests/WalletKitTests/AuthenticatorTests.swift +++ b/swift/tests/WalletKitTests/AuthenticatorTests.swift @@ -7,10 +7,16 @@ final class AuthenticatorTests: XCTestCase { // MARK: - Helper Functions + struct U256HexTestCase { + let hexInput: String + let expectedDecimal: String + let expectedHex: String + } + func generateRandomSeed() -> Data { var bytes = [UInt8](repeating: 0, count: 32) - for i in 0..<32 { - bytes[i] = UInt8.random(in: 0...255) + for index in 0..<32 { + bytes[index] = UInt8.random(in: 0...255) } return Data(bytes) } @@ -60,33 +66,41 @@ final class AuthenticatorTests: XCTestCase { func testU256WrapperDeterministicHexParsing() throws { // Test with known values from Rust tests - let testCases: [(String, String, String)] = [ - ( - "0x0000000000000000000000000000000000000000000000000000000000000001", - "1", - "0x0000000000000000000000000000000000000000000000000000000000000001" - ), - ( - "0x000000000000000000000000000000000000000000000000000000000000002a", - "42", - "0x000000000000000000000000000000000000000000000000000000000000002a" + let testCases: [U256HexTestCase] = [ + U256HexTestCase( + hexInput: "0x0000000000000000000000000000000000000000000000000000000000000001", + expectedDecimal: "1", + expectedHex: "0x0000000000000000000000000000000000000000000000000000000000000001" ), - ( - "0x00000000000000000000000000000000000000000000000000000000000f423f", - "999999", - "0x00000000000000000000000000000000000000000000000000000000000f423f" + U256HexTestCase( + hexInput: "0x000000000000000000000000000000000000000000000000000000000000002a", + expectedDecimal: "42", + expectedHex: "0x000000000000000000000000000000000000000000000000000000000000002a" ), - ( - "0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6", - "80084422859880547211683076133703299733277748156566366325829078699459944778998", - "0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6" + U256HexTestCase( + hexInput: "0x00000000000000000000000000000000000000000000000000000000000f423f", + expectedDecimal: "999999", + expectedHex: "0x00000000000000000000000000000000000000000000000000000000000f423f" ), + U256HexTestCase( + hexInput: "0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6", + expectedDecimal: "80084422859880547211683076133703299733277748156566366325829078699459944778998", + expectedHex: "0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6" + ) ] - for (hexInput, expectedDecimal, expectedHex) in testCases { - let u256 = try U256Wrapper.tryFromHexString(hexString: hexInput) - XCTAssertEqual(u256.toDecimalString(), expectedDecimal, "Decimal mismatch for \(hexInput)") - XCTAssertEqual(u256.toHexString(), expectedHex, "Hex mismatch for \(hexInput)") + for testCase in testCases { + let u256 = try U256Wrapper.tryFromHexString(hexString: testCase.hexInput) + XCTAssertEqual( + u256.toDecimalString(), + testCase.expectedDecimal, + "Decimal mismatch for \(testCase.hexInput)" + ) + XCTAssertEqual( + u256.toHexString(), + testCase.expectedHex, + "Hex mismatch for \(testCase.hexInput)" + ) } } @@ -96,7 +110,7 @@ final class AuthenticatorTests: XCTestCase { "0x0000000000000000000000000000000000000000000000000000000000000001", "0x00000000000000000000000000000000000000000000000000000000000000ff", "0x0000000000000000000000000000000000000000000000000000000000001234", - "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" ] for hexString in hexStrings { @@ -117,7 +131,7 @@ final class AuthenticatorTests: XCTestCase { "0xZZZZ", "1g", "not a hex string", - "0xGGGG", + "0xGGGG" ] for invalidInput in invalidInputs { @@ -180,7 +194,7 @@ final class AuthenticatorTests: XCTestCase { let testCases: [(UInt64, String)] = [ (1, "0x0000000000000000000000000000000000000000000000000000000000000001"), (2, "0x0000000000000000000000000000000000000000000000000000000000000002"), - (255, "0x00000000000000000000000000000000000000000000000000000000000000ff"), + (255, "0x00000000000000000000000000000000000000000000000000000000000000ff") ] for (value, expectedHex) in testCases {