-
-
Notifications
You must be signed in to change notification settings - Fork 2
feat: privy account abstraction #61
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: draft
Are you sure you want to change the base?
Changes from 4 commits
8298729
7953609
84a5737
d86583f
45770e1
c1462fa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import { PrivyAAFlow } from "@geist/ui-react/components/privy-aa/account-abstraction.js"; | ||
| import { PrivyAAProvider } from "@geist/ui-react/components/privy-aa/provider.js"; | ||
|
|
||
| import type { Meta, StoryObj } from "@storybook/react"; | ||
|
|
||
| interface PrivyAAProps { | ||
| appId: string; | ||
| } | ||
|
|
||
| function PrivyAAStories({ appId }: PrivyAAProps) { | ||
| return ( | ||
| <PrivyAAProvider appId={appId}> | ||
| <PrivyAAFlow /> | ||
| </PrivyAAProvider> | ||
| ); | ||
| } | ||
|
|
||
| const meta = { | ||
| title: "Privy/Account Abstraction", | ||
| component: PrivyAAStories, | ||
| argTypes: { | ||
| appId: { | ||
| control: "text", | ||
| name: "Privy App ID", | ||
| }, | ||
| }, | ||
| } satisfies Meta<typeof PrivyAAStories>; | ||
|
|
||
| export default meta; | ||
|
|
||
| type Story = StoryObj<typeof meta>; | ||
|
|
||
| export const Default: Story = { | ||
| args: { | ||
| appId: "", | ||
| }, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,4 +21,6 @@ AKAVE_API_KEY= | |
| LIGHTHOUSE_API_KEY= | ||
|
|
||
| STORACHA_KEY= | ||
| STORACHA_PROOF= | ||
| STORACHA_PROOF= | ||
|
|
||
| PIMLICO_API_KEY= | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| import { usePrivy, useWallets } from "@privy-io/react-auth"; | ||
| import { useSetActiveWallet } from "@privy-io/wagmi"; | ||
| import { SmartAccountClient, createSmartAccountClient } from "permissionless"; | ||
| import { toSimpleSmartAccount } from "permissionless/accounts"; | ||
| import { useEffect, useMemo, useState } from "react"; | ||
| import { zeroAddress } from "viem"; | ||
| import { entryPoint07Address } from "viem/account-abstraction"; | ||
| import { sepolia } from "viem/chains"; | ||
| import { http, useAccount, usePublicClient, useWalletClient } from "wagmi"; | ||
|
|
||
| import { Button } from "#components/shadcn/button"; | ||
| import { TransferButton } from "#components/transfer/transfer-button"; | ||
| import { usePrivyAA } from "./provider"; | ||
|
|
||
| export function PrivyAAFlow() { | ||
| const { pimlicoRpcUrl, pimlicoClient } = usePrivyAA(); | ||
| const { login, ready: isReady, authenticated: isAuthenticated } = usePrivy(); | ||
|
|
||
| const { isConnected } = useAccount(); | ||
| const [smartAccountClient, setSmartAccountClient] = | ||
| useState<SmartAccountClient | null>(null); | ||
|
|
||
| const publicClient = usePublicClient(); | ||
| const { wallets, ready: isWalletsReady } = useWallets(); | ||
| const { data: walletClient } = useWalletClient(); | ||
|
|
||
| const embeddedWallet = useMemo( | ||
| () => wallets.find((wallet) => wallet.walletClientType === "privy"), | ||
| [wallets], | ||
| ); | ||
|
|
||
| const { setActiveWallet } = useSetActiveWallet(); | ||
|
|
||
| useEffect(() => { | ||
| (async () => { | ||
| if (isConnected && walletClient && publicClient && embeddedWallet) { | ||
| const owner = await embeddedWallet.getEthereumProvider(); | ||
|
|
||
| if (!owner) { | ||
| throw new Error("No owner found"); | ||
| } | ||
|
|
||
| const simpleSmartAccount = await toSimpleSmartAccount({ | ||
| owner, | ||
| client: publicClient, | ||
| entryPoint: { | ||
| address: entryPoint07Address, | ||
| version: "0.7", | ||
| }, | ||
| }); | ||
|
|
||
| const smartAccountClient = createSmartAccountClient({ | ||
| account: simpleSmartAccount, | ||
| chain: sepolia, | ||
| bundlerTransport: http(pimlicoRpcUrl), | ||
| paymaster: pimlicoClient, | ||
| userOperation: { | ||
| estimateFeesPerGas: async () => { | ||
| return (await pimlicoClient.getUserOperationGasPrice()).fast; | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| setSmartAccountClient(smartAccountClient); | ||
| } | ||
| })(); | ||
| }, [isConnected, walletClient, publicClient]); | ||
|
|
||
| if (!isReady) { | ||
| return null; | ||
| } | ||
|
|
||
| if (isConnected && smartAccountClient && embeddedWallet) { | ||
| return ( | ||
| <div> | ||
| <TransferButton | ||
| amount={0} | ||
| to={zeroAddress} | ||
| smartAccountClient={smartAccountClient} | ||
| /> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| return ( | ||
| <> | ||
| {!isAuthenticated && <Button onClick={login}>Sign in with Privy</Button>} | ||
|
|
||
| {isWalletsReady && | ||
| wallets.map((wallet) => { | ||
| return ( | ||
| <Button | ||
| key={wallet.address} | ||
| onClick={() => { | ||
| setActiveWallet(wallet); | ||
| }} | ||
| > | ||
| Make active: {wallet.address} | ||
| </Button> | ||
| ); | ||
| })} | ||
| </> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| import { type PrivyClientConfig, PrivyProvider } from "@privy-io/react-auth"; | ||
| import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; | ||
| import { | ||
| type PimlicoClient, | ||
| createPimlicoClient, | ||
| } from "permissionless/clients/pimlico"; | ||
| import { | ||
| type PropsWithChildren, | ||
| createContext, | ||
| useContext, | ||
| useState, | ||
| } from "react"; | ||
| import { http } from "viem"; | ||
| import { entryPoint07Address } from "viem/account-abstraction"; | ||
| import { sepolia } from "viem/chains"; | ||
| import { WagmiProvider, createConfig } from "wagmi"; | ||
| import { generatePimlicoRpcUrl } from "./utils"; | ||
|
|
||
| interface PrivyAAContextProps { | ||
| appId: string; | ||
| } | ||
|
|
||
| interface PrivyAAContext { | ||
| appId: string; | ||
| pimlicoClient: PimlicoClient; | ||
| pimlicoRpcUrl: string; | ||
| } | ||
|
|
||
| const privyConfig: PrivyClientConfig = { | ||
| embeddedWallets: { | ||
| createOnLogin: "users-without-wallets", | ||
| requireUserPasswordOnCreate: true, | ||
| noPromptOnSignature: false, | ||
| }, | ||
| loginMethods: ["wallet", "email", "sms"], | ||
| appearance: { | ||
| showWalletLoginFirst: true, | ||
| }, | ||
| }; | ||
|
|
||
| const queryClient = new QueryClient({ | ||
| defaultOptions: { | ||
| queries: { | ||
| refetchOnWindowFocus: false, | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| const wagmiConfig = createConfig({ | ||
| chains: [sepolia], | ||
| transports: { | ||
| [sepolia.id]: http(), | ||
| }, | ||
| }); | ||
|
|
||
| const PrivyAAContext = createContext<PrivyAAContext>(undefined as never); | ||
|
|
||
| export function PrivyAAProvider({ | ||
| children, | ||
| appId, | ||
| }: PropsWithChildren<PrivyAAContextProps>) { | ||
| const pimlicoRpcUrl = generatePimlicoRpcUrl(sepolia.id); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. does pimlico able to support multiple chain choice? when user try to switch chain via wagmi, are we able to pick the right rpc? see how can we read the chains supported from wagmi
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes |
||
|
|
||
| const [pimlicoClient] = useState( | ||
| createPimlicoClient({ | ||
| transport: http(pimlicoRpcUrl), | ||
| entryPoint: { | ||
| address: entryPoint07Address, | ||
| version: "0.7", | ||
| }, | ||
| }), | ||
| ); | ||
|
|
||
| return ( | ||
| <PrivyAAContext.Provider value={{ appId, pimlicoClient, pimlicoRpcUrl }}> | ||
| <PrivyProvider appId={appId} config={privyConfig}> | ||
| <QueryClientProvider client={queryClient}> | ||
| <WagmiProvider config={wagmiConfig} reconnectOnMount={false}> | ||
| {children} | ||
| </WagmiProvider> | ||
| </QueryClientProvider> | ||
| </PrivyProvider> | ||
| </PrivyAAContext.Provider> | ||
| ); | ||
| } | ||
|
|
||
| export function usePrivyAA() { | ||
| const context = useContext(PrivyAAContext); | ||
|
|
||
| if (!context) { | ||
| throw new Error("Missing PrivyAAContext"); | ||
| } | ||
|
|
||
| return context; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import config from "@geist/domain/config"; | ||
|
|
||
| export const generatePimlicoRpcUrl = (chainId: number) => { | ||
| const pimlicoApiKey = config.pimlico.apiKey; | ||
| if (!pimlicoApiKey) { | ||
| throw new Error("Missing pimlicoApiKey"); | ||
| } | ||
| return `https://api.pimlico.io/v2/${chainId}/rpc?apikey=${pimlicoApiKey}`; | ||
| }; |
Uh oh!
There was an error while loading. Please reload this page.