LazorKit LogoLazorKit
SDK (web3.js v1)

Types

Signer helpers, PDA derivation, account decoders, and error utilities.

The public types and helpers of @lazorkit/sdk-legacy 1.4.0 (@next; protocol v2).

Signer helpers

Three signer constructors are shared across every client.* method that takes a signer (or adminSigner / ownerSigner):

// Signatures of ed25519, session and secp256r1, exported by '@lazorkit/sdk-legacy':
import type { Commitment, PublicKey } from '@solana/web3.js';
import type {
  Ed25519SignerConfig,
  Secp256r1Signer,
  Secp256r1SignerConfig,
  SessionSignerConfig,
} from '@lazorkit/sdk-legacy';

declare function ed25519(publicKey: PublicKey, authorityPda?: PublicKey): Ed25519SignerConfig;
declare function session(sessionPda: PublicKey, sessionKeyPubkey: PublicKey): SessionSignerConfig;
declare function secp256r1(
  signer: Secp256r1Signer,          // { publicKeyBytes, credentialIdHash, rpId, sign(challenge) }
  opts?: { authorityPda?: PublicKey; slotOverride?: bigint; minContextSlot?: number; commitment?: Commitment },
): Secp256r1SignerConfig;

secp256r1(signer) is for the one-shot methods (execute, authorize, addAuthority, …) with a signer that can sign in-process. In a browser, use the prepare* / finalize* pairs with a Secp256r1Params object instead:

import type { Commitment, PublicKey } from '@solana/web3.js';

interface Secp256r1Params {
  /** 32-byte SHA-256 of the WebAuthn credential ID. */
  credentialIdHash: Uint8Array;
  /** 33-byte compressed pubkey. Read from the on-chain authority if omitted. */
  publicKeyBytes?: Uint8Array;
  /** Pre-derived authority PDA. Derived from credentialIdHash if omitted. */
  authorityPda?: PublicKey;
  /** Override slot (read from the connection if omitted). */
  slotOverride?: bigint;
  /** 1.3.0+: read the counter, key and slot from a node at or past this slot. */
  minContextSlot?: number;
  /** 1.3.0+: commitment for those reads. Default 'confirmed'. */
  commitment?: Commitment;
}

Only credentialIdHash is required: the client reads the public key and derives the PDA. minContextSlot and commitment are explained in Passkey signing › Two passkey transactions in a row.


createWallet params

import type { PublicKey } from '@solana/web3.js';

// client.createWallet({ payer, userSeed, owner })
interface CreateWalletParams {
  payer: PublicKey;
  userSeed: Uint8Array;                                    // 32 bytes, unique per wallet
  owner: CreateWalletOwner;
}

type CreateWalletOwner = CreateWalletEd25519 | CreateWalletSecp256r1; // exported

interface CreateWalletEd25519 {                                       // exported
  type: 'ed25519';
  publicKey: PublicKey;
}

interface CreateWalletSecp256r1 {                                     // exported
  type: 'secp256r1';
  credentialIdHash: Uint8Array;                           // SHA-256 of the credential ID
  compressedPubkey: Uint8Array;                           // 33 bytes: the passkey's own key
  rpId: string;
}

createWallet returns { instructions, walletPda, vaultPda, depositAddress, authorityPda }; depositAddress is the vault. The same owner shape is used by addAuthority (newAuthority), transferOwnership (newOwner) and migrateV1Wallet (owner).


WebAuthnResponse

What finalize* takes, built from the browser's assertion (exported type):

interface WebAuthnResponse {
  signature: Uint8Array;           // 64-byte r||s, low-S — not the DER the browser returns
  authenticatorData: Uint8Array;   // WebAuthn authenticator data
  clientDataJsonHash: Uint8Array;  // SHA-256 of clientDataJSON
  clientDataJson: Uint8Array;      // raw clientDataJSON bytes
}

The conversion from navigator.credentials.get: Passkey signing › The WebAuthnResponse helper. OwnershipProof (below) takes the browser's DER signature as it is.


SessionAction

Union of all on-chain action types; type is the numeric SessionActionType enum. See Session Keys concept for semantics. The same list, serialized with serializeActions, is a Delegate's policy.

enum SessionActionType {
  SolLimit = 1, SolRecurringLimit = 2, SolMaxPerTx = 3,
  TokenLimit = 4, TokenRecurringLimit = 5, TokenMaxPerTx = 6,
  ProgramWhitelist = 10, ProgramBlacklist = 11,
}

