Skip to main content

Phantom dApp Integration - Code Changes Reference

File: src/lib/phantomDeeplink.ts

Change 1: Module-Level Validation (Lines 16-36)

Added validation that runs when the module is imported:

// Validate Phantom dApp keys for mainnet (fail early with clear messaging)
const validatePhantomKeysForCluster = () => {
const cluster = import.meta.env.VITE_SOLANA_CLUSTER as string | undefined;
const publicKey = import.meta.env.VITE_PUBLIC_PHANTOM_DAPP_PUBLIC_KEY as string | undefined;

if (cluster === 'mainnet-beta' && !publicKey) {
throw new Error(
'❌ CRITICAL: Mainnet deployment requires Phantom dApp encryption keys.\n' +
'Missing VITE_PUBLIC_PHANTOM_DAPP_PUBLIC_KEY environment variable.\n\n' +
'Phantom requires a stable, persistent dApp encryption identity.\n' +
'These keys CANNOT be regenerated or the connection will break.\n\n' +
'Action: Generate keys with: node scripts/generate-phantomkeypair.js\n' +
'Then add to .env.mainnet:\n' +
' VITE_PUBLIC_PHANTOM_DAPP_PUBLIC_KEY=<public_key>\n' +
' PHANTOM_DAPP_SECRET_KEY=<secret_key>'
);
}
};

// Run validation at module load
validatePhantomKeysForCluster();

Purpose: Prevents mainnet deployments without proper configuration. Fails early with clear instructions.


Change 2: getDappKeyPair() Function (Lines 127-159)

Before (localStorage fallback - broken):

export const getDappKeyPair = (): nacl.BoxKeyPair => {
const stored = safeLocalStorage.getItem(STORAGE_KEYS.DAPP_KEYPAIR);
if (stored) {
try {
const parsed = JSON.parse(stored);
return {
publicKey: bs58.decode(parsed.publicKey),
secretKey: bs58.decode(parsed.secretKey),
};
} catch {
// Invalid stored data, generate new
}
}

// ❌ PROBLEM: Generates new keypair each session
const keypair = nacl.box.keyPair();
safeLocalStorage.setItem(STORAGE_KEYS.DAPP_KEYPAIR, JSON.stringify({
publicKey: bs58.encode(keypair.publicKey),
secretKey: bs58.encode(keypair.secretKey),
}));
return keypair;
};

After (environment variables - stable):

// Get the dApp keypair from environment variables (stable, not regenerated)
export const getDappKeyPair = (): nacl.BoxKeyPair => {
const publicKeyB58 = import.meta.env.VITE_PUBLIC_PHANTOM_DAPP_PUBLIC_KEY as string | undefined;
const secretKeyB58 = import.meta.env.PHANTOM_DAPP_SECRET_KEY as string | undefined;

if (!publicKeyB58) {
throw new Error(
'Missing VITE_PUBLIC_PHANTOM_DAPP_PUBLIC_KEY environment variable. ' +
'Phantom requires a stable dApp encryption identity. ' +
'Generate keys using: node scripts/generate-phantomkeypair.js'
);
}

if (!secretKeyB58) {
throw new Error(
'Missing PHANTOM_DAPP_SECRET_KEY environment variable. ' +
'This is required for Phantom deeplink encryption. ' +
'Generate keys using: node scripts/generate-phantomkeypair.js'
);
}

try {
return {
publicKey: bs58.decode(publicKeyB58),
secretKey: bs58.decode(secretKeyB58),
};
} catch (error) {
throw new Error(
'Failed to decode Phantom dApp keys from environment variables. ' +
'Ensure keys are valid base58-encoded strings.'
);
}
};

Key Improvements:

  • ✅ Loads from environment (stable across sessions)
  • ✅ Clear error messages if keys missing
  • ✅ Specific error if keys invalid (base58 decode)
  • ✅ No localStorage fallback (prevents key regeneration)
  • ✅ Same key used for all operations (Phantom recognizes dApp)

No Changes Required

The following functions use getDappKeyPair() and automatically benefit from the changes:

buildConnectUrl()          // Uses getDappKeyPair() internally
buildDisconnectUrl() // Uses getDappKeyPair() internally
buildSignMessageUrl() // Uses getDappKeyPair() internally
buildSignTransactionUrl() // Uses getDappKeyPair() internally
handleConnectCallback() // Uses getDappKeyPair() internally

No modifications needed to these functions - they automatically get the stable keys.


File: .env (Development)

Added:

# Phantom dApp Encryption Keys (devnet)
# Generate keypair using: node scripts/generate-phantomkeypair.js
VITE_PUBLIC_PHANTOM_DAPP_PUBLIC_KEY=your_devnet_public_key_here
PHANTOM_DAPP_SECRET_KEY=your_devnet_secret_key_here

Why:

  • VITE_ prefix: Public key available in browser
  • No prefix: Secret key only available at build time

File: .env.mainnet (Production)

Added:

# Phantom dApp Encryption Keys (mainnet)
# ⚠️ CRITICAL: These keys establish your dApp's identity with Phantom on mainnet
# NEVER regenerate these keys - Phantom requires a stable encryption identity
# Obtain from your Forsyt deployment configuration or generate with: node scripts/generate-phantomkeypair.js
VITE_PUBLIC_PHANTOM_DAPP_PUBLIC_KEY=your_mainnet_public_key_here
PHANTOM_DAPP_SECRET_KEY=your_mainnet_secret_key_here

Why:

  • Different keys for mainnet vs devnet (security best practice)
  • Clear warning about not regenerating
  • Clear instructions for obtaining keys

