fix(privy): migrate wallet delegation to useSigners for TEE compatibility — GHB-200#105
Merged
Gastonfoncea merged 1 commit intoMay 20, 2026
Conversation
…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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
AgentDelegationCardfromuseHeadlessDelegatedActions(on-device only, hangs in TEE) touseSigners().addSigners(...)(TEE-compatible) and source the Solana wallet from@privy-io/react-auth/solanaso 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.apps/mcp/lib/privy/delegated-signer.ts): passauthorization_context.authorization_private_keystosignTransactionusing the newPRIVY_SIGNER_PRIVATE_KEYenv var (PKCS8 base64, no PEM headers).ApiKeysSection/ConnectedAppsSectiondepended on the fullprivyobject (whose identity changes every render) insideuseCallback, producing an infinite render loop onceuseSignerstriggered more frequent re-renders. Capturedprivyvia closure on mount.Why the previous code was broken
The Privy app runs in TEE execution (keys in a secure enclave). The legacy hooks
useHeadlessDelegatedActionsanduseDelegatedActionsonly support the on-device model and either hang (headless) or throw (modal variant) on a TEE app. The TEE-correct API isuseSigners().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.localand prod equivalents):PRIVY_APP_ID— same value as frontend'sNEXT_PUBLIC_PRIVY_APP_ID.PRIVY_APP_SECRET— server-side app secret.PRIVY_SIGNER_KEY_QUORUM_ID— same value as frontend'sNEXT_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)/app/credentials(ApiKeys and ConnectedApps sections load once)apps/mcp/tests/privy/delegated-signer.test.ts— 5/5 passingpnpm typecheckworkspace-wide — passesmcp__ghbounty__whoamireturnswallet_pubkeypopulated (depends on stake; tracked separately, not a blocker for this PR)mcp__ghbounty__submissions_createagainst a valid PR returns the submission row (depends on stake; tracked separately)Not included in this PR (tracked separately)
DATABASE_URLinpackages/db/drizzle.config.tsresolution — during testing,pnpm db:migrateapplied 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.🤖 Generated with Claude Code