LazorKit LogoLazorKit
React Native SDK

useWallet

Hook API — connect, sign, manage sessions & authorities.

import { useWallet } from '@lazorkit/wallet-mobile-adapter';
// Back-compat alias — both exports point to the same hook:
import { useLazorWallet } from '@lazorkit/wallet-mobile-adapter';

useWallet returns the wallet state plus every mutation method. Each mutation opens the portal, signs the challenge with a passkey, then relays the resulting transaction through the paymaster.

Every mutation takes options

Passkey-signed methods require a trailing options: SignOptions argument with your redirectUrl and optional onSuccess / onFail callbacks. Session-signed methods (no portal) only need callbacks.

Quick reference

MethodPortal promptUse for
connectyesAuthenticate + create/load the smart wallet
disconnectnoClear local state
signAndSendTransactionyesSingle-transaction Execute
signMessageyesOff-chain passkey signature
createSessionyesMint an Ed25519 session key with optional limits
signAndSendWithSessionnoSend a tx using a stored session keypair
revokeSessionyesClose a session, refund rent
addAuthorityEd25519yesGrant an Ed25519 key admin / spender rights
removeAuthorityyesRemove any authority by PDA
listAuthoritiesnoRead all authorities on the wallet
authorizeAndExecuteyes (×1)Deferred 2-tx flow for oversized payloads (atomic, bundled)
authorizeDeferredyesTX1 only — returns DeferredPayload to run TX2 later elsewhere
executeDeferrednoTX2 only — submit from a previously-authorized payload
reclaimDeferrednoClose an expired DeferredExec and reclaim its rent
transferSolyesConvenience — SOL transfer from the vault

State

FieldTypeNotes
smartWalletPubkeyPublicKey | nullVault PDA — where funds live. Use for balances, QRs.
vaultPubkeyPublicKey | nullAlias of smartWalletPubkey.
walletPdaPubkeyPublicKey | nullWallet metadata PDA — only for raw LazorKitClient.
passkeyPubkeynumber[] | null33-byte compressed secp256r1 pubkey.
isConnectedboolean
isLoadingbooleanAny async op in progress.
isConnectingboolean
isSigningbooleanPortal round-trip in flight.
errorError | null
connectionConnectionThe @solana/web3.js connection from the provider.

Option shapes

// Passkey-signed flows (open the portal). Every mutation that prompts the user uses this.
interface SignOptions {
  redirectUrl: string;                  // your app's deep-link scheme
  onSuccess?: (result: any) => void;
  onFail?: (error: Error) => void;
}

// No-passkey flows (session / executeDeferred / reclaimDeferred). No redirectUrl needed.
interface TxCallbacks {
  onSuccess?: (signature: string) => void;
  onFail?: (error: Error) => void;
}

Connection

connect

Open the portal, authenticate the user with their passkey, and either load the existing smart wallet or create a fresh one on-chain.

const wallet = await connect({ redirectUrl: 'myapp://home' });
// wallet.smartWallet   → vault PDA (base58)
// wallet.walletPda     → metadata PDA
// wallet.walletDevice  → authority PDA for this passkey

Returns Promise<WalletInfo>.

disconnect

await disconnect({ onSuccess, onFail });

Clears the persisted state. The on-chain wallet is untouched — reconnect any time.


Transactions

signAndSendTransaction

Single-transaction execution. For payloads up to ~574 bytes of inner instructions.

const sig = await signAndSendTransaction(
  {
    instructions: [ix],
    transactionOptions: {
      feeToken: 'USDC',              // optional: pay fees in any SPL mint
      computeUnitLimit: 400_000,
      addressLookupTableAccounts: [alt],
      clusterSimulation: 'devnet',   // portal-side preview cluster
    },
  },
  { redirectUrl: 'myapp://callback' },
);

Returns Promise<string> — confirmed signature.

authorizeAndExecute

Two-transaction deferred flow for payloads that don't fit in a single transaction (Jupiter swaps with routes + ATAs, multi-CPI batches). One passkey prompt covers both txs, and the SDK submits them back-to-back. Use this when everything runs in the same process.

const sig = await authorizeAndExecute(
  {
    instructions: jupiterSwapIxs,
    expiryOffset: 300,                       // slots the authorization stays valid (~2m)
    transactionOptions: { clusterSimulation: 'devnet' },
  },
  { redirectUrl: 'myapp://callback' },
);

How it works

TX1 Authorize records a SHA-256 hash of your instruction set. TX2 ExecuteDeferred replays those exact instructions — the program rejects any mismatch. authorizeAndExecute submits both transactions automatically and returns TX2's signature.


authorizeDeferred

TX1 only. Passkey-signed Authorize. Returns the payload needed for TX2 so you can run it elsewhere — a server relayer, another device, a scheduled job.

const { signature, deferredPayload, deferredExecPda, counter } = await authorizeDeferred(
  {
    instructions: jupiterSwapIxs,
    expiryOffset: 600,                       // 10 min
    transactionOptions: { clusterSimulation: 'devnet' },
  },
  { redirectUrl: 'myapp://callback' },
);

Returns Promise<AuthorizeResult>.

executeDeferred

TX2 only. No passkey prompt. Submit ExecuteDeferred from a payload produced by a prior authorizeDeferred call (either from this device, or deserialized from a wire format — see serializeDeferredPayload).

// Same machine — use the object directly:
const txSig = await executeDeferred({
  deferredPayload,                           // from authorizeDeferred result
  refundDestination: smartWalletPubkey,      // optional — rent goes to vault
  transactionOptions: { clusterSimulation: 'devnet' },
});

