Skip to content

deriveSolanaPrivateKey

Derives the Ed25519 private key of a Solana account from a PRF output. The PRF output is the entropy of a BIP-39 phrase, and the key sits at the path m/44'/501'/{accountIndex}'/0' of the phrase’s SLIP-0010 key tree.

import {
function deriveSolanaPrivateKey({ prfOutput, accountIndex, }: DerivePrivateKeyOptions): Uint8Array<ArrayBuffer>

Derives the private key of a Solana account from a PRF output.

@param ― options - PRF output and account index.

@returns ― A 32-byte Ed25519 private key (the seed).

@remarks ―

The PRF output is the entropy of a 24-word BIP-39 phrase from the English wordlist, and the phrase's seed uses an empty passphrase. The key is the SLIP-0010 Ed25519 key at m/44'/501'/{accountIndex}'/0' under that seed. Wallet apps such as Phantom use the same path, so importing the phrase into one shows the same accounts.

The phrase exists as a JavaScript string, which cannot be zeroed.

@see ― https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki BIP-39

@see ― https://github.com/satoshilabs/slips/blob/master/slip-0010.md SLIP-0010

@throws ― MeraError with code INPUT_INVALID when prfOutput is not 32 bytes, or accountIndex is not an integer from 0 to 2^31 - 1.

deriveSolanaPrivateKey
} from "@category-labs/mera";
import {
function createEd25519SigningSession({ privateKey, }: CreateSigningSessionOptions): Ed25519SigningSession

Creates a signing session from an Ed25519 private key.

@param ― options - Signing session inputs.

@returns ― A live Ed25519 signing session.

@throws ― MeraError with code INPUT_INVALID when privateKey is not 32 bytes.

createEd25519SigningSession
,
function deriveSolanaPrivateKey({ prfOutput, accountIndex, }: DerivePrivateKeyOptions): Uint8Array<ArrayBuffer>

Derives the private key of a Solana account from a PRF output.

@param ― options - PRF output and account index.

@returns ― A 32-byte Ed25519 private key (the seed).

@remarks ―

The PRF output is the entropy of a 24-word BIP-39 phrase from the English wordlist, and the phrase's seed uses an empty passphrase. The key is the SLIP-0010 Ed25519 key at m/44'/501'/{accountIndex}'/0' under that seed. Wallet apps such as Phantom use the same path, so importing the phrase into one shows the same accounts.

The phrase exists as a JavaScript string, which cannot be zeroed.

@see ― https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki BIP-39

@see ― https://github.com/satoshilabs/slips/blob/master/slip-0010.md SLIP-0010

@throws ― MeraError with code INPUT_INVALID when prfOutput is not 32 bytes, or accountIndex is not an integer from 0 to 2^31 - 1.

deriveSolanaPrivateKey
,
function getPasskeyPrfOutput({ rpId, credential: allowCredential, prfSalt, timeout, immediate, webAuthnClient, }: getPasskeyPrfOutput.Options): Promise<getPasskeyPrfOutput.Result>

Requests a passkey PRF evaluation and returns the first output.

@param ― options - Passkey PRF request inputs.

@returns ― The selected credential ID and first WebAuthn PRF output.

@remarks ―

Runs one assertion ceremony and shows one user-verification prompt.

With immediate, Chrome 148 and later (as of 2026-10) and the React Native client show nothing when the device holds no passkey for rpId. Browsers without immediate mode ignore the option and show their usual prompt. Without a user gesture, or in Incognito, Chrome rejects an immediate request with the same error even when a passkey exists.

The WebAuthn challenge is generated internally.

The default salt is sha256("mera.prf.salt.v1") and will not change across library versions. The PRF output is a deterministic function of the credential, rpId, and salt; a different salt yields an unrelated output.

The assertion requires user verification, and the requirement is not configurable. User verification is the authenticator's local check; the gesture depends on the platform (a biometric, a device PIN, or a password). Authenticators built on CTAP's hmac-secret keep two PRFs per credential, one for user-verified requests and one for the rest; WebAuthn exposes only the user-verified PRF and overrides a weaker userVerification setting when evaluating it, so a configurable setting could neither change the PRF output nor skip the check.

@see ― https://www.w3.org/TR/webauthn-3/#prf-extension WebAuthn: the PRF extension

@see ― https://www.w3.org/TR/webauthn-3/#enumdef-userverificationrequirement WebAuthn: UserVerificationRequirement

@throws ― MeraError with code PRF_UNAVAILABLE when the authenticator does not return a usable 32-byte PRF output.

@throws ― MeraError with code INPUT_INVALID when an explicit prfSalt is not 32 bytes, credential.credentialId is empty or not canonical base64url, or immediate is combined with credential.

@throws ― MeraError with code CRYPTO_UNAVAILABLE when crypto.getRandomValues is unavailable.

@throws ― MeraError with code PASSKEY_CANCELLED when the person dismisses the prompt, it times out, or no passkey answers, including an immediate request that finds none.

@throws ― MeraError with code PASSKEY_OPERATION_FAILED when WebAuthn is unavailable, fails for another reason, or returns an unexpected credential.

getPasskeyPrfOutput
,
function getSolanaAddress(publicKey: Uint8Array): string

Derives the base58-encoded Solana address for an Ed25519 public key.

@param ― publicKey - A 32-byte Ed25519 public key.

@returns ― The base58-encoded Solana address.

@throws ― MeraError with code INPUT_INVALID when publicKey is not 32 bytes.

getSolanaAddress
,
} from "@category-labs/mera";
const {
const prfOutput: Uint8Array<ArrayBuffer>

First PRF output from WebAuthn. Always 32 bytes.

prfOutput
} = await
function getPasskeyPrfOutput({ rpId, credential: allowCredential, prfSalt, timeout, immediate, webAuthnClient, }: getPasskeyPrfOutput.Options): Promise<getPasskeyPrfOutput.Result>

