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:
Rumor (kind 14): Unsigned event containing the actual message
Seal (kind 13): Encrypts the rumor with NIP-44, signed by sender
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