Skip to content

Add React Native Nitro bindings for CDK#1982

Open
crodas wants to merge 11 commits into
cashubtc:mainfrom
crodas:features/nitro
Open

Add React Native Nitro bindings for CDK#1982
crodas wants to merge 11 commits into
cashubtc:mainfrom
crodas:features/nitro

Conversation

@crodas

@crodas crodas commented May 19, 2026

Copy link
Copy Markdown
Collaborator

Description

Fixes #1902

Add a new cdk-nitro crate and React Native package that exposes CDK functionality via Nitro Modules. The bindings provide a C FFI layer with a C++ HybridObject bridge that lets React Native apps create and manipulate Cashu tokens (OutputData, proofs, keysets, etc.).

Includes:

  • Rust FFI crate (bindings/react-native/rust) with OutputDataCreator
  • C++ HybridObject bridge wrapping the FFI calls
  • TypeScript Nitro spec and package entry point
  • Android CMakeLists and iOS podspec for native builds
  • Node.js FFI test suite using native addon loading
  • CI job (nitro-binding-tests) and justfile recipes (test-nitro, test-nitro-node)
  • Added nodejs to the bindings Nix devshell

Notes to the reviewers


Suggested CHANGELOG Updates

CHANGED

ADDED

REMOVED

FIXED


Checklist

  • I followed the code style guidelines
  • I ran just quick-check before committing
  • If the Wallet API was modified (added/removed/changed), I have reflected those changes in the FFI bindings (crates/cdk-ffi)

@codecov

codecov Bot commented May 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.70886% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 71.68%. Comparing base (d481edc) to head (745b127).

Files with missing lines Patch % Lines
bindings/react-native/rust/src/lib.rs 96.70% 13 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1982      +/-   ##
==========================================
+ Coverage   71.55%   71.68%   +0.13%     
==========================================
  Files         356      357       +1     
  Lines       73999    74394     +395     
==========================================
+ Hits        52950    53332     +382     
- Misses      21049    21062      +13     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@crodas crodas force-pushed the features/nitro branch 5 times, most recently from 1dc1d1c to c7c2964 Compare May 26, 2026 15:16
@crodas crodas self-assigned this May 26, 2026
@crodas crodas force-pushed the features/nitro branch 2 times, most recently from 08e9361 to f5d4910 Compare May 29, 2026 21:14
@crodas crodas marked this pull request as ready for review May 29, 2026 23:35
@ye0man ye0man moved this from Backlog to Needs Review in CDK Jun 2, 2026
@ye0man ye0man added this to the 0.18.0 milestone Jun 2, 2026
@thesimplekid

Copy link
Copy Markdown
Collaborator

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6963f16291

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