reclaimDeferred

No passkey prompt. Close an expired DeferredExec PDA and recover its rent. Gated on the original payer.

await reclaimDeferred({
  deferredExecPda,                           // from authorizeDeferred result
  refundDestination: smartWalletPubkey,
});

Cross-machine deferred flows

Any party holding a serialized deferredPayload can submit TX2 — the program verifies the instruction / accounts hash. Use serializeDeferredPayload() to get a JSON-safe string; deserializeDeferredPayload() to reconstruct it before calling executeDeferred.

import {
  serializeDeferredPayload,
  deserializeDeferredPayload,
} from '@lazorkit/wallet-mobile-adapter';

// Device A:
const { deferredPayload } = await authorizeDeferred(...);
await fetch('/relayer', { method: 'POST', body: serializeDeferredPayload(deferredPayload) });

// Relayer / device B:
const payload = deserializeDeferredPayload(await req.text());
const sig = await executeDeferred({ deferredPayload: payload });

transferSol

Shortcut — SystemProgram.transfer from the vault.

await transferSol(
  { recipient: new PublicKey('…'), lamports: 50_000_000 },
  { redirectUrl: 'myapp://callback' },
);

Session keys

Sessions let you skip the passkey prompt for a scoped set of operations. Create once with a passkey, then sign many transactions with a local Ed25519 key.

createSession

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

const sessionKp = Keypair.generate();
const currentSlot = BigInt(await connection.getSlot());

const { signature, sessionPda } = await createSession(
  {
    sessionKey: sessionKp.publicKey,
    expiresAtSlot: currentSlot + 216_000n,    // ~24h on mainnet
    actions: [
      Actions.solRecurringLimit({ limit: 1_000_000_000n, window: 216_000n }),
      Actions.solMaxPerTx(500_000_000n),
      Actions.programWhitelist(JUPITER_PROGRAM_ID),
    ],
  },
  { redirectUrl: 'myapp://callback' },
);

Persist sessionKp.secretKey securely (e.g. expo-secure-store) along with sessionPda.

Actions are immutable

Once a session is created, its action list can't be modified. To change limits, revoke the session and create a new one.

Returns Promise<{ signature: string; sessionPda: PublicKey }>.

signAndSendWithSession

Send a transaction using the stored session keypair. No portal prompt.

const sig = await signAndSendWithSession({
  sessionKeypair: sessionKp,
  sessionPda,
  instructions: [ix],
  transactionOptions: { clusterSimulation: 'devnet' },
});

Fails with 0xbd0 (ActionSolLimitExceeded) or related action errors once limits are hit. See Troubleshooting.

revokeSession

await revokeSession(
  {
    sessionPda,
    refundDestination: smartWalletPubkey,   // send rent back to the vault
  },
  { redirectUrl: 'myapp://callback' },
);

Authorities

Give additional keys access to the wallet — typically an Ed25519 device key for a desktop or hardware companion.

addAuthorityEd25519

import { ROLE_ADMIN, ROLE_SPENDER } from '@lazorkit/wallet-mobile-adapter';

const { signature, newAuthorityPda } = await addAuthorityEd25519(
  { newEd25519Pubkey: deviceKp.publicKey, role: ROLE_SPENDER },
  { redirectUrl: 'myapp://callback' },
);

Role defaults to ROLE_SPENDER (execute only). Use ROLE_ADMIN to also allow session / authority management.

removeAuthority

await removeAuthority(
  { targetAuthorityPda, refundDestination: smartWalletPubkey },
  { redirectUrl: 'myapp://callback' },
);

You cannot remove the last Owner. Transfer ownership first (use LazorKitClient directly — transferOwnership isn't exposed through the hook yet).

listAuthorities

Read all authority accounts on the connected wallet. No passkey prompt.

const authorities = await listAuthorities();
// [{
//   authorityPda, authorityType, role,
//   credential, secp256r1Pubkey?
// }, …]

Messages

signMessage

Off-chain passkey signature over an arbitrary string. Ideal for session-less auth.

const { signature, signedPayload } = await signMessage(
  'Sign in as user-123',
  { redirectUrl: 'myapp://callback' },
);

Both values are base64 strings. Verify server-side against the on-chain authority pubkey.

Returns Promise<{ signature: string; signedPayload: string }>.


Low-level escape hatch

The SDK re-exports everything from the program module — use these for flows the hook doesn't cover (admin protocol ops, manual prepare/finalize, custom signer layouts):

import {
  LazorKitClient,
  // PDAs
  findWalletPda, findVaultPda, findAuthorityPda,
  findSessionPda, findDeferredExecPda,
  // On-chain state readers (single RPC, combined counter + pubkey)
  readAuthorityState, readAuthorityCounter, readAuthorityPubkey,
  // Session policy builders
  Actions,
  // Constants
  ROLE_OWNER, ROLE_ADMIN, ROLE_SPENDER,
  AUTH_TYPE_ED25519, AUTH_TYPE_SECP256R1,
  // Cross-machine deferred helpers
  serializeDeferredPayload, deserializeDeferredPayload,
} from '@lazorkit/wallet-mobile-adapter';

const client = new LazorKitClient(connection);
const [walletPda] = findWalletPda(userSeed);
const authorities = await client.listAuthorities(walletPda);

Use `readAuthorityState` for custom flows

If you're building your own prepare/finalize loop, call readAuthorityState(connection, authorityPda) to get { counter, publicKeyBytes } in a single getAccountInfo — saves an RPC round-trip over calling readAuthorityCounter and readAuthorityPubkey separately.