type SessionAction =
  | { type: SessionActionType.SolLimit; remaining: bigint; expiresAt?: bigint }
  | { type: SessionActionType.SolRecurringLimit; limit: bigint; window: bigint; expiresAt?: bigint }
  | { type: SessionActionType.SolMaxPerTx; max: bigint; expiresAt?: bigint }
  | { type: SessionActionType.TokenLimit; mint: PublicKey; remaining: bigint; expiresAt?: bigint }
  | { type: SessionActionType.TokenRecurringLimit; mint: PublicKey; limit: bigint; window: bigint; expiresAt?: bigint }
  | { type: SessionActionType.TokenMaxPerTx; mint: PublicKey; max: bigint; expiresAt?: bigint }
  | { type: SessionActionType.ProgramWhitelist; programId: PublicKey; expiresAt?: bigint }
  | { type: SessionActionType.ProgramBlacklist; programId: PublicKey; expiresAt?: bigint };

Actions builders

Prefer the builder helpers — they match the object shape exactly and are easier to read:

import { Actions, serializeActions } from '@lazorkit/sdk-legacy';
import { PublicKey, SystemProgram } from '@solana/web3.js';

const mint = new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v');
const actions = [
  Actions.solLimit(10_000_000_000n),                          // (remaining, expiresAt?)
  Actions.solRecurringLimit({ limit: 1_000_000_000n, window: 216_000n }),
  Actions.solMaxPerTx(500_000_000n),                          // (max, expiresAt?)
  Actions.tokenLimit({ mint, remaining: 100_000_000n }),
  Actions.tokenRecurringLimit({ mint, limit: 10_000_000n, window: 216_000n }),
  Actions.tokenMaxPerTx({ mint, max: 5_000_000n }),
  Actions.programWhitelist(SystemProgram.programId),          // (programId, expiresAt?)
];
const policy = serializeActions(actions);                     // a Delegate's policy bytes

From the v2 program release that adds errors 3037 and 3038 (not deployed yet), an asset no action names cannot leave the vault: SOL needs a Sol* action, rent the vault pays included, and each mint a Token* action; wSOL is a mint. See Session Keys › What a policy bounds. When a passkey approves the transaction that registers the actions, keep them within about 244 bytes (How many actions fit): the list above is 301.


WalletAuthorityRecord

Returned by findWalletsByAuthority — a raw lookup: every wallet that lists the key, at any rank, including wallets someone else created or added the key to.

interface WalletAuthorityRecord {
  walletPda: PublicKey;
  authorityPda: PublicKey;
  vaultPda: PublicKey;                       // ← the address funds live at
  role: number;                              // 0=Owner, 1=Admin, 2=Delegate
  authorityType: number;                     // 0=Ed25519, 1=Secp256r1
}

Not how to find a returning passkey user

Don't adopt the first record for a credential-id hash: the hash is public, and anyone can create a wallet that lists it. Use findOwnPasskeyWallet.


Returning-user lookup

Version note

Everything in this section is in @lazorkit/sdk-legacy 1.2.0 and later (next is 1.4.0), except createTaggedOwnershipChallenge and OWNERSHIP_PROOF_DOMAIN (1.4.0). 0.3.x (latest) and 1.1.x do not have it.

How to use them together: Getting Started › Look up a returning user.

Client methods

// Find, prove, describe and decide in one call.
client.findOwnPasskeyWallet(params: {
  credentialIdHash: Uint8Array;
  rpId: string;
  proof: OwnershipProof;
  trustedKeys?: (PublicKey | string)[];      // your own Ed25519 keys on users' wallets
  watchMints?: (PublicKey | string)[];       // SPL Token mints your app receives
  includeV1?: boolean;                       // also scan the paired v1 deployment (default true)
}): Promise<{
  adopt: WalletFacts | null;                 // use it
  needsConfirmation: WalletFacts[];          // let the user choose, by vault address
  unproven: number;                          // found, but the proof did not verify: ignored
}>;

// The steps on their own.
client.findPasskeyWalletCandidates(params: {
  credentialIdHash: Uint8Array;
  rpId: string;
  includeV1?: boolean;
}): Promise<PasskeyWalletCandidate[]>;      // Owner-rank, under rpId — not proven yet

client.describeWalletCandidates(
  candidates: PasskeyWalletCandidate[],
  options?: { trustedKeys?: (PublicKey | string)[]; watchMints?: (PublicKey | string)[] },
): Promise<WalletFacts[]>;                   // a wallet whose account is gone is left out

unproven counts the candidates whose stored key the proof did not verify against: with a good proof, wallets planted with another key; with a proof that fails, all of them, the passkey's own included. So both empty (adopt: null, needsConfirmation: []) means no wallet is proven to be the passkey's. It owns none only if the proof is good: before you create a wallet, check that verifyOwnershipProof([{ publicKey }], proof, rpId) returns the key you create it for.

Helpers

import {
  OWNERSHIP_PROOF_DOMAIN,
  createTaggedOwnershipChallenge,
  createOwnershipChallenge,
  verifyOwnershipProof,
  pickOwnWallet,
  selectWalletByAddress,
} from '@lazorkit/sdk-legacy';

