LazorKit LogoLazorKit
React Native SDK

Types

TypeScript interfaces, constants, and session action builders.

Every public type grouped by what you're doing. Import anything below from @lazorkit/wallet-mobile-adapter directly.

Wallet state

WalletInfo

The persisted record written to AsyncStorage after connect.

interface WalletInfo {
  credentialId: string;         // WebAuthn credential ID (base64)
  passkeyPubkey: number[];      // 33-byte compressed secp256r1 pubkey
  expo: string;
  platform: string;             // 'ios' | 'android' | …

  /** Vault PDA (base58) — the address funds live at. Show this to users. */
  smartWallet: string;

  /** Wallet metadata PDA (base58). Needed for raw LazorKitClient calls. */
  walletPda: string;

  /** Authority PDA (base58) tied to this passkey credential. */
  walletDevice: string;

  /** 1 = made before LazorKit v2, 2 = since. Absent on records saved before 2.0.0 (= v1). */
  protocolVersion?: 1 | 2;
}

On mobile, smartWallet is the vault PDA (since the April 2026 store format; older persisted records are migrated on load). On the web SDK the same field is the wallet PDA — see Networks & versions.

WalletConfig / LazorKitProviderProps

interface WalletConfig {
  portalUrl: string;
  configPaymaster: { paymasterUrl: string; apiKey?: string };   // v2 wallets
  v1ConfigPaymaster?: { paymasterUrl: string; apiKey?: string }; // v1 wallets (2.0.0+)
  rpcUrl?: string;
  cluster?: 'mainnet' | 'devnet';                                // when rpcUrl does not say (2.0.0+)
  rpId?: string;
}

interface LazorKitProviderProps {
  rpcUrl?: string;
  portalUrl?: string;
  configPaymaster?: { paymasterUrl: string; apiKey?: string };
  v1ConfigPaymaster?: { paymasterUrl: string; apiKey?: string };
  cluster?: 'mainnet' | 'devnet';
  rpId?: string;
  isDebug?: boolean;
  children: ReactNode;
}

Wallet confirmation

Version note

Everything in this section is in @lazorkit/wallet-mobile-adapter 2.0.0 and later. Earlier versions — 1.x, and the 2.0.0 betas up to 2.0.0-beta.4 — do not have it. How it is used: Wallet Confirmation.

Provider and connect options

interface LazorKitProviderProps {
  // ...the props above, plus:
  onConfirmWallet?: OnConfirmWallet;      // default 'builtin'
  trustedAuthorities?: readonly string[]; // your own Ed25519 keys, base58
  watchMints?: readonly string[];         // SPL Token mints your app receives, base58
}

interface ConnectOptions {
  redirectUrl: string;
  confirmWallet?: string;                 // vault (or wallet PDA) the user chose
  onConfirmWallet?: OnConfirmWallet;      // overrides the provider's for this call
  onSuccess?: (wallet: WalletInfo) => void;
  onFail?: (error: Error) => void;
}

WalletChoice

A wallet the passkey is proven to hold a key of, that connect will not adopt without the user. Show the vault address; the order is not a recommendation.

interface WalletChoice {
  wallet: string;                         // wallet PDA
  vault: string;                          // vault PDA — where the funds are. Show this one.
  version: 1 | 2;                         // 1 = made before LazorKit v2, not migrated yet
  lamports: number;                       // vault balance
  signatureCount: number;                 // 0 = not used with this passkey yet
  vaultIsSystemAccount: boolean;          // false: vault handed to another program
  otherAuthorities: {                     // every authority except this passkey's
    address: string;                      // the authority account
    type: 'passkey' | 'key';              // passkey = secp256r1, key = Ed25519
    role: 'owner' | 'admin' | 'spender' | 'unknown'; // 'spender' = Delegate
    key?: string;                         // 'key' only: the Ed25519 key
    sameRelyingParty?: boolean;           // 'passkey' only: same relying party
    trusted: boolean;                     // in trustedAuthorities; a passkey never is
  }[];
  liveSessions: {                         // sessions that can still sign
    address: string;
    sessionKey: string | null;            // null when unreadable
    approxExpiresAt: number | null;       // ms since the epoch (slots × 400 ms)
    trusted: boolean;
  }[];
  pendingDeferred: {                      // authorized, not yet run or expired. Never trusted.
    address: string;
    approxExpiresAt: number | null;
  }[];
  tokenGrants: {                          // rights over the vault's token accounts
    tokenAccount: string;
    tokenProgram: string;
    mint: string | null;
    kind: 'delegate' | 'closeAuthority' | 'owner' | 'unreadable';
    grantee: string | null;
    trusted: boolean;
  }[];
}

