LazorKit LogoLazorKit

Changelog

Protocol and SDK changes that affect integrators.

A running log of the changes that move user-visible SDK or on-chain behaviour. Internal refactors and perf work are summarised when they affect CU budgets or account sizes. Each package's own CHANGELOG has the full detail: @lazorkit/wallet, @lazorkit/wallet-mobile-adapter, protocol and contract SDKs.

2026-10-03 — wallet 3.4.0, wallet-mobile-adapter 2.4.0, sdk-legacy 1.4.0, sdk 1.0.0-rc.6

Checklists: React › Upgrading to 3.4.0, React Native › Upgrading to 2.4.0.

Security (web)

  • A send running when the wallet is disconnected is not finished. A signAndSendWithSession or signAndSendWithAuthority that loaded its kept key before any disconnect (disconnect(), LazorkitWalletAdapter.disconnect(), standard:disconnect) is refused right before the key signs and right before each attempt to send, retries included. It rejects with KeyWalletMismatchError: reason 'no-wallet', or the new 'disconnected' when the same wallet is connected again by then (send again). If an earlier attempt got no answer from the paymaster, that attempt may still land: the send rejects with TransactionOutcomeUnknownError and is not sent again. In 3.3.1 only the store's own disconnect() stopped such a send, and only at signing.
  • The adapter's and the Wallet Standard's disconnect disconnect the store. LazorkitWalletAdapter.disconnect() and standard:disconnect disconnect it as its own disconnect() does: useWallet() shows no wallet, also after a reload; a connect it is running rejects with PortalCancelledError; a kept key signs only once its wallet is connected again. In 3.3.1 they deleted the session key but left the store connected, so the authority key, and a session key kept with keepSessionKeys, went on signing. See Wallet Standard › Disconnecting.

New

  • SpendingLimits.tokens (web): one entry per mint a session may spend, { mint, lifetimeCap?, perTxMax?, recurring?: { limit, windowSlots } }, in the mint's base units; type TokenSpendingLimit. spendingLimitsToActions(limits) gives the actions a SpendingLimits stands for, so serializeActions(spendingLimitsToActions(limits)) is a Delegate policy.
  • UnlistedSolOutflowError and UnlistedTokenOutflowError (both packages), which signAndSendWithSession (and on web signAndSendWithAuthority) reject with for the program's 3037 / 3038, with signer and the original error as cause; predicates isUnlistedSolOutflowError / isUnlistedTokenOutflowError, codes UNLISTED_SOL_OUTFLOW_CODE / UNLISTED_TOKEN_OUTFLOW_CODE. See Errors › Assets a policy does not name.
  • ERROR_NAMES / errorFromCode name 3036 (SessionNotExpired), 3037 (ActionUnlistedSolOutflow), 3038 (ActionUnlistedTokenOutflow) and 4018 (RetiredDeployment): in both wallet packages and in @lazorkit/sdk-legacy 1.4.0.
  • Web: the Paymaster constructor's options take beforeAttempt, run right before each attempt to send, retries included; what it throws stops the send.
  • sdk-legacy 1.4.0 / sdk rc.6: tagged challenges. createTaggedOwnershipChallenge() (with OWNERSHIP_PROOF_DOMAIN) gives LazorKit ownership proof v1 followed by 32 random bytes (59 bytes), and signedMessageChallenge(message) (with SIGNED_MESSAGE_DOMAIN and the type SignedMessageInput) the 58-byte message challenge: the same bytes as the wallet packages' own since 3.3.1 / 2.3.1. Use the tagged ownership challenge for every new proof. createOwnershipChallenge() is unchanged, 32 random bytes, and deprecated as a passkey challenge, since those bytes look like a transaction challenge. verifyOwnershipProof, recoverPasskeyPublicKeys, resolvePasskeyPublicKey and findOwnPasskeyWallet accept proofs over either. See Passkey signing › Messages and ownership proofs.

Changed

  • Web: createSession checks the limits before anything is read or the passkey is asked, and throws on 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, which would not fit in the transaction beside the passkey's response.
  • Behaviour change (web): an external sessionKey that already has a session. 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 that session as it was made: to change its limits, revoke it (revokeSession({ sessionPda })) or register a new key.
  • Web: the paymaster does not resend a transaction refused with 3037 or 3038 (the same bytes move the same assets), unless an earlier attempt's answer was lost.

