# @avalabs/crypto-nitro

React Native Nitro Modules wrapper around [`@avalabs/crypto-core`]. Exposes
the pubkey-only batched address derivation surface (xpub batch + per-chain
encoders for EVM / BTC / SVM / Avalanche) and the secp256k1 / Schnorr /
Ed25519 primitives to iOS and Android via a JS-thread-free C++ bridge.

Most consumers should depend on [`@avalabs/crypto-sdk`] instead — it
routes Metro to this package via the `"react-native"` exports condition
and to [`@avalabs/crypto-wasm`] for web / extension / Node via
`"browser"` / `"import"`. The SDK-mirrored exports here carry
`@deprecated` JSDoc tags to nudge consumers toward the unified entry
point.

[`@avalabs/crypto-core`]: ../../packages-internal/crypto-core/README.md
[`@avalabs/crypto-sdk`]: ../crypto-sdk/README.md
[`@avalabs/crypto-wasm`]: ../crypto-wasm/README.md

## Layout

```
src/
  specs/crypto.nitro.ts     Nitro spec — consumed by `nitrogen` codegen
  index.ts                  Public TypeScript API (init + xpub batch +
                            4 per-chain encoders + 7 primitives +
                            MAX_BATCH_SIZE)
cpp/
  CryptoHybrid.{hpp,cpp}    C++ bridge — extends generated HybridCryptoSpec
                            and delegates to crypto-core's api/ headers
nitrogen/generated/         CODEGEN OUTPUT — committed to git and shipped
                            in the npm tarball. Regenerated from
                            src/specs/crypto.nitro.ts on spec edits; the
                            diff is committed alongside the spec change.
                            Local regen: `pnpm --filter @avalabs/crypto-nitro
                            codegen`. Output includes:
                            - shared/c++/HybridCryptoSpec.{hpp,cpp}
                            - shared/c++/DerivedSecp256k1Addresses.hpp
                            - shared/c++/DerivedAvalancheAddresses.hpp
                            - shared/c++/ExtendedPublicKey.hpp
                            - ios/* (autolinking, Swift bridge)
                            - android/* (autolinking, Kotlin bridge)
android/
  build.gradle              RN autolinking entry point
  CMakeLists.txt            NDK build — adds crypto-core as subdirectory
  src/main/                 Android-side Kotlin (generated)
ios/                        iOS-side Swift / Obj-C++ (generated)
AvalabsCryptoNitro.podspec  iOS Pod spec — rsyncs crypto-core sources
nitro.json                  Nitro module configuration
```

## Setup

### One-time

```bash
# 1. Vendor libsecp256k1 into ../../packages-internal/crypto-core/deps/secp256k1
pnpm --filter @avalabs/crypto-nitro run setup:secp256k1

# 2. Install workspace deps (pulls in react, react-native, react-native-nitro-modules)
pnpm install
```

### Regenerate codegen

The `nitrogen/generated/` tree is **committed to git and shipped in the
published tarball**, matching the convention used by upstream Nitro
libraries (e.g. `react-native-nitro-sqlite`). Consumers do not run
nitrogen — they install the package and CocoaPods / Gradle autolinking
picks up the pre-generated files directly.

Regenerate after every edit to `src/specs/crypto.nitro.ts` and commit
the diff alongside the spec change:

```bash
pnpm --filter @avalabs/crypto-nitro codegen
git add packages/crypto-nitro/nitrogen/generated
```

The `codegen` script is also wired into the turbo pipeline (`typecheck`,
`lint`, `test`, `build` all `dependsOn: ["codegen"]`), so running any of
those tasks transparently refreshes the tree if the spec is ahead.

## Consumer setup

A downstream RN app (e.g. core-mobile) adds `@avalabs/crypto-nitro` and
`react-native-nitro-modules` to its `package.json` and builds normally:

```bash
pnpm install
cd ios && pod install
pnpm ios                  # or react-native run-ios
```

No `nitrogen` devDependency, no `postinstall` hook — the published
tarball already contains the generated C++ / Swift / Kotlin / Gradle /
Ruby files under `nitrogen/generated/`, and the podspec + Android
`CMakeLists.txt` reference them at that path.