# Link against pre-built cdk_ffi native library
# The .so must be placed at: android/src/main/jniLibs/<abi>/libcdk_ffi.so
find_library(CDK_FFI cdk_ffi PATHS

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Link Android against the generated Nitro library

For Android package consumers, this CMake file searches for libcdk_ffi.so, but this binding crate is named cdk_nitro and the publish workflow copies libcdk_nitro.so into android/src/main/jniLibs/<abi>. As a result the packaged React Native module will not link/load its Rust symbols on Android; the CMake target should find and link cdk_nitro instead of cdk_ffi.

Useful? React with 👍 / 👎.


# Link against pre-built cdk_ffi static library
# Place libcdk_ffi.a in ios/Frameworks/ for each architecture
s.vendored_libraries = 'ios/Frameworks/libcdk_ffi.a'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Point the podspec at the shipped Nitro XCFramework

For iOS releases, the workflow installs ios/Frameworks/CdkNitro.xcframework, but the podspec still declares a non-existent ios/Frameworks/libcdk_ffi.a and -lcdk_ffi. Users running pod install/building the package from the published artifacts will therefore fail to find or link the native library; the podspec needs to vend/link the CdkNitro.xcframework/cdk_nitro artifact that this PR actually builds.

Useful? React with 👍 / 👎.

Comment thread bindings/react-native/rust/src/lib.rs Outdated
pubkeys: add_pks,
refund_keys: refund_pks,
num_sigs: if num_sigs > 1 { Some(num_sigs) } else { None },
sig_flag: Default::default(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor the requested P2PK signature flag

When callers pass sigFlag: 'SigAll' through the Nitro API, C++ forwards it here but Rust ignores _sig_flag and always serializes Default::default() (SIG_INPUTS). That changes the NUT-10 spending condition the wallet creates, so outputs that the app intended to require signatures on both inputs and outputs are minted with input-only enforcement; parse/map the provided flag and reject unknown values instead of defaulting unconditionally.

Useful? React with 👍 / 👎.

Comment thread bindings/react-native/rust/src/lib.rs Outdated
Comment on lines +101 to +102
let add_pks = parse_pubkey_array(additional_pubkeys, additional_pubkeys_len);
let refund_pks = parse_pubkey_array(refund_pubkeys, refund_pubkeys_len);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject invalid optional P2PK keys instead of dropping them

When an additionalPubkeys or refundPubkeys array is present but one entry is malformed, parse_pubkey_array returns None, which these assignments treat the same as if the caller omitted the array. For example, a typo in a multisig additional key will create a valid output locked only to the primary key rather than throwing, weakening the requested spending condition; return a Result<Option<Vec<_>>> and fail the call on parse errors.

Useful? React with 👍 / 👎.

@crodas crodas force-pushed the features/nitro branch 4 times, most recently from b1d57ab to 9e59899 Compare June 8, 2026 20:10
@thesimplekid

Copy link
Copy Markdown
Collaborator

@crodas Are the codex reviews addressed they are not marked resolved?

@crodas

crodas commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator Author

@crodas Are the codex reviews addressed they are not marked resolved?

Some of it is revisited; lemme double-check and close the addressed feedback

@cdk-bot cdk-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified findings approved for disclosure:

  • Validate custom split sums against requested amount (medium) - Callers can accidentally create blinded output requests whose total value differs from the amount they requested, leading to over/under-requested minting or change outputs in the new React Native binding.
  • Check seed pointer before from_raw_parts (medium) - A malformed FFI call can trigger undefined behavior in the new React Native Nitro Rust binding instead of returning a null result for invalid seed input.
  • Check FFI string pointers before CStr::from_ptr (medium) - Malformed FFI calls with null string pointers can trigger undefined behavior in the new React Native Nitro Rust binding instead of returning a null result.

const std::vector<KeyEntry>& keys,
const std::optional<std::vector<double>>& customSplit) {

if (customSplit.has_value()) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this validate the custom split before returning it? With the current path, amount is ignored whenever customSplit is present, so a call like createRandomData(10, ..., [100]) will create outputs totaling 100 while the API call requested 10. Since the non-custom path enforces that the requested amount can be split exactly, the custom path should probably sum the converted denominations and throw if they do not equal amount (and ideally reject invalid/non-integer values too).

Comment thread bindings/react-native/rust/src/lib.rs Outdated
};

let seed_slice = unsafe { slice::from_raw_parts(seed, seed_len as usize) };

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this validate seed before creating the slice? slice::from_raw_parts requires a non-null, properly aligned pointer even for a zero-length slice, so a bad FFI call such as seed = NULL, seed_len = 0 hits undefined behavior before the length check can return null_mut(). Please check seed.is_null() (and preferably seed_len == 64) before calling from_raw_parts.

sig_flag_ptr: *const c_char,
) -> *mut CdkBlindResult {
let pubkey_str = match unsafe { CStr::from_ptr(pubkey_hex) }.to_str() {
Ok(s) => s,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these FFI string pointers be checked for null before calling CStr::from_ptr? from_ptr requires a non-null pointer, so malformed calls can invoke undefined behavior instead of returning null_mut(). This applies at least to pubkey_hex, keyset_id, and the entries in parse_pubkey_array; adding explicit is_null() checks would make these invalid-input paths safe like the existing sig_flag_ptr handling.

crodas added 4 commits June 18, 2026 21:52
Add a new `cdk-nitro` crate and React Native package that exposes CDK
functionality via Nitro Modules. The bindings provide a C FFI layer with a C++
HybridObject bridge that lets React Native apps create and manipulate Cashu
tokens (OutputData, proofs, keysets, etc.).

Includes:
- Rust FFI crate (`bindings/react-native/rust`) with OutputDataCreator
- C++ HybridObject bridge wrapping the FFI calls
- TypeScript Nitro spec and package entry point
- Android CMakeLists and iOS podspec for native builds
- Node.js FFI test suite using native addon loading
- CI job (nitro-binding-tests) and justfile recipes (test-nitro,
  test-nitro-node)
- Added nodejs to the bindings Nix devshell
Add CI workflow (.github/workflows/nitro-publish.yml) that syncs
React Native bindings to a separate cdk-nitro repo, cross-compiles
native libraries for iOS and Android, creates an XCFramework, and
publishes to npm with prebuilt binaries.

Add ffi-release-nitro justfile recipe and hook it into the main
release flow alongside Swift, Kotlin, and Go.
Add README.md for the React Native Nitro bindings with installation
from GitHub, usage examples, API reference, and supported platforms.

Make npm publish step conditional on NPM_TOKEN secret availability,
emitting a warning with the git install URL when skipped.
- Rename kotlin-build → cross-build in flake.nix (with backwards-compat alias)
- Add aarch64-apple-ios-sim to cross-build targets
- Use cdk#cross-build instead of cdk#bindings in nitro workflow
- Stop copying rust-toolchain.toml to cdk-nitro (conflicts with Nix toolchain)
- Remove any rust-toolchain.toml before building as safety measure
crodas added 7 commits June 18, 2026 21:52
- Add bindings/react-native/nitrogen/generated/ and lib/ to .gitignore
- Add npm install + codegen step to nitro-publish.yml so generated files sync to cdk-nitro repo
- Add codegen verification step to ci.yml nitro-binding-tests job
CMakeLists.txt searched for libcdk_ffi.so but the publish workflow ships
libcdk_nitro.so. Similarly the podspec referenced a nonexistent libcdk_ffi.a
instead of the CdkNitro.xcframework that gets built.
Parse the sig_flag C string into SigFlag::SigInputs or SigFlag::SigAll, reject
unknown values, and default to SigInputs only when null.
parse_pubkey_array now returns Result<Option<Vec<_>>, ()> so a bad entry in
additional_pubkeys or refund_pubkeys aborts the call rather than being treated
as if the caller omitted the array entirely.
Five tests covering the two previous fixes:
- SigAll flag appears in serialized NUT-10 secret
- null sig_flag defaults to SIG_INPUTS
- unknown sig_flag rejected with null
- malformed additional/refund pubkeys rejected with null
…tion

- Validate seed pointer and length before slice::from_raw_parts in deterministic blinding
- Add null checks for pubkey_hex, keyset_id, and parse_pubkey_array entries
- Validate custom split sums to requested amount and contains valid denominations

@cdk-bot cdk-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified findings approved for disclosure:

  • React Native P2PK bindings omit refund multisig threshold (medium) - React Native callers cannot create multisig refund P2PK conditions; multiple refund keys are downgraded to the default one required refund signature.
    Additional locations included in summary:
    • bindings/react-native/src/specs/OutputDataCreator.nitro.ts:35
    • bindings/react-native/cpp/HybridOutputDataCreator.cpp:106
      Unanchored locations included in summary:
    • bindings/react-native/cpp/cdk_nitro.h:79

refund_keys: refund_pks,
num_sigs: if num_sigs > 1 { Some(num_sigs) } else { None },
sig_flag,
num_sigs_refund: None,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new React Native P2PK binding exposes refundPubkeys, but it never exposes or forwards the corresponding num_sigs_refund setting. At this PR head, the TS P2PKOptions has no numSigsRefund, the C++/C FFI call has no refund-signature-count argument, and Rust always serializes Conditions { num_sigs_refund: None }.

That means callers can create a refund path with multiple refund pubkeys but cannot require (for example) 2-of-2 refund signatures; CDK will fall back to the default single refund signature requirement (unwrap_or(1) in the core spending checks). Please plumb a numSigsRefund option through the Nitro spec, C++ bridge, C header, and Rust FFI into Conditions.num_sigs_refund, just like numSigs is handled for the primary path.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Needs Review

Development

Successfully merging this pull request may close these issues.

Publish React native nitro module for cashu crate

4 participants