From 14ef319c648428d7734d54a60afab4204597570a Mon Sep 17 00:00:00 2001 From: notV4l Date: Thu, 15 Aug 2024 20:51:39 +0200 Subject: [PATCH] settings --- .../keychain/src/components/Execute/index.tsx | 8 +- .../keychain/src/components/Menu/index.tsx | 131 +++++----------- .../keychain/src/components/SetDelegate.tsx | 18 ++- .../src/components/SetExternalOwner.tsx | 64 ++++++++ .../src/components/Settings/index.tsx | 140 ++++++++++++++++++ .../layout/Container/Header/BackButton.tsx | 22 +++ .../layout/Container/Header/Banner.tsx | 55 ++++++- .../Container/Header/SettingsButton.tsx | 22 +++ .../layout/Container/Header/TopBar.tsx | 24 ++- .../layout/Container/Header/index.tsx | 13 +- .../src/components/layout/Container/index.tsx | 2 + .../src/components/layout/Footer/index.tsx | 3 +- packages/keychain/src/hooks/connection.tsx | 97 +++++++++++- packages/keychain/src/hooks/external.ts | 46 ++++++ packages/keychain/src/pages/index.tsx | 61 ++++++-- .../keychain/src/utils/connection/external.ts | 27 ++++ .../keychain/src/utils/connection/index.ts | 2 + .../keychain/src/utils/connection/settings.ts | 16 ++ .../keychain/src/utils/connection/types.ts | 25 +++- 19 files changed, 635 insertions(+), 141 deletions(-) create mode 100644 packages/keychain/src/components/SetExternalOwner.tsx create mode 100644 packages/keychain/src/components/Settings/index.tsx create mode 100644 packages/keychain/src/components/layout/Container/Header/BackButton.tsx create mode 100644 packages/keychain/src/components/layout/Container/Header/SettingsButton.tsx create mode 100644 packages/keychain/src/hooks/external.ts create mode 100644 packages/keychain/src/utils/connection/external.ts create mode 100644 packages/keychain/src/utils/connection/settings.ts diff --git a/packages/keychain/src/components/Execute/index.tsx b/packages/keychain/src/components/Execute/index.tsx index 0648193ea..f31d1f9ce 100644 --- a/packages/keychain/src/components/Execute/index.tsx +++ b/packages/keychain/src/components/Execute/index.tsx @@ -183,7 +183,13 @@ export function Execute() { submit - + ); diff --git a/packages/keychain/src/components/Menu/index.tsx b/packages/keychain/src/components/Menu/index.tsx index 71feab5c3..e1c4c770f 100644 --- a/packages/keychain/src/components/Menu/index.tsx +++ b/packages/keychain/src/components/Menu/index.tsx @@ -2,34 +2,25 @@ import { Button, VStack, Text, HStack, Spacer } from "@chakra-ui/react"; import { Container, Content, Footer } from "components/layout"; import { CopyIcon } from "@cartridge/ui"; import { useConnection } from "hooks/connection"; -import { useEffect, useState } from "react"; +import { useState } from "react"; -const shortAddress = (address: string) => { +export const shortAddress = (address: string) => { return `${address.slice(0, 6)}...${address.slice( address.length - 4, address.length, )}`; }; -export function Menu({ - onLogout, - onSetDelegate, -}: { - onLogout: () => void; - onSetDelegate: () => void; -}) { +export const mediumAddress = (address: string) => { + return `${address.slice(0, 20)}...${address.slice( + address.length - 20, + address.length, + )}`; +}; + +export function Menu({ onLogout }: { onLogout: () => void }) { const { controller } = useConnection(); const [isCopying, setIsCopying] = useState(false); - const [isCopyingDelegate, setIsCopyingDelegate] = useState(false); - const [delegateAccount, setDelegateAccount] = useState(""); - - useEffect(() => { - const init = async () => { - const delegate = await controller.delegateAccount(); - setDelegateAccount(delegate); - }; - init(); - }, [controller]); const onCopy = () => { setIsCopying(true); @@ -39,85 +30,33 @@ export function Menu({ }, 1_000); }; - const onCopyDelegate = () => { - setIsCopyingDelegate(true); - navigator.clipboard.writeText(delegateAccount); - setTimeout(() => { - setIsCopyingDelegate(false); - }, 1_000); - }; - return ( - - - - - - - Controller - - - {!isCopying ? ( - - {shortAddress(controller.address)} - - - - ) : ( - Copied to clipboard! - )} - - - {delegateAccount && BigInt(delegateAccount) !== 0n && ( - - - Delegate - - - {!isCopyingDelegate ? ( - - {shortAddress(delegateAccount)} - - - - ) : ( - Copied to clipboard! - )} - - )} - - - - - + + {!isCopying ? ( + + {shortAddress(controller.address)} + + + + ) : ( + Copied to clipboard! + )} - + } + > +
- + */}
); diff --git a/packages/keychain/src/components/SetExternalOwner.tsx b/packages/keychain/src/components/SetExternalOwner.tsx new file mode 100644 index 000000000..cb2c9bd1e --- /dev/null +++ b/packages/keychain/src/components/SetExternalOwner.tsx @@ -0,0 +1,64 @@ +import { AlertIcon } from "@cartridge/ui"; +import { Button, VStack, Text, HStack, Input } from "@chakra-ui/react"; +import { Container, Content, Footer } from "components/layout"; +import { useConnection } from "hooks/connection"; +import { useEffect, useState } from "react"; +import { CallData, num } from "starknet"; + +export function SetExternalOwner() { + const { context, openSettings, setExternalOwnerTransaction } = + useConnection(); + const [externalOwnerAddress, setExternalOwnerAddress] = useState(""); + const [isValid, setIsValid] = useState(true); + + useEffect(() => { + try { + CallData.compile([externalOwnerAddress]); + setIsValid(num.isHex(externalOwnerAddress)); + } catch (e: any) { + setIsValid(false); + } + }, [externalOwnerAddress]); + + return ( + openSettings(context)} + > + + + + Your controller can be owned by an existing Starknet wallet + + + setExternalOwnerAddress(e.target.value)} + /> + {!isValid && externalOwnerAddress !== "" && ( + + Invalid address! + + )} + + + +
+ +
+
+ ); +} diff --git a/packages/keychain/src/components/Settings/index.tsx b/packages/keychain/src/components/Settings/index.tsx new file mode 100644 index 000000000..d0f8267e7 --- /dev/null +++ b/packages/keychain/src/components/Settings/index.tsx @@ -0,0 +1,140 @@ +import { + Button, + VStack, + Text, + HStack, + UnorderedList, + ListItem, +} from "@chakra-ui/react"; +import { Container, Content, Footer } from "components/layout"; +import { TrashIcon } from "@cartridge/ui"; +import { useConnection } from "hooks/connection"; +import { useEffect, useState } from "react"; +import { useExternalOwners } from "hooks/external"; +import { mediumAddress } from "components/Menu"; + +export function Settings({ onLogout }: { onLogout: () => void }) { + const { + context, + controller, + openMenu, + setDelegate, + setExternalOwner, + setDelegateTransaction, + removeExternalOwnerTransaction, + } = useConnection(); + + const [delegateAccount, setDelegateAccount] = useState(""); + + useEffect(() => { + const init = async () => { + const delegate = await controller.delegateAccount(); + setDelegateAccount(delegate); + }; + init(); + }, [controller]); + + const { externalOwners } = useExternalOwners(); + + return ( + openMenu(context)} + > + + + {/* + {controller.account.cartridge.hasSession( + controller.account.cartridge.sessionJson(), + ) ? ( + Session active + ) : ( + No Session + )} + + */} + + + + Recovery Account(s) + + + Controllers can be owned by an existing Starknet wallet. Setting a + recovery account will allow you to recover a controller if you + lose your passkey. + + + + {externalOwners.map((externalOwner) => { + return ( + + + {mediumAddress(externalOwner)} + + + + ); + })} + + + + + + + + Delegate Account + + + You may optionally send rewards you earn in game to an external + wallet. + + {delegateAccount && BigInt(delegateAccount) != 0n ? ( + + {mediumAddress(delegateAccount)} + + + ) : ( + + )} + + + +
+ +
+
+ ); +} diff --git a/packages/keychain/src/components/layout/Container/Header/BackButton.tsx b/packages/keychain/src/components/layout/Container/Header/BackButton.tsx new file mode 100644 index 000000000..cbdd867e5 --- /dev/null +++ b/packages/keychain/src/components/layout/Container/Header/BackButton.tsx @@ -0,0 +1,22 @@ +import { ArrowLeftIcon } from "@cartridge/ui"; +import { IconButton } from "@chakra-ui/react"; +import { isIframe } from "components/connect/utils"; + +export function BackButton({ onClick }: { onClick?: () => void }) { + if (!isIframe()) { + return null; + } + + return ( + } + onClick={onClick} + /> + ); +} diff --git a/packages/keychain/src/components/layout/Container/Header/Banner.tsx b/packages/keychain/src/components/layout/Container/Header/Banner.tsx index c672ba94b..51fd17ec0 100644 --- a/packages/keychain/src/components/layout/Container/Header/Banner.tsx +++ b/packages/keychain/src/components/layout/Container/Header/Banner.tsx @@ -34,7 +34,6 @@ export function Banner({ Icon, icon, title, description }: BannerProps) { switch (variant) { case "connect": case "error": - case "menu": return ( ); + case "menu": + return ( + + + + + {!!Icon ? ( + + + + ) : !!icon ? ( + + {icon} + + ) : ( + Controller Icon + )} + + + + {title} + + + {description && ( + + {description} + + )} + + + + ); default: return ( diff --git a/packages/keychain/src/components/layout/Container/Header/SettingsButton.tsx b/packages/keychain/src/components/layout/Container/Header/SettingsButton.tsx new file mode 100644 index 000000000..372726755 --- /dev/null +++ b/packages/keychain/src/components/layout/Container/Header/SettingsButton.tsx @@ -0,0 +1,22 @@ +import { DotsIcon } from "@cartridge/ui"; +import { IconButton } from "@chakra-ui/react"; +import { isIframe } from "components/connect/utils"; + +export function SettingsButton({ onClick }: { onClick?: () => void }) { + if (!isIframe()) { + return null; + } + + return ( + } + onClick={onClick} + /> + ); +} diff --git a/packages/keychain/src/components/layout/Container/Header/TopBar.tsx b/packages/keychain/src/components/layout/Container/Header/TopBar.tsx index cf0756ac8..cbd77b622 100644 --- a/packages/keychain/src/components/layout/Container/Header/TopBar.tsx +++ b/packages/keychain/src/components/layout/Container/Header/TopBar.tsx @@ -1,14 +1,18 @@ -import { Spacer, IconButton, HStack } from "@chakra-ui/react"; -import { ArrowLeftIcon } from "@cartridge/ui"; +import { Spacer, HStack } from "@chakra-ui/react"; import { CloseButton } from "./CloseButton"; import { NetworkButton } from "./NetworkButton"; +import { SettingsButton } from "./SettingsButton"; +import { useConnection } from "hooks/connection"; +import { BackButton } from "./BackButton"; export type TopBarProps = { onBack?: () => void; hideAccount?: boolean; + showSettings?: boolean; }; -export function TopBar({ onBack, hideAccount }: TopBarProps) { +export function TopBar({ onBack, hideAccount, showSettings }: TopBarProps) { + const { context, openSettings } = useConnection(); return ( - {onBack ? ( - } - onClick={onBack} - /> - ) : ( - - )} + {onBack ? : } @@ -47,6 +41,8 @@ export function TopBar({ onBack, hideAccount }: TopBarProps) { )} */} )} + + {showSettings && openSettings(context)} />} ); } diff --git a/packages/keychain/src/components/layout/Container/Header/index.tsx b/packages/keychain/src/components/layout/Container/Header/index.tsx index 9fd0709dc..fca4a5759 100644 --- a/packages/keychain/src/components/layout/Container/Header/index.tsx +++ b/packages/keychain/src/components/layout/Container/Header/index.tsx @@ -4,11 +4,20 @@ import { TopBar, TopBarProps } from "./TopBar"; export type HeaderProps = TopBarProps & BannerProps; -export function Header({ onBack, hideAccount, ...bannerProps }: HeaderProps) { +export function Header({ + onBack, + hideAccount, + showSettings, + ...bannerProps +}: HeaderProps) { return ( - + ); } diff --git a/packages/keychain/src/components/layout/Container/index.tsx b/packages/keychain/src/components/layout/Container/index.tsx index 600410e6d..a7f55eef3 100644 --- a/packages/keychain/src/components/layout/Container/index.tsx +++ b/packages/keychain/src/components/layout/Container/index.tsx @@ -14,6 +14,7 @@ export function Container({ children, onBack, hideAccount, + showSettings, Icon, icon, title, @@ -29,6 +30,7 @@ export function Container({
@@ -135,7 +136,7 @@ export function Footer({ {children} - {(variant === "connect" || variant === "menu") && ( + {(variant === "connect") && ( void; + setExternalOwnerTransaction: ( + context: ConnectionCtx, + externalOwnerAddress: string, + ) => void; + removeExternalOwnerTransaction: ( + context: ConnectionCtx, + externalOwnerAddress: string, + ) => void; + openSettings: (context: ConnectionCtx) => void; + openMenu: (context: ConnectionCtx) => void; + setExternalOwner: (context: ConnectionCtx) => void; }; export function ConnectionProvider({ children }: PropsWithChildren) { @@ -154,6 +168,24 @@ export function ConnectionProvider({ children }: PropsWithChildren) { } as LogoutCtx); }, []); + const openMenu = useCallback((context: ConnectionCtx) => { + setContext({ + origin: context.origin, + type: "open-menu", + resolve: context.resolve, + reject: context.reject, + } as OpenMenuCtx); + }, []); + + const openSettings = useCallback((context: ConnectionCtx) => { + setContext({ + origin: context.origin, + type: "open-settings", + resolve: context.resolve, + reject: context.reject, + } as OpenSettingsCtx); + }, []); + const setDelegate = useCallback((context: ConnectionCtx) => { setContext({ origin: context.origin, @@ -163,6 +195,15 @@ export function ConnectionProvider({ children }: PropsWithChildren) { } as SetDelegateCtx); }, []); + const setExternalOwner = useCallback((context: ConnectionCtx) => { + setContext({ + origin: context.origin, + type: "set-external-owner", + resolve: context.resolve, + reject: context.reject, + } as SetExternalOwnerCtx); + }, []); + const setDelegateTransaction = useCallback( (context: ConnectionCtx, delegateAddress: string) => { setContext({ @@ -178,9 +219,58 @@ export function ConnectionProvider({ children }: PropsWithChildren) { type: "execute", resolve: context.resolve, reject: context.reject, + onCancel: () => { + openSettings(context); + }, + } as ExecuteCtx); + }, + [controller?.address, openSettings], + ); + + const setExternalOwnerTransaction = useCallback( + (context: ConnectionCtx, externalOwnerAddress: string) => { + setContext({ + origin: context.origin, + transactions: [ + { + contractAddress: controller.address, + entrypoint: "register_external_owner", + calldata: CallData.compile([externalOwnerAddress]), + }, + ], + transactionsDetail: {}, + type: "execute", + resolve: context.resolve, + reject: context.reject, + onCancel: () => { + openSettings(context); + }, + } as ExecuteCtx); + }, + [controller?.address, openSettings], + ); + + const removeExternalOwnerTransaction = useCallback( + (context: ConnectionCtx, externalOwnerAddress: string) => { + setContext({ + origin: context.origin, + transactions: [ + { + contractAddress: controller.address, + entrypoint: "remove_external_owner", + calldata: CallData.compile([externalOwnerAddress]), + }, + ], + transactionsDetail: {}, + type: "execute", + resolve: context.resolve, + reject: context.reject, + onCancel: () => { + openSettings(context); + }, } as ExecuteCtx); }, - [controller?.address], + [controller?.address, openSettings], ); return ( @@ -200,8 +290,13 @@ export function ConnectionProvider({ children }: PropsWithChildren) { setContext, cancel, logout, + openMenu, + openSettings, setDelegate, setDelegateTransaction, + setExternalOwnerTransaction, + removeExternalOwnerTransaction, + setExternalOwner, }} > {children} diff --git a/packages/keychain/src/hooks/external.ts b/packages/keychain/src/hooks/external.ts new file mode 100644 index 000000000..3c37c9057 --- /dev/null +++ b/packages/keychain/src/hooks/external.ts @@ -0,0 +1,46 @@ +import { useEffect, useState } from "react"; +import { hash, num } from "starknet"; +import { useConnection } from "./connection"; + +export function useExternalOwners() { + const { controller } = useConnection(); + + const [externalOwners, setExternalOwners] = useState>([]); + + const provider = controller.account.rpc; + + const externalOwnerRegisteredSelector = num.toHex( + hash.starknetKeccak("ExternalOwnerRegistered"), + ); + const externalOwnerRemovedSelector = num.toHex( + hash.starknetKeccak("ExternalOwnerRemoved"), + ); + + useEffect(() => { + const init = async () => { + const events = await provider.getEvents({ + address: controller.address, + from_block: { block_number: 0 }, + keys: [[externalOwnerRegisteredSelector, externalOwnerRemovedSelector]], + chunk_size: 100, + }); + + const external = new Set(); + + for (let event of events.events) { + if (event.keys[0] === externalOwnerRegisteredSelector) { + external.add(event.data[0]); + } + if (event.keys[0] === externalOwnerRemovedSelector) { + external.delete(event.data[0]); + } + } + + setExternalOwners(Array.from(external.values())); + }; + + init(); + }, [controller.address]); + + return { externalOwners }; +} diff --git a/packages/keychain/src/pages/index.tsx b/packages/keychain/src/pages/index.tsx index bec60f238..e28065414 100644 --- a/packages/keychain/src/pages/index.tsx +++ b/packages/keychain/src/pages/index.tsx @@ -8,23 +8,21 @@ import { ExecuteCtx, LogoutCtx, OpenMenuCtx, + OpenSettingsCtx, SetDelegateCtx, + SetExternalOwnerCtx, SignMessageCtx, } from "utils/connection"; import { logout } from "utils/connection/logout"; import { LoginMode } from "components/connect/types"; import { ErrorPage } from "components/ErrorBoundary"; import { SetDelegate } from "components/SetDelegate"; +import { SetExternalOwner } from "components/SetExternalOwner"; +import { Settings } from "components/Settings"; function Home() { - const { - context, - policies, - controller, - error, - setDelegate, - setDelegateTransaction, - } = useConnection(); + const { context, controller, error, setDelegateTransaction, policies } = + useConnection(); if (window.self === window.top || !context?.origin) { return <>; @@ -133,7 +131,31 @@ function Home() { message: "User logged out", }); }} - onSetDelegate={() => setDelegate(ctx)} + /> + + ); + } + + case "open-settings": { + const ctx = context as OpenSettingsCtx; + return ( + + ctx.resolve({ + code: ResponseCodes.CANCELED, + message: "Canceled", + }) + } + > + { + logout(ctx.origin)(); + + ctx.resolve({ + code: ResponseCodes.NOT_CONNECTED, + message: "User logged out", + }); + }} /> ); @@ -151,12 +173,6 @@ function Home() { } > - ctx.resolve({ - code: ResponseCodes.CANCELED, - message: "Canceled", - }) - } onSetDelegate={(delegateAddress) => { setDelegateTransaction(ctx, delegateAddress); }} @@ -164,6 +180,21 @@ function Home() { ); } + case "set-external-owner": { + const ctx = context as SetExternalOwnerCtx; + return ( + + ctx.resolve({ + code: ResponseCodes.CANCELED, + message: "Canceled", + }) + } + > + + + ); + } default: return <>*Waves*; } diff --git a/packages/keychain/src/utils/connection/external.ts b/packages/keychain/src/utils/connection/external.ts new file mode 100644 index 000000000..ec7e04a1f --- /dev/null +++ b/packages/keychain/src/utils/connection/external.ts @@ -0,0 +1,27 @@ +import { ConnectionCtx, SetExternalOwnerCtx } from "./types"; +import Controller from "utils/controller"; + +export function setExternalOwnerFactory(setContext: (ctx: ConnectionCtx) => void) { + return (_: Controller, origin: string) => (account: string) => { + return new Promise((resolve, reject) => { + setContext({ + type: "set-external-owner", + origin, + account, + resolve, + reject, + } as SetExternalOwnerCtx); + }); + }; +} + +export function setExternalOwner(origin: string) { + return async () => { + const controller = Controller.fromStore(origin); + if (!controller) { + throw new Error("no controller"); + } + + // return await controller.delegateAccount(); + }; +} diff --git a/packages/keychain/src/utils/connection/index.ts b/packages/keychain/src/utils/connection/index.ts index d4be85537..e94ed0f42 100644 --- a/packages/keychain/src/utils/connection/index.ts +++ b/packages/keychain/src/utils/connection/index.ts @@ -14,6 +14,7 @@ import { username } from "./username"; import { ConnectionCtx } from "./types"; import { openMenuFactory } from "./menu"; import { delegateAccount, setDelegateFactory } from "./delegate"; +import { openSettingsFactory } from "./settings"; export function connectToController({ setOrigin, @@ -46,6 +47,7 @@ export function connectToController({ probe: normalize(probeFactory(setController)), signMessage: normalize(validate(signMessageFactory(setContext))), openMenu: normalize(validate(openMenuFactory(setContext))), + openSettings: normalize(validate(openSettingsFactory(setContext))), setDelegate: normalize(validate(setDelegateFactory(setContext))), reset: normalize(() => () => setContext(undefined)), username: normalize(username), diff --git a/packages/keychain/src/utils/connection/settings.ts b/packages/keychain/src/utils/connection/settings.ts new file mode 100644 index 000000000..243917fdc --- /dev/null +++ b/packages/keychain/src/utils/connection/settings.ts @@ -0,0 +1,16 @@ +import { ConnectionCtx, OpenSettingsCtx } from "./types"; +import Controller from "utils/controller"; + +export function openSettingsFactory(setContext: (ctx: ConnectionCtx) => void) { + return (_: Controller, origin: string) => (account: string) => { + return new Promise((resolve, reject) => { + setContext({ + type: "open-settings", + origin, + account, + resolve, + reject, + } as OpenSettingsCtx); + }); + }; +} diff --git a/packages/keychain/src/utils/connection/types.ts b/packages/keychain/src/utils/connection/types.ts index 9c5afb4ac..14736619e 100644 --- a/packages/keychain/src/utils/connection/types.ts +++ b/packages/keychain/src/utils/connection/types.ts @@ -19,7 +19,9 @@ export type ConnectionCtx = | ExecuteCtx | SignMessageCtx | OpenMenuCtx - | SetDelegateCtx; + | OpenSettingsCtx + | SetDelegateCtx + | SetExternalOwnerCtx; export type ConnectCtx = { origin: string; @@ -46,6 +48,7 @@ export type ExecuteCtx = { }; resolve: (res: ExecuteReply | ConnectError) => void; reject: (reason?: unknown) => void; + onCancel: () => void; }; export type SignMessageCtx = { @@ -65,10 +68,26 @@ export type OpenMenuCtx = { reject: (reason?: unknown) => void; }; +export type OpenSettingsCtx = { + origin: string; + type: "open-settings"; + account: string; + resolve: (res: ConnectError) => void; + reject: (reason?: unknown) => void; +}; + export type SetDelegateCtx = { origin: string; type: "set-delegate"; account: string; - resolve: (res: ConnectError) => void; // ? - reject: (reason?: unknown) => void; // ? + resolve: (res: ConnectError) => void; + reject: (reason?: unknown) => void; +}; + +export type SetExternalOwnerCtx = { + origin: string; + type: "set-external-owner"; + account: string; + resolve: (res: ConnectError) => void; + reject: (reason?: unknown) => void; };