## How a downstream RN app consumes this

1. App adds `@avalabs/crypto-sdk` (the canonical entry point) plus
   `@avalabs/crypto-nitro` and `react-native-nitro-modules` to its
   `package.json`. The latter two are needed so React Native's
   autolinker finds the native module — even though application code
   imports from `@avalabs/crypto-sdk`, the bundler still has to be
   able to resolve `@avalabs/crypto-nitro` for the codegen + podspec.
2. React Native's autolinking discovers the package by scanning
   `node_modules` for `react-native.config.js` files. Our
   [`react-native.config.js`](./react-native.config.js) declares the iOS +
   Android module locations.
3. iOS: `cd ios && pod install` — CocoaPods picks up
   `AvalabsCryptoNitro.podspec`. Its `prepare_command` rsyncs
   `packages-internal/crypto-core/` into `vendor/crypto-core/` (real files,
   not a symlink — CocoaPods drops `source_files` whose realpath escapes
   the pod root). The Pod then compiles all of crypto-core + libsecp256k1
   + our bridge into a single static lib.
4. Android: `./gradlew :app:assembleDebug` — Gradle's RN autolinking adds
   this module to the build graph. `android/build.gradle` declares CMake
   as the native build system; `android/CMakeLists.txt` does
   `add_subdirectory(${CRYPTO_CORE_DIR})` to pull in crypto-core's CMake
   tree, then links the resulting `libavalabs-crypto-core.a` into our
   `libAvalabsCryptoNitro.so`.
5. At runtime, `react-native-nitro-modules` finds the auto-linked module
   and `NitroModules.createHybridObject<Crypto>('Crypto')` returns the
   C++-backed implementation. The hybrid handle is created lazily on
   first use; `init()` forces resolution so wiring problems surface
   early.

## TypeScript API

> ℹ️ Cross-platform consumers should import these from
> [`@avalabs/crypto-sdk`] — same signatures for the address-derivation
> surface, but the SDK picks the right backend at bundle time. The
> direct exports below carry `@deprecated` JSDoc tags for everything
> the SDK re-exports. The primitive surface (`sign`, `verify`,
> `signSchnorr`, `verifySchnorr`, `getPublicKey`, `getExtendedPublicKey`,
> `pointAddScalar`) is **not** re-exported by the SDK — direct import
> from this package is the supported path for those.

