From f6a0d2317dcbefc10d2ecdc5440cbde1dd74b856 Mon Sep 17 00:00:00 2001 From: KONFeature Date: Mon, 9 Dec 2024 16:40:49 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A7=20Privy=20signature=20for=20smart?= =?UTF-8?q?=20wallet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/domain/auth/routes/wallet.ts | 25 ++-- .../domain/auth/services/WebAuthNService.ts | 27 ++++- .../context/wallet/smartWallet/connector.ts | 35 ++++-- .../context/wallet/smartWallet/provider.ts | 114 ++++++++++-------- .../authentication/component/Privy/index.tsx | 23 ++-- .../common/hook/useEnforceWagmiConnection.ts | 20 +-- packages/wallet/app/types/Session.ts | 7 +- 7 files changed, 153 insertions(+), 98 deletions(-) diff --git a/packages/backend-elysia/src/domain/auth/routes/wallet.ts b/packages/backend-elysia/src/domain/auth/routes/wallet.ts index 61198186..9b509dfa 100644 --- a/packages/backend-elysia/src/domain/auth/routes/wallet.ts +++ b/packages/backend-elysia/src/domain/auth/routes/wallet.ts @@ -59,11 +59,12 @@ export const walletAuthRoutes = new Elysia({ prefix: "/wallet" }) "/privyLogin", async ({ // Request - body: { expectedChallenge, signature, wallet, walletId, ssoId }, + body: { expectedChallenge, signature, wallet, ssoId }, // Response error, // Context client, + getEcdsaWalletAddress, walletJwt, generateSdkJwt, resolveSsoSession, @@ -81,33 +82,38 @@ export const walletAuthRoutes = new Elysia({ prefix: "/wallet" }) return error(404, "Invalid signature"); } - const authenticatorId = `privy-${walletId}` as const; + const authenticatorId = `privy-${wallet}` as const; + + // Get the wallet address + const walletAddress = await getEcdsaWalletAddress({ + ecdsaAddress: wallet, + }); // Create the token and set the cookie const token = await walletJwt.sign({ - address: wallet, + address: walletAddress, authenticatorId, publicKey: wallet, - sub: wallet, + sub: walletAddress, iat: Date.now(), transports: undefined, }); // Finally, generate a JWT token for the SDK - const sdkJwt = await generateSdkJwt({ wallet }); + const sdkJwt = await generateSdkJwt({ wallet: walletAddress }); // If all good, mark the sso as done if (ssoId) { await resolveSsoSession({ id: ssoId, - wallet, + wallet: walletAddress, authenticatorId, }); } return { token, - address: wallet, + address: walletAddress, authenticatorId, publicKey: wallet, sdkJwt, @@ -118,7 +124,6 @@ export const walletAuthRoutes = new Elysia({ prefix: "/wallet" }) body: t.Object({ expectedChallenge: t.String(), wallet: t.Address(), - walletId: t.String(), signature: t.Hex(), // potential sso id ssoId: t.Optional(t.Hex()), @@ -224,7 +229,7 @@ export const walletAuthRoutes = new Elysia({ prefix: "/wallet" }) generateSdkJwt, authenticatorRepository, walletJwt, - getAuthenticatorWalletAddress, + getWebAuthnWalletAddress, parseCompressedWebAuthNResponse, resolveSsoSession, }) => { @@ -268,7 +273,7 @@ export const walletAuthRoutes = new Elysia({ prefix: "/wallet" }) // Get the wallet address const walletAddress = previousWallet ?? - (await getAuthenticatorWalletAddress({ + (await getWebAuthnWalletAddress({ authenticatorId: credential.id, pubKey: publicKey, })); diff --git a/packages/backend-elysia/src/domain/auth/services/WebAuthNService.ts b/packages/backend-elysia/src/domain/auth/services/WebAuthNService.ts index 0a8f5da8..e35eead7 100644 --- a/packages/backend-elysia/src/domain/auth/services/WebAuthNService.ts +++ b/packages/backend-elysia/src/domain/auth/services/WebAuthNService.ts @@ -10,7 +10,7 @@ import { import {WebAuthN, kernelAddresses, KernelWallet} from "@frak-labs/app-essentials"; import { Elysia } from "elysia"; import { getSenderAddress } from "permissionless/actions"; -import { type Hex, concatHex, keccak256, toHex } from "viem"; +import { type Address, type Hex, concatHex, keccak256, toHex } from "viem"; import { entryPoint06Address } from "viem/account-abstraction"; import { AuthenticatorRepository } from "../repositories/AuthenticatorRepository"; @@ -34,7 +34,7 @@ export const webAuthNService = new Elysia({ name: "Service.webAuthN" }) /** * Get a wallet address from an authenticator */ - async function getAuthenticatorWalletAddress({ + async function getWebAuthnWalletAddress({ authenticatorId, pubKey, }: { authenticatorId: string; pubKey: { x: Hex; y: Hex } }) { @@ -52,6 +52,24 @@ export const webAuthNService = new Elysia({ name: "Service.webAuthN" }) }); } + /** + * Get a wallet address from an authenticator + */ + async function getEcdsaWalletAddress({ + ecdsaAddress, + }: { ecdsaAddress: Address }) { + // Compute base stuff to fetch the smart wallet address + const initCode = KernelWallet.getFallbackWalletInitCode({ + ecdsaAddress, + }); + + // Get the sender address based on the init code + return getSenderAddress(client, { + initCode: concatHex([kernelAddresses.factory, initCode]), + entryPointAddress: entryPoint06Address, + }); + } + /** * Check if a signature is valid for a given wallet */ @@ -76,7 +94,7 @@ export const webAuthNService = new Elysia({ name: "Service.webAuthN" }) } // Check if the address match the signature provided - const walletAddress = await getAuthenticatorWalletAddress({ + const walletAddress = await getWebAuthnWalletAddress({ authenticatorId: signature.id, pubKey: authenticator.publicKey, }); @@ -122,7 +140,8 @@ export const webAuthNService = new Elysia({ name: "Service.webAuthN" }) isValidWebAuthNSignature, parseCompressedWebAuthNResponse, authenticatorRepository, - getAuthenticatorWalletAddress, + getWebAuthnWalletAddress, + getEcdsaWalletAddress, }; }) .as("plugin"); diff --git a/packages/wallet/app/context/wallet/smartWallet/connector.ts b/packages/wallet/app/context/wallet/smartWallet/connector.ts index 589a4870..259690f7 100644 --- a/packages/wallet/app/context/wallet/smartWallet/connector.ts +++ b/packages/wallet/app/context/wallet/smartWallet/connector.ts @@ -1,8 +1,8 @@ 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 type { PrivyInterface } from "@privy-io/react-auth"; +import type { Hex, Transport } from "viem"; import { createConnector } from "wagmi"; smartAccountConnector.type = "frakSmartAccountConnector" as const; @@ -26,10 +26,17 @@ export function smartAccountConnector< // The current provider let provider: Provider | undefined; + // The privy message signer (null if not ready) + let signViaPrivy: PrivyInterface["signMessage"] | undefined = undefined; + // Create the wagmi connector itself return createConnector< Provider, - { onPrivyWalletsUpdate: (args: { wallets: ConnectedWallet[] }) => void } + { + onPrivyInterfaceUpdate: ( + args: PrivyInterface["signMessage"] + ) => void; + } >((config) => ({ id: "frak-wallet-connector", name: "Frak Smart Account", @@ -146,6 +153,21 @@ export function smartAccountConnector< chainId: config.chains[0].id, }); }, + + async signViaPrivy(message, address) { + if (!signViaPrivy) { + throw new Error("Privy not ready yet"); + } + return (await signViaPrivy( + message, + { + title: "Action confirmation", + description: + "By signing the following hash, you will authorize the current frak action", + }, + address + )) as Hex; + }, }); } return provider; @@ -159,10 +181,9 @@ 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 }); + + onPrivyInterfaceUpdate(privySignMsg: PrivyInterface["signMessage"]) { + signViaPrivy = privySignMsg; }, })); } diff --git a/packages/wallet/app/context/wallet/smartWallet/provider.ts b/packages/wallet/app/context/wallet/smartWallet/provider.ts index e34b81c1..0b08c6b6 100644 --- a/packages/wallet/app/context/wallet/smartWallet/provider.ts +++ b/packages/wallet/app/context/wallet/smartWallet/provider.ts @@ -4,6 +4,7 @@ import { } from "@/context/blockchain/aa-provider"; import { currentChain, currentViemClient } from "@/context/blockchain/provider"; import { getSignOptions } from "@/context/wallet/action/signOptions"; +import { frakFallbackWalletSmartAccount } from "@/context/wallet/smartWallet/FrakFallbackSmartWallet"; import { frakWalletSmartAccount } from "@/context/wallet/smartWallet/FrakSmartWallet"; import type { SmartAccountV06 } from "@/context/wallet/smartWallet/utils"; import { parseWebAuthNAuthentication } from "@/context/wallet/smartWallet/webAuthN"; @@ -19,17 +20,24 @@ import { createSmartAccountClient, } from "permissionless"; import { getUserOperationGasPrice } from "permissionless/actions/pimlico"; -import type { Transport } from "viem"; +import type { Address, Hex, Transport } from "viem"; import type { SmartAccount } from "viem/account-abstraction"; /** * Properties */ -type SmartAccountProvierParameters = { +type SmartAccountProviderParameters = { /** * Method when the account has changed */ onAccountChanged: (newWallet?: WebAuthNWallet | PrivyWallet) => void; + + /** + * Method used to sign aa message via privy + * @param data + * @param address + */ + signViaPrivy: (data: Hex, address: Address) => Promise; }; /** @@ -42,7 +50,7 @@ type SmartAccountProvierParameters = { export function getSmartAccountProvider< transport extends Transport = Transport, account extends SmartAccountV06 = SmartAccountV06, ->({ onAccountChanged }: SmartAccountProvierParameters) { +>({ onAccountChanged, signViaPrivy }: SmartAccountProviderParameters) { console.log("Building a new smart account provider"); // A few types shortcut type ConnectorClient = SmartAccountClient< @@ -103,24 +111,11 @@ 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 - if (privyWallet) { - // todo: Custom for privy here - return undefined; - } - targetSmartAccount = webauthnWallet - ? await buildSmartAccount({ - wallet: webauthnWallet, - }) - : undefined; + targetSmartAccount = await buildSmartAccount({ + wallet: currentWebAuthNWallet, + signViaPrivy, + }); // Save the new one currentSmartAccountClient = targetSmartAccount; @@ -149,39 +144,56 @@ async function buildSmartAccount< account extends SmartAccountV06 = SmartAccountV06, >({ wallet, -}: { wallet: WebAuthNWallet }): Promise< + signViaPrivy, +}: { + wallet: WebAuthNWallet | PrivyWallet; + signViaPrivy: (data: Hex, address: Address) => Promise; +}): Promise< SmartAccountClient> > { - // Get the smart wallet client - const smartAccount = await frakWalletSmartAccount(currentViemClient, { - authenticatorId: wallet.authenticatorId, - signerPubKey: wallet.publicKey, - signatureProvider: async (message) => { - // Get the signature options from server - const options = await getSignOptions({ - authenticatorId: wallet.authenticatorId, - toSign: message, - }); - - // Start the client authentication - const authenticationResponse = await startAuthentication({ - optionsJSON: options, - }); - - // Store that in our last webauthn action atom - jotaiStore.set(lastWebAuthNActionAtom, { - wallet: wallet.address, - signature: authenticationResponse, - msg: options.challenge, - }); - - // Store this shit somewhere - - // Perform the verification of the signature - return parseWebAuthNAuthentication(authenticationResponse); - }, - preDeterminedAccountAddress: wallet.address, - }); + let smartAccount: SmartAccountV06; + // Get the webauthn smart wallet client + if (typeof wallet.publicKey === "object") { + // That's a webauthn wallet + smartAccount = await frakWalletSmartAccount(currentViemClient, { + authenticatorId: wallet.authenticatorId, + signerPubKey: wallet.publicKey, + signatureProvider: async (message) => { + // Get the signature options from server + const options = await getSignOptions({ + authenticatorId: wallet.authenticatorId, + toSign: message, + }); + + // Start the client authentication + const authenticationResponse = await startAuthentication({ + optionsJSON: options, + }); + + // Store that in our last webauthn action atom + jotaiStore.set(lastWebAuthNActionAtom, { + wallet: wallet.address, + signature: authenticationResponse, + msg: options.challenge, + }); + + // Store this shit somewhere + + // Perform the verification of the signature + return parseWebAuthNAuthentication(authenticationResponse); + }, + preDeterminedAccountAddress: wallet.address, + }); + } else { + // That's a privy wallet + smartAccount = await frakFallbackWalletSmartAccount(currentViemClient, { + ecdsaAddress: wallet.publicKey, + preDeterminedAccountAddress: wallet.address, + signatureProvider({ hash }) { + return signViaPrivy(hash, wallet.publicKey); + }, + }); + } // Get the bundler and paymaster clients const pimlicoTransport = getPimlicoTransport(); diff --git a/packages/wallet/app/module/authentication/component/Privy/index.tsx b/packages/wallet/app/module/authentication/component/Privy/index.tsx index 06326d6a..e54c966a 100644 --- a/packages/wallet/app/module/authentication/component/Privy/index.tsx +++ b/packages/wallet/app/module/authentication/component/Privy/index.tsx @@ -11,7 +11,7 @@ import { } from "@privy-io/react-auth"; import { useMutation } from "@tanstack/react-query"; import { useState } from "react"; -import type { Address } from "viem"; +import type { Address, Hex } from "viem"; import { generatePrivateKey } from "viem/accounts"; export function PrivyLogin() { @@ -29,6 +29,7 @@ export function PrivyLogin() { } function DoPrivyLogin() { + // todo: Maybe a fork session stuff for the SDK post SSO? const { ready, login, authenticated } = usePrivy(); if (!ready || authenticated) { @@ -43,9 +44,10 @@ function DoPrivyLogin() { } function DoPrivyAuthentication() { + const { signMessage } = usePrivy(); const { wallets } = useWallets(); const [wallet, setWallet] = useState( - wallets.length > 1 ? undefined : wallets[0] + wallets.length > 2 ? undefined : wallets[0] ); const { mutate: authenticate } = useMutation({ mutationKey: ["privy-login", wallet?.address], @@ -54,9 +56,6 @@ function DoPrivyAuthentication() { throw new Error("No wallet selected"); } - // Get the provider - const provider = await wallet.getEthereumProvider(); - // Generate a random challenge const challenge = generatePrivateKey(); @@ -64,10 +63,15 @@ function DoPrivyAuthentication() { 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], - }); + const signature = (await signMessage( + message, + { + title: "Frak authentication", + description: + "After this message approval, you will be logged in", + }, + wallet.address + )) as Hex; // Launch the backend authentication process const { data, error } = @@ -75,7 +79,6 @@ function DoPrivyAuthentication() { expectedChallenge: challenge, signature, wallet: wallet.address as Address, - walletId: wallet.meta.id, }); if (error) { throw error; diff --git a/packages/wallet/app/module/common/hook/useEnforceWagmiConnection.ts b/packages/wallet/app/module/common/hook/useEnforceWagmiConnection.ts index 0035238e..5aaf0d81 100644 --- a/packages/wallet/app/module/common/hook/useEnforceWagmiConnection.ts +++ b/packages/wallet/app/module/common/hook/useEnforceWagmiConnection.ts @@ -2,7 +2,7 @@ import { type FrakWalletConnector, smartAccountConnector, } from "@/context/wallet/smartWallet/connector"; -import { useWallets } from "@privy-io/react-auth"; +import { usePrivy } from "@privy-io/react-auth"; import { useEffect, useMemo } from "react"; import { useConfig, useConnect } from "wagmi"; @@ -26,11 +26,6 @@ export function useEnforceWagmiConnection() { [connectors] ); - /** - * Get the current privy wallets - */ - const { wallets } = useWallets(); - /** * Connect to the nexus connector */ @@ -63,14 +58,19 @@ export function useEnforceWagmiConnection() { connect({ connector: frakConnector }); }, [connect, frakConnector, isPending, state.current, state.status]); + /** + * Get the current privy wallets + */ + const { signMessage } = usePrivy(); + useEffect(() => { if (!frakConnector) { return; } // Update the privy wallets from the frak connectors - (frakConnector as unknown as FrakWalletConnector).onPrivyWalletsUpdate({ - wallets, - }); - }, [wallets, frakConnector]); + ( + frakConnector as unknown as FrakWalletConnector + ).onPrivyInterfaceUpdate(signMessage); + }, [signMessage, frakConnector]); } diff --git a/packages/wallet/app/types/Session.ts b/packages/wallet/app/types/Session.ts index e96741d9..d5c9cf71 100644 --- a/packages/wallet/app/types/Session.ts +++ b/packages/wallet/app/types/Session.ts @@ -14,18 +14,13 @@ export type PrivyWallet = { /* -* A new session would looks like either: - token: string - wallet: Address - { type: "webauthn" ...}, - { type: "fallback" ...} Need to generate a session token for the fallback type though Impact on: - wagmi connector - everything that use webauthn directly - - backend authentication - wallet settings page? + - recovery (use privy recovery insteead) - how to transmit the signature provider to the wagmi provider? Need to check how the privy custom conenctor is build - Maybe a privy store?