ConfirmWalletRequest

interface ConfirmWalletRequest {
  credentialId: string;
  candidates: WalletChoice[];
}

ConfirmWalletHandler

Your own chooser. Resolve with the chosen candidate's vault (or wallet), or null when the user recognises none of them. Never choose for the user.

type ConfirmWalletHandler = (
  request: ConfirmWalletRequest,
) => Promise<{ wallet: string } | null> | { wallet: string } | null;

OnConfirmWallet

type OnConfirmWallet = ConfirmWalletHandler | 'builtin' | 'throw';

<WalletChooser />

The built-in chooser, for rendering inside your own <Modal> on iOS — while it is mounted it draws instead of the provider's. See Wallet Confirmation › iOS.

Errors

class WalletNeedsConfirmationError extends Error {      // onConfirmWallet: 'throw'
  readonly credentialId: string;
  readonly candidates: WalletChoice[];
}
class WalletConfirmationDeclinedError extends Error {}  // the user chose none; nothing saved
class PortalCancelledError extends LazorKitError {}     // code 'PORTAL_CANCELLED'
class WalletChooserNotShownError extends LazorKitError {} // code 'WALLET_CHOOSER_NOT_SHOWN' (iOS)

Options & payloads

SignOptions

Trailing argument on every passkey-signed method (opens the portal).

interface SignOptions {
  redirectUrl: string;                    // your app deep-link scheme
  onSuccess?: (result: any) => void;
  onFail?: (error: Error) => void;
}

DisconnectOptions

disconnect's argument, on the hook and on the store (2.3.0+).

interface DisconnectOptions {
  onSuccess?: () => void;
  onFail?: (error: Error) => void;
}

TxCallbacks

Trailing argument on non-passkey flows — session-signed txs, executeDeferred, reclaimDeferred. No redirectUrl needed.

interface TxCallbacks {
  onSuccess?: (signature: string) => void;
  onFail?: (error: Error) => void;
}

Transaction payloads

interface TransactionOptions {
  feeToken?: string;                                       // SPL mint base58
  addressLookupTableAccounts?: AddressLookupTableAccount[];
  computeUnitLimit?: number;
  clusterSimulation?: 'devnet' | 'mainnet';
}

interface SignAndSendTransactionPayload {
  instructions: TransactionInstruction[];
  transactionOptions?: TransactionOptions;
}

interface AuthorizeExecutePayload extends SignAndSendTransactionPayload {
  expiryOffset?: number;                  // slots after TX1, 10–9000; default 1500
}

// For `authorizeDeferred` — TX1 only, returns payload for later TX2.
interface AuthorizePayload {
  instructions: TransactionInstruction[];
  expiryOffset?: number;                  // slots after TX1, 10–9000; default 1500
  transactionOptions?: TransactionOptions;
}

// Returned from `authorizeDeferred`.
interface AuthorizeResult {
  signature: string;                      // TX1 signature
  deferredPayload: DeferredPayload;       // feed to executeDeferred later
  deferredExecPda: PublicKey;
  counter: number;
}

// For `executeDeferred` — TX2 only.
interface ExecuteDeferredPayload {
  deferredPayload: DeferredPayload;
  refundDestination?: PublicKey;          // leave unset: the program accepts only the Authorize payer
  transactionOptions?: TransactionOptions;
}

// For `reclaimDeferred` — cleanup path, after the window.
interface ReclaimDeferredPayload {
  deferredExecPda: PublicKey;
  refundDestination?: PublicKey;          // leave unset: the program accepts only the Authorize payer
}

interface SessionSignPayload {
  sessionKeypair: Keypair;                // Ed25519 keypair from createSession
  sessionPda: PublicKey;
  instructions: TransactionInstruction[];
  transactionOptions?: TransactionOptions;
}

