diff --git a/src/components/Faucet/Faucet.tsx b/src/components/Faucet/Faucet.tsx index 2c7b1ec..fc053c5 100644 --- a/src/components/Faucet/Faucet.tsx +++ b/src/components/Faucet/Faucet.tsx @@ -1,49 +1,65 @@ -import axios from 'axios'; -import { TextInput, Text, Paper, Button, Divider, Anchor, Stack, Alert } from '@mantine/core'; -import { useForm } from '@mantine/form'; -import { notifications } from '@mantine/notifications'; -import { IconAlertCircle, IconExternalLink, IconCheck, IconX } from '@tabler/icons-react'; +import axios from "axios"; +import { + TextInput, + Text, + Paper, + Button, + Divider, + Anchor, + Stack, + Alert, +} from "@mantine/core"; +import { useForm } from "@mantine/form"; +import { notifications } from "@mantine/notifications"; +import { + IconAlertCircle, + IconExternalLink, + IconCheck, + IconX, +} from "@tabler/icons-react"; -import { useConnection } from '@/contexts/TezosContext/TezosContext'; -import { WalletButton } from '@/components/WalletButton/WalletButton'; -import { TezosIcon } from '@/icons/TezosIcon/TezosIcon'; +import { useConnection } from "@/contexts/TezosContext/TezosContext"; +import { WalletButton } from "@/components/WalletButton/WalletButton"; +import { TezosIcon } from "@/icons/TezosIcon/TezosIcon"; export function Faucet() { const { address, connect } = useConnection(); const form = useForm({ initialValues: { - address: '', + address: "", }, }); const getTokens = async (targetAddress: string) => { try { notifications.show({ - id: 'faucet', - title: 'Sending...', - message: 'Your tokens are on the way! 🚀', + id: "faucet", + title: "Sending...", + message: "Your tokens are on the way! 🚀", loading: true, autoClose: false, }); await axios.get( - `https://faucet-bot.marigold.dev/network/ghostnet/getmoney/CTEZ/${targetAddress}` + `https://faucet-bot.marigold.dev/network/ghostnet/getmoney/CTEZ/${targetAddress}`, ); notifications.update({ - id: 'faucet', - title: 'Success!', - message: 'Check your wallet !', + id: "faucet", + title: "Success!", + message: "Check your wallet !", icon: , - color: 'teal', + color: "teal", }); - } catch (err: any) { + } catch (error: unknown) { notifications.update({ - id: 'faucet', - title: 'Error!', - message: `Something went wrong: ${(err as Error).message}`, + id: "faucet", + title: "Error!", + message: `Something went wrong: ${ + error instanceof Error ? error.message : "Unknown request error" + }`, icon: , - color: 'red', + color: "red", }); } }; @@ -53,7 +69,7 @@ export function Faucet() { } mb="1.5rem"> This faucet is provided by Marigold.
- {' '} + {" "} Check it out.
@@ -67,13 +83,19 @@ export function Faucet() { } - onClick={async () => getTokens(address)} + onClick={() => { + void getTokens(address); + }} > Request ctez tokens for {address} ) : ( - } onClick={connect}> + } + onClick={connect} + > Connect )} @@ -86,9 +108,7 @@ export function Faucet() {
{ - (async () => { - await getTokens(values.address); - })(); + void getTokens(values.address); })} > @@ -97,8 +117,10 @@ export function Faucet() { label="Tezos address" placeholder="tz1..." value={form.values.address} - onChange={(event) => form.setFieldValue('address', event.currentTarget.value)} - error={form.errors.email && 'Invalid address'} + onChange={(event) => + form.setFieldValue("address", event.currentTarget.value) + } + error={form.errors.address && "Invalid address"} radius="xl" size="lg" /> diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index 7c8f77c..fe45e39 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -10,7 +10,7 @@ import { Button, useMantineColorScheme, Badge, -} from '@mantine/core'; +} from "@mantine/core"; import { IconSun, IconMoonStars, @@ -18,46 +18,53 @@ import { IconChevronDown, IconSwitchHorizontal, IconNetwork, -} from '@tabler/icons-react'; -import { NetworkType } from '@airgap/beacon-types'; +} from "@tabler/icons-react"; +import { NetworkType } from "@airgap/beacon-types"; +import { notifications } from "@mantine/notifications"; -import { useConnection } from '@/contexts/TezosContext/TezosContext'; -import { TezosIcon } from '@/icons/TezosIcon/TezosIcon'; -import { WalletButton } from '@/components/WalletButton/WalletButton'; -import { shortAddress } from '@/lib/utils/utils'; +import { useConnection } from "@/contexts/TezosContext/TezosContext"; +import { TezosIcon } from "@/icons/TezosIcon/TezosIcon"; +import { WalletButton } from "@/components/WalletButton/WalletButton"; +import { shortAddress } from "@/lib/utils/utils"; const useStyles = createStyles((theme) => ({ inner: { height: rem(56), - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', + display: "flex", + justifyContent: "space-between", + alignItems: "center", }, links: { - [theme.fn.smallerThan('sm')]: { - display: 'none', + [theme.fn.smallerThan("sm")]: { + display: "none", }, }, burger: { - [theme.fn.largerThan('sm')]: { - display: 'none', + [theme.fn.largerThan("sm")]: { + display: "none", }, }, link: { - display: 'block', + display: "block", lineHeight: 1, padding: `${rem(8)} ${rem(12)}`, borderRadius: theme.radius.sm, - textDecoration: 'none', - color: theme.colorScheme === 'dark' ? theme.colors.dark[0] : theme.colors.gray[7], + textDecoration: "none", + color: + theme.colorScheme === "dark" + ? theme.colors.dark[0] + : theme.colors.gray[7], fontSize: theme.fontSizes.sm, fontWeight: 500, - '&:hover': { - backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[0], + "&:hover": { + backgroundColor: + theme.colorScheme === "dark" + ? theme.colors.dark[6] + : theme.colors.gray[0], }, }, @@ -71,6 +78,18 @@ export function Header() { const { classes } = useStyles(); const { address, connect, disconnect, network } = useConnection(); + const runWalletAction = (action: () => Promise): void => { + void action().catch((error: unknown) => { + notifications.show({ + id: "wallet-action-error", + title: "Wallet action failed", + message: + error instanceof Error ? error.message : "Unknown wallet error", + color: "red", + }); + }); + }; + return ( @@ -78,7 +97,7 @@ export function Header() { Etherlink Bridge - {network === NetworkType.MAINNET ? 'Mainnet' : 'Testnet'} + {network === NetworkType.MAINNET ? "Mainnet" : "Testnet"} @@ -88,7 +107,9 @@ export function Header() { variant="default" color="gray" radius="xl" - leftIcon={} + leftIcon={ + + } rightIcon={} disabled > @@ -110,20 +131,27 @@ export function Header() { - } onClick={connect}> + } + onClick={() => runWalletAction(connect)} + > Switch Account } - onClick={disconnect} + onClick={() => runWalletAction(disconnect)} > Disconnect ) : ( - } onClick={connect}> + } + onClick={() => runWalletAction(connect)} + > Connect )} @@ -133,11 +161,20 @@ export function Header() { radius="xl" sx={(theme) => ({ backgroundColor: - theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[0], - color: theme.colorScheme === 'dark' ? theme.colors.yellow[4] : theme.colors.blue[6], + theme.colorScheme === "dark" + ? theme.colors.dark[6] + : theme.colors.gray[0], + color: + theme.colorScheme === "dark" + ? theme.colors.yellow[4] + : theme.colors.blue[6], })} > - {colorScheme === 'dark' ? : } + {colorScheme === "dark" ? ( + + ) : ( + + )} diff --git a/src/contexts/TezosContext/TezosContext.tsx b/src/contexts/TezosContext/TezosContext.tsx index ddf49d6..3ae6151 100644 --- a/src/contexts/TezosContext/TezosContext.tsx +++ b/src/contexts/TezosContext/TezosContext.tsx @@ -9,86 +9,89 @@ * All modifications are licensed under: MIT */ -// todo: this shouldnt know about nextjs -// the email modal should live in @kanvas/client/ui -// and we should simply handle it here -import { Context, createContext, useContext, useEffect, useState } from 'react'; -// import { useCookies } from 'react-cookie' -import { setCookie } from 'cookies-next'; -import { NetworkType } from '@airgap/beacon-types'; +import { createContext, useContext, useEffect, useState } from "react"; +import type { ReactNode } from "react"; +import { setCookie } from "cookies-next"; +import { NetworkType } from "@airgap/beacon-types"; -import { connectBeacon } from '@/lib/beacon/beacon'; -import { WalletApi } from '@/lib/beacon/beacon-types'; +import { connectBeacon } from "@/lib/beacon/beacon"; +import { WalletApi } from "@/lib/beacon/beacon-types"; interface ConnectionContextType extends Partial { connect: () => Promise; - network: string; - setNetwork: (network: NetworkType) => void; + network: NetworkType; + setNetwork: (network: NetworkType) => Promise; } const ConnectionContext = createContext(null); -export const ConnectionProvider = ({ children }: { children: any }) => { +export const ConnectionProvider = ({ children }: { children: ReactNode }) => { const [wallet, setWallet] = useState(); - const [network, setNetwork] = useState(NetworkType.GHOSTNET); + const [network, setNetworkState] = useState( + NetworkType.GHOSTNET, + ); - const setWalletCookie = (address: string | undefined) => { - setCookie('viewer-address', address, { + const setWalletCookie = (address: string | undefined): void => { + setCookie("viewer-address", address, { maxAge: 60 * 60 * 24 * 30, }); }; - useEffect(() => { - setWalletCookie(wallet?.address); - }, [wallet?.address]); + const disconnect = async (): Promise => { + const activeWallet = wallet; + setWallet(undefined); + setWalletCookie(undefined); + await activeWallet?.disconnect(); + }; useEffect(() => { - connectBeacon(false, network) - .then(setWallet) + let cancelled = false; + + void connectBeacon(false, network) + .then((nextWallet) => { + if (!cancelled) { + setWallet(nextWallet); + } + }) .catch(() => { - console.log('no existing beacon connection'); + if (!cancelled) { + console.info("No existing Beacon connection was found."); + } }); - }, []); - const onInitialConnectionComplete = async (walletApi: WalletApi): Promise => { - setWallet(walletApi); - // const { address, connection } = wallet; - // return connection; - }; + return () => { + cancelled = true; + }; + }, [network]); - // eslint-disable-next-line @typescript-eslint/require-await - const disconnect = async function () { - console.log('disconnecting'); - setWallet(undefined); - setWalletCookie(undefined); + useEffect(() => { + setWalletCookie(wallet?.address); + }, [wallet?.address]); + + const connect = async (): Promise => { + try { + const connectedWallet = await connectBeacon(true, network); + setWallet(connectedWallet); + } catch (error: unknown) { + await disconnect(); + const reason = + error instanceof Error ? error.message : "Unknown connection error"; + throw new Error( + `Error connecting to wallet. Please try again later: ${reason}`, + ); + } }; return ( - connectBeacon(true, network) - .then(onInitialConnectionComplete) - .catch((err) => { - disconnect(); - throw new Error(`Error connecting to wallet, please try again later ${err.message}`); - }), + connect, ...wallet, - disconnect: async () => { - await disconnect(); - await wallet?.disconnect(); - }, + disconnect, network, - setNetwork: async (newNetwork: NetworkType) => { - setNetwork(newNetwork); + setNetwork: async (newNetwork: NetworkType): Promise => { await disconnect(); - await wallet?.disconnect(); - connectBeacon(true, newNetwork) - .then(onInitialConnectionComplete) - .catch(() => { - disconnect(); - throw new Error('Error connecting to wallet, please try again later'); - }); + setNetworkState(newNetwork); }, }} > @@ -97,13 +100,13 @@ export const ConnectionProvider = ({ children }: { children: any }) => { ); }; -export type NotNothing = T extends null | undefined ? never : T; - /** - * Managing wallet connects and disconnects with kukai and beacon - * and registering the connection with kanvas + * Provides access to the active wallet connection and network controls. */ export const useConnection = (): ConnectionContextType => { - if (!ConnectionContext) throw new Error('WalletContext not initialized'); - return useContext(ConnectionContext as NotNothing>); + const context = useContext(ConnectionContext); + if (!context) { + throw new Error("Wallet context is not initialized."); + } + return context; }; diff --git a/src/lib/beacon/beacon.ts b/src/lib/beacon/beacon.ts index a4f585b..75ee0d7 100644 --- a/src/lib/beacon/beacon.ts +++ b/src/lib/beacon/beacon.ts @@ -9,20 +9,22 @@ * All modifications are licensed under: MIT */ /* eslint-disable @typescript-eslint/no-use-before-define */ -import { BeaconWallet } from '@taquito/beacon-wallet'; -import { PermissionScope, NetworkType } from '@airgap/beacon-types'; +import { BeaconWallet } from "@taquito/beacon-wallet"; +import { PermissionScope, NetworkType } from "@airgap/beacon-types"; -import { MichelCodecPacker, TezosToolkit } from '@taquito/taquito'; -import { ConnectFn } from './beacon-types'; +import { MichelCodecPacker, TezosToolkit } from "@taquito/taquito"; +import { ConnectFn } from "./beacon-types"; const createBeaconWallet = (network: NetworkType): BeaconWallet | undefined => - typeof window === 'undefined' + typeof window === "undefined" ? undefined : new BeaconWallet({ - name: 'Etherlink Bridge', + name: "Etherlink Bridge", network: { type: network, - rpcUrl: 'https://rpc.ghostnet.teztnets.com', + rpcUrl: + process.env.NEXT_PUBLIC_NODE_URL || + "https://rpc.ghostnet.teztnets.com", }, // featuredWallets: ['kukai', 'trust', 'temple', 'umami'], }); @@ -40,7 +42,7 @@ export const connectBeacon: ConnectFn = async (isNew, network) => { address: acc.address, connection: { imageUrl: undefined, - connectionType: 'beacon', + connectionType: "beacon", name: undefined, }, // callcontract: callContractBeaconFn(existingWallet), @@ -54,13 +56,14 @@ export const connectBeacon: ConnectFn = async (isNew, network) => { tezosToolkit.setWalletProvider(beaconWallet); if (!beaconWallet) { - throw new Error('Tried to connect on the server'); + throw new Error("Tried to connect on the server"); } const response = await beaconWallet.client.requestPermissions({ network: { - type: NetworkType.GHOSTNET, - rpcUrl: process.env.NEXT_PUBLIC_NODE_URL, + type: network, + rpcUrl: + process.env.NEXT_PUBLIC_NODE_URL || "https://rpc.ghostnet.teztnets.com", }, scopes: [PermissionScope.OPERATION_REQUEST], }); @@ -79,6 +82,6 @@ export const connectBeacon: ConnectFn = async (isNew, network) => { }; export const tezosToolkit = new TezosToolkit( - process.env.NEXT_PUBLIC_NODE_URL || 'https://rpc.ghostnet.teztnets.com' + process.env.NEXT_PUBLIC_NODE_URL || "https://rpc.ghostnet.teztnets.com", ); tezosToolkit.setPackerProvider(new MichelCodecPacker()); diff --git a/src/lib/types/type-aliases.ts b/src/lib/types/type-aliases.ts index e2b6f5d..5667748 100644 --- a/src/lib/types/type-aliases.ts +++ b/src/lib/types/type-aliases.ts @@ -1,36 +1,42 @@ -import { assertMichelsonInstruction, Expr, MichelsonCode } from '@taquito/michel-codec'; -import { MichelsonMap } from '@taquito/taquito'; -import { BigNumber } from 'bignumber.js'; +import { + assertMichelsonInstruction, + Expr, + MichelsonCode, +} from "@taquito/michel-codec"; +import { MichelsonMap } from "@taquito/taquito"; +import { BigNumber } from "bignumber.js"; export type Instruction = MichelsonCode; -export type unit = (true | undefined) & { __type: 'unit' }; +export type unit = (true | undefined) & { __type: "unit" }; -export type address = string & { __type: 'address' }; -export type bytes = string & { __type: 'bytes' }; -export type contract = string & { __type: 'contract' }; -export type operation = string & { __type: 'operation' }; -export type key = string & { __type: 'key' }; -export type key_hash = string & { __type: 'key_hash' }; -export type signature = string & { __type: 'signature' }; -export type ticket = string & { __type: 'ticket' }; +export type address = string & { __type: "address" }; +export type bytes = string & { __type: "bytes" }; +export type contract = string & { __type: "contract" }; +export type operation = string & { __type: "operation" }; +export type key = string & { __type: "key" }; +export type key_hash = string & { __type: "key_hash" }; +export type signature = string & { __type: "signature" }; +export type ticket = string & { __type: "ticket" }; -export type timestamp = string & { __type: 'timestamp' }; +export type timestamp = string & { __type: "timestamp" }; -export type int = BigNumber & { __type: 'int' }; -export type nat = BigNumber & { __type: 'nat' }; +export type int = BigNumber & { __type: "int" }; +export type nat = BigNumber & { __type: "nat" }; -export type mutez = BigNumber & { __type: 'mutez' }; -export type tez = BigNumber & { __type: 'tez' }; +export type mutez = BigNumber & { __type: "mutez" }; +export type tez = BigNumber & { __type: "tez" }; -type MapKey = Array | object | string | boolean | number; -export type MMap = Omit, 'get'> & { get: (key: K) => V }; -export type BigMap = Omit, 'get'> & { +type MapKey = Array | object | string | boolean | number; +export type MMap = Omit, "get"> & { + get: (key: K) => V; +}; +export type BigMap = Omit, "get"> & { get: (key: K) => Promise; }; -export type chest = string & { __type: 'chest' }; -export type chest_key = string & { __type: 'chest_key' }; +export type chest = string & { __type: "chest" }; +export type chest_key = string & { __type: "chest_key" }; const createStringTypeTas = () => @@ -46,8 +52,8 @@ const createBigNumberTypeTas = type asMapParamOf = K extends string ? { [key: string]: V } | Array<{ key: K; value: V }> : K extends number - ? { [key: number]: V } | Array<{ key: K; value: V }> - : Array<{ key: K; value: V }>; + ? { [key: number]: V } | Array<{ key: K; value: V }> + : Array<{ key: K; value: V }>; function asMap(value: asMapParamOf): MMap { const m = new MichelsonMap(); @@ -56,7 +62,9 @@ function asMap(value: asMapParamOf): MMap { vArray.forEach((x) => m.set(x.key, x.value)); } else { const vObject = value as { [key: string]: V }; - Object.keys(vObject).forEach((key) => m.set(key as unknown as K, vObject[key])); + Object.keys(vObject).forEach((key) => + m.set(key as unknown as K, vObject[key]), + ); } return m as MMap; } @@ -82,7 +90,8 @@ export const tas = { contract: createStringTypeTas(), chest: createStringTypeTas(), chest_key: createStringTypeTas(), - timestamp: (value: string | Date): timestamp => new Date(value).toISOString() as timestamp, + timestamp: (value: string | Date): timestamp => + new Date(value).toISOString() as timestamp, int: createBigNumberTypeTas(), nat: createBigNumberTypeTas(),