// OWNERSHIP_PROOF_DOMAIN = 'LazorKit ownership proof v1' (1.4.0+)

// 1.4.0+. OWNERSHIP_PROOF_DOMAIN (UTF-8) followed by 32 random bytes: 59 bytes, the same
// form as the wallet packages' createOwnershipChallenge. Fresh for every proof; never reuse one.
createTaggedOwnershipChallenge(): Uint8Array;

// 32 random bytes, unchanged. @deprecated as a passkey challenge (1.4.0): those bytes look
// like a transaction challenge. Use createTaggedOwnershipChallenge for every new proof.
createOwnershipChallenge(): Uint8Array;

// Pure. The candidates whose stored key produced the proof: a `webauthn.get` over exactly
// `proof.challenge` (either form, at least 16 bytes), under `rpId`, with the user present.
// [] on any failure; never throws.
verifyOwnershipProof<T extends { publicKey: Uint8Array }>(
  candidates: T[], proof: OwnershipProof, rpId: string,
): T[];

// Pure. The rule: adopt only the one wallet with signatureCount > 0, when it is
// controlledAlone. Otherwise every wallet goes to needsConfirmation, ordered signed-for
// first, then v1 before v2, then the fuller vault, then the wallet address.
pickOwnWallet(facts: WalletFacts[]): {
  adopt: WalletFacts | null;
  needsConfirmation: WalletFacts[];
};

// Pure. The candidate whose vault OR wallet PDA equals `address`, else null.
selectWalletByAddress<T extends { walletPda: PublicKey; vaultPda: PublicKey }>(
  candidates: T[], address: string | PublicKey,
): T | null;

OwnershipProof

/** A WebAuthn assertion over a challenge the caller chose. */
interface OwnershipProof {
  challenge: Uint8Array;           // >= 16 bytes; use createTaggedOwnershipChallenge()
  signature: Uint8Array;           // DER or 64-byte r||s
  authenticatorData: Uint8Array;
  clientDataJson: Uint8Array;
}

PasskeyWalletCandidate

interface PasskeyWalletCandidate {
  version: 1 | 2;                  // 1 = pre-v2 wallet on the paired v1 deployment
  programId: PublicKey;
  walletPda: PublicKey;
  vaultPda: PublicKey;
  authorityPda: PublicKey;         // this passkey's authority on the wallet
  publicKey: Uint8Array;           // the 33-byte compressed P-256 key stored there
}

WalletFacts

What describeWalletCandidates reads about each candidate — what to show the user, and everything besides this passkey that can spend from the wallet.

type AuthorityRoleName = 'owner' | 'admin' | 'spender' | 'unknown'; // 'spender' = Delegate

interface WalletFacts extends PasskeyWalletCandidate {
  lamports: number;                // vault balance
  slot: bigint;                    // slot the reads were made at
  otherAuthorities: {              // every authority except this passkey's
    authorityPda: PublicKey;
    type: 'ed25519' | 'secp256r1';
    role: AuthorityRoleName;
    publicKey?: PublicKey;         // Ed25519 only
    sameRelyingParty?: boolean;    // Secp256r1 only: created under the same rpId
    trusted: boolean;              // Ed25519 key in trustedKeys; a passkey never is
  }[];
  liveSessions: {                  // expiry at or after `slot`
    sessionPda: PublicKey;
    sessionKey: PublicKey;
    expiresAtSlot: bigint;
    trusted: boolean;
  }[];
  pendingDeferred: {               // expiry at or after `slot`
    deferredPda: PublicKey;
    authorizedBy: PublicKey;       // the authority PDA that authorized it
    expiresAtSlot: bigint;
    trusted: boolean;              // always false
  }[];
  vaultIsSystemAccount: boolean;   // false: the vault was handed to another program
  tokenGrants: {                   // rights over the vault's token accounts
    tokenAccount: PublicKey;
    tokenProgram: PublicKey;
    mint: PublicKey | null;        // null when too short to read
    kind: 'delegate' | 'closeAuthority' | 'owner' | 'unreadable';
    grantee: PublicKey | null;
    trusted: boolean;              // grantee is in trustedKeys
  }[];
  controlledAlone: boolean;        // nothing but this passkey and trusted keys can spend
  signatureCount: number;          // times this passkey has signed for it; 0 = never
}

A pending deferred execution is never trusted — the program does not tie it to the key that signed it — and no key makes up for vaultIsSystemAccount: false. tokenGrants cannot be complete: a vault token account handed away for a mint nobody watches does not show, which is why a wallet with signatureCount: 0 is never adopted.


Signed messages

1.4.0+. The challenge a passkey signs for a message, never the message itself; how to use it: Passkey signing › Messages and ownership proofs.