Next v2 program release (not deployed)

The v2 program release that adds errors 3037 and 3038 is not deployed on devnet or mainnet yet (Networks & versions). From it, a session's actions and a Delegate's policy name what may leave the wallet, and nothing else may: SOL only with a Sol* action, rent the vault pays for a new account included (3037), and a token only with a Token* action naming its mint (3038); the vault's token accounts passed writable may change only their balance (3032). Breaking for a session or Delegate that names SOL only, as every SpendingLimits session made before 3.4.0 does: it will move no token. Until that release an asset the actions do not name is not bounded at all, and named token limits are weaker than described: the release also tightens how they are measured. The SDK releases above work with the program deployed today. See Session Keys › What a policy bounds.

@lazorkit/wallet-mobile-adapter has no wallet-adapter or Wallet Standard disconnect and keeps no session key, so the disconnect changes are web only.


2026-10-02 — wallet 3.3.1, wallet-mobile-adapter 2.3.1

Checklists: React › Upgrading to 3.3.1, React Native › Upgrading to 2.3.1.

Security

  • Message signatures are domain-separated. signMessage (web: the hook, the store, LazorkitWalletAdapter and the Wallet Standard solana:signMessage; mobile) has the passkey sign signedMessageChallenge(message), tag || SHA-256(tag || message) with tag = LazorKit signed message v1 (58 bytes), never the app's bytes. A message signature can no longer be confused with a transaction approval, whose challenge is a 32-byte hash. Connect's ownership proofs get their own tag: createOwnershipChallenge() returns LazorKit ownership proof v1 followed by 32 random bytes (59 bytes), and verifyOwnershipProof accepts it unchanged. The portal gets the message challenge as message and the text as displayMessage, and the SDKs refuse a reply over any other challenge. A message signature made by an earlier release does not verify: sign again. See useWallet › Messages.
  • Web: wallet-adapter and Wallet Standard sign-outs delete the session key. LazorkitWalletAdapter.disconnect() and standard:disconnect delete the session key the SDK keeps, as useWallet().disconnect() has since 3.3.0; in 3.3.0 they left it, and it signed again once its wallet was connected. adapter.disconnect({ keepSessionKeys: true }) keeps it (new type LazorkitAdapterDisconnectOptions). The authority key is kept on both paths. See Wallet Standard › Disconnecting.
  • Web: the sign dialog sends the stored credentials to the portal's origin only. The credential id, passkey public key and wallet address it posts to its iframe are addressed to the origin of portalUrl instead of '*', so a page the frame was navigated to receives nothing. A portalUrl with no origin to address gets nothing.

New

  • SignMessageResult. signMessage resolves with signature and signedPayload as before, plus clientDataJsonBase64 and authenticatorDataBase64, which a verifier needs. The adapter's and the Wallet Standard signature stay UTF-8 JSON, now with the same four fields.
  • verifyWalletMessage checks that a wallet signed a message, with the passkey's key read from the chain (an Owner authority of the wallet for the credential, created under rpId), never taken from the client. verifySignedMessage is its offline part, for a key you read from chain yourself. Also signedMessageChallenge, SIGNED_MESSAGE_DOMAIN, OWNERSHIP_PROOF_DOMAIN and the types SignedMessageInput, VerifySignedMessageParams, VerifyWalletMessageParams. In both packages.

Deprecated

  • Web: useWallet().verifyMessage and verifySignatureBrowser. They check only that signature is over signedPayload, not which message was signed: never use them for authentication.

Fixed

  • signMessage signed the base64 decoding of the text instead of the text.
  • Web: the wallet installs next to @solana/kit 8. The unused peers @solana/kit ^5, @solana/kora ^0.1 and @solana-program/token ^0.9 are gone: an app on kit 6, 7 or 8 no longer gets npm ERESOLVE, and an app without kit no longer gets kit 5, Kora and the token program installed for nothing. The published bundle is unchanged.

Both packages gain the dependency @noble/curves, already a dependency of @solana/web3.js.


2026-10-02 — wallet 3.3.0, wallet-mobile-adapter 2.3.0

Checklists: React › Upgrading to 3.3.0, React Native › Upgrading to 2.3.0.

Breaking

  • addAuthority (web) and addAuthorityEd25519 (mobile) need a role. There is no default any more (3.2.1 made the key an Admin, 2.2.1 a Delegate). A call without role does not compile. At runtime a missing role, one that is not ROLE_OWNER, ROLE_ADMIN or ROLE_SPENDER, or ROLE_OWNER on a v2 wallet is refused before anything is read or the passkey is prompted, with an error that says what each rank may do. Migration: pass the role you relied on, ROLE_ADMIN on web, ROLE_SPENDER with a policy on mobile. For a key your app holds, prefer ROLE_SPENDER with a policy.

Security (web)

  • Session and authority keys are no longer kept in localStorage as plaintext. The SDK keeps each as a non-extractable WebCrypto Ed25519 key in IndexedDB (lazorkit-keys); where the browser has no WebCrypto Ed25519, as its seed sealed with AES-GCM in the same database; without IndexedDB, or outside a secure context, in the page's memory. Keys an earlier release left in localStorage are moved when LazorkitProvider mounts, or on first use. New prop keyStorage: 'auto' | 'memory' ('memory' keeps nothing at rest) and new export forgetStoredKeys(). An app that removed lazorkit-session / lazorkit-authority at sign-out must call forgetStoredKeys() instead. See What the SDK stores.
  • A kept key signs only for the wallet it was registered for. signAndSendWithSession, signAndSendWithAuthority and revokeSession() (without sessionPda) reject with the new KeyWalletMismatchError when another wallet, or none, is connected; new isKeyWalletMismatchError and type KeyWalletMismatchReason. A key 3.2 left whose wallet cannot be confirmed is never used (reason: 'unbound').
  • disconnect() deletes the session key, also one whose createSession lands after it; disconnect({ keepSessionKeys: true }) keeps it. The authority key is kept. A session key whose session has expired is deleted when it is next read. revokeSession() deletes the kept session key once the revoke lands, and removeAuthority deletes the kept authority key when it removes that authority.

Changed

  • One callback contract on web and mobile. onSuccess / onFail run once the action is over (isSigning or isConnecting already false), right before the promise settles, exactly one per call; a refusal ("Already signing", "No wallet connected", "Already connecting") calls onFail too. A send started from onSuccess runs, and what a callback throws is logged and changes nothing. Web: disconnect, removeAuthority and signMessage take onSuccess / onFail, and the package exports ActionCallbacks, DisconnectOptions, RemoveAuthorityOptions and SignMessageOptions. Mobile: the store's connect and disconnect honour their callbacks, and transferSol with no wallet connected calls onFail and sets error.
  • Web: disconnect no longer clears isSigning while an action runs. LazorkitWalletAdapter and the Wallet Standard wallet call each listener on its own: one that throws is logged, and no longer fails a connect that has connected.
  • Web: a 4018 is not resent. The paymaster fails on the first answer, with V1WalletRetiredError for a retired v1 wallet; 3.2.1 sent it three times.

Fixed

  • isSignatureReusedError and isRetiredDeploymentError are true for the SDK's own SignatureReusedError and V1WalletRetiredError. All three is*Error predicates read through cause and a wallet-adapter WalletError (so they work through the Wallet Standard), and match a copy from another build of the package. For a raw error they also read a paymaster's data and a TransactionError; isRetiredDeploymentError accepts Kora's Custom(4018).
  • Web: the portal dialog no longer leaves a timer polling every 500 ms when a sign request is answered or closed within its first half second.

Mobile only

  • The README says how to keep a session key: in expo-secure-store, never in AsyncStorage, with the wallet it belongs to. The adapter never stored one.

2026-10-01 — wallet 3.2.1, wallet-mobile-adapter 2.2.1

Fixed

  • An inner program's 3014 is no longer reported as DeferredExpiredError. A 3014 from TX2 is DeferredExpiredError only when it is established: its logs name LazorKit as the first program to fail, it landed after expires_at, or the chain is past expires_at when the account is read again (now at processed). Otherwise — Anchor's AccountNotAssociatedTokenAccount in a swap, say — the error is thrown as it came, instead of telling the user to approve again for a failure that would repeat.
  • isDeferredExpiredError is true for every DeferredExpiredError (it was false for the one thrown before sending), including one from another copy of the package. For a raw 3014 it is true only when the logs name LazorKit.
  • Every error from sending TX2 carries deferredExecPda, authorizeSignature and expiresAtSlot: new type DeferredFailureContext.

2026-09-30 — wallet 3.2.0, wallet-mobile-adapter 2.2.0: the deferred window

Changed

  • authorizeAndExecute / authorizeDeferred authorize 1500 slots by default (DEFAULTS.DEFERRED_EXPIRY_SLOTS), up from 300, which could run out while the wallet waited up to two minutes for TX1. expiryOffset must be 10–9000, or the call throws a RangeError before the prompt.
  • Before TX2 is sent, the authorization's expiry is read; an expired one is not sent, and a 3014 rejects with DeferredExpiredError (authorizeSignature, deferredExecPda, expiresAtSlot). A paymaster's 3014 is no longer retried.
  • New exports: DeferredExpiredError, isDeferredExpiredError, DEFERRED_EXPIRED_CODE, MIN_DEFERRED_EXPIRY_SLOTS, MAX_DEFERRED_EXPIRY_SLOTS; web also DeferredTxPayload.

Mobile only

  • Every action's promise settles, and its onSuccess / onFail runs, once isSigning is false again: two sends in a row both run, and so does a send made from onSuccess. A callback that throws is logged and does not change the outcome. The store's signAndExecuteTransaction, signMessage and transferSol resolve with their results.

2026-09-30 — wallet 3.1.0, wallet-mobile-adapter 2.1.0, sdk-legacy 1.3.0–1.3.1, sdk 1.0.0-rc.4–rc.5

New

  • A passkey with no wallet can connect. Its public key is recovered from two of its signatures (resolvePasskeyPublicKey / recoverPasskeyPublicKeys, sdk-legacy 1.3.0), at the cost of one extra prompt, and the wallet is created for that key. Before, such a passkey could never get a wallet. See Wallet Confirmation.
  • Every send resolves once its transaction is confirmed, and rejects if it failed on chain. New errors: TransactionFailedError, TransactionExpiredError, TransactionOutcomeUnknownError, ConfirmationTimeoutError, PreviousTransactionPendingError, SignatureReusedError, PaymasterError. See Sending transactions.
  • sdk-legacy / kit: challenge-read options. minContextSlot and commitment on every prepare*, on secp256r1(signer, opts), migrateV1Wallet and readCounter; reads retried on -32016 for up to 10 s (30 s at finalized), then MinContextSlotNotReachedError. Default commitment confirmed. See Passkey signing.

Fixed

  • Passkey transactions back to back no longer fail with SignatureReused (3006). Signatures for one passkey are prepared one at a time, each read at confirmed from a node that has executed the previous transaction. The slot floor is kept in localStorage / AsyncStorage for ten minutes. A 3006 is not retried; an inner program's 3006 is reported as what it is.
  • A landed transaction is no longer reported as expired, and a lost paymaster answer rejects with TransactionOutcomeUnknownError instead of a plain error. On web that happens once the same signed bytes, resent up to 3 attempts in all, have brought no answer back; the mobile adapter does not resend.
  • Previews over 1232 bytes are compiled with the lookup tables instead of failing before the prompt; the web adapter now handles dApp v0 transactions with lookup tables.
  • sdk-legacy 1.3.1 / rc.5: a failed protocol-fee read stops the challenge reads.

Mobile only

  • expo-web-browser 15 (Expo SDK 54) is accepted alongside 14.2.

2026-09-29 — wallet 3.0.0–3.0.2, wallet-mobile-adapter 2.0.0, sdk-legacy 1.2.0, sdk 1.0.0-rc.3: protocol v2 support

The UI SDKs speak protocol v2 beside v1, by building on @lazorkit/sdk-legacy 1.x (and 0.3.2 for v1 wallets). Upgrade guides: React, React Native.

New

  • v1 and v2 side by side, routed per wallet. connect finds a passkey's own wallet on either protocol; only a passkey with none gets a new (v2) wallet. New: protocolVersion, v1PaymasterConfig (web) / v1ConfigPaymaster (mobile) for the relayer that still sponsors v1, cluster for RPC URLs that do not name one, V1WalletRetiredError (4018), V1WalletMigratedError. See Migrating from v1.
  • Wallet confirmation. connect uses a wallet on its own only when it is the one wallet the passkey has signed for and nothing else can spend from it; otherwise it asks the user: onConfirmWallet (built-in chooser, your handler, or 'throw'), confirmWallet, trustedAuthorities, watchMints, WalletNeedsConfirmationError, WalletConfirmationDeclinedError; mobile <WalletChooser /> and WalletChooserNotShownError. Closing the portal rejects at once with PortalCancelledError.
  • sdk-legacy 1.2.0 / rc.3: findOwnPasskeyWallet and its steps; the v2 program ids (PROGRAM_ID_MAINNET = LazorFroi…, PROGRAM_ID_DEVNET = 57bTNW…) and PROGRAM_ID_*_V1; a safe migrateV1Wallet.

Breaking

  • PROGRAM_ID is the v2 mainnet id; the PDA helpers derive v2 addresses. Use the stored vault (vaultPubkey / smartWalletPubkey), never a derived one.
  • The web wallet adapter's publicKey is the vault (it was the wallet PDA).
  • createSession without limits throws unless unrestricted: true.
  • addAuthority with ROLE_SPENDER (a Delegate) needs a policy on v2; adding a key to a v1 wallet needs unrestricted: true.
  • No export of @lazorkit/wallet 2.1.0 was removed. The package CHANGELOG also lists the create*Ix builders, appendProtocolFeeAccounts and readAuthorityState as removed from the web package, but no published 2.x exported them.

Fixed

  • 3.0.1: peer @solana/web3.js ^1.99.0.
  • 3.0.2: the wallet adapter reads at confirmed (it read at finalized, up to 15 s behind, and could sign a spent counter); no global Buffer needed.

2026-09-21 → 09-23 — sdk-legacy 0.3.2, 1.0.0, 1.1.0, 1.1.1; sdk 1.0.0-rc.1, rc.2

  • @lazorkit/sdk-legacy 1.0.0 speaks protocol v2 and is published under next; latest stays on 0.3.2, the v1 line (no Node polyfills needed). Breaking for apps moving to 1.x: every PDA address changes (lk2: seeds), sessions need actions or unrestricted, a Delegate needs a policy, Owners need allowOwner, deferred payloads are versioned, and the protocol-fee accounts are mandatory.
  • 1.1.0 / 1.1.1: findV1WalletsByOwner and migrateV1Wallet without the user seed; the owner key returned from the scan. Do not use 1.1.x's migrateV1Wallet — fixed in 1.2.0.
  • @lazorkit/sdk (Solana Kit) release candidates rc.1 and rc.2, with the same migration surface. See SDK (Solana Kit).

Protocol v2 (program 2.0.0) — devnet now, mainnet pending

Deployed on devnet at 57bTNWqtYTJbWuLWASKo6GqUTAK6oFDUR5c6hEc6V8nv; the mainnet id LazorFroiVuAjcwwQ2me83vTr5nc5NRxSaTg3pmEXC8 is not deployed yet. v2 runs at its own program id; v1 keeps running until it is retired to a sunset binary that serves only ReclaimDeferred, MigrateWallet and CloseExpiredSession and answers everything else with RetiredDeployment (4018). See Networks & versions.

  • Ranks and policies. Owner / Admin / Delegate; a Delegate must carry a spending policy (3033–3035); several Owners per wallet. v1's Execute never checked rank.
  • The passkey challenge names the wallet; a client building the old challenge fails with 3005.
  • MigrateWallet (17) moves a v1 wallet's funds to v2 in one Owner-signed instruction; CloseExpiredSession (18) lets anyone close an expired session and keep its rent (3036 for a live one).
  • Fees: collection is skipped rather than reverted when not configured; each fee is capped at 0.01 SOL (4014); every fee payer's FeeRecord is created automatically; the fee accounts are mandatory (4008).
  • ExecuteDeferred moves its rent after its inner calls, so an inner instruction can repay the payer; ReclaimDeferred pays only the original payer.
  • lk2: seeds and discriminators 0x21–0x27; serialized deferred payloads carry a version.
  • Security fixes C-1, H-1 to H-4 and M-2 to M-6 from the 2026 audit.

