@bkey-inc/bmoni_embedded_sdk
v0.0.1
Published
Bmoni Embedded SDK for React Native — Ethereum wallet provisioning and signing backed by Android Keystore / iOS Secure Enclave.
Readme
@bkey-inc/bmoni_embedded_sdk
A React Native library that exposes the BMONISigner native SDKs for Ethereum wallet provisioning and transaction/message signing on Android and iOS.
Security First: Private keys are generated on-device, encrypted with a platform-managed wrapping key (Android Keystore on Android, Secure Enclave on iOS), and persisted only as ciphertext. Plaintext keys never leave the secure boundary and are zeroized in RAM after each operation.
✨ Features
- Customizable PIN policies:
initialize({ pinLength, requirePin })— opt into a custom PIN length (default6) and toggle whether sign/delete operations verify the PIN before invoking the native module (defaulttrue). - One-tap Provisioning:
initWallet()— provision a fresh secp256k1 wallet and return its EIP-55 checksummed address. The address is persisted in the SDK's secure-storage cache so subsequent launches can read it back viawalletAddress()/hasWallet(). - PIN Management:
setPin,changePin,removePin,matchPin,hasPin. The PIN is persisted as a salted PBKDF2-HMAC-SHA256 digest in platform secure storage (Android Keystore-wrapped preferences / iOS Keychain). - Robust Signing:
signTransactionHash(hashHex, pin?)— sign a pre-computed 32-byte digest (ERC-4337userOpHash, EIP-712 digest, raw transaction hash, etc.).signMessage(message, pin?)— sign a UTF-8 message with the EIP-191personal_signprefix (SIWE, login challenges, etc.).
- Lifecycle Management:
deleteWallet(pin?)— remove the encrypted private key from device storage. Idempotent at the native layer. - Typed Errors:
BmoniSignerErrorcarries the native or SDK-level error code for easy branching.
Note: All signatures are returned as 0x-prefixed 130-character hex strings in recoverable r(32) || s(32) || v(1) format with v ∈ {27, 28} and low-s normalized (EIP-2 compliant), so they can be verified server-side with ecrecover.
🚀 Getting Started
npm install @bkey-inc/bmoni_embedded_sdk
# or
yarn add @bkey-inc/bmoni_embedded_sdkThen install the iOS pods:
cd ios && pod installNo manual linking is required — the library ships a TurboModule and is picked up by React Native autolinking on both platforms, and pod install downloads the pinned BMONISigner.xcframework.
Android: declare Bkey's Maven repository
The native BMONISigner AAR is published to Bkey's own Maven repository, and your app resolves it transitively, so the repository has to be declared by the app. Add it to your root android/build.gradle:
allprojects {
repositories {
maven {
url "https://bkey-inc.github.io/package-distribution/maven"
content { includeGroup "me.bkey.ip" }
}
}
}Without it the build fails with:
Could not find me.bkey.ip:bmonisigner:1.0.0.
Required by: project ':app' > project :bkey-inc_bmoni_embedded_sdk💻 Usage
BmoniEmbeddedSdk is a static facade — call its methods directly without instantiating it. Configure it once as your app starts:
import { BmoniEmbeddedSdk } from '@bkey-inc/bmoni_embedded_sdk';
// pinLength defaults to 6. requirePin defaults to true.
BmoniEmbeddedSdk.initialize({ pinLength: 6, requirePin: true });Basic Wallet Flow
import {
BmoniEmbeddedSdk,
BmoniSignerError,
BmoniSignerErrorCode,
} from '@bkey-inc/bmoni_embedded_sdk';
try {
// 1. Provision a wallet (one-time per device). The returned address is
// also cached in secure storage; subsequent launches can read it back
// without re-provisioning.
const address =
(await BmoniEmbeddedSdk.walletAddress()) ??
(await BmoniEmbeddedSdk.initWallet());
console.log('Wallet address:', address);
// 2. Set the PIN that gates future signing operations. PINs are exactly
// `BmoniEmbeddedSdk.pinLength` (default 6) characters; other lengths
// throw `pinInvalid`.
if (!(await BmoniEmbeddedSdk.hasPin())) {
await BmoniEmbeddedSdk.setPin('123456');
}
// 3. Sign a personal message (EIP-191) — requires a matching PIN when
// requirePin is true.
const messageSig = await BmoniEmbeddedSdk.signMessage(
'Welcome to BMONI!',
'123456'
);
// 4. Sign a 32-byte digest (e.g. ERC-4337 userOpHash).
const hashSig = await BmoniEmbeddedSdk.signTransactionHash(
'0x1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8',
'123456'
);
} catch (error) {
if (!(error instanceof BmoniSignerError)) throw error;
switch (error.errorCode) {
case BmoniSignerErrorCode.walletAlreadyExists:
// Re-provision flow — destructive, the on-chain address becomes
// unrecoverable from this device.
await BmoniEmbeddedSdk.deleteWallet('123456');
await BmoniEmbeddedSdk.initWallet();
break;
case BmoniSignerErrorCode.pinMismatch:
case BmoniSignerErrorCode.pinNotSet:
// Prompt the user to (re)enter / set their PIN.
break;
}
}⚙️ Configuration
BmoniEmbeddedSdk.initialize(...) accepts:
| Option | Type | Default | Effect |
| --- | --- | --- | --- |
| pinLength | number | 6 | Number of characters required for a valid PIN. Enforced by setPin / changePin. |
| requirePin | boolean | true | Whether signMessage / signTransactionHash / deleteWallet verify a supplied PIN against the stored digest before forwarding to the native module. |
The active configuration is exposed via BmoniEmbeddedSdk.config (and the convenience getters BmoniEmbeddedSdk.pinLength / BmoniEmbeddedSdk.requirePin) so app-side UI can adapt — e.g. render a PIN input of the right length, or hide PIN flows entirely when gating is off.
requirePin: false mode
When the developer chooses to manage authentication elsewhere (biometrics, OS lockscreen, server-side challenge, …), pass requirePin: false. In that mode:
signMessage,signTransactionHashanddeleteWalletforward straight to the native module and ignore any suppliedpinargument.- The PIN management methods (
setPin,changePin, …) keep working — togglingrequirePinonly changes whether a stored PIN is enforced as a gate.
BmoniEmbeddedSdk.initialize({ requirePin: false });
await BmoniEmbeddedSdk.initWallet();
const sig = await BmoniEmbeddedSdk.signMessage('hi'); // no pin required🔍 Reading the Wallet Address Back
The native BMONISigner SDK only returns the address from the call that originally provisioned the wallet — there is no getAddress() on the native side. The TypeScript facade transparently caches the address in platform secure storage after initWallet() succeeds and wipes it on deleteWallet(), so you can recover it at any time:
const address = await BmoniEmbeddedSdk.walletAddress();
if (address !== null) {
// Render the wallet UI, fetch on-chain state, etc.
} else {
// Show the "create wallet" flow → call BmoniEmbeddedSdk.initWallet().
}🔐 PIN Management
The SDK enforces a fixed-length PIN equal to BmoniEmbeddedSdk.pinLength. Calling setPin / changePin with any other length throws BmoniSignerError with errorCode: pinInvalid. The PIN is persisted as a salted PBKDF2-HMAC-SHA256 digest (100 000 iterations) in platform secure storage; the raw PIN never touches disk.
// One-time setup.
await BmoniEmbeddedSdk.setPin('123456');
// Rotate.
await BmoniEmbeddedSdk.changePin({ currentPin: '123456', newPin: '654321' });
// Verify without throwing — useful for UI prompts.
const ok = await BmoniEmbeddedSdk.matchPin('654321');
// Tear down (e.g. on logout).
await BmoniEmbeddedSdk.removePin('654321');🛡️ Server-Side Verification
Signatures are ECDSA recoverable, so verifiers only need the address returned by initWallet():
address recovered = ecrecover(hash, v, r, s);
require(recovered == expectedAddress, "invalid signature");🛑 Error Handling
Native failures surface as a BmoniSignerError carrying both a numeric errorCode and a human-readable message. Compare against the BmoniSignerErrorCode constants:
| Constant | Hex | Meaning |
| --- | --- | --- |
| walletAlreadyExists | 0x30010010 | initWallet called while a wallet is already on disk. |
| signInvalidMessage | 0x30010001 | The supplied message could not be processed. |
| signInvalidPrivateKey | 0x30010002 | Stored key could not be recovered / decrypted. |
| signInvalidHash | 0x30010003 | Hash argument was not a valid 32-byte hex string. |
| signProcess | 0x30010004 | Generic ECDSA signing failure. |
| signKeygen | 0x30010005 | secp256k1 keypair generation failed. |
| signEip55 | 0x30010006 | EIP-55 checksum derivation failed. |
| pinNotSet | 0x40000001 | A PIN-gated call was attempted but no PIN exists. |
| pinAlreadySet | 0x40000002 | setPin called while a PIN already exists. |
| pinMismatch | 0x40000003 | The supplied PIN did not match the stored digest. |
| pinInvalid | 0x40000004 | The supplied PIN was the wrong length / missing. |
| unexpectedNativeNull | 0x50000001 | A native method that promised a non-null result did not deliver one (native-bridge bug). |
| walletAddressCacheFailed | 0x50000002 | A wallet was provisioned but its address could not be cached. The error message carries the address — persist it, or the wallet becomes unreachable from this device. |
0x3001xxxxcodes originate in the native BMONISigner SDK, and are the same on both platforms.- Storage failures (key creation, encrypt, decrypt) also come through unmapped, but the native SDK uses a different range per platform:
0x3000xxxxon iOS (Secure Enclave) and0x3002xxxxon Android (Keystore). ReaderrorCodeHexrather than matching one prefix or expecting a named constant. 0x4xxxxxxxcodes are SDK-level (PIN gating, etc.).0x5xxxxxxxcodes come from the TypeScript ↔ native bridge.
Failures that are not BMONISigner errors (a keychain problem, an unavailable native module) reject with their original React Native error, so instanceof BmoniSignerError is the reliable discriminator.
📱 Platform Support
| Android | iOS | |---------|-----| | ✅ | ✅ |
📋 Requirements
- React Native with the New Architecture enabled (default since
0.76). Built and verified against React Native0.85. - Android
minSdk 24+ - iOS
15.1+ - Android ABI: the BMONISigner AAR ships an
arm64-v8aslice only. Build forarm64-v8a(setreactNativeArchitectures=arm64-v8ainandroid/gradle.properties) and run on an arm64 device or emulator.
💡 Example
See the example directory for a working demo covering wallet provisioning, PIN management, message signing, and hash signing.
yarn
yarn example ios # or: yarn example android🤝 Contributing
See CONTRIBUTING.md.
📄 License
Copyright 2026 Bkey, Inc.
Licensed under the Apache License, Version 2.0 — you may use, modify, and distribute the SDK (including in proprietary applications) provided you preserve the copyright and license notices and comply with the terms in the LICENSE file.
For commercial support or enterprise inquiries, contact [email protected].
