Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/sovranBitcoin/sovran/llms.txt

Use this file to discover all available pages before exploring further.

Sovran implements multiple Nostr NIPs (Nostr Implementation Possibilities) for decentralized identity, encrypted messaging, and social features.

Supported NIPs

Sovran supports NIP-04, NIP-05, NIP-06, NIP-17, NIP-19, NIP-44, and NIP-59.

NIP-04: Encrypted Direct Messages (Legacy)

Legacy encrypted direct message support using NIP-04 encryption.
NIP-04 is considered deprecated in favor of NIP-17 (gift-wrapped DMs) for enhanced metadata privacy. Sovran primarily uses NIP-17 for new messages.

NIP-05: DNS-Based Verification

NIP-05 provides DNS-based verification for Nostr identities (Lightning addresses). User Interface (components/blocks/contacts/SearchResult.tsx):
  • Profile search with NIP-05 validation
  • Search by npub, NIP-05 identifier, or display name
  • Verification badge display
Search Flow:
User Input → NIP-05 Resolver → Profile Validation → Display Results

NIP-06: Deterministic Key Derivation

NIP-06 enables deterministic key derivation from BIP-39 seed phrases. Implementation (helper/keyDerivation.ts:14-31):
import * as nip06 from 'nostr-tools/nip06';
import { nip19 } from 'nostr-tools';

/**
 * Derive Nostr keys from a BIP-39 mnemonic using NIP-06.
 * Path: m/44'/1237'/<accountIndex>'/0/0
 */
export function deriveNostrKeys(mnemonic: string, accountIndex: number = 0): DerivedNostrKeys {
  const { privateKey: sk, publicKey: pk } = nip06.accountFromSeedWords(
    mnemonic,
    undefined,
    accountIndex
  );

  return {
    npub: nip19.npubEncode(pk),
    nsec: nip19.nsecEncode(sk),
    pubkey: pk,
    privateKey: sk,
  };
}
Derivation Path: m/44'/1237'/<accountIndex>'/0/0 Key Types Derived:
  • npub: Bech32-encoded public key (shareable)
  • nsec: Bech32-encoded private key (secret)
  • pubkey: Hex public key (for protocol usage)
  • privateKey: Raw private key bytes (for signing)
Provider Integration (providers/NostrKeysProvider.tsx:243-365): The NostrKeysProvider manages key derivation with caching:
// Fast path: Load cached keys from SecureStore
const [cachedDerived, cachedCashu] = await Promise.all([
  retrieveDerivedKeys(defaultAccountIndex),
  retrieveCashuMnemonic(defaultAccountIndex),
]);

const cacheValid =
  cachedDerived?.mnemonicHash === mHash && cachedCashu?.mnemonicHash === mHash;

if (cacheValid && cachedDerived && cachedCashu) {
  // Use cached keys (fast path)
  defaultKeys = {
    npub: cachedDerived.npub,
    nsec: cachedDerived.nsec,
    pubkey: cachedDerived.pubkey,
    privateKey: hexToBytes(cachedDerived.privateKeyHex),
  };
} else {
  // Derive from scratch and persist to SecureStore
  defaultKeys = deriveNostrKeys(mnemonicToUse, defaultAccountIndex);
  // Cache in background...
}
Benefits:
  • Single seed phrase for both Bitcoin and Nostr
  • Deterministic identity recovery
  • Multi-account support
  • Cross-platform compatibility

NIP-17: Private Direct Messages

NIP-17 provides gift-wrapped direct messages with enhanced metadata privacy. Implementation (utils/nip17.ts): NIP-17 uses a three-layer gift-wrapping protocol:
  1. Rumor (kind 14): Unsigned event containing the actual message
  2. Seal (kind 13): Encrypts the rumor with NIP-44, signed by sender
  3. Wrap (kind 1059): Encrypts the seal with a random throwaway key

Creating Gift-Wrapped Messages

Single Recipient (utils/nip17.ts:127-151):
export function buildGiftWrappedDM(params: {
  content: string;
  senderPrivateKey: Uint8Array;
  recipientPublicKey: string;
  extraTags?: string[][];
}): VerifiedEvent {
  const { content, senderPrivateKey, recipientPublicKey, extraTags } = params;

  // 1. Rumor (kind 14 – unsigned)
  const rumor = createRumor(
    {
      kind: 14,
      content,
      tags: [['p', recipientPublicKey], ...(extraTags ?? [])],
    },
    senderPrivateKey
  );

  // 2. Seal (kind 13 – signed by sender, encrypted to recipient)
  const seal = createSeal(rumor, senderPrivateKey, recipientPublicKey);

  // 3. Gift wrap (kind 1059 – signed by random key, encrypted to recipient)
  return createWrap(seal, recipientPublicKey);
}
With Self-Copy (utils/nip17.ts:162-190): For sender to retrieve their own sent messages:
export function buildGiftWrappedDMPair(params: {
  content: string;
  senderPrivateKey: Uint8Array;
  recipientPublicKey: string;
  extraTags?: string[][];
}): { recipientWrap: VerifiedEvent; senderWrap: VerifiedEvent } {
  // Same rumor for both wraps
  const rumor = createRumor(
    {
      kind: 14,
      content,
      tags: [['p', recipientPublicKey], ...(extraTags ?? [])],
    },
    senderPrivateKey
  );

  // 2a. Wrap for recipient
  const recipientSeal = createSeal(rumor, senderPrivateKey, recipientPublicKey);
  const recipientWrap = createWrap(recipientSeal, recipientPublicKey);

  // 2b. Wrap for sender (self-copy)
  const senderSeal = createSeal(rumor, senderPrivateKey, senderPublicKey);
  const senderWrap = createWrap(senderSeal, senderPublicKey);

  return { recipientWrap, senderWrap };
}

