Skip to content

Commit

Permalink
feat: aa-sdk integration with alchemy signer and alchemy modular acco…
Browse files Browse the repository at this point in the history
…unt provider
  • Loading branch information
denniswon committed Mar 2, 2024
1 parent 8be3ae2 commit 2ba0a4b
Show file tree
Hide file tree
Showing 24 changed files with 6,429 additions and 141 deletions.
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
NODE_ENV=<YOUR_NODE_ENV>

ALCHEMY_KEY=<YOUR_ALCHEMY_KEY>
ALCHEMY_API_KEY=<YOUR_ALCHEMY_API_KEY>
ALCHEMY_ACCESS_KEY=<YOUR_ALCHEMY_ACCESS_KEY>
NEXT_PUBLIC_ALCHEMY_GAS_MANAGER_POLICY_ID=<YOUR_ALCHEMY_GAS_MANAGER_POLICY_ID>

# Xata credentials
Expand Down
1 change: 0 additions & 1 deletion .npmrc
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
>=18.17.0 <19.0.0
engine-strict=true
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
18.18
5 changes: 5 additions & 0 deletions app/api/rpc/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { ALCHEMY_RPC_URL } from '@/utils/constants';
import { NextResponse } from 'next/server';

// Next.js edge runtime
// https://nextjs.org/docs/pages/api-reference/edge
export const runtime = 'edge';
export const preferredRegion = 'iad1';