Requests a passkey PRF evaluation and returns the first output.

@param ― options - Passkey PRF request inputs.

@returns ― The selected credential ID and first WebAuthn PRF output.

@remarks ―

Runs one assertion ceremony and shows one user-verification prompt.

With immediate, Chrome 148 and later (as of 2026-10) and the React Native client show nothing when the device holds no passkey for rpId. Browsers without immediate mode ignore the option and show their usual prompt. Without a user gesture, or in Incognito, Chrome rejects an immediate request with the same error even when a passkey exists.

The WebAuthn challenge is generated internally.

The default salt is sha256("mera.prf.salt.v1") and will not change across library versions. The PRF output is a deterministic function of the credential, rpId, and salt; a different salt yields an unrelated output.

The assertion requires user verification, and the requirement is not configurable. User verification is the authenticator's local check; the gesture depends on the platform (a biometric, a device PIN, or a password). Authenticators built on CTAP's hmac-secret keep two PRFs per credential, one for user-verified requests and one for the rest; WebAuthn exposes only the user-verified PRF and overrides a weaker userVerification setting when evaluating it, so a configurable setting could neither change the PRF output nor skip the check.

@see ― https://www.w3.org/TR/webauthn-3/#prf-extension WebAuthn: the PRF extension

@see ― https://www.w3.org/TR/webauthn-3/#enumdef-userverificationrequirement WebAuthn: UserVerificationRequirement

@throws ― MeraError with code PRF_UNAVAILABLE when the authenticator does not return a usable 32-byte PRF output.

@throws ― MeraError with code INPUT_INVALID when an explicit prfSalt is not 32 bytes, credential.credentialId is empty or not canonical base64url, or immediate is combined with credential.

@throws ― MeraError with code CRYPTO_UNAVAILABLE when crypto.getRandomValues is unavailable.

@throws ― MeraError with code PASSKEY_CANCELLED when the person dismisses the prompt, it times out, or no passkey answers, including an immediate request that finds none.

@throws ― MeraError with code PASSKEY_OPERATION_FAILED when WebAuthn is unavailable, fails for another reason, or returns an unexpected credential.

