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.
Overview
P2PK (Pay-to-Public-Key) locking allows you to lock ecash tokens so that only the holder of a specific private key can redeem them. This provides an additional layer of security for ecash transfers.
Key Management
Sovran includes a built-in keyring for managing P2PK keys:
Key Types
Derived Keys:
app/settings-pages/keyring.tsx
// Generated from your wallet's mnemonic
const keypair = await manager.keyring.generateKeyPair();
Imported Keys:
app/settings-pages/keyring.tsx
// Import from nsec (Nostr) or raw hex
if (input.startsWith('nsec1')) {
const decoded = nip19.decode(input);
if (decoded.type === 'nsec') {
await manager.keyring.addKeyPair(decoded.data as Uint8Array);
}
}
Keyring Storage
Keys are stored securely and managed through the keyring API:
app/settings-pages/keyring.tsx
const loadKeypairs = useCallback(async () => {
if (!manager) return;
try {
setIsLoading(true);
const allKeys = await manager.keyring.getAllKeyPairs();
setKeypairs(allKeys);
} catch (error) {
console.error('Failed to load keypairs:', error);
keysLoadFailedPopup();
} finally {
setIsLoading(false);
}
}, [manager]);
Quick Access
Enable quick access to show your latest P2PK key in the receive menu:
interface SettingsState {
quickAccessP2PK: boolean;
regenerateP2PKOnReceive: boolean;
// ...
}
setQuickAccessP2PK: (enabled: boolean) => set({ quickAccessP2PK: enabled }),
getQuickAccessP2PK: () => get().quickAccessP2PK,
Receive Screen Integration
components/screens/ReceiveScreen.tsx
// Load latest keypair when P2PK quick access is enabled
useEffect(() => {
const loadLatestKeypair = async () => {
if (!manager || !quickAccessP2PK) return;
try {
const latest = await manager.keyring.getLatestKeyPair();
setLatestKeypair(latest);
} catch (error) {
console.error('Failed to load latest keypair:', error);
}
};
loadLatestKeypair();
}, [manager, quickAccessP2PK]);
// Build tabs array based on settings
const tabs = quickAccessP2PK ? ['Lightning', 'P2PK'] : ['Lightning'];
P2PK Tab
When quick access is enabled, a P2PK tab appears in the receive screen:
components/screens/ReceiveScreen.tsx
const renderP2PKContent = () => (
<>
{latestKeypair ? (
<>
<PaymentInfo data={latestKeypair.publicKeyHex} copyTarget="p2pk" unit="p2pk" />
<View style={{ marginHorizontal: 16 }}>
<Section title="P2PK PUBLIC KEY">
<ListGroup variant="secondary">
<PressableFeedback animation={false} onPress={handleCopyP2PKKey}>
<PressableFeedback.Scale>
<ListGroup.Item disabled>
<ListGroup.ItemPrefix>
<Icon name="solar:key-bold" size={20} color={opacity(foreground, 0.4)} />
</ListGroup.ItemPrefix>
<ListGroup.ItemContent>
<ListGroup.ItemTitle>
{truncateMiddle(latestKeypair.publicKeyHex, 10)}
</ListGroup.ItemTitle>
</ListGroup.ItemContent>
<ListGroup.ItemSuffix>
<Icon name="lets-icons:copy" size={20} color={opacity(foreground, 0.4)} />
</ListGroup.ItemSuffix>
</ListGroup.Item>
</PressableFeedback.Scale>
<PressableFeedback.Ripple />
</PressableFeedback>
</ListGroup>
</Section>
</View>
</>
) : (
<View style={{ marginHorizontal: 16, marginTop: 32 }}>
<View
style={{
backgroundColor: surfaceSecondary,
borderRadius: 12,
padding: 24,
alignItems: 'center',
}}>
<Icon name="mdi:key-variant" size={48} color={opacity(foreground, 0.25)} />
<Text
size={14}
style={{
color: opacity(foreground, 0.4),
marginTop: 12,
textAlign: 'center',
}}>
No P2PK keys yet. Generate one in Settings → P2PK Keys.
</Text>
</View>
</View>
)}
</>
);
Auto-regeneration
Automatically generate a new P2PK key after redeeming locked tokens for improved privacy:
setRegenerateP2PKOnReceive: (enabled: boolean) => set({ regenerateP2PKOnReceive: enabled }),
getRegenerateP2PKOnReceive: () => get().regenerateP2PKOnReceive,
Default: Enabled (recommended for privacy)
Derived keys are displayed as P2PK hex:
app/settings-pages/keyring.tsx
const displayKey = keypair.publicKeyHex;
Imported keys can be displayed as Nostr npub:
app/settings-pages/keyring.tsx
const npubValue = !isDerived
? nip19.npubEncode(keypair.publicKeyHex.replace(/^02/, ''))
: undefined;
Current Key Display
app/settings-pages/keyring.tsx
const CurrentKeyItem: React.FC<{
keypair: Keypair;
onCopy: (publicKey: string) => void;
}> = ({ keypair, onCopy }) => {
const [selectedTab, setSelectedTab] = useState('P2PK');
const isDerived = keypair.derivationIndex !== undefined;
const npubValue = !isDerived
? nip19.npubEncode(keypair.publicKeyHex.replace(/^02/, ''))
: undefined;
const isNpubTab = selectedTab === 'NPUB' && !isDerived;
const activeData = isNpubTab ? npubValue! : keypair.publicKeyHex;
const displayKey = isDerived ? keypair.publicKeyHex : activeData;
return (
<View className="p-4">
{!isDerived && (
<View className="mb-4">
<Tabs tabs={['P2PK', 'NPUB']} selectedTab={selectedTab} handleTabPress={handleTabPress} />
</View>
)}
<PressableFeedback
onPress={handleShowQR}
className="mb-4 self-center overflow-hidden rounded-xl">
<PressableFeedback.Highlight />
<View
style={{
alignItems: 'center',
padding: 12,
backgroundColor: foreground,
borderRadius: 12,
}}>
<QRCode value={activeData} size={120} color={surface} backgroundColor={foreground} />
</View>
</PressableFeedback>
<HStack align="center" spacing={8} className="mb-3.5 flex-wrap gap-y-2">
<Badge variant="success" icon="solar:key-bold" size={11}>
ACTIVE
</Badge>
{!isDerived && (
<Badge variant="primary" icon={isNpubTab ? 'ph:user-bold' : 'solar:key-bold'} size={11}>
{isNpubTab ? 'NPUB' : 'P2PK'}
</Badge>
)}
{isDerived && (
<Badge variant="primary" icon="mdi:key-arrow-right" size={11}>
DERIVED {keypair.derivationIndex}
</Badge>
)}
</HStack>
<View className="bg-surface rounded-xl px-3.5 py-3">
<Text size={12} className="text-foreground">
{displayKey}
</Text>
</View>
<HStack spacing={10} className="mt-3.5">
<Button variant="secondary" className="flex-1" onPress={handleCopy}>
<Icon name="lets-icons:copy" size={16} color={muted} />
<Button.Label style={{ color: muted }}>Copy</Button.Label>
</Button>
<Button variant="secondary" className="flex-1" onPress={handleShowQR}>
<Icon name="stash:qr-code" size={16} color={muted} />
<Button.Label style={{ color: muted }}>Show QR</Button.Label>
</Button>
</HStack>
</View>
);
};
Key Import
Sovran supports importing keys in multiple formats:
app/settings-pages/keyring.tsx
const tryImportKey = async (input: string): Promise<boolean> => {
if (!manager) return false;
// Strategy 1: Try as nsec
if (input.startsWith('nsec1')) {
try {
const decoded = nip19.decode(input);
if (decoded.type === 'nsec') {
await manager.keyring.addKeyPair(decoded.data as Uint8Array);
return true;
}
} catch {}
}
// Strategy 2: Try as raw 64-char hex
const rawBytes = hexToBytes(input);
if (rawBytes) {
try {
await manager.keyring.addKeyPair(rawBytes);
return true;
} catch {}
}
return false;
};
const hexToBytes = (hex: string): Uint8Array | null => {
if (hex.length !== 64 || !/^[0-9a-fA-F]+$/.test(hex)) {
return null;
}
const bytes = new Uint8Array(32);
for (let i = 0; i < 32; i++) {
bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
}
return bytes;
};
Import Flow
app/settings-pages/keyring.tsx
const handleImportNsec = () => {
Alert.prompt(
'Import Private Key',
'Enter your nsec or hex private key',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Import',
onPress: async (value: string | undefined) => {
if (!value || !manager) return;
try {
const trimmedValue = value.trim();
const success = await tryImportKey(trimmedValue);
if (success) {
keyImportedPopup();
await loadKeypairs();
} else {
invalidKeyFormatPopup();
}
} catch (error) {
console.error('Failed to import key:', error);
keyImportFailedPopup();
}
},
},
],
'plain-text'
);
};
Key Generation
app/settings-pages/keyring.tsx
const handleGenerateKey = async () => {
if (!manager) return;
try {
setIsGenerating(true);
await manager.keyring.generateKeyPair();
keyGeneratedPopup();
await loadKeypairs();
} catch (error) {
console.error('Failed to generate keypair:', error);
keyGenerateFailedPopup();
} finally {
setIsGenerating(false);
}
};
Settings
Preferences
app/settings-pages/keyring.tsx
<Section title="Preferences">
<ListGroup variant="secondary">
<ListGroup.Item>
<ListGroup.ItemContent>
<ListGroup.ItemTitle>Quick Access to Lock</ListGroup.ItemTitle>
<ListGroup.ItemDescription>
Show your latest P2PK locking key in the receive ecash menu
</ListGroup.ItemDescription>
</ListGroup.ItemContent>
<ListGroup.ItemSuffix>
<HeroSwitch
isSelected={quickAccessP2PK ?? false}
onSelectedChange={setQuickAccessP2PK}
/>
</ListGroup.ItemSuffix>
</ListGroup.Item>
<Separator className="mx-4" />
<ListGroup.Item>
<ListGroup.ItemContent>
<ListGroup.ItemTitle>Regenerate Key on Receive</ListGroup.ItemTitle>
<ListGroup.ItemDescription>
Automatically generate a new P2PK key after redeeming a locked token for improved
privacy
</ListGroup.ItemDescription>
</ListGroup.ItemContent>
<ListGroup.ItemSuffix>
<HeroSwitch
isSelected={regenerateP2PKOnReceive ?? true}
onSelectedChange={setRegenerateP2PKOnReceive}
/>
</ListGroup.ItemSuffix>
</ListGroup.Item>
</ListGroup>
</Section>
Use Cases
Locked Transfers
Send ecash tokens that can only be redeemed by a specific recipient:
- Obtain recipient’s P2PK public key
- Create send token with P2PK lock
- Only the recipient with matching private key can claim
Personal Recovery
Lock tokens to your own P2PK key for additional security:
- Generate or import a P2PK key
- Lock tokens to your public key
- Keep private key secure as backup recovery method
Nostr Integration
Use Nostr keys (nsec/npub) for P2PK locking:
- Import your Nostr nsec
- Share your npub as receiving address
- Tokens locked to your Nostr identity