Unwrapping Received Messages

Decryption (utils/nip17.ts:221-258):
export function unwrapGiftWrap(
  wrapEvent: { content: string; pubkey: string },
  recipientPrivateKey: Uint8Array
): UnwrappedDM | null {
  try {
    // Layer 1: decrypt the gift wrap → seal
    const seal = nip44Decrypt(wrapEvent.content, recipientPrivateKey, wrapEvent.pubkey);

    if (seal.kind !== 13) return null;

    // Layer 2: decrypt the seal → rumor
    const rumor = nip44Decrypt(seal.content, recipientPrivateKey, seal.pubkey);

    // NIP-17: verify that the seal's pubkey matches the rumor's pubkey
    if (seal.pubkey !== rumor.pubkey) return null;

    return {
      senderPubkey: seal.pubkey,
      recipientPubkeys: (rumor.tags || []).filter((t) => t[0] === 'p').map((t) => t[1]),
      content: rumor.content,
      created_at: rumor.created_at,
      kind: rumor.kind,
      tags: rumor.tags || [],
    };
  } catch {
    return null;
  }
}
Privacy Features:
  • Random timestamps (within last 2 days) for metadata obfuscation
  • Throwaway keys prevent sender correlation
  • Seal verification ensures message authenticity

Messaging Interface

UI Components (components/screens/UserMessagesScreen.tsx):
  • NIP-17 DM conversation screen
  • Send/receive encrypted messages
  • Inline Cashu token messages
  • Inline payment request messages
Message Sending (hooks/useNostrDirectMessage.ts):
  • NIP-17 DM creation with NIP-44 encryption
  • Gift wrap generation (recipient + sender copy)
  • Relay publishing

NIP-19: Bech32 Encoding

NIP-19 defines bech32-encoded entities for Nostr. Supported Entity Types:
  • npub: Public key (user identity)
  • nsec: Private key (secret)
  • note: Note ID (event reference)
  • nprofile: Profile with relay hints
  • nevent: Event with relay hints
Decoding (helper/nostrClient.ts:7-17):
export function npubToPubkey(npub: string): string {
  if (!npub) return '';

  if (npub.startsWith('npub')) {
    const data = nip19.decode(npub);
    if (data.type === 'npub') {
      return data.data;
    }
  }
  return npub;
}
Safe Parsing (helper/nostrClient.ts:19-27):
export function npubToPubkeySafe(npub: string): string | null {
  try {
    const decoded = nip19.decode(npub);
    return decoded.type === 'npub' ? decoded.data : null;
  } catch {
    return null;
  }
}
Payment String Processing (hooks/coco/useProcessPaymentString.ts:52-75): Sovran handles both npub1... and nostr:npub1... formats:
const parseNpub = (data: string): string | null => {
  const trimmed = data.trim();

  // Remove 'nostr:' prefix if present
  const npubString = trimmed.startsWith('nostr:') ? trimmed.slice(6) : trimmed;

  // Check if it looks like an npub
  if (!npubString.startsWith('npub1')) {
    return null;
  }

  // Validate by attempting to decode
  try {
    const decoded = nip19.decode(npubString);
    if (decoded.type === 'npub') {
      return npubString;
    }
  } catch {
    return null;
  }

  return null;
};
Profile Navigation (hooks/coco/useProcessPaymentString.ts:387-398): Scanning an npub navigates to the user’s profile:
const validNpub = parseNpub(scanning.data);
if (validNpub) {
  // Store the scan in history
  addScan(scanning.data, validNpub, 'npub', source);

  router.navigate({
    pathname: '/(user-flow)/profile',
    params: {
      npub: validNpub,
    },
  });
}

NIP-44: Encrypted Payloads

NIP-44 provides versioned encryption for Nostr messages (improved NIP-04). Implementation (utils/nip17.ts:34-44):
import { nip44 } from 'nostr-tools';

