Skip to main content

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:

  1. Open Phantom → Settings → Trusted Apps
  2. Remove Forsyt (or your dApp)
  3. 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.mainnet has 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:

  1. Uninstall Phantom app from phone
  2. Clear phone cache/app data
  3. Reinstall Phantom
  4. Try connecting again

Key Rotation (Mainnet Only)

If you absolutely must rotate keys on mainnet:

  1. Deploy new keys to production
  2. Important: Give users 2-3 weeks notice
  3. Have them remove Forsyt from Phantom Trusted Apps
  4. Users reconnect automatically with new keys
  5. 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.