From f8662a3f8f4dbcfefefad01399abac71c40a4243 Mon Sep 17 00:00:00 2001 From: KONFeature Date: Mon, 9 Dec 2024 14:20:55 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A7=20Login=20+=20session=20token=20wi?= =?UTF-8?q?th=20privy=20wallet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- infra/wallet.ts | 3 +- .../app-essentials/src/blockchain/index.ts | 6 +- .../app-essentials/src/blockchain/wallet.ts | 12 +- .../domain/auth/models/WalletSessionDto.ts | 35 +++-- .../src/domain/auth/routes/wallet.ts | 76 +++++++++++ .../context/wallet/smartWallet/connector.ts | 15 ++- .../context/wallet/smartWallet/provider.ts | 23 +++- .../authentication/component/Privy/index.tsx | 124 ++++++++++++++++++ .../common/hook/useEnforceWagmiConnection.ts | 22 +++- packages/wallet/app/types/Session.ts | 24 +++- packages/wallet/app/views/auth/fallback.tsx | 14 +- 11 files changed, 310 insertions(+), 44 deletions(-) create mode 100644 packages/wallet/app/module/authentication/component/Privy/index.tsx diff --git a/infra/wallet.ts b/infra/wallet.ts index ba23813c3..fa1081761 100644 --- a/infra/wallet.ts +++ b/infra/wallet.ts @@ -4,7 +4,8 @@ import { indexerUrl, isProd, nexusRpcSecret, - pimlicoApiKey, privyAppId, + pimlicoApiKey, + privyAppId, umamiWalletWebsiteId, vapidPublicKey, } from "./config"; diff --git a/packages/app-essentials/src/blockchain/index.ts b/packages/app-essentials/src/blockchain/index.ts index 441652588..f9931d9bc 100644 --- a/packages/app-essentials/src/blockchain/index.ts +++ b/packages/app-essentials/src/blockchain/index.ts @@ -21,9 +21,7 @@ export { type GetTokenMetadataParams, } from "./actions/getTokenMetadata"; export { getTokenBalances } from "./actions/getTokenBalances"; -export { - KernelWallet -} from "./wallet" +export { KernelWallet } from "./wallet"; // Abis export { campaignFactoryAbi, @@ -62,4 +60,4 @@ export { getExecutionAbi, mintAbi } from "./abis/custom"; export { sendInteractionsSelector, sendInteractionSelector, -} from "./abis/selectors"; \ No newline at end of file +} from "./abis/selectors"; diff --git a/packages/app-essentials/src/blockchain/wallet.ts b/packages/app-essentials/src/blockchain/wallet.ts index 695ad5204..38de7056d 100644 --- a/packages/app-essentials/src/blockchain/wallet.ts +++ b/packages/app-essentials/src/blockchain/wallet.ts @@ -51,9 +51,9 @@ const webAuthNValidatorEnablingLayout = [ * @param signerPubKey */ function getWebAuthNSmartWalletInitCode({ - authenticatorIdHash, - signerPubKey, - }: { + authenticatorIdHash, + signerPubKey, +}: { authenticatorIdHash: Hex; signerPubKey: { x: Hex; y: Hex }; }): Hex { @@ -84,8 +84,8 @@ function getWebAuthNSmartWalletInitCode({ * @param ecdsaAddress */ function getFallbackWalletInitCode({ - ecdsaAddress, - }: { + ecdsaAddress, +}: { ecdsaAddress: Hex; }): Hex { if (!ecdsaAddress) throw new Error("Owner account not found"); @@ -108,4 +108,4 @@ function getFallbackWalletInitCode({ export const KernelWallet = { getWebAuthNSmartWalletInitCode, getFallbackWalletInitCode, -} \ No newline at end of file +}; diff --git a/packages/backend-elysia/src/domain/auth/models/WalletSessionDto.ts b/packages/backend-elysia/src/domain/auth/models/WalletSessionDto.ts index c2ee205f1..7425a5298 100644 --- a/packages/backend-elysia/src/domain/auth/models/WalletSessionDto.ts +++ b/packages/backend-elysia/src/domain/auth/models/WalletSessionDto.ts @@ -1,25 +1,32 @@ import { t } from "@backend-utils"; -export const WalletAuthResponseDto = t.Object({ - token: t.String(), +const PrivyWalletTokenDto = t.Object({ address: t.Address(), - authenticatorId: t.String(), - publicKey: t.Object({ - x: t.Hex(), - y: t.Hex(), - }), - transports: t.Optional(t.Array(t.String())), - sdkJwt: t.Object({ - token: t.String(), - expires: t.Number(), - }), + authenticatorId: t.TemplateLiteral([t.Literal("privy-"), t.String()]), + publicKey: t.Hex(), + transports: t.Undefined(), }); -export const WalletTokenDto = t.Object({ +export const WebAuthNWalletTokenDto = t.Object({ address: t.Address(), - authenticatorId: t.String(), + authenticatorId: t.String(), // 'Privy' in case of fallback authentication publicKey: t.Object({ x: t.Hex(), y: t.Hex(), }), transports: t.Optional(t.Array(t.String())), }); +export const WalletTokenDto = t.Union([ + PrivyWalletTokenDto, + WebAuthNWalletTokenDto, +]); + +export const WalletAuthResponseDto = t.Intersect([ + t.Object({ + token: t.String(), + sdkJwt: t.Object({ + token: t.String(), + expires: t.Number(), + }), + }), + WalletTokenDto, +]); diff --git a/packages/backend-elysia/src/domain/auth/routes/wallet.ts b/packages/backend-elysia/src/domain/auth/routes/wallet.ts index bec74e3c6..611981864 100644 --- a/packages/backend-elysia/src/domain/auth/routes/wallet.ts +++ b/packages/backend-elysia/src/domain/auth/routes/wallet.ts @@ -7,6 +7,7 @@ import { } from "@simplewebauthn/server"; import { Elysia } from "elysia"; import { Binary } from "mongodb"; +import { verifyMessage } from "viem/actions"; import { WalletAuthResponseDto } from "../models/WalletSessionDto"; import { walletSdkSessionService } from "../services/WalletSdkSessionService"; import { webAuthNService } from "../services/WebAuthNService"; @@ -53,6 +54,81 @@ export const walletAuthRoutes = new Elysia({ prefix: "/wallet" }) }, } ) + // Privy login + .post( + "/privyLogin", + async ({ + // Request + body: { expectedChallenge, signature, wallet, walletId, ssoId }, + // Response + error, + // Context + client, + walletJwt, + generateSdkJwt, + resolveSsoSession, + }) => { + // Rebuild the message that have been signed + const message = `I want to connect to Frak and I accept the CGU.\n Verification code:${expectedChallenge}`; + + // Verify the message signature + const isValidSignature = await verifyMessage(client, { + signature, + message, + address: wallet, + }); + if (!isValidSignature) { + return error(404, "Invalid signature"); + } + + const authenticatorId = `privy-${walletId}` as const; + + // Create the token and set the cookie + const token = await walletJwt.sign({ + address: wallet, + authenticatorId, + publicKey: wallet, + sub: wallet, + iat: Date.now(), + transports: undefined, + }); + + // Finally, generate a JWT token for the SDK + const sdkJwt = await generateSdkJwt({ wallet }); + + // If all good, mark the sso as done + if (ssoId) { + await resolveSsoSession({ + id: ssoId, + wallet, + authenticatorId, + }); + } + + return { + token, + address: wallet, + authenticatorId, + publicKey: wallet, + sdkJwt, + transports: undefined, + }; + }, + { + body: t.Object({ + expectedChallenge: t.String(), + wallet: t.Address(), + walletId: t.String(), + signature: t.Hex(), + // potential sso id + ssoId: t.Optional(t.Hex()), + }), + response: { + 404: t.String(), + 200: WalletAuthResponseDto, + }, + } + ) // Login .post( "/login", diff --git a/packages/wallet/app/context/wallet/smartWallet/connector.ts b/packages/wallet/app/context/wallet/smartWallet/connector.ts index 62674debd..589a4870d 100644 --- a/packages/wallet/app/context/wallet/smartWallet/connector.ts +++ b/packages/wallet/app/context/wallet/smartWallet/connector.ts @@ -1,11 +1,16 @@ import { currentChain } from "@/context/blockchain/provider"; import { getSmartAccountProvider } from "@/context/wallet/smartWallet/provider"; import type { SmartAccountV06 } from "@/context/wallet/smartWallet/utils"; +import type { ConnectedWallet } from "@privy-io/react-auth"; import type { Transport } from "viem"; import { createConnector } from "wagmi"; smartAccountConnector.type = "frakSmartAccountConnector" as const; +export type FrakWalletConnector = ReturnType< + ReturnType +>; + /** * Create a connector for the smart account */ @@ -22,7 +27,10 @@ export function smartAccountConnector< let provider: Provider | undefined; // Create the wagmi connector itself - return createConnector((config) => ({ + return createConnector< + Provider, + { onPrivyWalletsUpdate: (args: { wallets: ConnectedWallet[] }) => void } + >((config) => ({ id: "frak-wallet-connector", name: "Frak Smart Account", type: smartAccountConnector.type, @@ -151,5 +159,10 @@ export function smartAccountConnector< onDisconnect() { config.emitter.emit("disconnect"); }, + // When the list of privy wallets change + onPrivyWalletsUpdate({ wallets }: { wallets: ConnectedWallet[] }) { + // todo: Do some stuff with it + console.log("Wagmi provider wallets update", { wallets }); + }, })); } diff --git a/packages/wallet/app/context/wallet/smartWallet/provider.ts b/packages/wallet/app/context/wallet/smartWallet/provider.ts index 2b9c9366a..e34b81c10 100644 --- a/packages/wallet/app/context/wallet/smartWallet/provider.ts +++ b/packages/wallet/app/context/wallet/smartWallet/provider.ts @@ -10,6 +10,7 @@ import { parseWebAuthNAuthentication } from "@/context/wallet/smartWallet/webAut import { sessionAtom } from "@/module/common/atoms/session"; import { lastWebAuthNActionAtom } from "@/module/common/atoms/webauthn"; import { getSafeSession } from "@/module/listener/utils/localStorage"; +import type { PrivyWallet } from "@/types/Session"; import type { WebAuthNWallet } from "@/types/WebAuthN"; import { jotaiStore } from "@module/atoms/store"; import { startAuthentication } from "@simplewebauthn/browser"; @@ -28,7 +29,7 @@ type SmartAccountProvierParameters = { /** * Method when the account has changed */ - onAccountChanged: (newWallet?: WebAuthNWallet) => void; + onAccountChanged: (newWallet?: WebAuthNWallet | PrivyWallet) => void; }; /** @@ -102,10 +103,24 @@ export function getSmartAccountProvider< return undefined; } + const privyWallet = + currentWebAuthNWallet.authenticatorId.startsWith("privy-") + ? (currentWebAuthNWallet as PrivyWallet) + : undefined; + const webauthnWallet = privyWallet + ? undefined + : (currentWebAuthNWallet as WebAuthNWallet); + // Otherwise, build it - targetSmartAccount = await buildSmartAccount({ - wallet: currentWebAuthNWallet, - }); + if (privyWallet) { + // todo: Custom for privy here + return undefined; + } + targetSmartAccount = webauthnWallet + ? await buildSmartAccount({ + wallet: webauthnWallet, + }) + : undefined; // Save the new one currentSmartAccountClient = targetSmartAccount; diff --git a/packages/wallet/app/module/authentication/component/Privy/index.tsx b/packages/wallet/app/module/authentication/component/Privy/index.tsx new file mode 100644 index 000000000..06326d6a5 --- /dev/null +++ b/packages/wallet/app/module/authentication/component/Privy/index.tsx @@ -0,0 +1,124 @@ +import { authenticatedBackendApi } from "@/context/common/backendClient"; +import { sdkSessionAtom, sessionAtom } from "@/module/common/atoms/session"; +import { jotaiStore } from "@module/atoms/store"; +import { Button } from "@module/component/Button"; +import { Spinner } from "@module/component/Spinner"; +import { trackEvent } from "@module/utils/trackEvent"; +import { + type ConnectedWallet, + usePrivy, + useWallets, +} from "@privy-io/react-auth"; +import { useMutation } from "@tanstack/react-query"; +import { useState } from "react"; +import type { Address } from "viem"; +import { generatePrivateKey } from "viem/accounts"; + +export function PrivyLogin() { + const { ready, authenticated } = usePrivy(); + + if (!ready) { + return ; + } + + if (!authenticated) { + return ; + } + + return ; +} + +function DoPrivyLogin() { + const { ready, login, authenticated } = usePrivy(); + + if (!ready || authenticated) { + return ; + } + + return ( + + ); +} + +function DoPrivyAuthentication() { + const { wallets } = useWallets(); + const [wallet, setWallet] = useState( + wallets.length > 1 ? undefined : wallets[0] + ); + const { mutate: authenticate } = useMutation({ + mutationKey: ["privy-login", wallet?.address], + async mutationFn() { + if (!wallet) { + throw new Error("No wallet selected"); + } + + // Get the provider + const provider = await wallet.getEthereumProvider(); + + // Generate a random challenge + const challenge = generatePrivateKey(); + + // Build the message to sign + const message = `I want to connect to Frak and I accept the CGU.\n Verification code:${challenge}`; + + // Do the message signature + const signature = await provider.request({ + method: "personal_sign", + params: [message, wallet.address], + }); + + // Launch the backend authentication process + const { data, error } = + await authenticatedBackendApi.auth.wallet.privyLogin.post({ + expectedChallenge: challenge, + signature, + wallet: wallet.address as Address, + walletId: wallet.meta.id, + }); + if (error) { + throw error; + } + + // Extract a few data + const { token, sdkJwt, ...authentication } = data; + const session = { ...authentication, token }; + + // Store the session + jotaiStore.set(sessionAtom, session); + jotaiStore.set(sdkSessionAtom, sdkJwt); + + // Track the event + trackEvent("cta-privy-login"); + }, + }); + + if (!wallet) { + return ; + } + + return ( + + ); +} + +function PickPrivyWallet({ + wallets, + onPick, +}: { wallets: ConnectedWallet[]; onPick: (args: ConnectedWallet) => void }) { + return ( +
+ Pick a privy wallet + {wallets.map((wallet) => { + return ( + + ); + })} +
+ ); +} diff --git a/packages/wallet/app/module/common/hook/useEnforceWagmiConnection.ts b/packages/wallet/app/module/common/hook/useEnforceWagmiConnection.ts index 63098cfa8..0035238ef 100644 --- a/packages/wallet/app/module/common/hook/useEnforceWagmiConnection.ts +++ b/packages/wallet/app/module/common/hook/useEnforceWagmiConnection.ts @@ -1,4 +1,8 @@ -import { smartAccountConnector } from "@/context/wallet/smartWallet/connector"; +import { + type FrakWalletConnector, + smartAccountConnector, +} from "@/context/wallet/smartWallet/connector"; +import { useWallets } from "@privy-io/react-auth"; import { useEffect, useMemo } from "react"; import { useConfig, useConnect } from "wagmi"; @@ -22,6 +26,11 @@ export function useEnforceWagmiConnection() { [connectors] ); + /** + * Get the current privy wallets + */ + const { wallets } = useWallets(); + /** * Connect to the nexus connector */ @@ -53,4 +62,15 @@ export function useEnforceWagmiConnection() { }); connect({ connector: frakConnector }); }, [connect, frakConnector, isPending, state.current, state.status]); + + useEffect(() => { + if (!frakConnector) { + return; + } + + // Update the privy wallets from the frak connectors + (frakConnector as unknown as FrakWalletConnector).onPrivyWalletsUpdate({ + wallets, + }); + }, [wallets, frakConnector]); } diff --git a/packages/wallet/app/types/Session.ts b/packages/wallet/app/types/Session.ts index 20268e0f7..e96741d96 100644 --- a/packages/wallet/app/types/Session.ts +++ b/packages/wallet/app/types/Session.ts @@ -1,7 +1,15 @@ import type { WebAuthNWallet } from "@/types/WebAuthN"; +import type { Address, Hex } from "viem"; -export type Session = WebAuthNWallet & { +export type Session = { token: string; +} & (WebAuthNWallet | PrivyWallet); + +export type PrivyWallet = { + address: Address; + publicKey: Hex; + authenticatorId: `privy-${string}`; + transports: undefined; }; /* @@ -24,6 +32,20 @@ export type Session = WebAuthNWallet & { - Btw, need to setup google OAuth (so google account) + PWA stuff? + + For login: + - Privy sign msg, smth like "I accept Frak CGU by signing this message", pushed a new backend routes (login only, no rigstration on privy here) + + Transmit PrivyInstance to the wagmi connector? And then depending on the session type, check for embeded wallets with privy (if none ask to create new one) + - Maybe with privy instance we can get the wallet provider + - Nop, should use the useWallets hook, so LoginFallback should do multiple stuff: + 1. Privy login + 2. Privy wallets check (if none ask to create one) + 3. Sign message for backend login + 4. Transmit the wallets to the wagmi connector + + On logout, need to add the privy logout actions + */ export type InteractionSession = { diff --git a/packages/wallet/app/views/auth/fallback.tsx b/packages/wallet/app/views/auth/fallback.tsx index bdc66d7f9..720b22218 100644 --- a/packages/wallet/app/views/auth/fallback.tsx +++ b/packages/wallet/app/views/auth/fallback.tsx @@ -1,24 +1,14 @@ +import { PrivyLogin } from "@/module/authentication/component/Privy"; import { Back } from "@/module/common/component/Back"; import { Grid } from "@/module/common/component/Grid"; import styles from "@/views/auth/login.module.css"; -import { Button } from "@module/component/Button"; -import { Spinner } from "@module/component/Spinner"; -import { usePrivy } from "@privy-io/react-auth"; export default function Fallback() { - const { ready, login } = usePrivy(); - - if (!ready) { - return ; - } - return ( <> Back to biometric login CGU}> - + );