Skip to content
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

add wallet login #31

Merged
merged 5 commits into from
Oct 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,8 @@ soon: create release via github actions
`
console.log('VITE_VERCEL_ENV', import.meta.env.VITE_VERCEL_ENV);
`

# Data Scheme

# accounts
- key is an edcsa key not a fucking normal EVM address
16 changes: 16 additions & 0 deletions components.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/globals.css",
"baseColor": "slate",
"cssVariables": true
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}
9 changes: 9 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,13 @@
"@radix-ui/colors": "^2.1.0",
"@radix-ui/react-alert-dialog": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-toast": "^1.1.4",
"@radix-ui/react-tooltip": "^1.0.6",
"@radix-ui/themes": "^1.1.0",
"@rainbow-me/rainbowkit": "^1.0.11",
"@sentry/react": "^7.68.0",
"@sentry/vite-plugin": "^2.7.1",
"@supabase/auth-ui-react": "^0.4.4",
Expand All @@ -58,6 +61,8 @@
"@tauri-apps/api": "^1.4",
"@webscopeio/react-textarea-autocomplete": "^4.9.2",
"axios": "^1.5.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"command-score": "^0.1.2",
"esbuild": "^0.19.2",
"focus-trap-react": "^10.2.1",
Expand Down Expand Up @@ -91,12 +96,16 @@
"react-tweet": "^3.1.1",
"rollup": "^2.0.0",
"squire-rte": "^2.0.3",
"tailwind-merge": "^1.14.0",
"tailwindcss": "^3.3.3",
"tailwindcss-animate": "^1.0.7",
"tauri-plugin-sql-api": "https://github.com/tauri-apps/tauri-plugin-sql#v1",
"tauri-plugin-store-api": "https://github.com/tauri-apps/tauri-plugin-store#v1",
"tauri-plugin-window-state-api": "https://github.com/tauri-apps/tauri-plugin-window-state#v1",
"ts-key-enum": "^2.0.12",
"typescript": "^5.1.6",
"viem": "^1.14.0",
"wagmi": "^1.4.2",
"zustand": "^4.2.0"
},
"devDependencies": {
Expand Down
10 changes: 6 additions & 4 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import { RouterProvider } from 'react-router-dom';
import './index.css';
import '@/config/theme/globals.css';
import '@radix-ui/themes/styles.css';
import { router } from "@/router";
import { disableContextMenu } from '@/common/helpers/tauri/contextMenu';
import { init } from "@aptabase/web";
import { RUNNING_IN_TAURI } from './common/constants/tauri';
import { ThemeProvider } from './common/hooks/ThemeProvider';

import '@/globals.css';

export const VITE_APTABASE_KEY = import.meta.env.VITE_APTABASE_KEY

Expand All @@ -22,5 +22,7 @@ if (!rootElement) throw new Error('Failed to find the root element');
const root = createRoot(rootElement);

root.render(
<RouterProvider router={router} />
<ThemeProvider defaultTheme="dark" storageKey="herocast-ui-theme">
<RouterProvider router={router} />
</ThemeProvider>
);
4 changes: 1 addition & 3 deletions src/common/components/CastRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -224,10 +224,8 @@ export const CastRow = ({ cast, isSelected, showChannel, onSelect, isThreadView
<div className="flex items-top gap-x-4">
{!isThreadView && (
<img
className='relative h-10 w-10 flex-none bg-gray-50 rounded-lg'
src={`https://res.cloudinary.com/merkle-manufactory/image/fetch/c_fill,f_png,w_144/${authorPfpUrl}`}
alt=""
className="relative h-10 w-10 flex-none rounded-full bg-gray-50"
referrerPolicy="no-referrer"
/>
)}
<div className="flex flex-col w-full">
Expand Down
2 changes: 1 addition & 1 deletion src/common/components/CastThreadView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export const CastThreadView = ({ cast, onBack, fid, isActive, setSelectedCast }:
<>
<div className="relative">
<img
className="flex mt-1 h-10 w-10 items-center justify-center rounded-full bg-gray-400 ring-1 ring-radix-slate5"
className="flex mt-1 h-10 w-10 items-center justify-center rounded-lg bg-gray-400 ring-1 ring-radix-slate5"
src={`https://res.cloudinary.com/merkle-manufactory/image/fetch/c_fill,f_png,w_144/${cast.author?.pfp?.url}`}
alt=""
/>
Expand Down
8 changes: 6 additions & 2 deletions src/common/components/ChannelsDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,13 @@ const ChannelsDropdown = ({ selectedChannel, onChange }: ChannelsComboboxProps)
<>
<div className="flex items-center">
{channel.icon_url ?
<img src={channel.icon_url} alt="" className="h-6 w-6 flex-shrink-0 rounded-full" />
<img
src={channel.icon_url}
alt=""
className="h-6 w-6 flex-shrink-0 rounded-lg"
/>
: <div className="h-6 w-6 flex-shrink-0 rounded-full bg-radix-slate8"></div>}
<span className={classNames('ml-3 truncate', selected && 'font-semibold')}>{channel.name}</span>
<span className={classNames('ml-3 truncate', selected ? 'font-semibold' : '')}>{channel.name}</span>
</div>

{selected && (
Expand Down
5 changes: 4 additions & 1 deletion src/common/components/CommandPalette/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export const getNavigationCommands = (navigate?: (path: string) => void | null):
action: () => navigate && navigate('/search'),
options: {
enableOnFormTags: false,
preventDefault: true,
},
},
{
Expand Down Expand Up @@ -137,7 +138,9 @@ export default function CommandPalette() {
},
shortcut: '',
aliases: [],
enableOnFormTags: false,
options: {
enableOnFormTags: false,
},
});
});
commands = commands.concat(nonHotkeyCommands);
Expand Down
199 changes: 199 additions & 0 deletions src/common/components/ConfirmOnchainSignerButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import React, { useEffect, useState } from "react";
import { encodeAbiParameters } from 'viem';
import {
useAccount,
useContractRead,
useContractWrite,
useNetwork,
usePrepareContractWrite,
useSwitchNetwork,
useWaitForTransaction
} from 'wagmi';
import { Button } from "@/components/ui/button";
import { KEY_REGISTRY } from "../constants/contracts/key-registry";
import { CheckIcon, Cog6ToothIcon } from "@heroicons/react/24/outline";
import { ID_REGISTRY } from "../constants/contracts/id-registry";
import { mnemonicToAccount } from "viem/accounts";
import { AccountObjectType, hydrate, useAccountStore } from "@/stores/useAccountStore";
import isEmpty from "lodash.isempty";
import { useAccountModal } from "@rainbow-me/rainbowkit";
import { WarpcastLoginStatus, getWarpcastSignerStatus } from "../helpers/warpcastLogin";
import { getUserInfoByFid } from "../helpers/neynar";

const VITE_APP_FID = import.meta.env.VITE_APP_FID
const VITE_APP_MNENOMIC = import.meta.env.VITE_APP_MNENOMIC

const SIGNED_KEY_REQUEST_VALIDATOR_EIP_712_DOMAIN = {
name: 'Farcaster SignedKeyRequestValidator',
version: '1',
chainId: 10,
verifyingContract: '0x00000000fc700472606ed4fa22623acf62c60553'
} as const;

const SIGNED_KEY_REQUEST_TYPE = [
{ name: 'requestFid', type: 'uint256' },
{ name: 'key', type: 'bytes' },
{ name: 'deadline', type: 'uint256' }
] as const;

const SIGNED_KEY_REQUEST_TYPE_V2 = [
{
components: [
{
internalType: 'uint256',
name: 'requestFid',
type: 'uint256',
},
{
internalType: 'address',
name: 'requestSigner',
type: 'address',
},
{
internalType: 'bytes',
name: 'signature',
type: 'bytes',
},
{
internalType: 'uint256',
name: 'deadline',
type: 'uint256',
},
],
internalType: 'struct SignedKeyRequestValidator.SignedKeyRequestMetadata',
name: 'metadata',
type: 'tuple',
},
] as const


type ConfirmOnchainSignerButtonType = {
account: AccountObjectType
}

const ConfirmOnchainSignerButton = ({ account }: ConfirmOnchainSignerButtonType) => {
const { chain } = useNetwork();
const { switchNetwork } = useSwitchNetwork();
const [signature, setSignature] = useState('');
const { openAccountModal } = useAccountModal();

const { address } = useAccount();
const { data: idOfUser, error: idOfUserError } = useContractRead({
...ID_REGISTRY,
chainId: 10,
functionName: address ? 'idOf' : undefined,
args: address ? [address] : undefined
});

if (idOfUserError) console.log('idOfUserError', idOfUserError);

const enabled = !isEmpty(account) && !isEmpty(account?.data) && signature !== '';
const appAccount = mnemonicToAccount(VITE_APP_MNENOMIC);
const deadline = Math.floor(Date.now() / 1000) + 86400; // signature is valid for 1 day

useEffect(() => {
const getSignature = async () => {
const res = await appAccount.signTypedData({
domain: SIGNED_KEY_REQUEST_VALIDATOR_EIP_712_DOMAIN,
types: {
SignedKeyRequest: SIGNED_KEY_REQUEST_TYPE,
},
primaryType: "SignedKeyRequest",
message: {
requestFid: BigInt(VITE_APP_FID),
key: account.publicKey as `0x${string}`,
deadline: BigInt(deadline),
},
});

setSignature(res);
console.log('getSignature done', res);
};

getSignature();
}, [account, deadline]);

const { config: addKeyConfig, error: prepareToAddKeyError } = usePrepareContractWrite({
...KEY_REGISTRY,
chainId: 10,
functionName: enabled ? 'add' : undefined,
args: [
1,
account.publicKey as `0x${string}`,
1,
enabled
? encodeAbiParameters(SIGNED_KEY_REQUEST_TYPE_V2, [
{
requestFid: BigInt(VITE_APP_FID),
requestSigner: appAccount.address,
signature: signature as `0x${string}`,
deadline: BigInt(deadline),
}
])
: `0x00`
],
enabled
});

const {
write: addKey,
data: addKeySignResult,
isLoading: addKeySignPending,
isSuccess: addKeySignSuccess,
error: addKeyError
} = useContractWrite(addKeyConfig);

const {
data: addKeyTxReceipt,
isSuccess: isAddKeyTxSuccess,
isLoading: isAddKeyTxLoading
} = useWaitForTransaction({ hash: addKeySignResult?.hash });

const onClick = () => {
if (chain?.id !== 10) {
switchNetwork?.(10);
} else if (!idOfUser) {
openAccountModal?.();
} else {
addKey?.();
}
}

const isPending = addKeySignPending || isAddKeyTxLoading;
const isError = addKeyError !== null || prepareToAddKeyError !== null;

const getButtonText = () => {
if (addKeySignPending) return 'Waiting for you to sign in your wallet'
if (isAddKeyTxLoading) return 'Waiting for onchain transaction to be confirmed'
if (addKeySignSuccess) return 'Confirmed onchain'
if (chain?.id !== 10) return 'Switch to Optimism'
if (!idOfUser) return 'Switch wallet'
if (prepareToAddKeyError) return 'Failed to prepare onchain request'
if (addKeyError) return 'Failed to execute onchain request'
return 'Confirm account onchain'
}

return (
<>
{address && !idOfUser && (
<p className="mb-2 text-sm text-gray-400">Connected wallet {address.slice(0, 6)}...{address.slice(-6)} is not registered on Farcaster</p>
)}
<Button
variant="default"
className="w-full"
onClick={() => onClick()}
disabled={!enabled || addKeySignPending || addKeySignSuccess || isError}
>
{isPending && (
<Cog6ToothIcon className="mr-1.5 h-5 w-5 text-gray-500 animate-spin" aria-hidden="true" />
)}
{addKeySignSuccess && (
<CheckIcon className="mr-1.5 h-5 w-5 text-gray-400" aria-hidden="true" />
)}
{getButtonText()}
</Button>
</>
)
}

export default ConfirmOnchainSignerButton;
4 changes: 2 additions & 2 deletions src/common/components/Embeds/NounsBuildEmbed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -184,14 +184,14 @@ const NounsBuildEmbed = ({ url }: { url: string }) => {
<p className="mt-1 flex items-baseline gap-x-2">
<span className="text-2xl font-semibold tracking-tight text-white">{stat.value}</span>
</p>
{stat.unit ? <span className="text-sm text-gray-100">{stat.unit}</span> : null}
{stat.unit ? <span className="text-sm text-gray-100">{stat.unit}</span> : null}
</div>
))}
{token?.image && (
<div className="border-t border-white/5 py-6 px-4 sm:px-6 lg:px-8">
<p className="text-sm font-medium leading-6 text-gray-400">Image</p>
<div className="mt-2 flex items-center gap-x-2">
<img src={token.image} className="w-16 h-16 rounded-sm" />
<img src={token.image} className="w-16 h-16 rounded-lg" />
</div>
</div>
)}
Expand Down
Loading