LazorKit LogoLazorKit
React SDK

useWallet

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

import { useWallet } from '@lazorkit/wallet';

Returns wallet state and every mutation method. Each passkey method opens the LazorKit portal (a dialog iframe, or a popup) where the user approves with their passkey; there is no redirectUrl argument. Every send resolves once its transaction is confirmed — see Sending transactions.

Quick reference

MethodPasskey promptUse for
connectfirst timeAuthenticate; returns the stored wallet without a prompt
disconnectnoClear the stored wallet and the kept session key
signAndSendTransactionyesSingle-tx Execute via paymaster
signMessageyesOff-chain signature; check it with verifyWalletMessage
verifyMessagenoDeprecated. Never use it to authenticate
createSessionyesMint a scoped Ed25519 session (SpendingLimits)
signAndSendWithSessionnoSend using the kept session key
revokeSessionyesClose a session, refund rent
addAuthorityyesAdd an Ed25519 key with the rank you name (key generated + kept)
signAndSendWithAuthoritynoSend signed by the kept Ed25519 authority
removeAuthorityyesRemove an authority by PDA
authorizeAndExecuteyes (×1)Deferred 2-tx flow for oversized payloads
authorizeDeferredyesTX1 only — returns a serialized payload for a later TX2
executeDeferrednoTX2 only — submit a previously authorized payload

All send-tx methods take { instructions, transactionOptions? }. transactionOptions accepts addressLookupTableAccounts, txVersion: 'legacy' | 'v0' (default 'v0') and clusterSimulation; feeToken and computeUnitLimit are in the types but ignored by the web SDK (3.4.0).

Every method except verifyMessage also takes onSuccess / onFail (in the payload; connect, disconnect, signMessage and removeAuthority in their options). One of them runs per call, once the action is over, right before the promise settles; see Sending transactions › Callbacks.


State

FieldTypeNotes
vaultPubkeyPublicKey | nullThe vault — where funds live. Show it, fund it, use it as fromPubkey.
smartWalletPubkeyPublicKey | nullWallet PDA (internal). Never send funds here.
walletWalletInfo | nullFull stored record. wallet.vaultPda is the vault.
protocolVersion1 | 2 | nullThe connected wallet's protocol; null while disconnected.
isConnectedboolean
isLoadingbooleanisConnecting || isSigning || …
isConnectingboolean
isSigningbooleantrue from the prompt until the transaction is confirmed.
errorError | nullThe last action's error.

smartWalletPubkey ≠ vault

On the web SDK, smartWalletPubkey is the wallet PDA the program uses to resolve authorities. The SOL-holding account is vaultPubkey. (The React Native SDK names them the other way round; vaultPubkey is the vault in both.) Do not derive the vault with findVaultPda: it derives a v2 mainnet address, wrong for v1 wallets and devnet.


Connection

connect

const wallet = await connect();
  • A stored wallet is returned as stored, without the portal. A stored v1 wallet that has since been migrated is dropped, and connect runs afresh.
  • No stored wallet: the portal opens. connect proves the passkey, looks up its wallet on v1 and v2, and uses it on its own only when it is the one wallet this passkey has signed for and nothing else can spend from it; otherwise it asks the user. A passkey with no wallet gets a new v2 wallet.
  • An existing passkey with no wallet (made on another device or browser) costs one extra prompt (3.1.0+): its public key is recovered from two of its signatures, so the new wallet is created for the right key.
OptionTypeNotes
confirmWalletstringVault (or wallet PDA) the user chose. Within 2 minutes of WalletNeedsConfirmationError, no second passkey prompt. Naming a wallet the passkey is not proven to hold throws.
onConfirmWallet'builtin' | 'throw' | ConfirmWalletHandlerOverrides the provider's for this call.
feeMode'paymaster' | 'user'Accepted, ignored by the web SDK.

Returns Promise<WalletInfo>.

