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 bytesFrom 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 outunproven 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:
| Range | Category |
|---|---|
| 3001–3019 | Authentication and permission failures |
| 3020–3029 | Spending-limit (action) violations |
| 3030–3032 | Vault and token-authority invariants |
| 3033–3035 | Ranks and policies |
| 3036 | SessionNotExpired |
| 3037–3038 | An asset the policy does not name: SOL (3037), a mint (3038). From the v2 program release that adds them, not deployed yet |
| 4001–4018 | Protocol fee and deployment errors, including 4018 RetiredDeployment |
Full map in Errors › Program error codes.