```ts
import {
  // Address derivation — SDK has equivalents (prefer those)
  init,
  deriveAddressesFromXpubs,
  deriveAddressesForEvm,
  deriveAddressesForSvm,
  deriveAddressesForBtc,
  deriveAddressesForAvalanche,
  MAX_BATCH_SIZE,
  // Primitives — nitro-only, no SDK equivalent
  sign,
  verify,
  signSchnorr,
  verifySchnorr,
  getPublicKey,
  getExtendedPublicKey,
  pointAddScalar,
  // Types
  type DerivedSecp256k1Addresses,
  type DerivedAvalancheAddresses,
  type ExtendedPublicKeyResult,
} from '@avalabs/crypto-nitro';

// 1. Force native-module resolution. Surfaces a clear error early if
//    autolinking isn't wired up. Idempotent after first call.
await init();

// 2. Xpub-driven batch derivation (Ledger / watch-only). BIP-32 walk +
//    address encoding runs on a native background thread so the JS
//    thread stays free. `avalancheXpubs[i]` and `accountIndices[i]`
//    pair up.
const rows: DerivedSecp256k1Addresses[] = await deriveAddressesFromXpubs(
  'xpub...', // EVM xpub at m/44'/60'/0'
  ['xpub...', 'xpub...'], // Avalanche xpubs (one per account)
  false, // isTestnet
  [0, 1], // account indices
);
// rows[i] === { accountIndex, evm, btc, avm, pvm, coreEth }

// 3. Per-chain encoders for batches of already-derived pubkeys. Each
//    hops to a native background thread; 1024-pubkey batches don't
//    block the UI. Inputs accept Uint8Array | ArrayBuffer per element.
const evmAddrs = await deriveAddressesForEvm(compressedSecp256k1Pubkeys33);
const btcAddrs = await deriveAddressesForBtc(compressedSecp256k1Pubkeys33, false);
const svmAddrs = await deriveAddressesForSvm(ed25519Pubkeys32);
const avaxBundles: DerivedAvalancheAddresses[] = await deriveAddressesForAvalanche(
  compressedAvaxPubkeys33, // drives X- / P-
  compressedEvmPubkeys33, // drives C-
  false,
);
// avaxBundles[i] === { x, p, coreEth }

// 4. Low-level primitives — nitro-only.
//    getPublicKey accepts hex / Uint8Array / ArrayBuffer / bigint.
const pubkey = getPublicKey(secretKey, /* isCompressed = */ true);
const ecdsaSig = sign(secretKey, msg32); // returns compact(64)
const okEcdsa = verify(pubkey, msg32, ecdsaSig); // accepts DER or compact

// BIP-340 Schnorr. auxRand defaults to 32 bytes from
// globalThis.crypto.getRandomValues; THROWS if no CSPRNG is available
// (silently falling back to zeros would weaken §3.3 side-channel
// protection). On RN, `import 'react-native-get-random-values'` first.
const schnorrSig = signSchnorr(msg32, secretKey);
const okSchnorr = verifySchnorr(pubkey, msg32, schnorrSig);

// Ed25519 extended public key (RFC 8032 §5.1.5). The scalar is
// reconstructed in JS from `head` as a bigint to keep secret-derived
// bytes out of the engine's interned string table.
const xpk: ExtendedPublicKeyResult = getExtendedPublicKey(secretKey);

// BIP-32 public-child tweak: P' = P + tweak·G
const child = pointAddScalar(pubkey, tweak32);

// Hard upper bound on items per batch (1024). Mirrors MAX_BATCH_SIZE
// in CryptoHybrid.cpp.
console.log(MAX_BATCH_SIZE);
```

## What this package does NOT do

- **Mnemonic / private-key ingestion via the bridge.** The bridge
  surface is deliberately pubkey-only. PBKDF2-based seed derivation and
  BIP-32 hardened walks run in caller code (`@scure/bip39` +
  `@scure/bip32`) or on a Ledger device; only pubkeys and xpubs cross
  the JS↔C++ boundary. This matches CP-14228 § Security Architecture's
  handle-based contract.
- **OpenSSL backend on iOS/Android.** Currently links the portable
  hash/HMAC backend. OpenSSL is already linked on mobile for TLS;
  switching to it would match the PR #3799 perf characteristics.
  Tracked as a follow-up — swap the `add_subdirectory(crypto-core)`
  line to set `CRYPTO_BACKEND=openssl` once that backend exists in
  crypto-core.

## Verification

This package currently has no in-package automated runtime tests — it
requires an iOS or Android build environment with the Nitro Modules
runtime to load. Verification flows:

1. **TypeScript:** `pnpm --filter @avalabs/crypto-nitro typecheck`
   (runs in CI on every push).
2. **C++ correctness:** `pnpm --filter @avalabs/crypto-core test` runs
   crypto-core's host CTest suite (32 cases covering libsecp256k1
   integration, the portable hash backend, BIP-32 derivation against
   the canonical BIP-39 *abandon × 11 about* vector, ECDSA / Schnorr /
   Ed25519 RFC test vectors, and parse_xpub against BIP-32 test
   vector 1). Any algorithmic regression in crypto-core surfaces here
   before the Nitro layer ever sees it.
3. **Mobile end-to-end (manual / local):** Install this package into a
   fresh RN app (or use the `playground.local/crypto-nitro-playground`
   workspace), run a known xpub through `deriveAddressesFromXpubs`, and
   confirm the EVM address matches an external reference (MetaMask,
   ethers.js, the canonical `0x9858EfFD232B4033E47d90003D41EC34EcaEda94`
   for the *abandon* mnemonic at `m/44'/60'/0'/0/0`).
