REST is not part of the SDK surface. REST endpoints remain a compatibility wrapper for existing integrations and tests. The SDK surface is exposed through the Rust library + UniFFI bindings.
Current lifecycle/threading model:
- UniFFI SDK is instance-based via
SdkNodeobject handles. - Create a node with
SdkNode.create(SdkInitRequest), then call methods on that instance. - Multiple SDK node instances are supported per process.
- Legacy global
sdk_*wrappers remain in Rust for compatibility but are no longer part of generated UniFFI bindings. - UniFFI methods are synchronous entrypoints bridged to async internals:
MultiThreadTokio runtime usesblock_in_place + handle.block_on, whileCurrentThread(or no runtime) uses a shared dedicated Tokio runtime to avoidblock_in_placepanic paths.
Current internal layering is:
uniffi_api->sdk->ldk(+ shared node/app state)routes(HTTP compatibility layer) ->sdk/ldk
Important notes:
- UniFFI does not call HTTP route handlers; it calls SDK methods directly.
- SDK is expected to depend on LDK core logic (it is a wrapper, not a separate node implementation).
ldk::start_ldknow acceptscore_types::UnlockRequestand SDK unlock usessdk::UnlockRequest, so unlock flow is not typed against route-layer DTOs.- A small
routesdiff remains for sharedAppStatetransition helpers (pub(crate)visibility), used by SDK unlock lifecycle handling.
Harness-specific runbooks live in:
test/python-e2e/README.mdtest/kotlin-e2e/README.mdandroid-e2e/README.mdtest/swift-e2e/README.mdsrc/test/lib_sdk/README.md
Implemented mappings:
address->addressasset_balance->asset_balanceasset_metadata->asset_metadatabtc_balance->btc_balancecheck_indexer_url->check_indexer_urlcheck_proxy_endpoint->check_proxy_endpointclose_channel->closechannelconnect_peer->connectpeercreate_utxos->createutxosdecode_ln_invoice->decode_ln_invoicedecode_rgb_invoice->decode_rgb_invoicedisconnect_peer->disconnectpeerestimate_fee->estimate_feefail_transfers->failtransfersget_asset_media->get_asset_mediaget_channel_id->get_channel_idget_payment->get_paymentget_swap->get_swapinit->initinvoice_status->invoice_statusissue_asset_cfa->issueassetcfaissue_asset_nia->issueassetniaissue_asset_uda->issueassetudakeysend->keysendlist_assets->list_assetslist_channels->list_channelslist_payments->list_paymentslist_peers->list_peerslist_swaps->list_swapslist_transactions->list_transactionslist_transfers->list_transferslist_unspents->list_unspentsln_invoice->ln_invoice(SDK internals usecreate_ln_invoice)maker_execute->makerexecutemaker_init->makerinitnetwork_info->network_infonode_info->node_infoopen_channel->openchannelpost_asset_media->postassetmediarefresh_transfers->refreshtransfersrgb_invoice->rgbinvoicesend_btc->sendbtcsend_onion_message->sendonionmessagesend_payment->sendpaymentsend_rgb->send_rgbsign_message->sign_messagesync->synctaker->takerunlock->unlock
Still missing route mappings in SDK/UniFFI:
backupchange_passwordlockrestorerevoke_token
Special note:
shutdownexists onSdkNodeas a native node-handle operation, but there is no dedicated route-paritysdk::shutdownfunction matchingroutes::shutdown.
Manual mixed-language interop runbook:
RUST_PYTHON_INTEROP_MANUAL.mdin this directory (Rust daemon node + Python UniFFI node).
rgb-lightning-node is a fork and we keep feature parity with upstream.
UniFFI support adds a required manual sync checklist when upstream changes:
- Sync upstream changes into this fork.
- Re-check SDK/REST parity for changed endpoints in
src/routes.rsandsrc/sdk/mod.rs. - Update
bindings/rgb_lightning_node.udlfor any public API shape changes. - Add/update converters in
src/ffi/types.rsfor new structured identifiers. - Regenerate bindings:
./scripts/ci/uniffi_generate_all.sh
- Re-run required tests:
cargo test -- --test-threads=1cargo test --features uniffi --lib uniffi_smoke_tests:: -- --test-threads=1cargo test zero_amount_invoice -- --test-threads=1cargo test send_receive -- --test-threads=1
- Run Rust checks:
cargo checkcargo check --features uniffi
- Run core tests:
cargo test -- --test-threads=1cargo test --features uniffi --lib uniffi_smoke_tests:: -- --test-threads=1
- Regenerate bindings and verify changed output:
./scripts/ci/uniffi_generate_all.sh
- Ensure CI workflows pass:
.github/workflows/test.yaml.github/workflows/sdk-e2e.yaml.github/workflows/uniffi-artifacts.yaml
mod.rs is now the main UniFFI API surface and was reworked around four goals:
- Keep UniFFI entrypoints thin and deterministic.
- Reuse SDK/core logic from
crate::sdkinstead of route handlers. - Move from global process state to instance-based node usage.
- Preserve temporary compatibility for older global
sdk_*call patterns.
Detailed structure:
- Input normalization and node construction:
network_from_strvalidates external string input and maps it torgb_lib::BitcoinNetwork.handle_from_requestbuildsNodeConfigand creates aNodeHandleusingblock_on_app(...).- This isolates initialization concerns so both object API and compatibility wrappers reuse the same constructor path.
- Shared operation helpers:
send_rgb_from_stateperforms argument validation and request conversion from UniFFI DTOs into internalroutes/rgb_librecipient representations.- The function is intentionally separated from
SdkNodemethods so both instance and compatibility paths share identical behavior.
- Instance-based API (
SdkNode):
SdkNode::createowns node initialization and returns a concrete instance.- Methods (
node_info,get_channel_id,get_payment,get_swap,ln_invoice,send_rgb,shutdown) callcrate::sdk::*and map outputs into UniFFI-safe DTOs. - This removed the previous hard single-global-handle requirement for generated bindings and enables multiple independent SDK node objects per process.
- Compatibility wrappers (
sdk_*):
sdk_initialize,sdk_shutdown, andsdk_*read/write a global slot throughstate.rsand internally delegate toSdkNodemethods.- They remain for transition and internal compatibility, but the generated UDL surface is instance-first.
- Error conversion behavior:
- Conversion failures and internal mapping failures return
RlnError::Internal. - User/input/domain validation failures are surfaced as
RlnError::InvalidRequest. - Missing resources (
PaymentNotFound,SwapNotFound,Unknown*) map toRlnError::NotFound. - State conflicts (
OpenChannelInProgress,Already*, etc.) map toRlnError::Conflict. - Initialization/locked-state situations return
RlnError::NotInitialized.
state.rs was refactored into a dedicated bridge layer for three concerns:
global compatibility state, sync/async runtime bridging, and stable error
mapping.
Detailed structure:
- Global compatibility state management:
- The global slot stores
Option<NodeHandle>(not raw daemon-only state), so wrappers can delegate through the same node abstraction used by instance API. set_uniffi_node_handle,clear_uniffi_node_handle,is_uniffi_app_state_initialized, andget_uniffi_app_stateare the only access points.- Lock poisoning is handled explicitly; no
unwrap()is used on mutex lock. Failures are logged and mapped to controlled errors instead of panicking.
- Runtime bridge for synchronous UniFFI calls:
block_on_sdkexecutes futures returningResult<T, APIError>.block_on_appexecutes futures returningResult<T, AppError>.- Runtime policy:
- If in Tokio
MultiThread: useblock_in_place(|| handle.block_on(...)). - If in Tokio
CurrentThread: use shared fallback runtime. - If no runtime exists: use shared fallback runtime.
- If in Tokio
shared_uniffi_runtime()centralizes fallback runtime creation withOnceLock, avoiding per-call runtime creation and preventing current-threadblock_in_placepanic paths.
- Stable public error mapping:
map_api_errorconverts internalAPIErrorvariants into narrow publicRlnErrorcategories used by foreign bindings.map_app_errordoes the same forAppError.- This keeps external error contracts stable even when internal errors evolve.
- Why this split exists:
mod.rsfocuses on API behavior and request/response conversion.state.rsfocuses on execution context and safety boundaries.- Separating them reduces coupling, makes runtime logic testable in isolation, and avoids repeating bridge code in every UniFFI method.
Build with UniFFI enabled:
cargo check --features uniffiGenerate all bindings:
./scripts/ci/uniffi_generate_all.shGenerate per language:
./scripts/ci/uniffi_generate_kotlin.sh
./scripts/ci/uniffi_generate_kotlin_android.sh
./scripts/ci/uniffi_generate_python.sh
./scripts/ci/uniffi_generate_swift.shQuick SDK smoke tests:
cargo test --features uniffi --lib uniffi_smoke_tests:: -- --test-threads=1Library-only test suite (no daemon/API routing):
cargo test --features "uniffi,test-utils" \
--test lib_core_uniffi_state \
--test lib_core_type_converters \
--test lib_core_error_mapping \
--test lib_core_node_handle_invariantsStrict lint after library-core changes:
cargo clippy --all-targets --all-features -- -D warningsPR CI gate for these tests:
.github/workflows/uniffi-artifacts.yamljob:library-core-tests
Kotlin/JVM artifact build (Linux host):
cargo build --release --features uniffi --lib
./scripts/ci/uniffi_generate_kotlin.shKotlin/Android artifact build (requires ANDROID_NDK_HOME + cargo-ndk):
./scripts/ci/uniffi_generate_kotlin_android.sh
./scripts/ci/build_android_jni.shLocal Android NDK setup (Ubuntu, CLI):
# 1) Install Java + Rust helper
sudo apt-get update
sudo apt-get install -y openjdk-17-jdk unzip cmake clang pkg-config build-essential ninja-build
cargo install cargo-ndk --version 3.5.4 --locked
cargo install bindgen-cli --version 0.71.1 --locked
# 2) Install Android cmdline-tools (pick your own SDK root if needed)
export ANDROID_SDK_ROOT="$HOME/Android/Sdk"
mkdir -p "$ANDROID_SDK_ROOT/cmdline-tools"
cd /tmp
wget https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip -O cmdline-tools.zip
unzip -q cmdline-tools.zip
mkdir -p "$ANDROID_SDK_ROOT/cmdline-tools/latest"
mv cmdline-tools/* "$ANDROID_SDK_ROOT/cmdline-tools/latest/"
# 3) Install NDK + build tools used by local checks/CI
"$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager" --licenses
"$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager" \
"ndk;26.3.11579264" "platform-tools" "build-tools;34.0.0"
# 4) Export env vars for this shell
export ANDROID_NDK_HOME="$ANDROID_SDK_ROOT/ndk/26.3.11579264"
export PATH="$ANDROID_SDK_ROOT/cmdline-tools/latest/bin:$ANDROID_SDK_ROOT/platform-tools:$PATH"Then build Android artifacts:
./scripts/ci/uniffi_generate_kotlin_android.sh
./scripts/ci/build_android_jni.shSwift artifact build (macOS):
rustup target add aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios
cargo build --release --features uniffi --lib --target aarch64-apple-ios
cargo build --release --features uniffi --lib --target aarch64-apple-ios-sim
cargo build --release --features uniffi --lib --target x86_64-apple-ios
./scripts/ci/uniffi_generate_swift.sh
./scripts/ci/package_swift_xcframework.shParity tests used in CI:
cargo test zero_amount_invoice -- --test-threads=1
cargo test send_receive -- --test-threads=1CI artifact packaging workflow:
.github/workflows/uniffi-artifacts.yaml- Artifacts produced:
- Swift:
RGBLightningNode.xcframework - Kotlin JVM host bundle: generated Kotlin sources + Linux
librgb_lightning_node.so - Kotlin Android:
jniLibs+ generated Kotlin sources
- Swift: