> ## Documentation Index
> Fetch the complete documentation index at: https://bkey.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Signing

> Sign messages and transaction hashes with the on-device wallet.

<div className="bmoni-spine">
  <span>Lifecycle</span>
  <a data-stage="1" href="/lifecycle#1-create-the-user">User</a>
  <a data-stage="2" href="/lifecycle#2-provision-the-smart-wallet">Wallet</a>
  <a data-stage="3" href="/lifecycle#3-verify-identity-kyc">KYC</a>
  <a data-stage="4" href="/lifecycle#4-activate-the-rail">Rail</a>
  <a data-stage="5" href="/lifecycle#5-fund-the-wallet">Fund</a>
  <a data-stage="6" href="/lifecycle#6-move-money">Move money</a>
</div>

The SDK exposes two signing methods. Both require a provisioned wallet, and both verify the PIN before calling the native layer when `requirePin` is `true`.

All signatures are:

* `0x`-prefixed 130-character hex strings
* Recoverable ECDSA in `r(32) ‖ s(32) ‖ v(1)` format
* Low-s normalised (EIP-2 compliant)
* `v ∈ {27, 28}` — verifiable with `ecrecover`

***

## Sign a personal message (EIP-191)

`signMessage` prepends the `\x19Ethereum Signed Message:\n{length}` prefix before hashing and signing. This matches the `personal_sign` RPC method used by SIWE, login challenges, and most WalletConnect flows.

```ts theme={null}
const signature = await BmoniEmbeddedSdk.signMessage(
  'Welcome to BMONI!',
  '123456'
);
// 0x...130 chars...
```

When `requirePin` is `false`, omit the PIN:

```ts theme={null}
BmoniEmbeddedSdk.initialize({ requirePin: false });
const sig = await BmoniEmbeddedSdk.signMessage('Welcome to BMONI!');
```

***

## Sign a 32-byte hash

`signTransactionHash` signs a **pre-computed** 32-byte digest directly, without adding any prefix. Use it for:

* ERC-4337 `userOpHash`
* EIP-712 structured-data digests
* Raw Ethereum transaction hashes
* Any other 32-byte payload you have already hashed yourself

```ts theme={null}
const signature = await BmoniEmbeddedSdk.signTransactionHash(
  '0x1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8',
  '123456'
);
```

The argument must decode to exactly 32 bytes; the `0x` prefix is optional. Anything else throws `signInvalidHash`.

***

## Server-side verification

Signatures are ECDSA-recoverable. Your backend (or a smart contract) only needs the address from `initWallet()`:

```solidity theme={null}
// Solidity
address recovered = ecrecover(hash, v, r, s);
require(recovered == expectedAddress, "invalid signature");
```

```typescript theme={null}
// ethers.js
import { verifyMessage } from 'ethers';
const recovered = verifyMessage('Welcome to BMONI!', signature);
console.log(recovered === walletAddress); // true
```

***

## Gating pattern

A common pattern is to ask for the PIN only when `requirePin` is `true`, and skip the prompt otherwise:

```ts theme={null}
async function sign(message: string): Promise<string | null> {
  let pin: string | undefined;

  if (BmoniEmbeddedSdk.requirePin) {
    const entered = await showPinPrompt();
    if (entered === null) return null; // user dismissed
    pin = entered;
  }

  return BmoniEmbeddedSdk.signMessage(message, pin);
}
```

Because `pin` is `undefined` when the gate is off, the same call site works in both modes.

***

## Prerequisites checklist

Before calling either signing method you need:

1. A provisioned wallet (`initWallet()` succeeded, or `hasWallet()` is `true`)
2. A PIN set (`hasPin()` is `true`) — only when `requirePin` is `true`

Calling a signing method without satisfying these throws `pinNotSet`, or `pinInvalid` if you omit the PIN entirely while the gate is on.