// Internal shape produced by authorize*/used by executeDeferred. Re-exported from
// the SDK so you can persist it across sessions / machines via the wire helpers.
// Serialized, it carries `version: 2` and is refused across the v1/v2 boundary.
interface DeferredPayload {
  walletPda: PublicKey;
  deferredExecPda: PublicKey;
  compactInstructions: {
    programIdIndex: number;
    accountIndexes: number[];
    data: Uint8Array;
  }[];
  remainingAccounts: {
    pubkey: PublicKey;
    isSigner: boolean;
    isWritable: boolean;
  }[];
  executor?: PublicKey;                   // who the accounts hash expects to send TX2
  refundDestination?: PublicKey;          // the Authorize payer
}

// Wire helpers (JSON-safe strings) for cross-machine deferred flows.
serializeDeferredPayload(p: DeferredPayload): string;
deserializeDeferredPayload(s: string): DeferredPayload;

interface TransferSolPayload {
  recipient: PublicKey;
  lamports: bigint | number;
  transactionOptions?: TransactionOptions;
}

Session payloads

interface CreateSessionPayload {
  sessionKey: PublicKey;                  // fresh Ed25519 public key; you keep the secret key
  expiresAtSlot: bigint;                  // absolute slot, at most 6,480,000 slots ahead
  actions?: SessionAction[];              // required unless unrestricted. Name every asset it
                                          // may spend; at most 16, within 244 bytes (not checked)
  unrestricted?: boolean;                 // a session with no limits, explicitly (2.0.0+)
}

interface RevokeSessionPayload {
  sessionPda: PublicKey;
  refundDestination?: PublicKey;          // default: paymaster fee payer
}

Authority payloads

interface AddAuthorityPayload {
  newEd25519Pubkey: PublicKey;
  role: number;                           // required, no default: ROLE_SPENDER (2, a Delegate) | ROLE_ADMIN (1) | ROLE_OWNER (0, v1 only)
  policy?: Uint8Array;                    // required for ROLE_SPENDER on v2: serializeActions([...]).
                                          // Names every asset the key may spend; within 244 bytes
  unrestricted?: boolean;                 // required to add a key to a v1 wallet
}

interface RemoveAuthorityPayload {
  targetAuthorityPda: PublicKey;
  refundDestination?: PublicKey;
}

interface AuthorityEntry {
  authorityPda: PublicKey;
  authorityType: number;                  // 0 = ed25519, 1 = secp256r1
  role: number;                           // 0 = Owner, 1 = Admin, 2 = Delegate
  credential: Uint8Array;                 // 32 bytes
  secp256r1Pubkey?: Uint8Array;           // 33 bytes, secp256r1 only
}

type ListAuthoritiesResult = AuthorityEntry[];

Changed in 2.3.0

Breaking: AddAuthorityPayload.role is required. In 2.2.1 it was optional and defaulted to ROLE_SPENDER. See useWallet › addAuthorityEd25519.


Signed messages

Exported from 2.3.1, the same as @lazorkit/wallet's. How they are used: useWallet › Messages.

const SIGNED_MESSAGE_DOMAIN = 'LazorKit signed message v1';  // the challenge's tag
const OWNERSHIP_PROOF_DOMAIN = 'LazorKit ownership proof v1'; // connect's proofs

type SignedMessageInput = string | Uint8Array;  // a string is signed as its UTF-8 bytes

// What signMessage resolves with: the passkey's assertion over signedMessageChallenge(message).
interface SignMessageResult {
  readonly signature: string;               // P-256, 64 bytes (r || s, low-S), base64
  readonly signedPayload: string;           // authenticatorData || SHA-256(clientDataJSON), base64
  readonly clientDataJsonBase64: string;    // its challenge is base64url(signedMessageChallenge(message))
  readonly authenticatorDataBase64: string;
}

interface VerifySignedMessageParams {
  readonly message: SignedMessageInput;
  readonly signature: string | Uint8Array;  // 64 bytes, or their base64
  readonly clientDataJsonBase64: string;
  readonly authenticatorDataBase64: string;
  readonly signedPayload?: string;          // checked when given
  readonly publicKey: string | Uint8Array | ArrayLike<number>; // P-256: 33, 65 or 64 bytes, or base64. Read it from chain.
  readonly rpId?: string;
  readonly origin?: string;
}