connect can reject with WalletNeedsConfirmationError, WalletConfirmationDeclinedError or PortalCancelledError, and with "Already connecting" while one is running. Full flow: Wallet Confirmation.

// After catching WalletNeedsConfirmationError (onConfirmWallet: 'throw'):
await connect({ confirmWallet: 'CHOSEN_VAULT_ADDRESS' });

disconnect

await disconnect();
await disconnect({ keepSessionKeys: true }); // keep the session key for the next sign-in

Clears the stored wallet. The on-chain wallet is untouched. A connect still running rejects with PortalCancelledError and saves nothing. An action still running is not abandoned: it keeps isSigning until it ends, but a session or authority send among them neither signs nor sends after the disconnect, even if the same wallet is connected again by then (3.4.0; it rejects with KeyWalletMismatchError).

It also deletes the session key the SDK keeps (from IndexedDB, the page's memory and any plaintext an earlier release left), whichever wallet it belongs to, and a createSession still waiting for its transaction keeps no key once it lands. The authority key is kept, and signs only once the same wallet is connected again; removeAuthority and forgetStoredKeys() delete it. disconnect acts in its own tab: another tab of the app stays connected. See What the SDK stores. Since 3.3.1 a sign-out through wallet-adapter (LazorkitWalletAdapter.disconnect()) or the Wallet Standard (standard:disconnect) deletes the session key the same way. Since 3.4.0 it also disconnects this store, as disconnect() does: useWallet() shows no wallet, a connect the store is running rejects with PortalCancelledError, error is cleared, and a kept key signs only once its wallet is connected again. See Wallet Standard › Disconnecting.

OptionTypeNotes
keepSessionKeysbooleanKeep the session key (default false: it is deleted).
onSuccess() => voidRuns once the disconnect is over.
onFail(error: Error) => voidRuns with the error the promise rejects with.

Changed in 3.4.0

In 3.3.1 a wallet-adapter or Wallet Standard disconnect left useWallet() connected, so the authority key (and a session key kept with keepSessionKeys) went on signing for the wallet, and a send that had loaded its key could still sign and send after such a disconnect. The store's own disconnect() stopped such a send only at signing.

Changed in 3.3.0

In 3.2.1 disconnect took no options, cleared isSigning while an action ran, and left the session and authority keys in localStorage.


Transactions

signAndSendTransaction

Single-transaction execution, for inner instructions of up to about 345 bytes without a lookup table (109 fewer when Chrome pads the passkey's clientDataJSON). Anything larger goes through authorizeAndExecute, and in practice so does a Jupiter swap. Measured 2026-09-29 with @lazorkit/sdk-legacy 1.2.0 and the portal in an iframe; from a popup, clientDataJSON is about 40 bytes shorter.

const sig = await signAndSendTransaction({
  instructions: [ix],
  transactionOptions: {
    addressLookupTableAccounts: [alt],  // v0 only
    clusterSimulation: 'devnet',        // the portal's preview
    txVersion: 'v0',                    // 'legacy' | 'v0' (default 'v0')
  },
});

Returns Promise<string> — the signature, once the transaction is confirmed. Rejects with TransactionFailedError, TransactionExpiredError, TransactionOutcomeUnknownError (ConfirmationTimeoutError), PreviousTransactionPendingError, SignatureReusedError, PaymasterError or PortalCancelledError — what each means: Errors.

isSigning stays true until the transaction is confirmed; another hook call meanwhile rejects with "Already signing". await one send before starting the next.

legacy vs v0

The SDK defaults to v0 (matches the mobile SDK and supports ALT). Use txVersion: 'legacy' only if your downstream RPC/indexer requires the older wire format. ALT is v0-only — passing addressLookupTableAccounts with legacy throws.

authorizeAndExecute

Two-transaction deferred flow for payloads that don't fit in a single tx (Jupiter swaps with routes + ATAs, multi-CPI batches). One passkey prompt covers both txs; TX2 is sent once TX1 is confirmed.

const sig = await authorizeAndExecute({ instructions: jupiterSwapIxs });

How it works

TX1 Authorize records a SHA-256 hash of your instruction set. TX2 ExecuteDeferred replays the exact instructions — any mismatch is rejected. The SDK submits both and returns TX2's signature.

authorizeDeferred + executeDeferred

Same protocol mechanism as authorizeAndExecute, but split into two callable steps so you can defer or delegate TX2. authorizeDeferred returns a serialized payload that any party can redeem later — no passkey required for the second step.

// Step 1 — on the device with the passkey (requires user gesture):
const { signature: authSig, deferredPayload } = await authorizeDeferred({
  instructions: jupiterSwapIxs,
  expiryOffset: 4_500,   // slots after TX1 that TX2 is accepted; default 1500, 10–9000
});

// Persist / transport — deferredPayload is a plain JSON string.
await saveToBackend(deferredPayload);

// Step 2 — later, from this app (no passkey):
const execSig = await executeDeferred({
  deferredPayload,
  transactionOptions: { txVersion: 'v0' },   // optional
});

A server has no hook: it redeems the payload with LazorKitClient.executeDeferredFromPayload from @lazorkit/sdk-legacy (see Session keys › Deferred execution).

The window. The authorization has an on-chain expiry, counted in slots from the slot TX1 landed in: expiryOffset (10 to 9000; default 1500, DEFAULTS.DEFERRED_EXPIRY_SLOTS). A value outside that range throws a RangeError before the prompt. A slot's length depends on the cluster and its load, and the maximum is tens of minutes, so this does not suit a job that runs hours later. authorizeAndExecute takes expiryOffset too; the wallet may wait up to two minutes for TX1 to be confirmed before it sends TX2, so do not pass a small value. Until it expires, an unused approval stays executable: nothing can cancel it.

If the window has passed, TX2 is not sent (and a paymaster's 3014 is not retried): the call rejects with DeferredExpiredError, carrying deferredExecPda, expiresAtSlot and, for authorizeAndExecute, TX1's authorizeSignature. Nothing in the payload ran, and the passkey approval is spent. The account keeps the paymaster's rent until the Authorize payer (the paymaster's fee payer) closes it with ReclaimDeferred (LazorKitClient.reclaimDeferred); only that payer can, and only after the window.

Since 3.2.1, an inner program's 3014 (Anchor's AccountNotAssociatedTokenAccount) is thrown as it came, not as DeferredExpiredError, and every TX2 error carries deferredExecPda, authorizeSignature and expiresAtSlot (DeferredFailureContext). isDeferredExpiredError(e) is true for every DeferredExpiredError. See DeferredPayload.


Messages

signMessage

Off-chain passkey signature over a string: a sign-in, or any statement your server checks.

const result = await signMessage('Sign in to example.com\nNonce: 8f2c…');
// { signature, signedPayload, clientDataJsonBase64, authenticatorDataBase64 }, all base64

The passkey never signs the message's bytes as its WebAuthn challenge. It signs a challenge made from them, with a tag no other kind of passkey challenge carries (format v1):

tag       = UTF-8 "LazorKit signed message v1"     (26 bytes)
challenge = tag || SHA-256(tag || message)        (58 bytes)

The message is signed as its UTF-8 bytes. signedMessageChallenge(message) computes the challenge and SIGNED_MESSAGE_DOMAIN is the tag; both are exported. Every challenge the SDK asks a passkey for has a shape of its own, so none can be read as another:

ChallengeShape
Transaction (what the programs verify)a 32-byte hash
Message58 bytes, starting with LazorKit signed message v1
Ownership proof (connect)59 bytes: LazorKit ownership proof v1, then 32 random bytes (createOwnershipChallenge())

The portal gets the challenge as message and the text to show the user as displayMessage. The SDK checks the portal's reply: one over any other challenge rejects with "The portal did not sign this message: its reply is over another challenge."

OptionTypeNotes
onSuccess(result: SignMessageResult) => voidRuns once isSigning is false.
onFail(error: Error) => voidRuns with the error the promise rejects with.

Returns Promise<SignMessageResult>:

FieldWhat it is
signatureThe P-256 signature, 64 bytes (r || s, low-S), base64.
signedPayloadWhat the passkey signed: authenticatorData || SHA-256(clientDataJSON), base64.
clientDataJsonBase64The WebAuthn clientDataJSON, base64. Its challenge is the base64url of the challenge above.
authenticatorDataBase64The WebAuthn authenticatorData, base64.

Changed in 3.3.1

Message signatures can no longer be confused with transaction approvals. In 3.3.0 and earlier the passkey signed the message itself as its challenge (on the hook, the base64 decoding of the text rather than the text), and the result had only signature and signedPayload. A signature made by an earlier release does not verify with verifyWalletMessage or verifySignedMessage: ask the user to sign again. See Upgrading to 3.3.1.

Verifying a message signature

A message signature proves that a passkey's key signed the message, not which wallet that key belongs to. To authenticate a wallet, read the key from the chain, never from the client: a server that takes the key from the request accepts anyone's passkey for any wallet they name. verifyWalletMessage, a package export, does the lookup:

// In the app: sign a message your server issued, and send the result with the wallet.
const { wallet, signMessage } = useWallet();
const result = await signMessage(message);
await fetch('/api/sign-in', {
  method: 'POST',
  body: JSON.stringify({ wallet: wallet!.vaultPda, credentialId: wallet!.credentialId, ...result }),
});
// On the server
import { Connection } from '@solana/web3.js';
import { verifyWalletMessage } from '@lazorkit/wallet';

const ok = await verifyWalletMessage({
  connection: new Connection(RPC_URL),
  cluster: 'devnet',                   // when RPC_URL does not say which cluster
  wallet: body.wallet,                 // the wallet the client claims: vault or wallet PDA
  credentialId: body.credentialId,     // the passkey's credential id, base64
  rpId: 'portal.lazor.sh',             // the passkey's relying party: the portal's hostname
  message,                             // the message you issued, with your domain and nonce
  signature: body.signature,
  clientDataJsonBase64: body.clientDataJsonBase64,
  authenticatorDataBase64: body.authenticatorDataBase64,
  signedPayload: body.signedPayload,   // optional
  origin: 'https://portal.lazor.sh',   // optional: the page that ran the passkey
});

It is true only when the signature is over signedMessageChallenge(message) (a webauthn.get with the user present, under rpId), verifies against the key stored on chain in an Owner authority of wallet for credentialId (v2 or v1), and the wallet account still exists. A key the client sends is never used. It reads the chain with one getProgramAccounts per program, as connect does, so use an RPC endpoint that allows it. It rejects when the chain cannot be read: treat that as not verified. Malformed input is false. Check the domain and the nonce in the message yourself, as with any sign-in message.

ParamTypeNotes
connectionConnectionA connection to the cluster the wallet lives on.
walletstring | PublicKeyThe wallet the signer claims: its vault or its wallet PDA.
credentialIdstringThe passkey's credential id, base64 (wallet.credentialId).
rpIdstringThe relying party the passkey was created under: the portal's hostname.
cluster'mainnet' | 'devnet'Optional, for an RPC URL that does not say. Otherwise picked as the SDK picks it: a cluster pinned with registerCluster, else the URL, else mainnet.
messagestring | Uint8ArrayThe message that was signed: the same string, or the same bytes.
signature, clientDataJsonBase64, authenticatorDataBase64stringFrom the SignMessageResult.
signedPayloadstringOptional; checked when given.
originstringOptional: clientDataJSON's origin must equal it.

Returns Promise<boolean>.

Offline: verifySignedMessage. verifySignedMessage({ message, publicKey, ...result }) is the part of that check that needs no RPC: true only when publicKey's passkey signed signedMessageChallenge(message) with the user present, and false for any other challenge, the raw message bytes included. It never throws. It proves that a key signed the message and nothing about which wallet holds that key, so use it alone only with a key you read from the claimed wallet's authority on chain yourself. publicKey is the P-256 key: 33 bytes (compressed), 65 or 64, as bytes, a number array or base64. rpId and origin are optional checks. Returns boolean.

verifyMessage (deprecated)

Never use it to authenticate

useWallet().verifyMessage and the exported verifySignatureBrowser are deprecated since 3.3.1. They check only that signature is over signedPayload, not which message was signed, so any assertion the passkey ever made passes, and they trust the public key the caller passes. Use verifyWalletMessage, or verifySignedMessage with a key read from chain.

Both remain for compatibility: verifyMessage({ signedPayload, signature, publicKey }) takes the three as bytes and resolves with a boolean.


Session keys

createSession

Mint an Ed25519 session key and register it on the wallet.

const USDC = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';

const { sessionPda, sessionPublicKey } = await createSession({
  expiresInSlots: 216_000n,             // ~24h at 400 ms; default 50,000 slots
  spendingLimits: {
    solPerTxMax:    500_000_000n,       // 0.5 SOL per tx, rent the vault pays included
    solRecurring:   {
      limit: 1_000_000_000n,            // 1 SOL per window
      windowSlots: 216_000n,            // 24h window
    },
    // Each token the session may spend, by mint (3.4.0+). Amounts in base units.
    tokens: [{ mint: USDC, perTxMax: 5_000_000n, lifetimeCap: 100_000_000n }],
  },
});
FieldTypeNotes
expiresInSlotsbigintDefault 50,000 slots. The program allows at most 6,480,000.
spendingLimitsSpendingLimitsSOL limits and, since 3.4.0, tokens: one entry per mint the session may spend. Required unless unrestricted: true. Checked before anything is read or the passkey is asked.
unrestrictedbooleanMint a session with no limits: it can spend the whole vault through any program until it expires. Without limits and without this, createSession throws.
sessionKeyPublicKey | stringRegister a key you hold elsewhere (a backend, an agent). The SDK generates nothing and stores nothing. If this key already has a session, createSession resolves with that session as it was made, without a prompt, whatever spendingLimits you pass.

What the limits bound. Name every asset the session may spend: a SOL limit for SOL (rent the vault pays for a new account counts), and a tokens entry for each mint, with at least one of lifetimeCap, perTxMax and recurring: { limit, windowSlots }. wSOL is a mint of its own. From the v2 program release that adds errors 3037 and 3038, an asset the limits do not name cannot leave the wallet: SOL limits alone let the session spend no token, and token limits alone no SOL. That release is not deployed yet; until it is, an asset the limits do not name is not bounded at all. See Session Keys › What a policy bounds.

Checked before the prompt (3.4.0): a tokens entry with no limit, a mint named twice, an amount outside a u64, a window of 0 slots, more than 16 actions, or more than 244 bytes of actions (what fits in the transaction beside the passkey's response) throws. A SOL limit takes 19 bytes (solRecurring 43), a token's lifetimeCap or perTxMax 51, its recurring 75: solPerTxMax with perTxMax and lifetimeCap fits for 2 mints, or with perTxMax alone for 4. Nothing is added that you did not ask for.

A sessionKey that already has a session. Since 3.4.0 the limits are checked first, so a call with no spendingLimits (and not unrestricted), or invalid ones, throws where 3.3.1 resolved with the existing session. With valid limits it still resolves with the session as it was made: to change an external key's limits, revoke its session (revokeSession({ sessionPda })) or register a new key.

Without sessionKey, the SDK generates the key and keeps it for signAndSendWithSession, as a non-extractable WebCrypto key in IndexedDB (see keyStorage), bound to the connected wallet. There is one slot: a new session replaces it. See What the SDK stores.

Limits are immutable

Session actions can't be changed once created. To adjust, revoke and recreate. Every session made with SpendingLimits before 3.4.0 names SOL only, so from the program release that adds errors 3037 and 3038 it moves no token: create a new one with tokens. Program filters and per-action expiries are not in SpendingLimits; for those use LazorKitClient with the Actions catalogue.

Returns Promise<{ sessionPda: string; sessionPublicKey: string }>.

signAndSendWithSession

Send a transaction signed by the stored session key. No passkey prompt.

const sig = await signAndSendWithSession({ instructions: [ix] });

Fails with 0xbd0 (ActionSolLimitExceeded) and related errors once limits are hit — see Errors. From the program release that adds errors 3037 and 3038, a transaction that would move SOL, or a token, that the session's limits do not name rejects with UnlistedSolOutflowError or UnlistedTokenOutflowError (3.4.0): nothing in it ran, and it is not resent. See Errors › Assets a policy does not name.

The key signs only for the wallet that created the session: with no wallet connected, or another one, signAndSendWithSession rejects with KeyWalletMismatchError before anything is signed or sent. A send still running when the wallet is disconnected, by disconnect(), wallet-adapter or the Wallet Standard, neither signs nor sends after it (3.4.0): it rejects with KeyWalletMismatchError, reason 'disconnected' when the same wallet is connected again by then (send again). A key whose session has expired is deleted when it is next read, and the call rejects. See Errors › Kept session and authority keys.

import { isKeyWalletMismatchError, type KeyWalletMismatchError } from '@lazorkit/wallet';

const USDC = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';

try {
  await signAndSendWithSession({ instructions: [ix] });
} catch (e) {
  if (!isKeyWalletMismatchError(e)) throw e;
  const { reason } = e as KeyWalletMismatchError;
  if (reason === 'disconnected') {
    // Its wallet is connected again, but a disconnect ran during this send: send again.
    await signAndSendWithSession({ instructions: [ix] });
  } else if (reason === 'no-wallet') {
    throw e; // connect, then send again
  } else {
    // 'other-wallet' or 'unbound': create a session for the connected wallet.
    await createSession({
      spendingLimits: {
        solPerTxMax: 100_000_000n,
        tokens: [{ mint: USDC, perTxMax: 5_000_000n }],
      },
    });
  }
}

Changed in 3.3.0

In 3.2.1 the stored key signed whichever wallet was connected, or none.

revokeSession

await revokeSession();                                   // the session createSession stored
await revokeSession({ sessionPda: 'SESSION_PDA_BASE58' }); // a specific one, e.g. an external key's

Closes the session PDA and refunds the rent. Without sessionPda, it revokes the session whose key the SDK keeps, which must be the connected wallet's (KeyWalletMismatchError otherwise, before the prompt), and deletes that key once the revoke lands. After expiry, anyone may close a session with CloseExpiredSession and keep the rent: revoke yours if you want it back.


Ed25519 authorities

addAuthority

Adds a fresh Ed25519 authority on the wallet. The SDK generates the key and keeps it for signAndSendWithAuthority, like a session key (see What the SDK stores): it signs only while the wallet it was added to is connected.

import { Actions, PublicKey, ROLE_ADMIN, ROLE_SPENDER, serializeActions } from '@lazorkit/wallet';

const USDC = new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v');

// A Delegate: spends only within its policy, which a v2 wallet requires.
// Up to 0.1 SOL and 5 USDC a transaction; no other token can leave.
const { authorityPda, authorityPublicKey } = await addAuthority({
  role: ROLE_SPENDER,
  policy: serializeActions([
    Actions.solMaxPerTx(100_000_000n),
    Actions.tokenMaxPerTx({ mint: USDC, max: 5_000_000n }),
  ]),
});

// An Admin: manages Delegates and spends without limit.
await addAuthority({ role: ROLE_ADMIN });

role is required: there is no default. A call without it, or with a value that is not a rank, throws before anything is read or the passkey is prompted, with a message that says what each rank may do:

RankConstantMay add and removeSpends
OwnerROLE_OWNER (0)any authority, other Owners included (never the last Owner, never itself)without limit. addAuthority does not pass allowOwner, so on a v2 wallet it refuses this rank before anything is read or prompted. On a v1 wallet it adds one.
AdminROLE_ADMIN (1)Delegates onlywithout limit: no policy, no expiry, until removeAuthority
DelegateROLE_SPENDER (2)nothingonly within its policy, which v2 requires

For a key your app holds, use ROLE_SPENDER with a policy.

FieldTypeNotes
rolenumberRequired. ROLE_SPENDER (Delegate), ROLE_ADMIN, or ROLE_OWNER on a v1 wallet only.
policyUint8ArrayRequired for ROLE_SPENDER on a v2 wallet; refused for any other rank and on v1. It names what may leave the wallet (What a policy bounds). Keep it within 244 bytes: addAuthority does not check its size, and a policy that does not fit fails after the user approved.
unrestrictedbooleanRequired to add a key to a v1 wallet, where any added key can spend the whole vault. Ignored on v2.

Returns Promise<{ authorityPda: string; authorityPublicKey: string }>. See Ranks & policies.

spendingLimitsToActions(limits) (3.4.0) gives the actions a SpendingLimits stands for, checked as createSession checks them, so a Delegate can carry the same limits as a session: policy: serializeActions(spendingLimitsToActions(limits)).

Changed in 3.3.0: role is required

Breaking. In 3.2.1 role was optional and defaulted to ROLE_ADMIN, which made the new key an Admin with no policy and no expiry. role: ROLE_ADMIN keeps that behaviour.

signAndSendWithAuthority

Send a transaction signed by the stored Ed25519 authority. No passkey prompt.

const sig = await signAndSendWithAuthority({ instructions: [ix] });

Only while the wallet the authority was added to is connected: otherwise it rejects with KeyWalletMismatchError, and nothing is sent. As with a session, a send still running when the wallet is disconnected neither signs nor sends after it (3.4.0). A Delegate's send that would move an asset its policy does not name rejects with UnlistedSolOutflowError or UnlistedTokenOutflowError ("This key is not allowed to spend …"), from the program release that adds errors 3037 and 3038.

removeAuthority

await removeAuthority('TARGET_AUTHORITY_PDA');   // base58 string
await removeAuthority('TARGET_AUTHORITY_PDA', { onSuccess, onFail });

Removing the authority whose key the SDK keeps also deletes that key.

You can't remove the last Owner, nor the passkey you sign with. An Admin can remove only Delegates.


v1 wallets

protocolVersion is 1 for a wallet made before LazorKit v2. Every method routes to the v1 program for it, through v1PaymasterConfig. After LazorKit retires v1, its actions reject with V1WalletRetiredError (4018): the funds are safe and the wallet must move to v2. A stored v1 wallet that has been migrated rejects with V1WalletMigratedError; call connect again. See Migrating from v1.


Lower-level escape hatch

The package re-exports the client and helpers from @lazorkit/sdk-legacy:

import {
  LazorKitClient,
  PROGRAM_ID_DEVNET,
  findWalletPda, findVaultPda, findAuthorityPda,
  findSessionPda, findDeferredExecPda,
  Actions, serializeActions,
  ROLE_OWNER, ROLE_ADMIN, ROLE_SPENDER,
  AUTH_TYPE_ED25519, AUTH_TYPE_SECP256R1,
  PublicKey, Connection,
} from '@lazorkit/wallet';

const connection = new Connection('https://api.devnet.solana.com', 'confirmed');
const client = new LazorKitClient(connection, PROGRAM_ID_DEVNET);
const authorities = await client.findAuthoritiesByWallet(new PublicKey('WALLET_PDA_BASE58'));

The PDA helpers derive v2 addresses (at the v2 mainnet id unless you pass a program id); never use them to derive a user's address.