Environment Variable Details

VITE_PUBLIC_PHANTOM_DAPP_PUBLIC_KEY

PropertyValue
Typebase58-encoded public key
AccessBrowser (VITE_ prefix)
Size~44-46 characters
Example4VQW5T3...
RequiredYES for mainnet, optional for devnet

Used in:

  • Connect URL generation
  • Sign message URL generation
  • Sign transaction URL generation
  • Phantom encryption key exchange

PHANTOM_DAPP_SECRET_KEY

PropertyValue
Typebase58-encoded secret key
AccessBuild-time only (no VITE_ prefix)
Size~88-90 characters
Example3kF8P...
RequiredYES (secret decoding won't work without it)
Security⚠️ KEEP SECRET - don't commit to git

Used in:

  • Creating shared secret with Phantom
  • Decrypting Phantom responses
  • Internal encryption/decryption

Build Process

Vite Substitution

// In source code
const publicKey = import.meta.env.VITE_PUBLIC_PHANTOM_DAPP_PUBLIC_KEY;

// In built JavaScript (after build)
const publicKey = '4VQW5T3...'; // Substituted at build time

Secret Key Handling

Secret keys are read at build time but NOT embedded in output:

// Source
const secret = import.meta.env.PHANTOM_DAPP_SECRET_KEY;

// Build process
// 1. Read from environment
// 2. Use to decode/validate
// 3. NEVER write to output files

This is important: The secret key helps initialize the module validation, but the actual secret bytes aren't in the built code.


Integration Points

All Phantom Operations Use the Same Key

getDappKeyPair()

┌───────────────────────────────┐
│ Used by: │
├───────────────────────────────┤
│ • buildConnectUrl() │
│ • buildSignMessageUrl() │
│ • buildSignTransactionUrl() │
│ • buildDisconnectUrl() │
│ • handleConnectCallback() │
│ • createSharedSecret() │
└───────────────────────────────┘

Same encryption identity for all operations

Phantom recognizes dApp across sessions ✅

Error Handling Examples

Missing Public Key

// Before build (caught immediately)
if (import.meta.env.VITE_SOLANA_CLUSTER === 'mainnet-beta' && !publicKey) {
throw Error('❌ CRITICAL: Mainnet deployment requires Phantom dApp keys...');
}

// At runtime (if somehow missing)
if (!publicKeyB58) {
throw new Error(
'Missing VITE_PUBLIC_PHANTOM_DAPP_PUBLIC_KEY environment variable. ' +
'Phantom requires a stable dApp encryption identity...'
);
}

Invalid Base58

try {
bs58.decode(publicKeyB58); // ← May throw
} catch (error) {
throw new Error(
'Failed to decode Phantom dApp keys from environment variables. ' +
'Ensure keys are valid base58-encoded strings.'
);
}

Migration Path (If Updating Existing Code)

If you're updating from the old localStorage version:

  1. Remove localStorage storage (optional cleanup)

    // No longer needed, keys come from env
    safeLocalStorage.removeItem('phantom_dapp_keypair');
  2. Add environment variables

    • Generate new keypair (or use existing)
    • Add to .env files
  3. Test thoroughly

    • Clear browser storage
    • Clear Phantom session
    • Test connect/sign workflows
  4. Deploy

    • Old sessions will use new keys automatically
    • No data migration needed

Deployment Integration

Vercel

# vercel.json or Vercel UI
Environment Variables:
VITE_PUBLIC_PHANTOM_DAPP_PUBLIC_KEY: "..." (production value)
PHANTOM_DAPP_SECRET_KEY: "..." (production secret)

Build Command:
npm run build:mainnet

GitHub Actions

- name: Deploy
env:
VITE_PUBLIC_PHANTOM_DAPP_PUBLIC_KEY: ${{ secrets.PHANTOM_PUBLIC }}
PHANTOM_DAPP_SECRET_KEY: ${{ secrets.PHANTOM_SECRET }}
run: npm run build:mainnet

Testing the Changes

Local Testing

# 1. Add keys to .env
VITE_PUBLIC_PHANTOM_DAPP_PUBLIC_KEY=4VQW5T3...
PHANTOM_DAPP_SECRET_KEY=3kF8P...

# 2. Build
npm run build:dev

# 3. Run and check console
npm run preview:dev

# 4. Test Phantom connection
# Should work without localStorage manipulation

Mainnet Validation Testing

# 1. Create test .env without keys
VITE_SOLANA_CLUSTER=mainnet-beta

# 2. Try to build
npm run build:mainnet

# Expected: Build fails with clear error message
# "❌ CRITICAL: Mainnet deployment requires Phantom dApp keys..."

Rollback Plan

If you need to revert these changes:

# 1. Restore old getDappKeyPair() function
git checkout HEAD~1 src/lib/phantomDeeplink.ts

# 2. Remove from .env files
# Remove VITE_PUBLIC_PHANTOM_DAPP_PUBLIC_KEY
# Remove PHANTOM_DAPP_SECRET_KEY

# 3. Rebuild
npm run build

# 4. Deploy

Note: Using the old localStorage version will still work, but you'll have the key regeneration problem on mainnet. Better to fix it properly than revert.


Summary

AspectBeforeAfter
Key StoragelocalStorageEnvironment variables
Key GenerationPer-session (new key each time)Once at setup (stable)
Mainnet ValidationNoneAutomatic, fails early
Error MessagesGenericDetailed with instructions
Phantom Recognition❌ Fails (different keys)✅ Works (same key)
Security❌ Keys in browser storage✅ Keys in secure vault