Skip to content

fix(privy): migrate wallet delegation to useSigners for TEE compatibility — GHB-200#105

Merged
Gastonfoncea merged 1 commit into
mainfrom
gastonfoncea09/ghb-200-wallet-delegation-se-cuelga-en-appcredentials
May 20, 2026
Merged

fix(privy): migrate wallet delegation to useSigners for TEE compatibility — GHB-200#105
Gastonfoncea merged 1 commit into
mainfrom
gastonfoncea09/ghb-200-wallet-delegation-se-cuelga-en-appcredentials

Conversation

@Gastonfoncea
Copy link
Copy Markdown
Collaborator

Summary

  • Migrate AgentDelegationCard from useHeadlessDelegatedActions (on-device only, hangs in TEE) to useSigners().addSigners(...) (TEE-compatible) and source the Solana wallet from @privy-io/react-auth/solana so the SDK sees an initialized wallet proxy. Treat Privy's "Duplicate signer" response as idempotent success so the backend POST still runs if a previous attempt half-completed.
  • MCP server (apps/mcp/lib/privy/delegated-signer.ts): pass authorization_context.authorization_private_keys to signTransaction using the new PRIVY_SIGNER_PRIVATE_KEY env var (PKCS8 base64, no PEM headers).
  • Collateral fix on the same page: ApiKeysSection / ConnectedAppsSection depended on the full privy object (whose identity changes every render) inside useCallback, producing an infinite render loop once useSigners triggered more frequent re-renders. Captured privy via closure on mount.

Why the previous code was broken

The Privy app runs in TEE execution (keys in a secure enclave). The legacy hooks useHeadlessDelegatedActions and useDelegatedActions only support the on-device model and either hang (headless) or throw (modal variant) on a TEE app. The TEE-correct API is useSigners().addSigners({ signers: [{ signerId, policyIds }] }), which attaches a server-side key quorum registered in the Privy dashboard.

Env vars required after merge

Frontend (frontend/.env.local):

  • NEXT_PUBLIC_PRIVY_SIGNER_ID — public quorum ID created in the Privy dashboard.

MCP server (apps/mcp/.env.local and prod equivalents):

  • PRIVY_APP_ID — same value as frontend's NEXT_PUBLIC_PRIVY_APP_ID.
  • PRIVY_APP_SECRET — server-side app secret.
  • PRIVY_SIGNER_KEY_QUORUM_ID — same value as frontend's NEXT_PUBLIC_PRIVY_SIGNER_ID.
  • PRIVY_SIGNER_PRIVATE_KEY — PKCS8 base64 of the EC P-256 private key, no PEM headers, single line.

Test plan

  • /app/credentials → click "Authorize" → card shows "Authorized" with the wallet pubkey (verified manually)
  • Click "Revoke authorization" → card returns to "Not authorized" (verify in this PR)
  • No infinite render loop on /app/credentials (ApiKeys and ConnectedApps sections load once)
  • apps/mcp/tests/privy/delegated-signer.test.ts — 5/5 passing
  • pnpm typecheck workspace-wide — passes
  • After stake + delegation: mcp__ghbounty__whoami returns wallet_pubkey populated (depends on stake; tracked separately, not a blocker for this PR)
  • After delegation + stake: mcp__ghbounty__submissions_create against a valid PR returns the submission row (depends on stake; tracked separately)

Not included in this PR (tracked separately)

  • DATABASE_URL in packages/db/drizzle.config.ts resolution — during testing, pnpm db:migrate applied migration 0026 to a different DB than the one the frontend uses. Worked around by running the SQL manually in Supabase Studio. To fix in a follow-up task.
  • MCP support for external wallets (Phantom / Solflare / etc.) — only embedded TEE wallets work today, which limits the MCP to email-login users. Tracked as GHB-201.

🤖 Generated with Claude Code

…lity — GHB-200

The Authorize button on /app/credentials hung indefinitely because
useHeadlessDelegatedActions / useDelegatedActions only support on-device
execution, and this Privy app runs in TEE mode. Migrated to the
TEE-compatible useSigners().addSigners(...) API.

Frontend (AgentDelegationCard.tsx):
- Swap useHeadlessDelegatedActions for useSigners().addSigners({ signers:
  [{ signerId }] }) and removeSigners({ address }).
- Source the Solana wallet from @privy-io/react-auth/solana useWallets() so
  the call sees an initialized wallet proxy (deriving from linkedAccounts
  caused "Wallet proxy not initialized").
- Treat Privy's "Duplicate signer" response as idempotent success so the
  backend POST runs even if a previous attempt added the signer but failed
  to persist the DB row.

MCP server (delegated-signer.ts):
- Pass authorization_context.authorization_private_keys to signTransaction
  using the new PRIVY_SIGNER_PRIVATE_KEY env var (PKCS8 base64).

Collateral fixes (siblings on the same page) needed to verify the flow:
- ApiKeysSection / ConnectedAppsSection: useCallback depended on the
  full `privy` object whose identity changes every render, producing an
  infinite render loop once useSigners triggered more frequent re-renders.
  Capture privy via closure on mount instead.

Tests:
- Add beforeEach in delegated-signer.test.ts to set PRIVY_SIGNER_PRIVATE_KEY
  so the new env-var guard does not bail out before the mocked client runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Gastonfoncea Gastonfoncea requested a review from tomazzi14 as a code owner May 20, 2026 23:28
@vercel
Copy link
Copy Markdown

vercel Bot commented May 20, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
gh-bounty-frontend Ready Ready Preview, Comment May 20, 2026 11:30pm
ghbounty-mcp Ready Ready Preview, Comment May 20, 2026 11:30pm

@Gastonfoncea Gastonfoncea merged commit 4b0843f into main May 20, 2026
4 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant