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
signAndSendWithSessionorsignAndSendWithAuthoritythat 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 withKeyWalletMismatchError: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 withTransactionOutcomeUnknownErrorand is not sent again. In 3.3.1 only the store's owndisconnect()stopped such a send, and only at signing. - The adapter's and the Wallet Standard's disconnect disconnect the store.
LazorkitWalletAdapter.disconnect()andstandard:disconnectdisconnect it as its owndisconnect()does:useWallet()shows no wallet, also after a reload; aconnectit is running rejects withPortalCancelledError; 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 withkeepSessionKeys, 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; typeTokenSpendingLimit.spendingLimitsToActions(limits)gives the actions aSpendingLimitsstands for, soserializeActions(spendingLimitsToActions(limits))is a Delegatepolicy.UnlistedSolOutflowErrorandUnlistedTokenOutflowError(both packages), whichsignAndSendWithSession(and on websignAndSendWithAuthority) reject with for the program's 3037 / 3038, withsignerand the original error ascause; predicatesisUnlistedSolOutflowError/isUnlistedTokenOutflowError, codesUNLISTED_SOL_OUTFLOW_CODE/UNLISTED_TOKEN_OUTFLOW_CODE. See Errors › Assets a policy does not name.ERROR_NAMES/errorFromCodename 3036 (SessionNotExpired), 3037 (ActionUnlistedSolOutflow), 3038 (ActionUnlistedTokenOutflow) and 4018 (RetiredDeployment): in both wallet packages and in@lazorkit/sdk-legacy1.4.0.- Web: the
Paymasterconstructor's options takebeforeAttempt, 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()(withOWNERSHIP_PROOF_DOMAIN) givesLazorKit ownership proof v1followed by 32 random bytes (59 bytes), andsignedMessageChallenge(message)(withSIGNED_MESSAGE_DOMAINand the typeSignedMessageInput) 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,resolvePasskeyPublicKeyandfindOwnPasskeyWalletaccept proofs over either. See Passkey signing › Messages and ownership proofs.
Changed
- Web:
createSessionchecks the limits before anything is read or the passkey is asked, and throws on atokensentry 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
sessionKeythat already has a session. The limits are checked first, so a call with nospendingLimits(and notunrestricted), 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,LazorkitWalletAdapterand the Wallet Standardsolana:signMessage; mobile) has the passkey signsignedMessageChallenge(message),tag || SHA-256(tag || message)withtag=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()returnsLazorKit ownership proof v1followed by 32 random bytes (59 bytes), andverifyOwnershipProofaccepts it unchanged. The portal gets the message challenge asmessageand the text asdisplayMessage, 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()andstandard:disconnectdelete the session key the SDK keeps, asuseWallet().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 typeLazorkitAdapterDisconnectOptions). 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
portalUrlinstead of'*', so a page the frame was navigated to receives nothing. AportalUrlwith no origin to address gets nothing.
New
SignMessageResult.signMessageresolves withsignatureandsignedPayloadas before, plusclientDataJsonBase64andauthenticatorDataBase64, which a verifier needs. The adapter's and the Wallet Standardsignaturestay UTF-8 JSON, now with the same four fields.verifyWalletMessagechecks 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 underrpId), never taken from the client.verifySignedMessageis its offline part, for a key you read from chain yourself. AlsosignedMessageChallenge,SIGNED_MESSAGE_DOMAIN,OWNERSHIP_PROOF_DOMAINand the typesSignedMessageInput,VerifySignedMessageParams,VerifyWalletMessageParams. In both packages.
Deprecated
- Web:
useWallet().verifyMessageandverifySignatureBrowser. They check only thatsignatureis oversignedPayload, not which message was signed: never use them for authentication.
Fixed
signMessagesigned the base64 decoding of the text instead of the text.- Web: the wallet installs next to
@solana/kit8. 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 getsnpm 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) andaddAuthorityEd25519(mobile) need arole. There is no default any more (3.2.1 made the key an Admin, 2.2.1 a Delegate). A call withoutroledoes not compile. At runtime a missing role, one that is notROLE_OWNER,ROLE_ADMINorROLE_SPENDER, orROLE_OWNERon 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_ADMINon web,ROLE_SPENDERwith apolicyon mobile. For a key your app holds, preferROLE_SPENDERwith apolicy.
Security (web)
- Session and authority keys are no longer kept in
localStorageas 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 inlocalStorageare moved whenLazorkitProvidermounts, or on first use. New propkeyStorage: 'auto' | 'memory'('memory'keeps nothing at rest) and new exportforgetStoredKeys(). An app that removedlazorkit-session/lazorkit-authorityat sign-out must callforgetStoredKeys()instead. See What the SDK stores. - A kept key signs only for the wallet it was registered for.
signAndSendWithSession,signAndSendWithAuthorityandrevokeSession()(withoutsessionPda) reject with the newKeyWalletMismatchErrorwhen another wallet, or none, is connected; newisKeyWalletMismatchErrorand typeKeyWalletMismatchReason. A key 3.2 left whose wallet cannot be confirmed is never used (reason: 'unbound'). disconnect()deletes the session key, also one whosecreateSessionlands 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, andremoveAuthoritydeletes the kept authority key when it removes that authority.
Changed
- One callback contract on web and mobile.
onSuccess/onFailrun once the action is over (isSigningorisConnectingalreadyfalse), right before the promise settles, exactly one per call; a refusal ("Already signing", "No wallet connected", "Already connecting") callsonFailtoo. A send started fromonSuccessruns, and what a callback throws is logged and changes nothing. Web:disconnect,removeAuthorityandsignMessagetakeonSuccess/onFail, and the package exportsActionCallbacks,DisconnectOptions,RemoveAuthorityOptionsandSignMessageOptions. Mobile: the store'sconnectanddisconnecthonour their callbacks, andtransferSolwith no wallet connected callsonFailand setserror. - Web:
disconnectno longer clearsisSigningwhile an action runs.LazorkitWalletAdapterand the Wallet Standard wallet call each listener on its own: one that throws is logged, and no longer fails aconnectthat has connected. - Web: a 4018 is not resent. The paymaster fails on the first answer, with
V1WalletRetiredErrorfor a retired v1 wallet; 3.2.1 sent it three times.
Fixed
isSignatureReusedErrorandisRetiredDeploymentErrorare true for the SDK's ownSignatureReusedErrorandV1WalletRetiredError. All threeis*Errorpredicates read throughcauseand a wallet-adapterWalletError(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'sdataand a TransactionError;isRetiredDeploymentErroraccepts Kora'sCustom(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 inAsyncStorage, 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 isDeferredExpiredErroronly when it is established: its logs name LazorKit as the first program to fail, it landed afterexpires_at, or the chain is pastexpires_atwhen the account is read again (now atprocessed). Otherwise — Anchor'sAccountNotAssociatedTokenAccountin a swap, say — the error is thrown as it came, instead of telling the user to approve again for a failure that would repeat. isDeferredExpiredErroris true for everyDeferredExpiredError(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,authorizeSignatureandexpiresAtSlot: new typeDeferredFailureContext.
2026-09-30 — wallet 3.2.0, wallet-mobile-adapter 2.2.0: the deferred window
Changed
authorizeAndExecute/authorizeDeferredauthorize 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.expiryOffsetmust be 10–9000, or the call throws aRangeErrorbefore 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 alsoDeferredTxPayload.
Mobile only
- Every action's promise settles, and its
onSuccess/onFailruns, onceisSigningisfalseagain: two sends in a row both run, and so does a send made fromonSuccess. A callback that throws is logged and does not change the outcome. The store'ssignAndExecuteTransaction,signMessageandtransferSolresolve 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.
minContextSlotandcommitmenton everyprepare*, onsecp256r1(signer, opts),migrateV1WalletandreadCounter; reads retried on -32016 for up to 10 s (30 s atfinalized), thenMinContextSlotNotReachedError. Default commitmentconfirmed. 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 atconfirmedfrom a node that has executed the previous transaction. The slot floor is kept inlocalStorage/AsyncStoragefor 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
TransactionOutcomeUnknownErrorinstead 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-browser15 (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.
connectfinds 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,clusterfor RPC URLs that do not name one,V1WalletRetiredError(4018),V1WalletMigratedError. See Migrating from v1. - Wallet confirmation.
connectuses 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 />andWalletChooserNotShownError. Closing the portal rejects at once withPortalCancelledError. - sdk-legacy 1.2.0 / rc.3:
findOwnPasskeyWalletand its steps; the v2 program ids (PROGRAM_ID_MAINNET=LazorFroi…,PROGRAM_ID_DEVNET=57bTNW…) andPROGRAM_ID_*_V1; a safemigrateV1Wallet.
Breaking
PROGRAM_IDis 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
publicKeyis the vault (it was the wallet PDA). createSessionwithout limits throws unlessunrestricted: true.addAuthoritywithROLE_SPENDER(a Delegate) needs apolicyon v2; adding a key to a v1 wallet needsunrestricted: true.- No export of
@lazorkit/wallet2.1.0 was removed. The package CHANGELOG also lists thecreate*Ixbuilders,appendProtocolFeeAccountsandreadAuthorityStateas 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 atfinalized, up to 15 s behind, and could sign a spent counter); no globalBufferneeded.
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-legacy1.0.0 speaks protocol v2 and is published undernext;lateststays 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 orunrestricted, a Delegate needs a policy, Owners needallowOwner, deferred payloads are versioned, and the protocol-fee accounts are mandatory.- 1.1.0 / 1.1.1:
findV1WalletsByOwnerandmigrateV1Walletwithout the user seed; the owner key returned from the scan. Do not use 1.1.x'smigrateV1Wallet— 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
Executenever 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
FeeRecordis created automatically; the fee accounts are mandatory (4008). ExecuteDeferredmoves its rent after its inner calls, so an inner instruction can repay the payer;ReclaimDeferredpays only the original payer.lk2:seeds and discriminators0x21–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:
LazorKitClientinfers the id from the RPC URL (and throws for one it cannot place);PROGRAM_ID_MAINNET/PROGRAM_ID_DEVNETreplacePROGRAM_ID. - Error decoding for 3030–3032 and 4001–4007;
PROGRAM_ID_FOUNDATION_DEVNET. @lazorkit/wallet-mobile-adapter2.0.0-beta.4 was published on 2026-04-30; the npmbetatag 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+reclaimDeferredexposed onuseWallet. The 2-tx deferred flow can now run in pieces across devices / servers / scheduled jobs.authorizeAndExecutestays 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 expiredDeferredExecPDA 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.TxCallbacksinterface. Shared shape ({ onSuccess?, onFail? }) for flows that don't go through the passkey portal (noredirectUrlrequired).
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 with0x2. Two-part fix:buildSecp256r1Paramsno longer forwards the cachedwallet.passkeyPubkey. The client reads the authoritative pubkey off-chain viareadAuthorityPubkey.saveWalletrefreshesWalletInfo.passkeyPubkeyfromreadAuthorityPubkeyon every recovery (whenfindWalletsByAuthorityreturns a match), so persisted records stay in sync with the Authority account.
Performance
- Single
getAccountInfofor counter + pubkey. NewreadAuthorityState(connection, authorityPda)helper decodes both fields in one round-trip.resolveSecp256r1now uses it when the caller doesn't pre-cachepublicKeyBytes— 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 intransactionOptions(default'v0'). Every send-tx method —signAndSendTransaction,signAndSendWithSession,signAndSendWithAuthority,authorizeAndExecute,authorizeDeferred,executeDeferred— accepts it. ALT is v0-only; passingaddressLookupTableAccountswith legacy throws.authorizeDeferred+executeDeferredsplit the 2-tx flow.authorizeDeferredreturns a serializeddeferredPayloadstring that any party (server relayer, another device, a cron job) can later submit viaexecuteDeferredwithout the passkey.authorizeAndExecutestays 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
readAuthorityPubkeyon the authority account matched byfindWalletsByAuthority, instead of the previouslistAuthorities(walletPda).find(...)which could return a non-matching authority on wallets with multiple secp256r1 entries. smartWalletsemantics.smartWalletis the internal Wallet PDA; the SOL-holding account isvaultPda. An earlier branch wrotevaultPdainto thesmartWalletslot which then causedfindVaultto be applied twice, producing a bogus signer at tx-level and failing sig verification. Guide and examples updated to always surfacewallet.vaultPdato users.- Stale passkey pubkey cache. All passkey-signing actions
(
signAndSendTransaction,createSession,revokeSession,addAuthority,removeAuthority,authorizeAndExecute,authorizeDeferred) now sourcepublicKeyBytesfrom the on-chain authority (viaresolvePasskeyWallet→readAuthorityPubkey) instead of the cachedwallet.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 includesigner_key = feePayer.toBase58()so multi-signer Kora pools always sign with the signer that matches the tx'sfeePayerposition. Fixes tx-level-32002 Transaction did not pass signature verificationseen with remote Kora deployments.
Developer experience
- Iframe signing re-enabled by default in
DialogManager; Safari/mobileconnectstill falls back to popup. Fixes the earlier "both branches opened a popup" bug in thesignpath. - Session / authority flows default to v0
VersionedTransaction+tx.sign([...])instead of legacypartialSign, aligning web SDK withwallet-mobile-adapter.
2026-04-20 — Phase 2.1 & API unification
Breaking
- Mode 0 dropped. The Secp256r1 auth path now only supports raw
clientDataJSONfrom a real browser authenticator.Secp256r1Signer.sign()must returnclientDataJson; thetypeAndFlagsparameter ofbuildAuthPayloadis gone. Bot / programmatic signing should use Ed25519 authorities instead. - Authority account layout changed. Secp256r1 authorities now store
rpIdHash(32 bytes) instead of the variable-lengthrpIdstring. Account size is fixed at 145 bytes (Ed25519 authorities stay at ~80 bytes). Instruction data forCreateWallet/AddAuthority/TransferOwnershipis unchanged — clients still sendrpId; 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*Preparemethods that returned an embeddedfinalizeclosure are gone. - Fee model. Protocol fee is now charged on every transaction when the protocol
is enabled — not opt-in per
fee_payer. Registering aFeeRecordis 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'sFeeRecordis 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. publicKeyBytesauto-fetch.Secp256r1Params.publicKeyBytesis now optional — the client reads the pubkey off the on-chain Authority account via the newreadAuthorityPubkeyhelper. Integrators only need to persistcredentialIdHashper user.- Serializable
DeferredPayload. NewserializeDeferredPayload()/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 ofSolMaxPerTx).H2— admin paths require program-ownedProtocolConfigandTreasuryShardaccounts (prevents account-spoofing on withdraw / update).M1—clientDataJSONparser correctly skips strings nested inside objects.payerandadminAuthoritymetas 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_payerFeeRecordcounters, 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).