Phantom dApp Integration Setup Guide
Overview
This guide explains how to set up stable Phantom dApp encryption keys for the GTA frontend. Phantom requires a persistent encryption identity - these keys cannot be regenerated once deployed to mainnet.
Why This Matters
Phantom uses a keypair-based encryption system to secure communication between the dApp and the Phantom wallet. If you regenerate these keys (like the old localStorage fallback did), Phantom will treat your dApp as a different identity and reject the connection with signature mismatches.
The old behavior: Generated new keys on each browser/session → breaks on mainnet The new behavior: Use stable environment-provided keys → persistent identity ✅
Step 1: Generate Your Phantom Keypair
Run the key generation script (available in smartbets-protocol):
cd path/to/smartbets-protocol
node scripts/generate-phantomkeypair.js
Output:
PUBLIC = 4VQW5...xyz (base58)
SECRET = 3kF8P...abc (base58)
Step 2: Configure Environment Variables
For Development (.env)
# Phantom dApp Encryption Keys (devnet)
VITE_PUBLIC_PHANTOM_DAPP_PUBLIC_KEY=<your_public_key>
PHANTOM_DAPP_SECRET_KEY=<your_secret_key>
For Mainnet (.env.mainnet)
# Phantom dApp Encryption Keys (mainnet)
# ⚠️ CRITICAL: Use DIFFERENT keys than devnet
# ⚠️ NEVER regenerate these keys once deployed
VITE_PUBLIC_PHANTOM_DAPP_PUBLIC_KEY=<your_mainnet_public_key>
PHANTOM_DAPP_SECRET_KEY=<your_mainnet_secret_key>
Security Note: Secret keys should never be committed to version control. Use secure secrets management in CI/CD (GitHub Secrets, Vercel Env Vars, etc.)
Step 3: Understand the Code Changes
What Changed
Before (localStorage fallback - ❌ broken on mainnet):
const getDappKeyPair = (): nacl.BoxKeyPair => {
const stored = localStorage.getItem('phantom_dapp_keypair');
if (stored) return stored;
// ❌ PROBLEM: Generates new key each session
const keypair = nacl.box.keyPair();
localStorage.setItem('phantom_dapp_keypair', keypair);
return keypair;
};
After (environment variables - ✅ stable identity):
const getDappKeyPair = (): nacl.BoxKeyPair => {
const publicKeyB58 = import.meta.env.VITE_PUBLIC_PHANTOM_DAPP_PUBLIC_KEY;
const secretKeyB58 = import.meta.env.PHANTOM_DAPP_SECRET_KEY;
if (!publicKeyB58 || !secretKeyB58) {
throw new Error('Missing Phantom dApp keys');
}
return {
publicKey: bs58.decode(publicKeyB58),
secretKey: bs58.decode(secretKeyB58),
};
};
Key Validation
The module includes automatic validation:
validatePhantomKeysForCluster();
// ↓
if (VITE_SOLANA_CLUSTER === 'mainnet-beta' && !publicKey) {
throw Error('Mainnet requires Phantom dApp keys');
}
This prevents deployments to mainnet without proper configuration.
Step 4: Build & Deploy
Development
npm run build:dev
npm run preview:dev
Mainnet
# Ensure .env.mainnet has the correct keys
npm run build:mainnet
# Deploy (keys passed via environment variables)
npm run build:mainnet && npm run preview:mainnet
Deployment Platforms
Vercel: Add secrets in project settings
VITE_PUBLIC_PHANTOM_DAPP_PUBLIC_KEY=<public>
PHANTOM_DAPP_SECRET_KEY=<secret>
Netlify: Add build environment variables
VITE_PUBLIC_PHANTOM_DAPP_PUBLIC_KEY=<public>
PHANTOM_DAPP_SECRET_KEY=<secret>
Step 5: Testing
Clear Phantom Session
After deployment, clear your Phantom session:
- Open Phantom → Settings → Trusted Apps
- Remove Forsyt (or your dApp)
- Try connecting again
Observable Signal (Success)
Phantom: Connect → Approve
Sign message → No error
Logs: handleSignMessageCallback success: true
Observable Signal (Failure)
Phantom: Shows "Error" screen
OR
Redirect loops back to Phantom
Troubleshooting
Error: "Missing VITE_PUBLIC_PHANTOM_DAPP_PUBLIC_KEY"
Cause: Environment variable not set
Fix:
- Verify
.env.mainnethas the keys - In CI/CD, ensure secrets are configured
- Rebuild and redeploy
Error: "Failed to decrypt data from Phantom"
Cause: Using wrong keys or keys changed
Fix:
- Verify keys match what was generated
- Clear Phantom session (Settings → Trusted Apps → Remove)
- Ensure each environment (devnet/mainnet) has separate keys
Connection works but Phantom shows "Trust Issue"
Cause: Phantom caching old encryption key
Fix:
- Uninstall Phantom app from phone
- Clear phone cache/app data
- Reinstall Phantom
- Try connecting again
Key Rotation (Mainnet Only)
If you absolutely must rotate keys on mainnet:
- Deploy new keys to production
- Important: Give users 2-3 weeks notice
- Have them remove Forsyt from Phantom Trusted Apps
- Users reconnect automatically with new keys
- Keep old deployment temporarily as fallback
Architecture Summary
┌─────────────────────────────────────┐
│ phantomDeeplink.ts Module │
├─────────────────────────────────────┤
│ 1. Load keys from ENV (not storage) │
│ 2. Validate keys exist │
│ 3. Build connect/sign URLs │
│ 4. Encrypt/decrypt payloads │
│ 5. Handle callbacks │
└─────────────────────────────────────┘
↓
.env files
├── VITE_PUBLIC_PHANTOM_DAPP_PUBLIC_KEY (browser)
└── PHANTOM_DAPP_SECRET_KEY (build time only)
↓
Phantom Deeplinks
├── /connect
├── /signMessage
├── /signTransaction
└── /disconnect
References
FAQ
Q: Can I use the same key for devnet and mainnet? A: No. Generate separate keypairs for each environment. This prevents key leakage and simplifies rotations.
Q: What if I accidentally commit the secret key? A: Immediately regenerate new keys and rotate in production. Revoke the old secret to prevent unauthorized access.
Q: How do I migrate from localStorage to environment variables? A: The new code automatically uses environment variables. Old localStorage keys are ignored.
Q: Does this affect WalletConnect or other wallets? A: No. This is specific to Phantom deeplink encryption. WalletConnect uses its own security model.
Q: What about mobile app builds? A: Ensure your build system passes environment variables. For React Native, use build-time variable injection.