export async function POST(req: Request) {
const res = await fetch(ALCHEMY_RPC_URL, {
method: 'POST',
Expand Down
68 changes: 68 additions & 0 deletions app/auth/login.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { OAuthButtonGroup } from '@/components/auth/OAuthButtonGroup';
import { PasswordField } from '@/components/auth/PasswordFields';
import { AppIcon } from '@/components/icons/appIcon';
import {
Box,
Button,
Checkbox,
Container,
Divider,
FormControl,
FormLabel,
Heading,
HStack,
Input,
Link,
Stack,
Text
} from '@chakra-ui/react';

export const App = () => (
<Container maxW="lg" py={{ base: '12', md: '24' }} px={{ base: '0', sm: '8' }}>
<Stack spacing="8">
<Stack spacing="6">
<AppIcon />
<Stack spacing={{ base: '2', md: '3' }} textAlign="center">
<Heading size={{ base: 'xs', md: 'sm' }}>Log in to your account</Heading>
<Text color="fg.muted">
Don&apos;t have an account? <Link href="#">Sign up</Link>
</Text>
</Stack>
</Stack>
<Box
py={{ base: '0', sm: '8' }}
px={{ base: '4', sm: '10' }}
bg={{ base: 'transparent', sm: 'bg.surface' }}
boxShadow={{ base: 'none', sm: 'md' }}
borderRadius={{ base: 'none', sm: 'xl' }}
>
<Stack spacing="6">
<Stack spacing="5">
<FormControl>
<FormLabel htmlFor="email">Email</FormLabel>
<Input id="email" type="email" />
</FormControl>
<PasswordField />
</Stack>
<HStack justify="space-between">
<Checkbox defaultChecked>Remember me</Checkbox>
<Button variant="text" size="sm">
Forgot password?
</Button>
</HStack>
<Stack spacing="6">
<Button>Sign in</Button>
<HStack>
<Divider />
<Text textStyle="sm" whiteSpace="nowrap" color="fg.muted">
or continue with
</Text>
<Divider />
</HStack>
<OAuthButtonGroup />
</Stack>
</Stack>
</Box>
</Stack>
</Container>
);
24 changes: 4 additions & 20 deletions app/providers.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,21 @@
'use client';

import { ANVIL_NFT2_ADDRESS } from '@/utils/anvil';
import { freelyMintableNftAbi } from '@/utils/wagmi';
import { AlchemyAccountProvider } from '@/context/account';
import { GlobalStyle } from '@/theme/globalstyles';
import { default as theme } from '@/theme/theme';
import { CacheProvider } from '@chakra-ui/next-js';
import { ChakraProvider, ColorModeScript, extendTheme } from '@chakra-ui/react';
import { Global } from '@emotion/react';
import { createWalletClient, getContract, http, publicActions } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { foundry } from 'viem/chains';
import { GlobalStyle } from '../theme/globalstyles';
import { default as theme } from '../theme/theme';

const customTheme = extendTheme(theme);

void (async () => {
const account = privateKeyToAccount('0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80');
const client = createWalletClient({ account, transport: http('http://localhost:8545'), chain: foundry }).extend(
publicActions
);
const nftContract = getContract({ abi: freelyMintableNftAbi, address: ANVIL_NFT2_ADDRESS, client });
const txHash = await nftContract.write.mint([account.address, 5]);
await client.waitForTransactionReceipt({ hash: txHash });
const tokenUri = await nftContract.read.tokenURI([BigInt(0)]);
console.log({ tokenUri });
})();

export function Providers({ children }: { children: React.ReactNode }) {
return (
<CacheProvider>
<ColorModeScript initialColorMode={theme.config.initialColorMode} />
<ChakraProvider theme={customTheme}>
<Global styles={GlobalStyle} />
{children}
<AlchemyAccountProvider>{children}</AlchemyAccountProvider>
</ChakraProvider>
</CacheProvider>
);
Expand Down
21 changes: 21 additions & 0 deletions components/auth/OAuthButtonGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'use client';

import { Button, ButtonGroup, VisuallyHidden } from '@chakra-ui/react';
import { GitHubIcon, GoogleIcon, TwitterIcon } from '../icons/ProviderIcons';

const providers = [
{ name: 'Google', icon: <GoogleIcon /> },
{ name: 'Twitter', icon: <TwitterIcon /> },
{ name: 'GitHub', icon: <GitHubIcon /> }
];

export const OAuthButtonGroup = () => (
<ButtonGroup variant="secondary" spacing="4">
{providers.map(({ name, icon }) => (
<Button key={name} flexGrow={1}>
<VisuallyHidden>Sign in with {name}</VisuallyHidden>
{icon}
</Button>
))}
</ButtonGroup>
);
55 changes: 55 additions & 0 deletions components/auth/PasswordFields.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
'use client';

import {
FormControl,
FormLabel,
IconButton,
Input,
InputGroup,
InputProps,
InputRightElement,
useDisclosure,
useMergeRefs
} from '@chakra-ui/react';
import { forwardRef, useRef } from 'react';
import { HiEye, HiEyeOff } from 'react-icons/hi';

export const PasswordField = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
const { isOpen, onToggle } = useDisclosure();
const inputRef = useRef<HTMLInputElement>(null);

const mergeRef = useMergeRefs(inputRef, ref);
const onClickReveal = () => {
onToggle();
if (inputRef.current) {
inputRef.current.focus({ preventScroll: true });
}
};

return (
<FormControl>
<FormLabel htmlFor="password">Password</FormLabel>
<InputGroup>
<InputRightElement>
<IconButton
variant="text"
aria-label={isOpen ? 'Mask password' : 'Reveal password'}
icon={isOpen ? <HiEyeOff /> : <HiEye />}
onClick={onClickReveal}
/>
</InputRightElement>
<Input
id="password"
ref={mergeRef}
name="password"
type={isOpen ? 'text' : 'password'}
autoComplete="current-password"
required
{...props}
/>
</InputGroup>
</FormControl>
);
});

PasswordField.displayName = 'PasswordField';
45 changes: 45 additions & 0 deletions components/icons/ProviderIcons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { createIcon } from '@chakra-ui/react';

export const GoogleIcon = createIcon({
displayName: 'GoogleIcon',
path: (
<g transform="matrix(1, 0, 0, 1, 27.009001, -39.238998)">
<path
fill="#4285F4"
d="M -3.264 51.509 C -3.264 50.719 -3.334 49.969 -3.454 49.239 L -14.754 49.239 L -14.754 53.749 L -8.284 53.749 C -8.574 55.229 -9.424 56.479 -10.684 57.329 L -10.684 60.329 L -6.824 60.329 C -4.564 58.239 -3.264 55.159 -3.264 51.509 Z"
/>
<path
fill="#34A853"
d="M -14.754 63.239 C -11.514 63.239 -8.804 62.159 -6.824 60.329 L -10.684 57.329 C -11.764 58.049 -13.134 58.489 -14.754 58.489 C -17.884 58.489 -20.534 56.379 -21.484 53.529 L -25.464 53.529 L -25.464 56.619 C -23.494 60.539 -19.444 63.239 -14.754 63.239 Z"
/>
<path
fill="#FBBC05"
d="M -21.484 53.529 C -21.734 52.809 -21.864 52.039 -21.864 51.239 C -21.864 50.439 -21.724 49.669 -21.484 48.949 L -21.484 45.859 L -25.464 45.859 C -26.284 47.479 -26.754 49.299 -26.754 51.239 C -26.754 53.179 -26.284 54.999 -25.464 56.619 L -21.484 53.529 Z"
/>
<path
fill="#EA4335"
d="M -14.754 43.989 C -12.984 43.989 -11.404 44.599 -10.154 45.789 L -6.734 42.369 C -8.804 40.429 -11.514 39.239 -14.754 39.239 C -19.444 39.239 -23.494 41.939 -25.464 45.859 L -21.484 48.949 C -20.534 46.099 -17.884 43.989 -14.754 43.989 Z"
/>
</g>
)
});

export const GitHubIcon = createIcon({
displayName: 'GitHubIcon',
path: (
<path
fill="currentColor"
d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"
/>
)
});

export const TwitterIcon = createIcon({
displayName: 'TwitterIcon',
path: (
<path
fill="#03A9F4"
d="M24 4.557c-.883.392-1.832.656-2.828.775 1.017-.609 1.798-1.574 2.165-2.724-.951.564-2.005.974-3.127 1.195-.897-.957-2.178-1.555-3.594-1.555-3.179 0-5.515 2.966-4.797 6.045-4.091-.205-7.719-2.165-10.148-5.144-1.29 2.213-.669 5.108 1.523 6.574-.806-.026-1.566-.247-2.229-.616-.054 2.281 1.581 4.415 3.949 4.89-.693.188-1.452.232-2.224.084.626 1.956 2.444 3.379 4.6 3.419-2.07 1.623-4.678 2.348-7.29 2.04 2.179 1.397 4.768 2.212 7.548 2.212 9.142 0 14.307-7.721 13.995-14.646.962-.695 1.797-1.562 2.457-2.549z"
/>
)
});
1 change: 1 addition & 0 deletions components/images/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
'use client';

import { ImageRecord, TagRecord } from '@/utils/xata';
import { Link } from '@chakra-ui/next-js';
import { Box, Flex, Heading, Select, SimpleGrid, Tag, Text } from '@chakra-ui/react';
Expand Down
1 change: 1 addition & 0 deletions components/images/individual.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
'use client';

import { ImageRecord, TagRecord } from '@/utils/xata';
import { Link } from '@chakra-ui/next-js';
import { Box, BoxProps, Button, Flex, FormControl, FormLabel, Heading, Tag, Text, useToast } from '@chakra-ui/react';
Expand Down
1 change: 1 addition & 0 deletions components/images/upload.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
'use client';

import {
Box,
Button,
Expand Down
3 changes: 2 additions & 1 deletion components/layout/base.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
'use client';

import { Link } from '@chakra-ui/next-js';
import { Flex, Icon, IconButton, Text, Tooltip, useColorMode } from '@chakra-ui/react';
import { WeatherMoon20Filled, WeatherSunny20Filled } from '@fluentui/react-icons';
Expand All @@ -17,7 +18,7 @@ export const BaseLayout: FC<BaseLayoutProps> = ({ children }) => {
const { toggleColorMode, colorMode } = useColorMode();
const isDark = colorMode === 'dark';
return (
<Flex flexDir="column" maxW={1200} minH="100vh" mx={{ base: 4, lg: 'auto' }}>
<Flex flexDir="column" padding={12} maxW={1200} minH="100vh" mx={{ base: 4, lg: 'auto' }}>
<Flex justifyContent="space-between" py={8}>
<NextLink href="/">
<AppIcon />
Expand Down
60 changes: 60 additions & 0 deletions context/account.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
'use client';

import { AlchemyModularAccountClient, useAlchemyModularAccountClient } from '@/hooks/useAlchemyModularAccountClient';
import { useAlchemySigner } from '@/hooks/useAlchemySigner';
import { MultiOwnerModularAccount } from '@alchemy/aa-accounts';
import { AlchemySigner, User } from '@alchemy/aa-alchemy';
import { ReactNode, createContext, useContext, useEffect } from 'react';

type AlchemyAccountContextProps = {
// Functions

// Properties
client: AlchemyModularAccountClient | undefined;
account: MultiOwnerModularAccount<AlchemySigner> | undefined;
user: User | undefined;
};

const defaultUnset: any = null;
const AlchemyAccountContext = createContext<AlchemyAccountContextProps>({
// Default Values
client: defaultUnset,
account: defaultUnset,
user: defaultUnset
});

export const useAlchemyAccountContext = () => useContext(AlchemyAccountContext);

export const AlchemyAccountProvider = ({ children }: { children: ReactNode }) => {
const { user, alchemySigner } = useAlchemySigner();
const { client, connect } = useAlchemyModularAccountClient();

useEffect(() => {
if (!user || !client || client.account) return;

async function init(signer: AlchemySigner) {
return connect(signer);
}

alchemySigner
.then((signer) => {
if (!signer) {
return;
}
return init(signer).then((_client) => console.log('[Alchemy Account] active', _client.account.address));
})
.catch((e) => console.error('[Alchemy Account] error', e));
}, [alchemySigner, client, client?.account, connect, user]);

return (
<AlchemyAccountContext.Provider
value={{
user,
account: client?.account,
client
}}
>
{children}
</AlchemyAccountContext.Provider>
);
};
Loading

0 comments on commit 2ba0a4b

Please sign in to comment.