2026-04-29 → 05-06 — sdk-legacy 0.2.0 and 0.3.1 (protocol v1)

  • Per-cluster program ids: LazorKitClient infers the id from the RPC URL (and throws for one it cannot place); PROGRAM_ID_MAINNET / PROGRAM_ID_DEVNET replace PROGRAM_ID.
  • Error decoding for 3030–3032 and 4001–4007; PROGRAM_ID_FOUNDATION_DEVNET.
  • @lazorkit/wallet-mobile-adapter 2.0.0-beta.4 was published on 2026-04-30; the npm beta tag still points at it.

2026-04-22 — React Native SDK: deferred split, cross-device fix, RPC opt

@lazorkit/wallet-mobile-adapter@2.0.0-beta.2.

New

  • authorizeDeferred + executeDeferred + reclaimDeferred exposed on useWallet. The 2-tx deferred flow can now run in pieces across devices / servers / scheduled jobs. authorizeAndExecute stays as the atomic one-call shortcut.
    • authorizeDeferred(payload, options) — passkey-signed TX1. Returns { signature, deferredPayload, deferredExecPda, counter }.
    • executeDeferred({ deferredPayload, refundDestination?, transactionOptions? }, callbacks?) — no passkey. Submit TX2 from a prior payload (same app, another device, or a relayer).
    • reclaimDeferred({ deferredExecPda, refundDestination? }, callbacks?) — no passkey. Close an expired DeferredExec PDA and reclaim its rent.
  • DeferredPayload + wire helpers re-exported. serializeDeferredPayload() / deserializeDeferredPayload() turn the in-memory payload into a JSON-safe string — unblocks cross-machine deferred flows.
  • TxCallbacks interface. Shared shape ({ onSuccess?, onFail? }) for flows that don't go through the passkey portal (no redirectUrl required).

Fixed

  • Cross-device passkey recovery → 0x2 InvalidSignature. When a user signs in on a secondary device with an iCloud-synced / Google Password Manager passkey, the portal-reported compressed pubkey can drift from what the on-chain Authority stored — the secp256r1 precompile rejects the tx with 0x2. Two-part fix:
    • buildSecp256r1Params no longer forwards the cached wallet.passkeyPubkey. The client reads the authoritative pubkey off-chain via readAuthorityPubkey.
    • saveWallet refreshes WalletInfo.passkeyPubkey from readAuthorityPubkey on every recovery (when findWalletsByAuthority returns a match), so persisted records stay in sync with the Authority account.

Performance

  • Single getAccountInfo for counter + pubkey. New readAuthorityState(connection, authorityPda) helper decodes both fields in one round-trip. resolveSecp256r1 now uses it when the caller doesn't pre-cache publicKeyBytes — one fewer RPC per passkey-signed tx in the default path (and after the cross-device recovery fix, that default path is now the common path).

Note on WalletInfo convention

The RN SDK's WalletInfo keeps smartWallet pointing to the vault PDA (where funds live) plus a separate walletPda field for the internal metadata account. The React SDK's latest convention flipped this: smartWallet = internal wallet PDA, vaultPda = vault. The two SDKs are therefore semantically different at the storage layer; vaultPubkey is the vault on both. See Networks & versions.


2026-04-22 — React SDK (@lazorkit/wallet 2.1.0): v0/legacy, deferred split, cross-device fixes

New

  • txVersion: 'legacy' | 'v0' per-call in transactionOptions (default 'v0'). Every send-tx method — signAndSendTransaction, signAndSendWithSession, signAndSendWithAuthority, authorizeAndExecute, authorizeDeferred, executeDeferred — accepts it. ALT is v0-only; passing addressLookupTableAccounts with legacy throws.
  • authorizeDeferred + executeDeferred split the 2-tx flow. authorizeDeferred returns a serialized deferredPayload string that any party (server relayer, another device, a cron job) can later submit via executeDeferred without the passkey. authorizeAndExecute stays as the atomic one-call shortcut.
  • Unified SendTxPayload. All send-tx methods now share the same payload shape.

