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
| Method | Passkey prompt | Use for |
|---|---|---|
connect | first time | Authenticate; returns the stored wallet without a prompt |
disconnect | no | Clear the stored wallet and the kept session key |
signAndSendTransaction | yes | Single-tx Execute via paymaster |
signMessage | yes | Off-chain signature; check it with verifyWalletMessage |
verifyMessage | no | Deprecated. Never use it to authenticate |
createSession | yes | Mint a scoped Ed25519 session (SpendingLimits) |
signAndSendWithSession | no | Send using the kept session key |
revokeSession | yes | Close a session, refund rent |
addAuthority | yes | Add an Ed25519 key with the rank you name (key generated + kept) |
signAndSendWithAuthority | no | Send signed by the kept Ed25519 authority |
removeAuthority | yes | Remove an authority by PDA |
authorizeAndExecute | yes (×1) | Deferred 2-tx flow for oversized payloads |
authorizeDeferred | yes | TX1 only — returns a serialized payload for a later TX2 |
executeDeferred | no | TX2 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
| Field | Type | Notes |
|---|---|---|
vaultPubkey | PublicKey | null | The vault — where funds live. Show it, fund it, use it as fromPubkey. |
smartWalletPubkey | PublicKey | null | Wallet PDA (internal). Never send funds here. |
wallet | WalletInfo | null | Full stored record. wallet.vaultPda is the vault. |
protocolVersion | 1 | 2 | null | The connected wallet's protocol; null while disconnected. |
isConnected | boolean | |
isLoading | boolean | isConnecting || isSigning || … |
isConnecting | boolean | |
isSigning | boolean | true from the prompt until the transaction is confirmed. |
error | Error | null | The 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
connectruns afresh. - No stored wallet: the portal opens.
connectproves 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.
| Option | Type | Notes |
|---|---|---|
confirmWallet | string | Vault (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' | ConfirmWalletHandler | Overrides 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-inClears 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.
| Option | Type | Notes |
|---|---|---|
keepSessionKeys | boolean | Keep the session key (default false: it is deleted). |
onSuccess | () => void | Runs once the disconnect is over. |
onFail | (error: Error) => void | Runs 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 base64The 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:
| Challenge | Shape |
|---|---|
| Transaction (what the programs verify) | a 32-byte hash |
| Message | 58 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."
| Option | Type | Notes |
|---|---|---|
onSuccess | (result: SignMessageResult) => void | Runs once isSigning is false. |
onFail | (error: Error) => void | Runs with the error the promise rejects with. |
Returns Promise<SignMessageResult>:
| Field | What it is |
|---|---|
signature | The P-256 signature, 64 bytes (r || s, low-S), base64. |
signedPayload | What the passkey signed: authenticatorData || SHA-256(clientDataJSON), base64. |
clientDataJsonBase64 | The WebAuthn clientDataJSON, base64. Its challenge is the base64url of the challenge above. |
authenticatorDataBase64 | The 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.
| Param | Type | Notes |
|---|---|---|
connection | Connection | A connection to the cluster the wallet lives on. |
wallet | string | PublicKey | The wallet the signer claims: its vault or its wallet PDA. |
credentialId | string | The passkey's credential id, base64 (wallet.credentialId). |
rpId | string | The 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. |
message | string | Uint8Array | The message that was signed: the same string, or the same bytes. |
signature, clientDataJsonBase64, authenticatorDataBase64 | string | From the SignMessageResult. |
signedPayload | string | Optional; checked when given. |
origin | string | Optional: 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 }],
},
});| Field | Type | Notes |
|---|---|---|
expiresInSlots | bigint | Default 50,000 slots. The program allows at most 6,480,000. |
spendingLimits | SpendingLimits | SOL 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. |
unrestricted | boolean | Mint a session with no limits: it can spend the whole vault through any program until it expires. Without limits and without this, createSession throws. |
sessionKey | PublicKey | string | Register 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'sCloses 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:
| Rank | Constant | May add and remove | Spends |
|---|---|---|---|
| Owner | ROLE_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. |
| Admin | ROLE_ADMIN (1) | Delegates only | without limit: no policy, no expiry, until removeAuthority |
| Delegate | ROLE_SPENDER (2) | nothing | only within its policy, which v2 requires |
For a key your app holds, use ROLE_SPENDER with a policy.
| Field | Type | Notes |
|---|---|---|
role | number | Required. ROLE_SPENDER (Delegate), ROLE_ADMIN, or ROLE_OWNER on a v1 wallet only. |
policy | Uint8Array | Required 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. |
unrestricted | boolean | Required 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.