interface VerifyWalletMessageParams extends Omit<VerifySignedMessageParams, 'publicKey' | 'rpId'> {
  readonly connection: Connection;
  readonly wallet: string | PublicKey;      // the claimed wallet: vault or wallet PDA
  readonly credentialId: string;            // base64
  readonly rpId: string;                    // the passkey's relying party (the provider's rpId)
  readonly cluster?: 'mainnet' | 'devnet';
}

function signedMessageChallenge(message: SignedMessageInput): Uint8Array; // 58 bytes
function verifySignedMessage(params: VerifySignedMessageParams): boolean;  // offline; never throws
function verifyWalletMessage(params: VerifyWalletMessageParams): Promise<boolean>; // key read from chain
function createOwnershipChallenge(): Uint8Array; // OWNERSHIP_PROOF_DOMAIN || 32 random bytes, 59 bytes

LazorWalletHook.signMessage and the store's resolve with SignMessageResult (2.3.1; it was { signature, signedPayload }).


Session actions

Actions are immutable policies attached to a session at creation — or, serialized with serializeActions, to a Delegate authority. The on-chain program enforces them on every Execute signed by that key. From the v2 program release that adds errors 3037 and 3038, an asset no action names cannot leave the wallet: SOL needs a sol* action (rent the vault pays counts) and each token a token* action for its mint. See Session Keys › What a policy bounds.

ActionBytes
solLimit, solMaxPerTx19
solRecurringLimit43
tokenLimit, tokenMaxPerTx51
tokenRecurringLimit75
programWhitelist, programBlacklist43

Keep a session's actions, or a policy, within 244 bytes (serializeActions(actions).length): that is what fits in the transaction beside the passkey's response. The adapter does not check it before the portal opens.

Builder — Actions

Prefer these helpers over hand-building the raw object:

import { Actions } from '@lazorkit/wallet-mobile-adapter';

// SOL
Actions.solLimit(remainingLamports, expiresAt?)
Actions.solRecurringLimit({ limit, window, expiresAt? })
Actions.solMaxPerTx(maxLamports, expiresAt?)

// SPL tokens
Actions.tokenLimit({ mint, remaining, expiresAt? })
Actions.tokenRecurringLimit({ mint, limit, window, expiresAt? })
Actions.tokenMaxPerTx({ mint, max, expiresAt? })

// CPI filters
Actions.programWhitelist(programId, expiresAt?)
Actions.programBlacklist(programId, expiresAt?)

Raw enum / union

enum SessionActionType {
  SolLimit            = 1,
  SolRecurringLimit   = 2,
  SolMaxPerTx         = 3,
  TokenLimit          = 4,
  TokenRecurringLimit = 5,
  TokenMaxPerTx       = 6,
  ProgramWhitelist    = 10,
  ProgramBlacklist    = 11,
}

type SessionAction =
  | { type: SessionActionType.SolLimit; remaining: bigint; expiresAt?: bigint }
  | { type: SessionActionType.SolRecurringLimit; limit: bigint; window: bigint; expiresAt?: bigint }
  | { type: SessionActionType.SolMaxPerTx; max: bigint; expiresAt?: bigint }
  | { type: SessionActionType.TokenLimit; mint: PublicKey; remaining: bigint; expiresAt?: bigint }
  | { type: SessionActionType.TokenRecurringLimit; mint: PublicKey; limit: bigint; window: bigint; expiresAt?: bigint }
  | { type: SessionActionType.TokenMaxPerTx; mint: PublicKey; max: bigint; expiresAt?: bigint }
  | { type: SessionActionType.ProgramWhitelist; programId: PublicKey; expiresAt?: bigint }
  | { type: SessionActionType.ProgramBlacklist; programId: PublicKey; expiresAt?: bigint };

Common limits (cheat sheet)

Slot windowat 400 ms a slot
9_000n1 hour
54_000n6 hours
216_000n1 day
1_512_000n7 days

