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