getPasskeyPrfOutput
({
rpId: string

Relying party ID for the WebAuthn assertion.

rpId
: "account.example.com",
});
const
const privateKey: Uint8Array<ArrayBuffer>
privateKey
=
function deriveSolanaPrivateKey({ prfOutput, accountIndex, }: DerivePrivateKeyOptions): Uint8Array<ArrayBuffer>

Derives the private key of a Solana account from a PRF output.

@param ― options - PRF output and account index.

@returns ― A 32-byte Ed25519 private key (the seed).

@remarks ―

The PRF output is the entropy of a 24-word BIP-39 phrase from the English wordlist, and the phrase's seed uses an empty passphrase. The key is the SLIP-0010 Ed25519 key at m/44'/501'/{accountIndex}'/0' under that seed. Wallet apps such as Phantom use the same path, so importing the phrase into one shows the same accounts.

The phrase exists as a JavaScript string, which cannot be zeroed.

@see ― https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki BIP-39

@see ― https://github.com/satoshilabs/slips/blob/master/slip-0010.md SLIP-0010

@throws ― MeraError with code INPUT_INVALID when prfOutput is not 32 bytes, or accountIndex is not an integer from 0 to 2^31 - 1.

deriveSolanaPrivateKey
({
prfOutput: Uint8Array<ArrayBufferLike>

PRF output used as the BIP-39 entropy. Must be exactly 32 bytes.

prfOutput
,
accountIndex: number

Account number in the derivation path, an integer from 0 to 2^31 - 1.

accountIndex
: 0 });
const
const session: Ed25519SigningSession
session
=
function createEd25519SigningSession({ privateKey, }: CreateSigningSessionOptions): Ed25519SigningSession

Creates a signing session from an Ed25519 private key.

@param ― options - Signing session inputs.

@returns ― A live Ed25519 signing session.

@throws ― MeraError with code INPUT_INVALID when privateKey is not 32 bytes.

createEd25519SigningSession
({
privateKey: Uint8Array<ArrayBufferLike>

Curve private key. Must be exactly 32 bytes and, for secp256k1, a valid scalar.

privateKey
});
const
const address: string
address
=
function getSolanaAddress(publicKey: Uint8Array): string

Derives the base58-encoded Solana address for an Ed25519 public key.

@param ― publicKey - A 32-byte Ed25519 public key.

@returns ― The base58-encoded Solana address.

@throws ― MeraError with code INPUT_INVALID when publicKey is not 32 bytes.

getSolanaAddress
(
const session: Ed25519SigningSession
session
.
publicKey: Uint8Array<ArrayBuffer>

32-byte Ed25519 public key for the session.

publicKey
);
  • Type: Uint8Array
  • Required

PRF output from a passkey ceremony. Must be exactly 32 bytes, the entropy of a 24-word phrase.

  • Type: number
  • Required

Account number, the third step of the path. An integer from 0 to 2³¹ − 1.

type
type ReturnType = Uint8Array<ArrayBuffer>
ReturnType
=
interface Uint8Array<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike>

A typed array of 8-bit unsigned integer values. The contents are initialized to 0. If the requested number of bytes could not be allocated an exception is raised.

Uint8Array
<
interface ArrayBuffer

Represents a raw buffer of binary data, which is used to store data for the different typed arrays. ArrayBuffers cannot be read from or written to directly, but can be passed to a typed array or DataView Object to interpret the raw buffer as needed.

ArrayBuffer
>;

The 32-byte Ed25519 private key, the seed that createEd25519SigningSession takes.

  • INPUT_INVALID: prfOutput is not 32 bytes, or accountIndex is not an integer from 0 to 2³¹ − 1.

The phrase uses the English wordlist, and its seed uses an empty passphrase. Wallet apps such as Phantom derive Solana accounts from the same path, so importing the phrase into one shows the same accounts.

mera computes the seed with @scure/bip39, as mnemonicToSeedSync(entropyToMnemonic(prfOutput, wordlist)), so the phrase exists as a JavaScript string, which cannot be zeroed.