A slot's length varies by cluster and load (devnet ran near 230 ms in September 2026), so these are approximations.


Roles & auth types

export const ROLE_OWNER   = 0;   // manages every authority, including other Owners
export const ROLE_ADMIN   = 1;   // manages Delegates and sessions
export const ROLE_SPENDER = 2;   // a Delegate: manages nothing, spends within its policy

export const AUTH_TYPE_ED25519   = 0;
export const AUTH_TYPE_SECP256R1 = 1;

Errors

All exported as classes. When each is thrown and what to do: Errors.

class LazorKitError extends Error { code?: string }
class WalletConnectionError extends LazorKitError {}   // "Already connecting", bad redirect
class SigningError          extends LazorKitError {}   // another sign action is running; no wallet
class PortalCancelledError  extends LazorKitError {}   // the portal was closed
class WalletChooserNotShownError extends LazorKitError {} // iOS: chooser under another modal

// Sending (2.1.0+)
class TransactionFailedError extends Error {           // landed and failed
  readonly signature: string;
  readonly transactionError: TransactionError;
  readonly slot: number;
  readonly logs?: string[];
}
class TransactionExpiredError extends Error { readonly signature: string }
class TransactionOutcomeUnknownError extends Error { readonly signature: string | undefined }
class ConfirmationTimeoutError extends TransactionOutcomeUnknownError { readonly waitedMs: number }
class PreviousTransactionPendingError extends Error { readonly pendingSignature: string | undefined }
class SignatureReusedError extends Error { readonly code: 3006 }
class PaymasterError extends Error {
  readonly code?: number; readonly data?: unknown; readonly httpStatus?: number;
  readonly signature?: string; maybeSent: boolean;
}

// Deferred execution (2.2.0+)
class DeferredExpiredError extends Error {
  readonly code: 3014;
  readonly deferredExecPda: PublicKey;             // holds the rent: pass it to reclaimDeferred
  readonly authorizeSignature: string | undefined; // TX1, when the same call sent it
  readonly expiresAtSlot: bigint | undefined;
}
interface DeferredFailureContext { deferredExecPda?: PublicKey; authorizeSignature?: string; expiresAtSlot?: bigint } // 2.2.1+

// v1 wallets (2.0.0+)
class V1WalletRetiredError extends Error { readonly code: 4018 }
class V1WalletMigratedError extends Error {}

// A session send moving an asset its actions do not name (2.4.0+), from the v2 program
// release that adds errors 3037 and 3038. Nothing in it ran.
type PolicySigner = 'session' | 'authority';  // always 'session' from this adapter
class UnlistedSolOutflowError extends Error {   // "This session is not allowed to spend SOL"
  readonly code: 3037;
  readonly signer: PolicySigner;
  readonly cause?: unknown;                     // the failure as it came
}
class UnlistedTokenOutflowError extends Error { // "This session is not allowed to spend this token"
  readonly code: 3038;
  readonly signer: PolicySigner;
  readonly cause?: unknown;
}

// Re-exported from @lazorkit/sdk-legacy
class MinContextSlotNotReachedError extends Error {}

Also exported: isDeferredExpiredError, isSignatureReusedError and isRetiredDeploymentError, each true for its class from any copy of the package, also wrapped in cause or a wallet-adapter WalletError (since 2.3.0; in 2.2.1 the last two read only the error's text and missed the adapter's own errors); since 2.4.0 isUnlistedSolOutflowError and isUnlistedTokenOutflowError, which also recognise a raw 3037 / 3038 (0xbdd, 0xbde) unless its logs name another program; SIGNATURE_REUSED_CODE, DEFERRED_EXPIRED_CODE, RETIRED_DEPLOYMENT_CODE, UNLISTED_SOL_OUTFLOW_CODE (3037), UNLISTED_TOKEN_OUTFLOW_CODE (3038), MIN_DEFERRED_EXPIRY_SLOTS (10) and MAX_DEFERRED_EXPIRY_SLOTS (9000). ERROR_NAMES and errorFromCode name 3036–3038 and 4018 since 2.4.0.

On-chain program errors appear inside error messages and logs as hex codes — 0xbd0, 0xbd1, etc. See Errors › Program error codes.