import { SIGNED_MESSAGE_DOMAIN, signedMessageChallenge, type SignedMessageInput } from '@lazorkit/sdk-legacy';

// SIGNED_MESSAGE_DOMAIN = 'LazorKit signed message v1'
// SignedMessageInput = string | Uint8Array: a string is signed as its UTF-8 bytes

// tag || SHA-256(tag || message), tag = SIGNED_MESSAGE_DOMAIN: 58 bytes. Throws a
// TypeError for anything but a string or a typed array. The same bytes as the wallet
// packages' signedMessageChallenge.
signedMessageChallenge(message: SignedMessageInput): Uint8Array;

PDA derivation

import {
  PROGRAM_ID_DEVNET,
  findWalletPda,
  findVaultPda,
  findAuthorityPda,
  findSessionPda,
  findDeferredExecPda,
} from '@lazorkit/sdk-legacy';
import type { PublicKey } from '@solana/web3.js';

declare const sessionKey: PublicKey, counter: number;
const programId = PROGRAM_ID_DEVNET;               // required: the helpers take the program id

const [walletPda]       = findWalletPda(userSeed, programId);
const [vaultPda]        = findVaultPda(walletPda, programId);
const [authorityPda]    = findAuthorityPda(walletPda, credentialIdHash, programId);
const [sessionPda]      = findSessionPda(walletPda, sessionKey.toBytes(), programId);
const [deferredExecPda] = findDeferredExecPda(walletPda, authorityPda, counter, programId);

Every helper returns [PublicKey, bump] and derives v2 addresses (lk2: seeds). The client has the same helpers without the program id: client.findWallet(userSeed), client.findVault(walletPda), …. For v1 addresses: findV1WalletPda, findV1VaultPda, findV1AuthorityPda, deriveV1Accounts.


Account readers

import { AuthorityAccount, SessionAccount } from '@lazorkit/sdk-legacy';

declare const sessionPda: import('@solana/web3.js').PublicKey;

const authority = await AuthorityAccount.fromAccountAddress(connection, authorityPda);
console.log(authority.role, authority.counter, authority.policyLen, authority.wallet.toBase58());

const session = await SessionAccount.fromAccountAddress(connection, sessionPda);
console.log(session.sessionKey.toBase58(), session.expiresAt);

// Every authority on a wallet, with rank, counter and policy:
const authorities = await client.findAuthoritiesByWallet(walletPda);
// Whether the wallet has an Owner left to manage it:
const { ownerCount, isRecoverable } = await client.getRecoveryStatus(walletPda);

Both classes also have fromBuffer(data). There is no exported reader for the wallet or DeferredExec accounts. readAuthorityCounter / readAuthorityPubkey (and client.readCounter) read one field and take the challenge-read options.


Role & auth constants

export const ROLE_OWNER   = 0;   // manages every authority, including other Owners
export const ROLE_ADMIN   = 1;   // manages Delegates and sessions
export const ROLE_SPENDER = 2;   // a Delegate: must carry a policy

export const AUTH_TYPE_ED25519   = 0;
export const AUTH_TYPE_SECP256R1 = 1;

Program ids: PROGRAM_ID_MAINNET, PROGRAM_ID_DEVNET (v2), PROGRAM_ID_MAINNET_V1, PROGRAM_ID_DEVNET_V1, and legacyProgramIdFor(v2Id) — see Networks & versions.


Error utilities

import { ERROR_NAMES, MinContextSlotNotReachedError, errorFromCode, extractErrorCode } from '@lazorkit/sdk-legacy';

// From a thrown SendTransactionError (reads the error's text):
const code = extractErrorCode(err);          // e.g. 3024, or null
const name = errorFromCode(3024);            // 'ActionSolLimitExceeded'
console.log(code, name, ERROR_NAMES[3006]);

if (err instanceof MinContextSlotNotReachedError) {
  console.log(err.minContextSlot, err.waitedMs, err.commitment);
}

extractErrorCode returns null for a { InstructionError: … } object, and both read only the number: an inner program can return the same code. Since 1.4.0 ERROR_NAMES names 3036 (SessionNotExpired), 3037 (ActionUnlistedSolOutflow), 3038 (ActionUnlistedTokenOutflow) and 4018 (RetiredDeployment); 1.3.1 has no entry for them.

Error ranges:

RangeCategory
3001–3019Authentication and permission failures
3020–3029Spending-limit (action) violations
3030–3032Vault and token-authority invariants
3033–3035Ranks and policies
3036SessionNotExpired
3037–3038An asset the policy does not name: SOL (3037), a mint (3038). From the v2 program release that adds them, not deployed yet
4001–4018Protocol fee and deployment errors, including 4018 RetiredDeployment

Full map in Errors › Program error codes.