diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..9c1e381
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,6 @@
+android/
+cpp/
+ios/
+node_modules/
+rust_modules/
+src/
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 0000000..dc82d4a
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,68 @@
+name: 🐛 Bug report
+description: Report a reproducible bug or regression in this library.
+labels: [bug]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ # Bug report
+
+ 👋 Hi!
+
+ **Please fill the following carefully before opening a new issue ❗**
+ *(Your issue may be closed if it doesn't provide the required pieces of information)*
+ - type: checkboxes
+ attributes:
+ label: Before submitting a new issue
+ description: Please perform simple checks first.
+ options:
+ - label: I tested using the latest version of the library, as the bug might be already fixed.
+ required: true
+ - label: I tested using a [supported version](https://github.com/reactwg/react-native-releases/blob/main/docs/support.md) of react native.
+ required: true
+ - label: I checked for possible duplicate issues, with possible answers.
+ required: true
+ - type: textarea
+ id: summary
+ attributes:
+ label: Bug summary
+ description: |
+ Provide a clear and concise description of what the bug is.
+ If needed, you can also provide other samples: error messages / stack traces, screenshots, gifs, etc.
+ validations:
+ required: true
+ - type: input
+ id: library-version
+ attributes:
+ label: Library version
+ description: What version of the library are you using?
+ placeholder: "x.x.x"
+ validations:
+ required: true
+ - type: textarea
+ id: react-native-info
+ attributes:
+ label: Environment info
+ description: Run `react-native info` in your terminal and paste the results here.
+ render: shell
+ validations:
+ required: true
+ - type: textarea
+ id: steps-to-reproduce
+ attributes:
+ label: Steps to reproduce
+ description: |
+ You must provide a clear list of steps and code to reproduce the problem.
+ value: |
+ 1. …
+ 2. …
+ validations:
+ required: true
+ - type: input
+ id: reproducible-example
+ attributes:
+ label: Reproducible example repository
+ description: Please provide a link to a repository on GitHub with a reproducible example.
+ render: js
+ validations:
+ required: true
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..45c655b
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,8 @@
+blank_issues_enabled: false
+contact_links:
+ - name: Feature Request 💡
+ url: https://github.com/Blockstream/lwk-rn/discussions/new?category=ideas
+ about: If you have a feature request, please create a new discussion on GitHub.
+ - name: Discussions on GitHub 💬
+ url: https://github.com/Blockstream/lwk-rn/discussions
+ about: If this library works as promised but you need help, please ask questions there.
diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml
index fb98c79..066f4f5 100644
--- a/.github/actions/setup/action.yml
+++ b/.github/actions/setup/action.yml
@@ -5,13 +5,13 @@ runs:
using: composite
steps:
- name: Setup Node.js
- uses: actions/setup-node@v3
+ uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
- - name: Cache dependencies
+ - name: Restore dependencies
id: yarn-cache
- uses: actions/cache@v3
+ uses: actions/cache/restore@v4
with:
path: |
**/node_modules
@@ -25,3 +25,12 @@ runs:
if: steps.yarn-cache.outputs.cache-hit != 'true'
run: yarn install --immutable
shell: bash
+
+ - name: Cache dependencies
+ if: steps.yarn-cache.outputs.cache-hit != 'true'
+ uses: actions/cache/save@v4
+ with:
+ path: |
+ **/node_modules
+ .yarn/install-state.gz
+ key: ${{ steps.yarn-cache.outputs.cache-primary-key }}
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a19a7d8..0d7e75f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,4 +1,4 @@
-name: CI
+name: CI Build
on:
push:
branches:
@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Setup
uses: ./.github/actions/setup
@@ -30,7 +30,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Setup
uses: ./.github/actions/setup
@@ -42,7 +42,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Setup
uses: ./.github/actions/setup
@@ -56,13 +56,13 @@ jobs:
TURBO_CACHE_DIR: .turbo/android
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Setup
uses: ./.github/actions/setup
- name: Cache turborepo for Android
- uses: actions/cache@v3
+ uses: actions/cache@v4
with:
path: ${{ env.TURBO_CACHE_DIR }}
key: ${{ runner.os }}-turborepo-android-${{ hashFiles('yarn.lock') }}
@@ -79,7 +79,7 @@ jobs:
- name: Install JDK
if: env.turbo_cache_hit != 1
- uses: actions/setup-java@v3
+ uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
@@ -91,7 +91,7 @@ jobs:
- name: Cache Gradle
if: env.turbo_cache_hit != 1
- uses: actions/cache@v3
+ uses: actions/cache@v4
with:
path: |
~/.gradle/wrapper
@@ -103,24 +103,22 @@ jobs:
- name: Build example for Android
env:
JAVA_OPTS: "-XX:MaxHeapSize=6g"
- PAT_USER: ${{ secrets.PAT_USER }}
- PAT_TOKEN: ${{ secrets.PAT_TOKEN }}
run: |
yarn turbo run build:android --cache-dir="${{ env.TURBO_CACHE_DIR }}"
build-ios:
- runs-on: macos-14
+ runs-on: macos-latest
env:
TURBO_CACHE_DIR: .turbo/ios
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Setup
uses: ./.github/actions/setup
- name: Cache turborepo for iOS
- uses: actions/cache@v3
+ uses: actions/cache@v4
with:
path: ${{ env.TURBO_CACHE_DIR }}
key: ${{ runner.os }}-turborepo-ios-${{ hashFiles('yarn.lock') }}
@@ -135,10 +133,10 @@ jobs:
echo "turbo_cache_hit=1" >> $GITHUB_ENV
fi
- - name: Cache cocoapods
+ - name: Restore cocoapods
if: env.turbo_cache_hit != 1
id: cocoapods-cache
- uses: actions/cache@v3
+ uses: actions/cache/restore@v4
with:
path: |
**/ios/Pods
@@ -147,13 +145,21 @@ jobs:
${{ runner.os }}-cocoapods-
- name: Install cocoapods
- #if: env.turbo_cache_hit != 1 && steps.cocoapods-cache.outputs.cache-hit != 'true'
+ if: env.turbo_cache_hit != 1 && steps.cocoapods-cache.outputs.cache-hit != 'true'
run: |
cd example/ios
pod install
env:
NO_FLIPPER: 1
+ - name: Cache cocoapods
+ if: env.turbo_cache_hit != 1 && steps.cocoapods-cache.outputs.cache-hit != 'true'
+ uses: actions/cache/save@v4
+ with:
+ path: |
+ **/ios/Pods
+ key: ${{ steps.cocoapods-cache.outputs.cache-key }}
+
- name: Build example for iOS
run: |
yarn turbo run build:ios --cache-dir="${{ env.TURBO_CACHE_DIR }}"
diff --git a/.github/workflows/generate.yml b/.github/workflows/generate.yml
new file mode 100644
index 0000000..24254a1
--- /dev/null
+++ b/.github/workflows/generate.yml
@@ -0,0 +1,65 @@
+name: CI Generate
+on:
+ workflow_dispatch:
+
+jobs:
+ build-android:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup
+ uses: ./.github/actions/setup
+
+ - name: Setup rust toolchain
+ uses: dtolnay/rust-toolchain@1.81.0
+ with:
+ targets: aarch64-linux-android,armv7-linux-androideabi,i686-linux-android,x86_64-linux-android
+
+ - name: Install NDK
+ run: cargo install cargo-ndk
+
+ - name: Fetch LWK
+ run: sh fetch_lwk.sh
+
+ - name: Generate android lib
+ run: yarn ubrn:android --profile release-smaller
+
+ - name: Temporarily save artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: lwk-android-artifact
+ path: android
+ retention-days: 1
+ build-ios:
+ runs-on: macos-14
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup
+ uses: ./.github/actions/setup
+
+ - name: Setup rust toolchain
+ uses: dtolnay/rust-toolchain@1.81.0
+ with:
+ targets: x86_64-apple-ios,aarch64-apple-ios,aarch64-apple-ios-sim
+
+ - name: Fetch LWK
+ run: sh fetch_lwk.sh
+
+ - name: Generate ios lib
+ run: yarn ubrn:ios --profile release-smaller
+
+ - name: Strip ios lib
+ run: |
+ strip LwkRnFramework.xcframework/ios-arm64/liblwk.a |
+ strip LwkRnFramework.xcframework/ios-arm64-simulator/liblwk.a
+
+ - name: Temporarily save artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: lwk-ios-artifact
+ path: LwkRnFramework.xcframework
+ retention-days: 1
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index cfe2bb4..a23406b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,6 +28,7 @@ DerivedData
*.ipa
*.xcuserstate
project.xcworkspace
+**/.xcode.env.local
# Android/IJ
#
@@ -80,3 +81,10 @@ lib/
# React Native Codegen
ios/generated
android/generated
+
+# React Native Nitro Modules
+nitrogen/
+
+# From uniffi-bindgen-react-native
+rust_modules/
+*.a
diff --git a/.nvmrc b/.nvmrc
index 3f430af..9a2a0e2 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-v18
+v20
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 6445658..c47edab 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -47,6 +47,14 @@ To run the example app on iOS:
yarn example ios
```
+To confirm that the app is running with the new architecture, you can check the Metro logs for a message like this:
+
+```sh
+Running "LwkRnExample" with {"fabric":true,"initialProps":{"concurrentRoot":true},"rootTag":1}
+```
+
+Note the `"fabric":true` and `"concurrentRoot":true` properties.
+
Make sure your code passes TypeScript and ESLint. Run the following to verify:
```sh
diff --git a/LICENSE b/LICENSE
index f87ae25..d16f7b9 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2024 Luca Vaccaro
+Copyright (c) 2025 lvaccaro
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
diff --git a/LwkRn.podspec b/LwkRn.podspec
new file mode 100644
index 0000000..26c9bbd
--- /dev/null
+++ b/LwkRn.podspec
@@ -0,0 +1,41 @@
+require "json"
+
+package = JSON.parse(File.read(File.join(__dir__, "package.json")))
+folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32'
+
+Pod::Spec.new do |s|
+ s.name = "LwkRn"
+ s.version = package["version"]
+ s.summary = package["description"]
+ s.homepage = package["homepage"]
+ s.license = package["license"]
+ s.authors = package["author"]
+
+ s.platforms = { :ios => min_ios_version_supported }
+ s.source = { :git => "https://github.com/Blockstream/lwk-rn.git", :tag => "#{s.version}" }
+
+ s.source_files = "ios/**/*.{h,m,mm}", "cpp/**/*.{hpp,cpp,c,h}"
+
+ # Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0.
+ # See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79.
+ if respond_to?(:install_modules_dependencies, true)
+ install_modules_dependencies(s)
+ else
+ s.dependency "React-Core"
+
+ # Don't install the dependencies when we run `pod install` in the old architecture.
+ if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then
+ s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1"
+ s.pod_target_xcconfig = {
+ "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"",
+ "OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1",
+ "CLANG_CXX_LANGUAGE_STANDARD" => "c++17"
+ }
+ s.dependency "React-Codegen"
+ s.dependency "RCT-Folly"
+ s.dependency "RCTRequired"
+ s.dependency "RCTTypeSafety"
+ s.dependency "ReactCommon/turbomodule/core"
+ end
+ end
+end
diff --git a/LwkRnFramework.xcframework/Info.plist b/LwkRnFramework.xcframework/Info.plist
new file mode 100644
index 0000000..943fcff
--- /dev/null
+++ b/LwkRnFramework.xcframework/Info.plist
@@ -0,0 +1,43 @@
+
+
+
+
+ AvailableLibraries
+
+
+ BinaryPath
+ liblwk.a
+ LibraryIdentifier
+ ios-arm64
+ LibraryPath
+ liblwk.a
+ SupportedArchitectures
+
+ arm64
+
+ SupportedPlatform
+ ios
+
+
+ BinaryPath
+ liblwk.a
+ LibraryIdentifier
+ ios-arm64-simulator
+ LibraryPath
+ liblwk.a
+ SupportedArchitectures
+
+ arm64
+
+ SupportedPlatform
+ ios
+ SupportedPlatformVariant
+ simulator
+
+
+ CFBundlePackageType
+ XFWK
+ XCFrameworkFormatVersion
+ 1.0
+
+
diff --git a/README.md b/README.md
index 75b1d86..adbfcfb 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
**LWK-rn** is a React Native module for [Liquid Wallet Kit](https://github.com/Blockstream/lwk). Its goal is to provide all the necessary building blocks for mobile development of a liquid wallet.
-** NOTE: LWK and LWK-rn is in public beta and still undergoing significant development. Use it at your own risk. **
+**NOTE: LWK and LWK-rn is in public beta and still undergoing significant development. Use it at your own risk.**
_Please consider reviewing, experimenting and contributing_
@@ -22,76 +22,135 @@ Using yarn:
yarn add lwk-rn
```
-[iOS Only] Install pods:
+Note: Use android sdk version >= 24 and iOS >= v13 .
-```bash
-npx pod-install
-or
-cd ios && pod install
-```
-
-Note: Use android sdk version >= 23 and iOS >= v13 .
+## Usage
-### Examples
+Import LWK-rn library
-You could run the example in iOS or android by the following
-```sh
-$ yarn example ios
-...
-$ yarn example android
+```js
+import { Mnemonic, Network, Signer, Wollet } from 'lwk-rn';
```
-Create a Wallet and sync with Electrum client
-
+Create a signer for a mnemonic and a network
```js
-import { Wollet, Client, Signer, Network } from 'lwk-rn';
-
-const mnemonic =
- 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
-const network = Network.Testnet;
-const signer = await new Signer().create(mnemonic, network);
-const descriptor = await signer.wpkhSlip77Descriptor();
-console.log(await descriptor.asString());
-
-const wollet = await new Wollet().create(network, descriptor, null);
-const client = await new Client().defaultElectrumClient(network);
-const update = await client.fullScan(wollet);
-await wollet.applyUpdate(update);
+let mnemonic = new Mnemonic("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about");
+let network = Network.testnet();
+let signer = new Signer(mnemonic, network);
+console.log(mnemonic.toString());
```
-Get a new address:
+Create and update a wollet from the signer descriptor
```js
-const address = await wollet.getAddress();
-console.log(address);
+let singlesigDesc = signer.wpkhSlip77Descriptor();
+let wollet = new Wollet(network, singlesigDesc, undefined);
+let client = network.defaultElectrumClient();
+let update = client.fullScan(wollet);
+if (update) {
+ wollet.applyUpdate(update);
+}
```
-Get a transaction list:
+Get a new unused address
```js
-const transactions = await wollet.getTransactions();
-console.log(transactions);
+let latest_address = wollet.address(undefined);
+console.log(latest_address.address().scriptPubkey().toString());
```
-Get balance as `[AssetId : UInt64]`:
+Get balance as `[AssetId : UInt64]`
```js
-const balance = await wollet.getBalance();
+let balance = wollet.balance();
console.log(balance);
+for (var b of balance.entries()) {
+ console.log("asset: ", b[0], ", value: ", b[1]);
+}
```
-Build, sign and broadcast a Transaction:
+Get a transaction list
```js
- const out_address = await wollet.getAddress().description;
- const satoshis = 900;
- const fee_rate = 280; // this is the sat/vB * 100 fee rate. Example 280 would equal a fee rate of .28 sat/vB. 100 would equal .1 sat/vB
- const builder = await new TxBuilder().create(network);
- await builder.addLbtcRecipient(out_address, satoshis);
- await builder.feeRate(fee_rate);
- let pset = await builder.finish(wollet);
- let signed_pset = await signer.sign(pset);
- let finalized_pset = await wollet.finalize(signed_pset);
- const tx = await finalized_pset.extractTx();
- await client.broadcast(tx);
- console.log("BROADCASTED TX!\nTXID: {:?}", (await tx.txId.toString()));
+let txs = wollet.transactions();
+console.log(txs);
+for (var tx of txs) {
+ for (var output of tx.outputs()) {
+ let script_pubkey = output?.scriptPubkey().toString();
+ let value = output?.unblinded().value().toString();
+ console.log("script_pubkey: ", script_pubkey, ", value: ", value);
+ }
+}
+```
+
+## Build
+
+LWK-rn repository contains the pre-generated lwk bindings for android and ios.
+
+Follow the steps to generate bindings by your own:
+
+Install C++ tooling
+
+```sh
+# For MacOS, using homebrew:
+brew install cmake ninja clang-format
+# For Debian flavoured Linux:
+apt-get install cmake ninja clang-format
+```
+
+Add the Android specific targets
+```sh
+rustup target add \
+ aarch64-linux-android \
+ armv7-linux-androideabi \
+ i686-linux-android \
+ x86_64-linux-android
+# Install cargo-ndk
+cargo install cargo-ndk
+```
+
+Add the iOS specific targets
+```sh
+rustup target add \
+ aarch64-apple-ios \
+ aarch64-apple-ios-sim \
+ x86_64-apple-ios
+# Ensure xcodebuild is available
+xcode-select --install
```
+
+Install deps and `uniffi-bindgen-react-native`.
+
+```sh
+$ yarn install
+```
+
+Fetch LWK library with some hacks.
+> The script changes path to avoid using workspace configuration and rust version. The project require rust >= v1.18 . The scipt replacing package name in `Cargo.toml` for a library name bug in `uniffi-bindgen-react-native`.
+```sh
+$ sh fetch_lwk.sh
+```
+
+Generate bindings for android and ios:
+```sh
+$ yarn ubrn:android
+$ yarn ubrn:ios
+```
+
+## Example
+
+Open demo application in `./example/` folder and read the code in `./example/src/App.tsx` .
+
+You could run the example on the Android demo app on device/emulator by:
+```sh
+$ yarn example android
+```
+
+Or you could run the iOS demo app by:
+```sh
+$ cd example
+$ bundle install
+$ bundle exec pod install # every time you update your native dependencies
+$ cd ..
+$ yarn example ios
+```
+
## Contributing
See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow.
@@ -115,3 +174,4 @@ MIT
---
Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)
+using [uniffi-bindgen-react-native](https://github.com/jhugman/uniffi-bindgen-react-native)
diff --git a/android/CMakeLists.txt b/android/CMakeLists.txt
new file mode 100644
index 0000000..f651f6e
--- /dev/null
+++ b/android/CMakeLists.txt
@@ -0,0 +1,77 @@
+# Generated by uniffi-bindgen-react-native
+cmake_minimum_required(VERSION 3.9.0)
+project(LwkRn)
+
+set (CMAKE_VERBOSE_MAKEFILE ON)
+set (CMAKE_CXX_STANDARD 17)
+
+# Resolve the path to the uniffi-bindgen-react-native package
+execute_process(
+ COMMAND node -p "require.resolve('uniffi-bindgen-react-native/package.json')"
+ OUTPUT_VARIABLE UNIFFI_BINDGEN_PATH
+ OUTPUT_STRIP_TRAILING_WHITESPACE
+)
+string(REGEX
+ REPLACE "/package\\.json$" ""
+ UNIFFI_BINDGEN_PATH ${UNIFFI_BINDGEN_PATH}
+)
+
+# Specifies a path to native header files.
+include_directories(
+ ../cpp
+ ../cpp/generated
+
+ ${UNIFFI_BINDGEN_PATH}/cpp/includes
+)
+
+add_library(lwk-rn SHARED
+ ../cpp/lwk-rn.cpp
+ ../cpp/generated/lwk.cpp
+ cpp-adapter.cpp
+)
+
+# Set C++ compiler flags
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O2")
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fexceptions")
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -frtti")
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fstack-protector-all")
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall")
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror")
+
+cmake_path(
+ SET MY_RUST_LIB
+ ${CMAKE_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}/liblwk.a
+ NORMALIZE
+)
+add_library(my_rust_lib STATIC IMPORTED)
+set_target_properties(my_rust_lib PROPERTIES IMPORTED_LOCATION ${MY_RUST_LIB})
+
+# Add ReactAndroid libraries, being careful to account for different versions.
+find_package(ReactAndroid REQUIRED CONFIG)
+find_library(LOGCAT log)
+
+# REACTNATIVE_MERGED_SO seems to be only be set in a build.gradle.kt file,
+# which we don't use. Thus falling back to version number sniffing.
+if (ReactAndroid_VERSION_MINOR GREATER_EQUAL 76)
+ set(REACTNATIVE_MERGED_SO true)
+endif()
+
+# https://github.com/react-native-community/discussions-and-proposals/discussions/816
+# This if-then-else can be removed once this library does not support version below 0.76
+if (REACTNATIVE_MERGED_SO)
+ target_link_libraries(lwk-rn ReactAndroid::reactnative)
+else()
+ target_link_libraries(lwk-rn
+ ReactAndroid::turbomodulejsijni
+ ReactAndroid::react_nativemodule_core
+ )
+endif()
+
+find_package(fbjni REQUIRED CONFIG)
+target_link_libraries(
+ lwk-rn
+ fbjni::fbjni
+ ReactAndroid::jsi
+ ${LOGCAT}
+ my_rust_lib
+)
diff --git a/android/build.gradle b/android/build.gradle
index 7e87cc2..2d9c5f2 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -1,62 +1,143 @@
+// Generated by uniffi-bindgen-react-native
+
buildscript {
- ext {
- agp_version = '8.4.2'
- kotlin_version = '1.9.0'
- }
- ext.safeExtGet = { prop, fallback ->
- return rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
- }
+ // Buildscript is evaluated before everything else so we can't use getExtOrDefault
+ def kotlin_version = rootProject.ext.has("kotlinVersion") ? rootProject.ext.get("kotlinVersion") : project.properties["DummyLibForAndroid_kotlinVersion"]
- repositories {
- google()
- mavenCentral()
- }
+ repositories {
+ google()
+ mavenCentral()
+ }
- dependencies {
- classpath "com.android.tools.build:gradle:$agp_version"
- classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
- }
+ dependencies {
+ classpath "com.android.tools.build:gradle:7.2.1"
+ // noinspection DifferentKotlinGradleVersion
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ }
+}
+
+def reactNativeArchitectures() {
+ def value = rootProject.getProperties().get("reactNativeArchitectures")
+ return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
}
-def safeExtGet(prop, fallback) {
- rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
+def isNewArchitectureEnabled() {
+ return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true"
}
-apply plugin: 'com.android.library'
-apply plugin: 'kotlin-android'
+apply plugin: "com.android.library"
+apply plugin: "kotlin-android"
+
+if (isNewArchitectureEnabled()) {
+ apply plugin: "com.facebook.react"
+}
+
+def getExtOrDefault(name) {
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["LwkRn_" + name]
+}
+
+def getExtOrIntegerDefault(name) {
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["LwkRn_" + name]).toInteger()
+}
+
+def supportsNamespace() {
+ def parsed = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.')
+ def major = parsed[0].toInteger()
+ def minor = parsed[1].toInteger()
+
+ // Namespace support was added in 7.3.0
+ return (major == 7 && minor >= 3) || major >= 8
+}
android {
- compileSdkVersion safeExtGet('compileSdkVersion', 34)
- namespace 'io.lwkrn'
+ if (supportsNamespace()) {
+ namespace "com.lwkrn"
+
+ sourceSets {
+ main {
+ manifest.srcFile "src/main/AndroidManifestNew.xml"
+ }
+ }
+ }
+
+ ndkVersion getExtOrDefault("ndkVersion")
+ compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
+
defaultConfig {
- minSdkVersion safeExtGet('minSdkVersion', 24)
- targetSdkVersion safeExtGet('targetSdkVersion', 34)
+ minSdkVersion getExtOrIntegerDefault("minSdkVersion")
+ targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
+ buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
+
+ buildFeatures {
+ prefab true
+ }
+ externalNativeBuild {
+ cmake {
+ arguments '-DANDROID_STL=c++_shared'
+ abiFilters (*reactNativeArchitectures())
+ }
+ }
+ ndk {
+ abiFilters "arm64-v8a", "armeabi-v7a", "x86", "x86_64"
+ }
+ }
+
+ externalNativeBuild {
+ cmake {
+ path "CMakeLists.txt"
+ }
+ }
+
+ buildFeatures {
+ buildConfig true
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ }
+ }
+
+ lintOptions {
+ disable "GradleCompatible"
}
+
compileOptions {
- sourceCompatibility JavaVersion.VERSION_17
- targetCompatibility JavaVersion.VERSION_17
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
}
- kotlinOptions {
- jvmTarget = '17'
+
+ sourceSets {
+ main {
+ if (isNewArchitectureEnabled()) {
+ java.srcDirs += [
+ "generated/java",
+ "generated/jni"
+ ]
+ }
+ }
}
}
repositories {
- google()
- mavenCentral()
- mavenLocal()
- maven {
- name = "GitHubPackages"
- url = uri("https://maven.pkg.github.com/blockstream/lwk")
- credentials {
- username = System.getenv('PAT_USER')
- password = System.getenv('PAT_TOKEN')
- }
- }
+ mavenCentral()
+ google()
}
+
+def kotlin_version = getExtOrDefault("kotlinVersion")
+
dependencies {
- //noinspection GradleDynamicVersion
- implementation 'com.facebook.react:react-android:0.75.2'
- implementation 'com.facebook.react:hermes-android:0.75.2'
- implementation 'com.blockstream:lwk_bindings:0.8.2'
+ // For < 0.71, this will be from the local maven repo
+ // For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin
+ //noinspection GradleDynamicVersion
+ implementation "com.facebook.react:react-native:+"
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
+}
+
+if (isNewArchitectureEnabled()) {
+ react {
+ jsRootDir = file("../src/")
+ libraryName = "LwkRn"
+ codegenJavaPackageName = "com.lwkrn"
+ }
}
diff --git a/android/cpp-adapter.cpp b/android/cpp-adapter.cpp
new file mode 100644
index 0000000..3c47b05
--- /dev/null
+++ b/android/cpp-adapter.cpp
@@ -0,0 +1,63 @@
+// Generated by uniffi-bindgen-react-native
+#include
+#include
+#include
+#include "lwk-rn.h"
+
+namespace jsi = facebook::jsi;
+namespace react = facebook::react;
+
+// Automated testing checks Java_com_lwkrn_LwkRnModule and lwkrn
+// by comparing the whole line here.
+/*
+Java_com_lwkrn_LwkRnModule_nativeMultiply(JNIEnv *env, jclass type, jdouble a, jdouble b) {
+ return lwkrn::multiply(a, b);
+}
+*/
+
+// Installer coming from LwkRnModule
+extern "C"
+JNIEXPORT jboolean JNICALL
+Java_com_lwkrn_LwkRnModule_nativeInstallRustCrate(
+ JNIEnv *env,
+ jclass type,
+ jlong rtPtr,
+ jobject callInvokerHolderJavaObj
+) {
+ // https://github.com/realm/realm-js/blob/main/packages/realm/binding/android/src/main/cpp/io_realm_react_RealmReactModule.cpp#L122-L145
+ // React Native uses the fbjni library for handling JNI, which has the concept of "hybrid objects",
+ // which are Java objects containing a pointer to a C++ object. The CallInvokerHolder, which has the
+ // invokeAsync method we want access to, is one such hybrid object.
+ // Rather than reworking our code to use fbjni throughout, this code unpacks the C++ object from the Java
+ // object `callInvokerHolderJavaObj` manually, based on reverse engineering the fbjni code.
+
+ // 1. Get the Java object referred to by the mHybridData field of the Java holder object
+ auto callInvokerHolderClass = env->GetObjectClass(callInvokerHolderJavaObj);
+ auto hybridDataField = env->GetFieldID(callInvokerHolderClass, "mHybridData", "Lcom/facebook/jni/HybridData;");
+ auto hybridDataObj = env->GetObjectField(callInvokerHolderJavaObj, hybridDataField);
+
+ // 2. Get the destructor Java object referred to by the mDestructor field from the myHybridData Java object
+ auto hybridDataClass = env->FindClass("com/facebook/jni/HybridData");
+ auto destructorField =
+ env->GetFieldID(hybridDataClass, "mDestructor", "Lcom/facebook/jni/HybridData$Destructor;");
+ auto destructorObj = env->GetObjectField(hybridDataObj, destructorField);
+
+ // 3. Get the mNativePointer field from the mDestructor Java object
+ auto destructorClass = env->FindClass("com/facebook/jni/HybridData$Destructor");
+ auto nativePointerField = env->GetFieldID(destructorClass, "mNativePointer", "J");
+ auto nativePointerValue = env->GetLongField(destructorObj, nativePointerField);
+
+ // 4. Cast the mNativePointer back to its C++ type
+ auto nativePointer = reinterpret_cast(nativePointerValue);
+ auto jsCallInvoker = nativePointer->getCallInvoker();
+
+ auto runtime = reinterpret_cast(rtPtr);
+ return lwkrn::installRustCrate(*runtime, jsCallInvoker);
+}
+
+extern "C"
+JNIEXPORT jboolean JNICALL
+Java_com_lwkrn_LwkRnModule_nativeCleanupRustCrate(JNIEnv *env, jclass type, jlong rtPtr) {
+ auto runtime = reinterpret_cast(rtPtr);
+ return lwkrn::cleanupRustCrate(*runtime);
+}
\ No newline at end of file
diff --git a/android/gradle.properties b/android/gradle.properties
index 2d8d1e4..0f0e3e3 100644
--- a/android/gradle.properties
+++ b/android/gradle.properties
@@ -1 +1,5 @@
-android.useAndroidX=true
\ No newline at end of file
+LwkRn_kotlinVersion=2.0.21
+LwkRn_minSdkVersion=24
+LwkRn_targetSdkVersion=34
+LwkRn_compileSdkVersion=35
+LwkRn_ndkVersion=27.1.12297006
diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml
index 7281ebc..8e8b969 100644
--- a/android/src/main/AndroidManifest.xml
+++ b/android/src/main/AndroidManifest.xml
@@ -1,3 +1,5 @@
-
-
-
+
+
+
+
\ No newline at end of file
diff --git a/android/src/main/AndroidManifestNew.xml b/android/src/main/AndroidManifestNew.xml
new file mode 100644
index 0000000..a2f47b6
--- /dev/null
+++ b/android/src/main/AndroidManifestNew.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/android/src/main/java/com/lwkrn/LwkRnModule.kt b/android/src/main/java/com/lwkrn/LwkRnModule.kt
new file mode 100644
index 0000000..1bed3eb
--- /dev/null
+++ b/android/src/main/java/com/lwkrn/LwkRnModule.kt
@@ -0,0 +1,43 @@
+// Generated by uniffi-bindgen-react-native
+package com.lwkrn
+
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.module.annotations.ReactModule
+import com.facebook.react.turbomodule.core.interfaces.CallInvokerHolder
+
+@ReactModule(name = LwkRnModule.NAME)
+class LwkRnModule(reactContext: ReactApplicationContext) :
+ NativeLwkRnSpec(reactContext) {
+
+ override fun getName(): String {
+ return NAME
+ }
+
+ // Two native methods implemented in cpp-adapter.cpp, and ultimately
+ // lwk-rn.cpp
+
+ external fun nativeInstallRustCrate(runtimePointer: Long, callInvoker: CallInvokerHolder): Boolean
+ external fun nativeCleanupRustCrate(runtimePointer: Long): Boolean
+
+ override fun installRustCrate(): Boolean {
+ val context = this.reactApplicationContext
+ return nativeInstallRustCrate(
+ context.javaScriptContextHolder!!.get(),
+ context.jsCallInvokerHolder!!
+ )
+ }
+
+ override fun cleanupRustCrate(): Boolean {
+ return nativeCleanupRustCrate(
+ this.reactApplicationContext.javaScriptContextHolder!!.get()
+ )
+ }
+
+ companion object {
+ const val NAME = "LwkRn"
+
+ init {
+ System.loadLibrary("lwk-rn")
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/src/main/java/com/lwkrn/LwkRnPackage.kt b/android/src/main/java/com/lwkrn/LwkRnPackage.kt
new file mode 100644
index 0000000..9a9f729
--- /dev/null
+++ b/android/src/main/java/com/lwkrn/LwkRnPackage.kt
@@ -0,0 +1,34 @@
+// Generated by uniffi-bindgen-react-native
+package com.lwkrn
+
+import com.facebook.react.TurboReactPackage
+import com.facebook.react.bridge.NativeModule
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.module.model.ReactModuleInfo
+import com.facebook.react.module.model.ReactModuleInfoProvider
+import java.util.HashMap
+
+class LwkRnPackage : TurboReactPackage() {
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
+ return if (name == LwkRnModule.NAME) {
+ LwkRnModule(reactContext)
+ } else {
+ null
+ }
+ }
+
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
+ return ReactModuleInfoProvider {
+ val moduleInfos: MutableMap = HashMap()
+ moduleInfos[LwkRnModule.NAME] = ReactModuleInfo(
+ LwkRnModule.NAME,
+ LwkRnModule.NAME,
+ false, // canOverrideExistingModule
+ false, // needsEagerInit
+ false, // isCxxModule
+ true // isTurboModule
+ )
+ moduleInfos
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/src/main/java/io/lwkrn/LwkRnModule.kt b/android/src/main/java/io/lwkrn/LwkRnModule.kt
deleted file mode 100644
index cdb1933..0000000
--- a/android/src/main/java/io/lwkrn/LwkRnModule.kt
+++ /dev/null
@@ -1,674 +0,0 @@
-package io.lwkrn
-
-import com.facebook.react.bridge.Arguments
-import com.facebook.react.bridge.Dynamic
-import com.facebook.react.bridge.Promise
-import com.facebook.react.bridge.ReactApplicationContext
-import com.facebook.react.bridge.ReactContextBaseJavaModule
-import com.facebook.react.bridge.ReactMethod
-import lwk.Address
-import lwk.Contract
-import lwk.ElectrumClient
-import lwk.Mnemonic
-import lwk.Pset
-import lwk.Signer
-import lwk.Transaction
-import lwk.TxBuilder
-import lwk.Txid
-import lwk.Update
-import lwk.WalletTx
-import lwk.Wollet
-import lwk.WolletDescriptor
-import lwk.Bip
-
-class LwkRnModule(reactContext: ReactApplicationContext) :
- ReactContextBaseJavaModule(reactContext) {
- override fun getName() = "LwkRnModule"
- override fun getConstants(): MutableMap {
- return hashMapOf("count" to 1)
- }
-
- private var _descriptors = mutableMapOf()
- private var _electrumClients = mutableMapOf()
- private var _wollets = mutableMapOf()
- private var _updates = mutableMapOf()
- private var _walletTxs = mutableMapOf()
- private var _transactions = mutableMapOf()
- private var _psets = mutableMapOf()
- private var _signers = mutableMapOf()
- private var _txBuilders = mutableMapOf()
- private var _contracts = mutableMapOf()
- private var _bips = mutableMapOf()
-
- /* Descriptor */
-
- @ReactMethod
- fun createDescriptor(
- descriptor: String, result: Promise
- ) {
- try {
- val id = randomId()
- _descriptors[id] = WolletDescriptor(descriptor)
- result.resolve(id)
- } catch (error: Throwable) {
- result.reject("WolletDescriptor create error", error.localizedMessage, error)
- }
- }
-
- @ReactMethod
- fun descriptorAsString(
- keyId: String,
- result: Promise
- ) {
- result.resolve(_descriptors[keyId]!!.toString())
- }
-
- /* Bip */
-
- @ReactMethod
- fun newBip49(
- result: Promise
- ) {
- try {
- val id = randomId()
- _bips[id] = Bip.newBip49()
- result.resolve(id)
- } catch (error: Throwable) {
- result.reject("Bip newBip49 error", error.localizedMessage, error)
- }
- }
-
- @ReactMethod
- fun newBip84(
- result: Promise
- ) {
- try {
- val id = randomId()
- _bips[id] = Bip.newBip84()
- result.resolve(id)
- } catch (error: Throwable) {
- result.reject("Bip newBip84 error", error.localizedMessage, error)
- }
- }
-
- @ReactMethod
- fun newBip87(
- result: Promise
- ) {
- try {
- val id = randomId()
- _bips[id] = Bip.newBip87()
- result.resolve(id)
- } catch (error: Throwable) {
- result.reject("Bip newBip87 error", error.localizedMessage, error)
- }
- }
-
- /* Signer */
-
- @ReactMethod
- fun createSigner(
- mnemonic: String,
- network: String,
- result: Promise
- ) {
- try {
- val id = randomId()
- val mnemonicObj = Mnemonic(mnemonic)
- val networkObj = setNetwork(network)
- _signers[id] = Signer(mnemonicObj, networkObj)
- result.resolve(id)
- } catch (error: Throwable) {
- result.reject("Signer create error", error.localizedMessage, error)
- }
- }
-
- @ReactMethod
- fun sign(
- signerId: String,
- psetId: String,
- result: Promise
- ) {
- try {
- val id = randomId()
- val signer = _signers[signerId]
- val pset = _psets[psetId]
- _psets[id] = signer!!.sign(pset!!)
- result.resolve(id)
- } catch (error: Throwable) {
- result.reject("Signer sign error", error.localizedMessage, error)
- }
- }
-
- @ReactMethod
- fun wpkhSlip77Descriptor(
- signerId: String,
- result: Promise
- ) {
- try {
- val id = randomId()
- val signer = _signers[signerId]
- _descriptors[id] = signer!!.wpkhSlip77Descriptor()
- result.resolve(id)
- } catch (error: Throwable) {
- result.reject("Signer wpkhSlip77Descriptor error", error.localizedMessage, error)
- }
- }
-
- @ReactMethod
- fun keyoriginXpub(
- signerId: String,
- bipId: String,
- result: Promise
- ) {
- try {
- val signer = _signers[signerId]
- val bip = _bips[bipId]
- val res = signer!!.keyoriginXpub(bip!!)
- result.resolve(res)
- } catch (error: Throwable) {
- result.reject("Signer keyoriginXpub error", error.localizedMessage, error)
- }
- }
-
- @ReactMethod
- fun mnemonic(
- signerId: String,
- result: Promise
- ) {
- try {
- val id = randomId()
- val signer = _signers[signerId]
- val res = signer!!.mnemonic()
- result.resolve(res)
- } catch (error: Throwable) {
- result.reject("Signer mnemonic error", error.localizedMessage, error)
- }
- }
-
- @ReactMethod
- fun createRandomSigner(
- network: String,
- result: Promise
- ) {
- try {
- val id = randomId()
- val networkObj = setNetwork(network)
- _signers[id] = Signer.random(networkObj)
- result.resolve(id)
- } catch (error: Throwable) {
- result.reject("Signer createRandomSigner error", error.localizedMessage, error)
- }
- }
-
- /* Electrum client */
-
- @ReactMethod
- fun initElectrumClient(
- electrumUrl: String,
- tls: Boolean,
- validateDomain: Boolean,
- result: Promise
- ) {
- try {
- val id = randomId()
- _electrumClients[id] = ElectrumClient(electrumUrl, tls, validateDomain)
- result.resolve(id)
- } catch (error: Throwable) {
- result.reject("ElectrumClient create error", error.localizedMessage, error)
- }
- }
-
- @ReactMethod
- fun defaultElectrumClient(
- network: String,
- result: Promise
- ) {
- try {
- val id = randomId()
- val networkObj = setNetwork(network)
- _electrumClients[id] = networkObj.defaultElectrumClient()
- result.resolve(id)
- } catch (error: Throwable) {
- result.reject(
- "ElectrumClient defaultElectrumClient error",
- error.localizedMessage,
- error
- )
- }
- }
-
- @ReactMethod
- fun broadcast(
- clientId: String,
- txId: String,
- result: Promise
- ) {
- Thread {
- try {
- val client = _electrumClients[clientId]
- val transaction = _transactions[txId]
- val txid = client!!.broadcast(transaction!!)
- result.resolve(txid.toString())
- } catch (error: Throwable) {
- result.reject(
- "ElectrumClient broadcast error",
- error.localizedMessage,
- error
- )
- }
- }.start()
- }
-
- @ReactMethod
- fun fullScan(
- wolletId: String,
- clientId: String,
- result: Promise
- ) {
- Thread {
- try {
- val id = randomId()
- val client = _electrumClients[clientId]
- val wollet = _wollets[wolletId]
- _updates[id] = client!!.fullScan(wollet!!)!!
- result.resolve(id)
- } catch (error: Throwable) {
- result.reject("ElectrumClient fullScan error", error.localizedMessage, error)
- }
- }.start()
- }
-
- /* Wollet */
-
- @ReactMethod
- fun createWollet(
- network: String,
- descriptorId: String,
- datadir: String?,
- result: Promise
- ) {
- try {
- val id = randomId()
- val networkObj = setNetwork(network)
- val descriptor = _descriptors[descriptorId]
- _wollets[id] = Wollet(networkObj, descriptor!!, datadir)
- result.resolve(id)
- } catch (error: Throwable) {
- result.reject("Wollet create error", error.localizedMessage, error)
- }
- }
-
- @ReactMethod
- fun applyUpdate(
- wolletId: String,
- updateId: String,
- result: Promise
- ) {
- Thread {
- try {
- val wollet = _wollets[wolletId]
- val update = _updates[updateId]
- wollet!!.applyUpdate(update!!)
- result.resolve(null)
- } catch (error: Throwable) {
- result.reject("Wollet applyUpdate error", error.localizedMessage, error)
- }
- }.start()
- }
-
- @ReactMethod
- fun getTransactions(wolletId: String, result: Promise) {
- Thread {
- try {
- val wollet = _wollets[wolletId]
- val list = wollet!!.transactions()
- val transactions: MutableList