SoroPass

Quickstart

One runnable path from zero to a working passkey wallet — create, sign, and recover a Stellar smart account, ES256-only and low-S by default.

One runnable path from zero to a working passkey wallet: create a smart account from a biometric, sign a transaction with it, and recover it on a new device. ES256-only, low-S by default, no seed phrase.

What you'll build

Interactive create preview

The real component runs in mock mode on the demo. Open the demo →

Install

The SDK is the only required dependency:

pnpm add @soropass/core
npm i @soropass/core

To go through Stellar Wallets Kit (Path B), add the kit and the module:

pnpm add @creit.tech/stellar-wallets-kit @soropass/wallets-kit-module

@stellar/stellar-sdk is a peer dependency — it's never bundled into the SDK output. Install it alongside if your app doesn't already depend on it.

Path A — headless / direct

Call the three ceremonies straight from @soropass/core. Lead with the mock path: it runs in CI with no authenticator and no network, and the live path has the exact same shape.

import { createPasskeyKit } from '@soropass/core/testing';

// Deterministic in-memory authenticator + backend — no network, no authenticator.
const kit = createPasskeyKit({ mode: 'mock', rpId: 'localhost' });

const account = await kit.createPasskey({ userName: 'alice' });
console.log(account.contractId);            // C-address

const signedEntry = await kit.signAuthEntry(entryXdr);
const accounts = await kit.recover();        // 0, 1, or many

// Swap mode 'mock' → 'live' (with real adapters) and the path stays green.
import { createPasskey } from '@soropass/core/create';
import { signTransaction, browserPasskeySigner } from '@soropass/core/sign';
import { recover, eventsIndexer } from '@soropass/core';

// 1. Create — registers an ES256 passkey and deploys the smart account.
const { contractId, credentialId } = await createPasskey({
  rpId: 'example.com',
  rpName: 'Example',
  userName: 'alice',
  deployer,                  // <- the only thing you supply (AccountDeployer)
});

// 2. Sign — the button press is the WebAuthn user gesture.
const signedXdr = await signTransaction(txXdr, {
  networkPassphrase,
  sign: browserPasskeySigner({ rpId: 'example.com', allowCredentials: [credentialId] }),
});

// 3. Recover — discoverable prompt + indexer, on any device.
const accounts = await recover({
  rpId: 'example.com',
  indexer: eventsIndexer({ rpcUrl, factoryContractId }),
});

The deployer (an AccountDeployer) is the only piece the adopter supplies — it deploys the smart account for a new passkey against your factory. Everything else (DER → raw, low-S, the Soroban auth struct) is handled inside the SDK.

Register a PasskeyModule and passkey becomes one more wallet in @creit.tech/stellar-wallets-kit — your existing getAddress / signTransaction calls are unchanged. StellarWalletsKit is a static class; never new it.

import { PasskeyModule, PASSKEY_ID } from '@soropass/wallets-kit-module';
import { eventsIndexer } from '@soropass/core';
import { StellarWalletsKit } from '@creit.tech/stellar-wallets-kit/sdk';
import { Networks } from '@creit.tech/stellar-wallets-kit';

const passkey = new PasskeyModule({
  rpId: 'example.com',
  rpName: 'Example',
  networkPassphrase: Networks.TESTNET,
  indexer: eventsIndexer({ rpcUrl, factoryContractId }),
  deployer,
});

StellarWalletsKit.init({ network: Networks.TESTNET, modules: [passkey] });

// createAccount lives on the MODULE, not the kit:
const account = await passkey.createAccount('alice');

// Then drive everything through the static kit:
StellarWalletsKit.setWallet(PASSKEY_ID);
const { address } = await StellarWalletsKit.getAddress();
const { signedTxXdr } = await StellarWalletsKit.signTransaction(txXdr, { networkPassphrase });

Two facts to keep in mind. module.isAvailable() resolves isUVPAA within a 500ms budget — that's what gates whether passkey appears in the wallet picker. And raise the resource budget after simulate(): on-chain secp256r1_verify is under-budgeted by simulation, so a transaction sent at the simulated budget will fail.

Handle errors

Every ceremony throws a KitError with a code from the frozen 10-code taxonomy. Narrow with isKitError(e), then switch on e.code:

import { isKitError, KIT_ERROR_CODES } from '@soropass/core/types';

// KIT_ERROR_CODES (frozen, 10):
//   USER_CANCELLED, ES256_NOT_SUPPORTED, RP_ID_MISMATCH, ORIGIN_MISMATCH,
//   CHALLENGE_MISMATCH, INVALID_SIGNATURE_DER, INVALID_PUBLIC_KEY,
//   CONTRACT_AUTH_FAILED, NETWORK_ERROR, UNSUPPORTED_AUTHENTICATOR

try {
  await signTransaction(txXdr, { networkPassphrase, sign });
} catch (e) {
  if (isKitError(e)) {
    switch (e.code) {
      case 'USER_CANCELLED': return; // user dismissed the sheet
      case 'NETWORK_ERROR': /* retry */ break;
      case 'ES256_NOT_SUPPORTED': /* wrong algorithm */ break;
    }
  }
  throw e;
}

The full taxonomy with per-code copy lives in the KitError taxonomy. The styled screens already render the right message per code.

Test in CI (no authenticator)

Two options for deterministic, zero-network tests: the createPasskeyKit({ mode: 'mock' }) facade shown above, or the lower-level primitives when you need to drive the raw ceremonies yourself.

import { mockAuthenticator, createInMemoryBackend } from '@soropass/core/testing';

// Deterministic P-256 keypair + credentialId from a seed; a real attestationObject.
const auth = mockAuthenticator({ rpId: 'localhost', seed: 'alice' });

// Zero-IO deployer + indexer + submission sharing one registry.
const backend = createInMemoryBackend();

// Run create → sign → recover with backend.deployer / backend.indexer,
// and auth as the WebAuthn client / signer — no network, no real authenticator.

What's next

Want a styled flow instead of wiring the screens yourself? Mount one from @soropass/ui/styled — see the component docs.

On this page