/** Derive a NIP-44 conversation key from a private key and a public key. */
const nip44ConversationKey = (privateKey: Uint8Array, publicKey: string) =>
  nip44.v2.utils.getConversationKey(privateKey, publicKey);

/** NIP-44-encrypt any JSON-serialisable data. */
const nip44Encrypt = (data: object, privateKey: Uint8Array, publicKey: string): string =>
  nip44.v2.encrypt(JSON.stringify(data), nip44ConversationKey(privateKey, publicKey));

/** NIP-44-decrypt a ciphertext and return the parsed JSON. */
const nip44Decrypt = (ciphertext: string, privateKey: Uint8Array, peerPublicKey: string): unknown =>
  JSON.parse(nip44.v2.decrypt(ciphertext, nip44ConversationKey(privateKey, peerPublicKey)));
Usage in NIP-17:
  • Encrypts rumor in seal (kind 13)
  • Encrypts seal in gift wrap (kind 1059)
  • Uses conversation keys for sender/recipient pairs
Security Properties:
  • Forward secrecy (each message uses unique keys)
  • Authentication (verifiable sender identity)
  • Confidentiality (only recipient can decrypt)

NIP-59: Gift Wrap

NIP-59 defines the gift wrap protocol structure used by NIP-17. Event Kinds:
  • Kind 13: Seal (encrypted rumor)
  • Kind 1059: Gift wrap (encrypted seal)
Rumor Creation (utils/nip17.ts:56-70):
function createRumor(
  event: { kind: number; content: string; tags?: string[][]; created_at?: number },
  senderPrivateKey: Uint8Array
): Rumor {
  const rumor: Record<string, unknown> = {
    created_at: now(),
    tags: [],
    ...event,
    pubkey: getPublicKey(senderPrivateKey),
  };

  rumor.id = getEventHash(rumor as UnsignedEvent);

  return rumor as unknown as Rumor;
}
Seal Creation (utils/nip17.ts:78-93):
function createSeal(
  rumor: Rumor,
  senderPrivateKey: Uint8Array,
  recipientPublicKey: string
): VerifiedEvent {
  return finalizeEvent(
    {
      kind: 13,
      content: nip44Encrypt(rumor, senderPrivateKey, recipientPublicKey),
      created_at: randomNow(), // Random timestamp for privacy
      tags: [],
    },
    senderPrivateKey
  ) as VerifiedEvent;
}
Gift Wrap Creation (utils/nip17.ts:101-114):
function createWrap(seal: VerifiedEvent, recipientPublicKey: string): VerifiedEvent {
  const randomKey = generateSecretKey();

  return finalizeEvent(
    {
      kind: 1059,
      content: nip44Encrypt(seal, randomKey, recipientPublicKey),
      created_at: randomNow(), // Random timestamp
      tags: [['p', recipientPublicKey]], // Only reveals recipient
    },
    randomKey // Signed with throwaway key
  ) as VerifiedEvent;
}

Social Features

User Profiles

Profile Viewer (app/(user-flow)/profile.tsx):
  • Banner and avatar display
  • Follower/following counts
  • Top followers (social proof)
  • Reputation score
  • Follow/unfollow actions
Profile Fetching (hooks/useNostrProfile.ts):
  • Profile metadata (kind 0)
  • Follower/following counts
  • Top followers list
  • User ranking

Mint Recommendations

Kind 38000: Cashu mint recommendation events Validation (helper/nostrClient.ts:64-67):
export function isCashuRecommendationEvent(e: NostrEvent): boolean {
  const kindTag = e.tags.find((t) => t[0] === 'k');
  return Boolean(kindTag && kindTag[1] === '38172');
}
Mint Discovery (hooks/coco/useNostrDiscoveredMints.ts):
  • Discovers mints from Nostr kind 38000 events
  • Filters by k tag (38172 = Cashu mint)
  • Extracts mint URLs from u tags

Dependencies

// package.json:56,113
{
  "@nostr-dev-kit/ndk-mobile": "^0.2.2",
  "nostr-tools": "^2.10.4"
}
Sovran uses nostr-tools for core Nostr operations and @nostr-dev-kit/ndk-mobile for mobile-optimized Nostr features including relay pool management.

Reference Implementation

  • NIP-06 Derivation: helper/keyDerivation.ts:14-31
  • NIP-17 Gift Wrap: utils/nip17.ts
  • NIP-19 Decoding: helper/nostrClient.ts:7-27
  • NIP-44 Encryption: utils/nip17.ts:34-44
  • Nostr Keys Provider: providers/NostrKeysProvider.tsx
  • Direct Messaging: hooks/useNostrDirectMessage.ts
  • Profile Management: hooks/useNostrProfile.ts

Learn More

BIP Standards

Learn about BIP-39 seed phrase derivation

Payment Requests

NUT-18 Nostr-based payment requests

Nostr Protocol

Official Nostr NIPs repository

Security

Secure key storage with expo-secure-store