Fixed

  • Cross-device passkey recovery (e.g. iCloud Keychain: wallet created on iPhone, reused on Mac). The recovery branch now reads the on-chain compressed pubkey via readAuthorityPubkey on the authority account matched by findWalletsByAuthority, instead of the previous listAuthorities(walletPda).find(...) which could return a non-matching authority on wallets with multiple secp256r1 entries.
  • smartWallet semantics. smartWallet is the internal Wallet PDA; the SOL-holding account is vaultPda. An earlier branch wrote vaultPda into the smartWallet slot which then caused findVault to be applied twice, producing a bogus signer at tx-level and failing sig verification. Guide and examples updated to always surface wallet.vaultPda to users.
  • Stale passkey pubkey cache. All passkey-signing actions (signAndSendTransaction, createSession, revokeSession, addAuthority, removeAuthority, authorizeAndExecute, authorizeDeferred) now source publicKeyBytes from the on-chain authority (via resolvePasskeyWallet → readAuthorityPubkey) instead of the cached wallet.passkeyPubkey. Prevents custom program error 0x2 (InvalidSignature) from the secp256r1 precompile when the cache drifts across sessions or devices.
  • Paymaster request format. Requests now use object params ({ transaction, signer_key }) and include signer_key = feePayer.toBase58() so multi-signer Kora pools always sign with the signer that matches the tx's feePayer position. Fixes tx-level -32002 Transaction did not pass signature verification seen with remote Kora deployments.

Developer experience

  • Iframe signing re-enabled by default in DialogManager; Safari/mobile connect still falls back to popup. Fixes the earlier "both branches opened a popup" bug in the sign path.
  • Session / authority flows default to v0 VersionedTransaction + tx.sign([...]) instead of legacy partialSign, aligning web SDK with wallet-mobile-adapter.

2026-04-20 — Phase 2.1 & API unification

Breaking

  • Mode 0 dropped. The Secp256r1 auth path now only supports raw clientDataJSON from a real browser authenticator. Secp256r1Signer.sign() must return clientDataJson; the typeAndFlags parameter of buildAuthPayload is gone. Bot / programmatic signing should use Ed25519 authorities instead.
  • Authority account layout changed. Secp256r1 authorities now store rpIdHash (32 bytes) instead of the variable-length rpId string. Account size is fixed at 145 bytes (Ed25519 authorities stay at ~80 bytes). Instruction data for CreateWallet / AddAuthority / TransferOwnership is unchanged — clients still send rpId; the program hashes it once at write time.
  • Unified prepare/finalize API. Every Secp256r1 op now has a matching client.prepare* / client.finalize* pair — prepareExecute/finalizeExecute, prepareCreateSession/finalizeCreateSession, etc. The old *Prepare methods that returned an embedded finalize closure are gone.
  • Fee model. Protocol fee is now charged on every transaction when the protocol is enabled — not opt-in per fee_payer. Registering a FeeRecord is opt-in for reward tracking; unregistered payers still pay the fee but don't accumulate a claim at token launch. (Protocol v1. In v2 every fee payer's FeeRecord is created automatically.)

Improvements

  • CU reductions. Execute Secp256r1: 10,883 → 9,495 CU (-12.8%). Execute Session: 4,723 → 4,105 CU (-13.1%). Zero-copy compact instructions + elimination of Vec concats in the hot path.
  • publicKeyBytes auto-fetch. Secp256r1Params.publicKeyBytes is now optional — the client reads the pubkey off the on-chain Authority account via the new readAuthorityPubkey helper. Integrators only need to persist credentialIdHash per user.
  • Serializable DeferredPayload. New serializeDeferredPayload() / deserializeDeferredPayload() helpers turn a payload into JSON-safe bytes. Unblocks cross-machine deferred flows (sign TX1 on the user's device, submit TX2 from a server relayer).

Security hardening

  • H1 — vault / token invariants enforced around CPIs in session+actions execute (prevents round-trip swap bypass of SolMaxPerTx).
  • H2 — admin paths require program-owned ProtocolConfig and TreasuryShard accounts (prevents account-spoofing on withdraw / update).
  • M1 — clientDataJSON parser correctly skips strings nested inside objects.
  • payer and adminAuthority metas switched to writable where required for rent refunds.

What to do

These April changes are all included in the current releases; follow the upgrade guides for React and React Native.


Earlier milestones

  • Solita artifacts removed. The SDK is now fully hand-written for fine-grained control over wire layout. No IDL-generated wrappers remain.
  • Protocol fee system live on mainnet (protocol v1). Sharded treasury (16 shards by default), per-fee_payer FeeRecord counters, fees collected in native SOL.
  • Seedless launched on protocol v1: the first production integrator running on the LazorKit execution layer.

For the authoritative change history, browse the lazorkit-protocol CHANGELOG and its git log (v2 work is on develop).