diff --git a/packages/controller/src/types.ts b/packages/controller/src/types.ts index 0cc0d0832..116015393 100644 --- a/packages/controller/src/types.ts +++ b/packages/controller/src/types.ts @@ -52,7 +52,7 @@ export type ControllerError = { export type ConnectReply = { code: ResponseCodes.SUCCESS; address: string; - policies: SessionPolicies; + policies?: SessionPolicies; }; export type ExecuteReply = diff --git a/packages/keychain/.env.development b/packages/keychain/.env.development index 5acd3bd8f..c29aa6772 100644 --- a/packages/keychain/.env.development +++ b/packages/keychain/.env.development @@ -4,7 +4,7 @@ NEXT_PUBLIC_ADMIN_URL="http://localhost:3002" NEXT_PUBLIC_CARTRIDGE_API_URL="http://localhost:8000" # NEXT_PUBLIC_ETH_RPC_MAINNET="https://eth-mainnet.g.alchemy.com/v2/OGPRMquXP3K7oTkLrmVZpjCd1DswtYz3" # NEXT_PUBLIC_ETH_RPC_SEPOLIA="https://eth-sepolia.g.alchemy.com/v2/mURnclB5pn5elDfyzgTN4W2GR-rOYevI" -# NEXT_PUBLIC_RPC_MAINNET="http://localhost:8001/x/starknet/mainnet" +NEXT_PUBLIC_RPC_MAINNET="http://localhost:8001/x/starknet/mainnet" NEXT_PUBLIC_RPC_SEPOLIA="http://localhost:8001/x/starknet/sepolia" NEXT_PUBLIC_STRIPE_PAYMENT="http://localhost:8000/stripe/payment-intent" NEXT_PUBLIC_POSTHOG_KEY=phc_UWaJajNQ00PjHhveZ81SJ2zVtBicKrzewdZHGiyavQQ diff --git a/packages/keychain/.env.production b/packages/keychain/.env.production index 046f74401..dfd2416f7 100644 --- a/packages/keychain/.env.production +++ b/packages/keychain/.env.production @@ -4,7 +4,7 @@ NEXT_PUBLIC_ADMIN_URL="https://cartridge.gg" NEXT_PUBLIC_CARTRIDGE_API_URL="https://api.cartridge.gg" # NEXT_PUBLIC_ETH_RPC_MAINNET="https://eth-mainnet.g.alchemy.com/v2/OGPRMquXP3K7oTkLrmVZpjCd1DswtYz3" # NEXT_PUBLIC_ETH_RPC_SEPOLIA="https://eth-sepolia.g.alchemy.com/v2/mURnclB5pn5elDfyzgTN4W2GR-rOYevI" -# NEXT_PUBLIC_RPC_MAINNET="https://api.cartridge.gg/x/starknet/mainnet" +NEXT_PUBLIC_RPC_MAINNET="https://api.cartridge.gg/x/starknet/mainnet" NEXT_PUBLIC_RPC_SEPOLIA="https://api.cartridge.gg/x/starknet/sepolia" NEXT_PUBLIC_STRIPE_PAYMENT="https://api.cartridge.gg/stripe/payment-intent" NEXT_PUBLIC_POSTHOG_KEY=phc_UWaJajNQ00PjHhveZ81SJ2zVtBicKrzewdZHGiyavQQ diff --git a/packages/keychain/.storybook/preview.tsx b/packages/keychain/.storybook/preview.tsx index cbed86111..2f8c020db 100644 --- a/packages/keychain/.storybook/preview.tsx +++ b/packages/keychain/.storybook/preview.tsx @@ -11,10 +11,16 @@ import { import { constants } from "starknet"; import { getChainName } from "@cartridge/utils"; import Script from "next/script"; -import { ETH_CONTRACT_ADDRESS } from "../src/utils/token"; import { ConnectCtx, ConnectionCtx } from "../src/utils/connection/types"; import { UpgradeInterface } from "../src/hooks/upgrade"; -import { defaultTheme, controllerConfigs } from "@cartridge/presets"; +import { + defaultTheme, + controllerConfigs, + SessionPolicies, + ControllerTheme, +} from "@cartridge/presets"; +import { mainnet } from "@starknet-react/chains"; +import { StarknetConfig, publicProvider, voyager } from "@starknet-react/core"; const inter = Inter({ subsets: ["latin"] }); const ibmPlexMono = IBM_Plex_Mono({ @@ -77,16 +83,27 @@ function Provider({ parameters, }: { parameters: StoryParameters } & PropsWithChildren) { const connection = useMockedConnection(parameters.connection); - const theme = parameters.preset || "cartridge"; + + if (parameters.preset) { + const config = controllerConfigs[parameters.preset]; + connection.theme = config.theme || connection.theme; + connection.policies = config.policies || connection.policies; + } return ( - - - - {children} - - - + + + + + {children} + + + + ); } @@ -98,6 +115,7 @@ interface StoryParameters extends Parameters { upgrade?: UpgradeInterface; }; preset?: string; + policies?: SessionPolicies; } export function useMockedConnection({ @@ -119,36 +137,8 @@ export function useMockedConnection({ rpcUrl: "http://api.cartridge.gg/x/sepolia", chainId, chainName, - policies: { - contracts: { - [ETH_CONTRACT_ADDRESS]: { - methods: [ - { - name: "Approve", - entrypoint: "approve", - description: - "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", - }, - { - name: "Transfer", - entrypoint: "transfer", - }, - { - name: "Mint", - entrypoint: "mint", - }, - { - name: "Burn", - entrypoint: "burn", - }, - { - name: "Allowance", - entrypoint: "allowance", - }, - ], - }, - }, - }, + policies: {}, + theme: defaultTheme, prefunds: [], hasPrefundRequest: false, error: undefined, @@ -170,30 +160,13 @@ export default preview; export function ControllerThemeProvider({ children, - theme, -}: PropsWithChildren<{ theme?: string }>) { - const preset = useMemo(() => { - if (!theme) return defaultTheme; - if (theme in controllerConfigs && controllerConfigs[theme].theme) { - return controllerConfigs[theme].theme; - } - return defaultTheme; - }, [theme]); - - const controllerTheme = useMemo( - () => ({ - name: preset.name, - icon: preset.icon, - cover: preset.cover, - }), - [preset], - ); - - useThemeEffect({ theme: preset, assetUrl: "" }); - const chakraTheme = useChakraTheme(preset); + theme = defaultTheme, +}: PropsWithChildren<{ theme?: ControllerTheme }>) { + useThemeEffect({ theme, assetUrl: "" }); + const chakraTheme = useChakraTheme(theme); return ( - + {children} ); diff --git a/packages/keychain/src/components/ConfirmTransaction.tsx b/packages/keychain/src/components/ConfirmTransaction.tsx index 1eb405146..b60f52d5f 100644 --- a/packages/keychain/src/components/ConfirmTransaction.tsx +++ b/packages/keychain/src/components/ConfirmTransaction.tsx @@ -51,7 +51,7 @@ export function ConfirmTransaction() { const entries = Object.entries(callPolicies.contracts || {}); const txnsApproved = entries.every(([target, policy]) => { - const contract = policies.contracts?.[target]; + const contract = policies?.contracts?.[target]; if (!contract) return false; return policy.methods.every((method) => contract.methods.some((m) => m.entrypoint === method.entrypoint), @@ -63,9 +63,13 @@ export function ConfirmTransaction() { return txnsApproved && !account?.session(callPolicies); }, [callPolicies, policiesUpdated, policies, account]); - if (updateSession) { + if (updateSession && policies) { return ( - setIsPoliciesUpdated(true)} /> + setIsPoliciesUpdated(true)} + /> ); } diff --git a/packages/keychain/src/components/Provider/connection.tsx b/packages/keychain/src/components/Provider/connection.tsx index 9f53e0c77..d08c00ed0 100644 --- a/packages/keychain/src/components/Provider/connection.tsx +++ b/packages/keychain/src/components/Provider/connection.tsx @@ -3,7 +3,8 @@ import Controller from "utils/controller"; import { ConnectionCtx } from "utils/connection"; import { Prefund } from "@cartridge/controller"; import { UpgradeInterface } from "hooks/upgrade"; -import { SessionPolicies } from "@cartridge/presets"; +import { ControllerTheme } from "@cartridge/presets"; +import { ParsedSessionPolicies } from "hooks/session"; export const ConnectionContext = createContext< ConnectionContextValue | undefined @@ -16,7 +17,8 @@ export type ConnectionContextValue = { rpcUrl?: string; chainId?: string; chainName?: string; - policies: SessionPolicies; + policies?: ParsedSessionPolicies; + theme: ControllerTheme; prefunds: Prefund[]; hasPrefundRequest: boolean; error?: Error; diff --git a/packages/keychain/src/components/Provider/index.tsx b/packages/keychain/src/components/Provider/index.tsx index 15cda4a4e..b1259d014 100644 --- a/packages/keychain/src/components/Provider/index.tsx +++ b/packages/keychain/src/components/Provider/index.tsx @@ -1,4 +1,4 @@ -import { PropsWithChildren } from "react"; +import { PropsWithChildren, useCallback } from "react"; import { QueryClient, QueryClientProvider } from "react-query"; import { useConnectionValue } from "hooks/connection"; import { CartridgeAPIProvider } from "@cartridge/utils/api/cartridge"; @@ -7,16 +7,43 @@ import { PostHogProvider } from "posthog-js/react"; import posthog from "posthog-js"; import { ConnectionContext } from "./connection"; import { ControllerThemeProvider } from "./theme"; +import { jsonRpcProvider, StarknetConfig, voyager } from "@starknet-react/core"; +import { sepolia, mainnet, Chain } from "@starknet-react/chains"; +import { constants, num } from "starknet"; export function Provider({ children }: PropsWithChildren) { const connection = useConnectionValue(); + const rpc = useCallback( + (chain: Chain) => { + let nodeUrl; + switch (num.toHex(chain.id)) { + case constants.StarknetChainId.SN_MAIN: + nodeUrl = process.env.NEXT_PUBLIC_RPC_MAINNET; + break; + case constants.StarknetChainId.SN_SEPOLIA: + nodeUrl = process.env.NEXT_PUBLIC_RPC_SEPOLIA; + break; + default: + nodeUrl = connection.rpcUrl; + } + + return { nodeUrl }; + }, + [connection.rpcUrl], + ); return ( - {children} + + {children} + diff --git a/packages/keychain/src/components/Provider/theme.tsx b/packages/keychain/src/components/Provider/theme.tsx index 79493f542..bb1272ffe 100644 --- a/packages/keychain/src/components/Provider/theme.tsx +++ b/packages/keychain/src/components/Provider/theme.tsx @@ -1,9 +1,4 @@ -import { - defaultTheme, - controllerConfigs, - ColorMode, - ControllerTheme, -} from "@cartridge/presets"; +import { ColorMode } from "@cartridge/presets"; import { useThemeEffect } from "@cartridge/ui-next"; import { ChakraProvider, useColorMode } from "@chakra-ui/react"; import { useConnection } from "hooks/connection"; @@ -12,21 +7,13 @@ import { useRouter } from "next/router"; import { PropsWithChildren, useEffect, useMemo } from "react"; export function ControllerThemeProvider({ children }: PropsWithChildren) { - const preset = useControllerThemePreset(); - const controllerTheme = useMemo( - () => ({ - name: preset.name, - icon: preset.icon, - cover: preset.cover, - }), - [preset], - ); + const { theme } = useConnection(); - useThemeEffect({ theme: preset, assetUrl: "" }); - const chakraTheme = useChakraTheme(preset); + useThemeEffect({ theme, assetUrl: "" }); + const chakraTheme = useChakraTheme(theme); return ( - + {children} @@ -45,36 +32,6 @@ function ChakraTheme({ children }: PropsWithChildren) { useEffect(() => { setColorMode(colorMode); }, [setColorMode, colorMode]); - return children; -} - -export function useControllerThemePreset() { - const router = useRouter(); - const { origin } = useConnection(); - - return useMemo(() => { - const themeParam = router.query.theme; - if (typeof themeParam === "undefined") { - return defaultTheme; - } - const val = decodeURIComponent( - Array.isArray(themeParam) - ? themeParam[themeParam.length - 1] - : themeParam, - ); - if ( - typeof val === "string" && - val in controllerConfigs && - controllerConfigs[val].theme - ) { - return controllerConfigs[val].theme; - } - - try { - return JSON.parse(val) as ControllerTheme; - } catch { - return defaultTheme; - } - }, [router.query.theme, origin]); + return children; } diff --git a/packages/keychain/src/components/SessionSummary.tsx b/packages/keychain/src/components/SessionSummary.tsx deleted file mode 100644 index c59c88106..000000000 --- a/packages/keychain/src/components/SessionSummary.tsx +++ /dev/null @@ -1,372 +0,0 @@ -import React, { PropsWithChildren, useEffect, useState } from "react"; -import { toArray } from "@cartridge/controller"; -import { - Card, - CardContent, - CardHeader, - CardHeaderRight, - CardTitle, - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, - CircleIcon, - InfoIcon, - CardIcon, - PencilIcon, - CheckboxIcon, - ArrowTurnDownIcon, - Badge, - SpaceInvaderIcon, - TooltipProvider, - Tooltip, - TooltipTrigger, - TooltipContent, - ExternalIcon, - cn, - Spinner, - CoinsIcon, - ErrorImage, - ScrollIcon, -} from "@cartridge/ui-next"; -import { - formatAddress, - isSlotChain, - SessionSummary as SessionSummaryType, - StarkscanUrl, -} from "@cartridge/utils"; -import { constants, StarknetEnumType, StarknetMerkleType } from "starknet"; -import Link from "next/link"; -import { useConnection } from "hooks/connection"; -import { useSessionSummary } from "@cartridge/utils"; -import { ContractPolicy, SessionPolicies } from "@cartridge/presets"; - -export function SessionSummary({ - policies, - setError, -}: { - policies: SessionPolicies; - setError?: (error: Error) => void; -}) { - const { controller } = useConnection(); - const { - data: summary, - error, - isLoading, - } = useSessionSummary({ - policies, - provider: controller, - }); - - useEffect(() => { - setError?.(error); - }, [error, setError]); - - if (isLoading) { - return ; - } - - if (error) { - return ( -
-
-
Oops! Something went wrong parsing session summary
-
Please try it again.
-
- -
- ); - } - - return ( -
- {Object.entries(summary.dojo).map(([address, { methods, meta }]) => ( - - ))} - - {Object.entries(summary.default).map(([address, { methods }], i) => ( - - - - } - /> - ))} - - {Object.entries(summary.ERC20).map(([address, { methods, meta }]) => ( - - ) : ( - - - - ) - } - /> - ))} - - {Object.entries(summary.ERC721).map(([address, { methods }]) => ( - - - - } - /> - ))} - - -
- ); -} - -function Contract({ - address, - title, - methods: _methods, - icon = , -}: { - address: string; - title: string; - methods: ContractPolicy["methods"]; - icon?: React.ReactNode; -}) { - const methods = toArray(_methods); - const { chainId } = useConnection(); - const isSlot = !!chainId && isSlotChain(chainId); - - return ( - - - {title} - - - {formatAddress(address, { size: "xs" })} - - - - - - - - - - - Approve{" "} - - {methods.length} {methods.length > 1 ? "methods" : "method"} - - - - - - {methods.map((c) => ( - - -
-
{c.name}
- - {c.description && ( - - - - - - - {c.description} - - - )} -
-
- ))} -
-
-
-
- ); -} - -function SignMessages({ - messages, -}: { - messages: SessionSummaryType["messages"]; -}) { - if (!messages || !messages.length) { - return null; - } - - return ( - - - -
- } - > - Sign Messages - - - - - - - You are agreeing to sign{" "} - - {messages.length} {messages.length > 1 ? "messages" : "message"} - {" "} - in the following format - - - - - {messages.map((m, i) => ( - - {Object.values(m.domain).filter((f) => typeof f !== "undefined") - .length && ( - - {m.domain.name && ( - - )} - {m.domain.version && ( - - )} - {m.domain.chainId && ( - - )} - {m.domain.revision && ( - - )} - - )} - - - - - {Object.entries(m.types).map(([name, types]) => ( - - {types.map((t) => ( - - ))} - - ))} - - - ))} - - - - - ); -} - -function CollapsibleRow({ - title, - children, -}: PropsWithChildren & { title: string }) { - const [value, setValue] = useState(""); - return ( - - - -
- -
{title}
-
-
- - - {children} - -
-
- ); -} - -function ValueRow({ - values, -}: { - values: { name: string; value: string | number }[]; -}) { - return ( -
- -
- {values.map((f) => ( -
- {f.name}: {f.value} -
- ))} -
-
- ); -} diff --git a/packages/keychain/src/components/connect/CreateSession.stories.tsx b/packages/keychain/src/components/connect/CreateSession.stories.tsx index f5341d698..a2409567b 100644 --- a/packages/keychain/src/components/connect/CreateSession.stories.tsx +++ b/packages/keychain/src/components/connect/CreateSession.stories.tsx @@ -1,5 +1,8 @@ import type { Meta, StoryObj } from "@storybook/react"; import { CreateSession } from "./CreateSession"; +import { ETH_CONTRACT_ADDRESS } from "@cartridge/utils"; +import { parseSessionPolicies } from "hooks/session"; +import { controllerConfigs } from "@cartridge/presets"; const meta: Meta = { component: CreateSession, @@ -18,15 +21,158 @@ type Story = StoryObj; export const Default: Story = { args: { + policies: parseSessionPolicies({ + verified: false, + policies: { + contracts: { + [ETH_CONTRACT_ADDRESS]: { + methods: [ + { + name: "Approve", + entrypoint: "approve", + description: + "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", + }, + { + name: "Transfer", + entrypoint: "transfer", + }, + { + name: "Mint", + entrypoint: "mint", + }, + { + name: "Burn", + entrypoint: "burn", + }, + { + name: "Allowance", + entrypoint: "allowance", + }, + ], + }, + "0x047d88C65A627b38d728a783382Af648D79AED80Bf396047F9E839e8501d7F6D": + { + name: "Pillage", + description: "Allows you raid a structure and pillage resources", + methods: [ + { + name: "Battle Pillage", + description: "Pillage a structure", + entrypoint: "battle_pillage", + }, + ], + }, + "0x001cE27792b23cE379398F5468b69739e89314b2657Cfa3A9c388BDFD33DcFbf": + { + name: "Battle contract", + description: "Required to engage in battles", + methods: [ + { + name: "Battle Start", + description: "Start a battle", + entrypoint: "battle_start", + }, + { + name: "Battle Force Start", + description: "Force start a battle", + entrypoint: "battle_force_start", + }, + { + name: "Battle Join", + description: "Join a battle", + entrypoint: "battle_join", + }, + ], + }, + "0x04718f5a0Fc34cC1AF16A1cdee98fFB20C31f5cD61D6Ab07201858f4287c938D": + { + name: "STRK Token", + description: "Starknet token contract", + methods: [ + { + name: "Mint", + entrypoint: "mint", + }, + { + name: "Burn", + entrypoint: "burn", + }, + { + name: "Allowance", + entrypoint: "allowance", + }, + ], + }, + }, + messages: [ + { + types: { + StarknetDomain: [ + { + name: "name", + type: "shortstring", + }, + { + name: "version", + type: "shortstring", + }, + { + name: "chainId", + type: "shortstring", + }, + { + name: "revision", + type: "shortstring", + }, + ], + "eternum-Message": [ + { + name: "identity", + type: "ContractAddress", + }, + { + name: "channel", + type: "shortstring", + }, + { + name: "content", + type: "string", + }, + { + name: "timestamp", + type: "felt", + }, + { + name: "salt", + type: "felt", + }, + ], + }, + primaryType: "eternum-Message", + domain: { + name: "Eternum", + version: "1", + chainId: "SN_SEPOLIA", + revision: "1", + }, + }, + ], + }, + }), onConnect: () => {}, }, }; -export const WithTheme: Story = { +export const WithPreset: Story = { parameters: { - preset: "loot-survivor", + preset: "eternum", }, args: { + policies: parseSessionPolicies({ + verified: true, + policies: controllerConfigs["eternum"].policies!, + }), onConnect: () => {}, }, }; diff --git a/packages/keychain/src/components/connect/CreateSession.tsx b/packages/keychain/src/components/connect/CreateSession.tsx index 84be6ae2d..30482e729 100644 --- a/packages/keychain/src/components/connect/CreateSession.tsx +++ b/packages/keychain/src/components/connect/CreateSession.tsx @@ -1,7 +1,7 @@ import { Container, Content, Footer } from "components/layout"; import { BigNumberish, shortString } from "starknet"; import { ControllerError } from "utils/connection"; -import { Button, VStack } from "@chakra-ui/react"; +import { Button, HStack, Text, Checkbox } from "@chakra-ui/react"; import { useCallback, useEffect, useState } from "react"; import { useConnection } from "hooks/connection"; import { ControllerErrorAlert } from "components/ErrorAlert"; @@ -9,19 +9,25 @@ import { SessionConsent } from "components/connect"; import { SESSION_EXPIRATION } from "const"; import { Upgrade } from "./Upgrade"; import { ErrorCode } from "@cartridge/account-wasm"; -import { SessionSummary } from "components/SessionSummary"; import { TypedDataPolicy } from "@cartridge/presets"; +import { ParsedSessionPolicies } from "hooks/session"; + +import { UnverifiedSessionSummary } from "components/session/UnverifiedSessionSummary"; +import { VerifiedSessionSummary } from "components/session/VerifiedSessionSummary"; export function CreateSession({ + policies, onConnect, isUpdate, }: { + policies: ParsedSessionPolicies; onConnect: (transaction_hash?: string) => void; isUpdate?: boolean; }) { - const { controller, policies, upgrade, chainId, logout } = useConnection(); + const { controller, upgrade, chainId, theme, logout } = useConnection(); const [isConnecting, setIsConnecting] = useState(false); const [isDisabled, setIsDisabled] = useState(false); + const [isConsent, setIsConsent] = useState(false); const [expiresAt] = useState(SESSION_EXPIRATION); const [maxFee] = useState(); const [error, setError] = useState(); @@ -30,7 +36,7 @@ export function CreateSession({ if (!chainId) return; const normalizedChainId = normalizeChainId(chainId); - const violatingPolicy = policies.messages?.find( + const violatingPolicy = policies?.messages?.find( (policy) => "domain" in policy && (!policy.domain.chainId || @@ -54,7 +60,7 @@ export function CreateSession({ }, [chainId, policies]); const onCreateSession = useCallback(async () => { - if (!controller) return; + if (!controller || !policies) return; try { setError(undefined); setIsConnecting(true); @@ -89,33 +95,65 @@ export function CreateSession({ }} > - - + + {policies?.verified ? ( + + ) : ( + + )} -
+ {!policies?.verified && ( + !isConnecting && setIsConsent(!isConsent)} + cursor="pointer" + > + + + This session's policies have not been verified. I understand + and agree to grant permission for the application to execute + actions listed above. + + + )} {error && isControllerError(error) && ( )} {!error && ( - + + - - + )}
diff --git a/packages/keychain/src/components/connect/RegisterSession.tsx b/packages/keychain/src/components/connect/RegisterSession.tsx index 342d7d0c1..f5db1af5b 100644 --- a/packages/keychain/src/components/connect/RegisterSession.tsx +++ b/packages/keychain/src/components/connect/RegisterSession.tsx @@ -8,16 +8,20 @@ import { TransactionFinalityStatus, } from "starknet"; import { SESSION_EXPIRATION } from "const"; -import { SessionSummary } from "components/SessionSummary"; +import { UnverifiedSessionSummary } from "components/session/UnverifiedSessionSummary"; +import { VerifiedSessionSummary } from "components/session/VerifiedSessionSummary"; +import { ParsedSessionPolicies } from "hooks/session"; export function RegisterSession({ + policies, onConnect, publicKey, }: { + policies: ParsedSessionPolicies; onConnect: (transaction_hash?: string) => void; publicKey?: string; }) { - const { controller, policies } = useConnection(); + const { controller, theme } = useConnection(); const [expiresAt] = useState(SESSION_EXPIRATION); const transactions = useMemo(() => { @@ -72,8 +76,12 @@ export function RegisterSession({ buttonText="Register Session" > - - + + {policies?.verified ? ( + + ) : ( + + )} ); diff --git a/packages/keychain/src/components/connect/SessionConsent.tsx b/packages/keychain/src/components/connect/SessionConsent.tsx index bd96cb719..60fd5fbd8 100644 --- a/packages/keychain/src/components/connect/SessionConsent.tsx +++ b/packages/keychain/src/components/connect/SessionConsent.tsx @@ -4,20 +4,14 @@ import { useConnection } from "hooks/connection"; import { useMemo } from "react"; import Link from "next/link"; -const verified: { id: string; url: string }[] = [ - { id: "flippyflop", url: "https://flippyflop.gg" }, -]; - -function isVerified(url: string) { - return !!verified.find((v) => new URL(v.url).host === new URL(url).host); -} - export function SessionConsent({ + isVerified, variant = "default", }: { + isVerified: boolean; variant?: "default" | "slot" | "signup"; }) { - const { origin, policies } = useConnection(); + const { origin } = useConnection(); const hostname = useMemo( () => (origin ? new URL(origin).hostname : undefined), [origin], @@ -38,7 +32,7 @@ export function SessionConsent({ case "default": return hostname && origin ? ( - {isVerified(origin) && ( + {isVerified && ( {origin} {" "} - to perform the following actions ( - {Object.keys(policies.contracts ?? {}).length + - (policies.messages?.length ?? 0)} - ) on your behalf + and allow the game to{" "} + + perform actions on your behalf + ) : null; diff --git a/packages/keychain/src/components/connect/create/useCreateController.ts b/packages/keychain/src/components/connect/create/useCreateController.ts index 53af4be43..3daad4db3 100644 --- a/packages/keychain/src/components/connect/create/useCreateController.ts +++ b/packages/keychain/src/components/connect/create/useCreateController.ts @@ -121,8 +121,8 @@ export function useCreateController({ if ( loginMode === LoginMode.Webauthn || - Object.keys(policies.contracts ?? {}).length + - (policies.messages?.length ?? 0) === + Object.keys(policies?.contracts ?? {}).length + + (policies?.messages?.length ?? 0) === 0 ) { await doLogin({ diff --git a/packages/keychain/src/components/session/AggregateCard.tsx b/packages/keychain/src/components/session/AggregateCard.tsx new file mode 100644 index 000000000..c97edf958 --- /dev/null +++ b/packages/keychain/src/components/session/AggregateCard.tsx @@ -0,0 +1,208 @@ +import React, { useMemo } from "react"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, + Card, + CardContent, + CardHeader, +} from "@cartridge/ui-next"; +import { formatAddress } from "@cartridge/utils"; +import { + Divider, + HStack, + Link, + Spacer, + Stack, + Text, + VStack, +} from "@chakra-ui/react"; +import { useExplorer } from "@starknet-react/core"; +import { constants } from "starknet"; +import { Method } from "@cartridge/presets"; +import { useChainId } from "hooks/connection"; +import { ParsedSessionPolicies } from "hooks/session"; + +interface AggregateCardProps { + title: string; + icon: React.ReactNode; + policies: ParsedSessionPolicies; +} + +export function AggregateCard({ title, icon, policies }: AggregateCardProps) { + const chainId = useChainId(); + const explorer = useExplorer(); + + const totalMethods = useMemo(() => { + return Object.values(policies.contracts || {}).reduce((acc, contract) => { + return acc + (contract.methods?.length || 0); + }, 0); + }, [policies.contracts]); + + const totalMessages = policies.messages?.length ?? 0; + + return ( + + + + {title} + + + + + + + + Approve{" "} + + {totalMethods} methods + {" "} + {totalMessages > 0 && ( + <> + and{" "} + + {totalMessages} messages + + + )} + + + + + {Object.entries(policies.contracts || {}).map( + ([address, { name, methods }]) => ( + <> + + + {name} + + + + {formatAddress(address, { first: 5, last: 5 })} + + + } + > + {methods.map((method: Method) => ( + + + + {method.name} + + + + {method.entrypoint} + + + {method.description && ( + + {method.description} + + )} + + ))} + + + ), + )} + + {policies.messages?.map((message) => ( + + + Messages + + } + w="full" + > + {Object.entries(message.types).map( + ([typeName, fields]) => ( + + + + {typeName} + + + {fields.map((field) => ( + + + {field.name} + + + + {field.type} + + + ))} + + ), + )} + + + ))} + + + + + + + ); +} diff --git a/packages/keychain/src/components/session/CollapsibleRow.tsx b/packages/keychain/src/components/session/CollapsibleRow.tsx new file mode 100644 index 000000000..8ef1276fb --- /dev/null +++ b/packages/keychain/src/components/session/CollapsibleRow.tsx @@ -0,0 +1,33 @@ +import React, { PropsWithChildren, useState } from "react"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, + CheckboxIcon, +} from "@cartridge/ui-next"; + +interface CollapsibleRowProps extends PropsWithChildren { + title: string; +} + +export function CollapsibleRow({ title, children }: CollapsibleRowProps) { + const [value, setValue] = useState(""); + + return ( + + + +
+ +
{title}
+
+
+ + + {children} + +
+
+ ); +} diff --git a/packages/keychain/src/components/session/ContractCard.tsx b/packages/keychain/src/components/session/ContractCard.tsx new file mode 100644 index 000000000..aa8a987f0 --- /dev/null +++ b/packages/keychain/src/components/session/ContractCard.tsx @@ -0,0 +1,128 @@ +import React from "react"; +import { + Card, + CardContent, + CardHeader, + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@cartridge/ui-next"; +import { formatAddress } from "@cartridge/utils"; +import { + Divider, + HStack, + Link, + Spacer, + Stack, + Text, + VStack, +} from "@chakra-ui/react"; +import { useExplorer } from "@starknet-react/core"; +import { constants } from "starknet"; +import { Method } from "@cartridge/presets"; +import { useChainId } from "hooks/connection"; + +interface ContractCardProps { + address: string; + methods: Method[]; + title: string; + icon: React.ReactNode; +} + +export function ContractCard({ + address, + methods, + title, + icon, +}: ContractCardProps) { + const chainId = useChainId(); + const explorer = useExplorer(); + + return ( + + + + + {title} + + + + {formatAddress(address, { first: 5, last: 5 })} + + + + + + + + + Approve{" "} + + {methods.length} {methods.length > 1 ? "methods" : "method"} + + + + + } + > + {methods.map((method) => ( + + + + {method.name} + + + + {method.entrypoint} + + + {method.description && ( + + {method.description} + + )} + + ))} + + + + + + + ); +} diff --git a/packages/keychain/src/components/session/MessageCard.tsx b/packages/keychain/src/components/session/MessageCard.tsx new file mode 100644 index 000000000..acb54ffd4 --- /dev/null +++ b/packages/keychain/src/components/session/MessageCard.tsx @@ -0,0 +1,123 @@ +import React from "react"; +import { + Card, + CardContent, + CardHeader, + CardTitle, + Accordion, + AccordionContent, + AccordionItem, + CardIcon, + PencilIcon, + AccordionTrigger, +} from "@cartridge/ui-next"; +import { ArrowTurnDownIcon, Badge } from "@cartridge/ui-next"; +import { StarknetEnumType, StarknetMerkleType } from "@starknet-io/types-js"; +import { SignMessagePolicy } from "@cartridge/presets"; +import { Text } from "@chakra-ui/react"; + +import { CollapsibleRow } from "./CollapsibleRow"; + +interface MessageContentProps { + message: SignMessagePolicy; +} + +function MessageContent({ message: m }: MessageContentProps) { + return ( + + {/* Domain section */} + {Object.values(m.domain).filter((f) => typeof f !== "undefined").length > + 0 && ( + + {m.domain.name && ( + + )} + {/* ... other domain fields ... */} + + )} + + + + + {Object.entries(m.types).map(([name, types]) => ( + + {types.map((t) => ( + + ))} + + ))} + + + ); +} + +interface MessageCardProps { + message: SignMessagePolicy; +} + +export function MessageCard({ message }: MessageCardProps) { + return ( + + + + + } + > + Sign Message + + + + + + + + The application will be able to sign the following message on + your behalf + + + + + + + + + + + ); +} + +interface ValueRowProps { + values: { name: string; value: string | number }[]; +} + +export function ValueRow({ values }: ValueRowProps) { + return ( +
+ +
+ {values.map((f) => ( +
+ {f.name}: {f.value} +
+ ))} +
+
+ ); +} diff --git a/packages/keychain/src/components/session/UnverifiedSessionSummary.tsx b/packages/keychain/src/components/session/UnverifiedSessionSummary.tsx new file mode 100644 index 000000000..56f7612dc --- /dev/null +++ b/packages/keychain/src/components/session/UnverifiedSessionSummary.tsx @@ -0,0 +1,49 @@ +import React from "react"; +import { CardIcon, CoinsIcon, ScrollIcon } from "@cartridge/ui-next"; +import { toArray } from "@cartridge/controller"; +import { ParsedSessionPolicies } from "hooks/session"; + +import { MessageCard } from "./MessageCard"; +import { ContractCard } from "./ContractCard"; + +export function UnverifiedSessionSummary({ + policies, +}: { + policies: ParsedSessionPolicies; +}) { + return ( +
+ {Object.entries(policies.contracts ?? {}).map(([address, contract]) => { + const methods = toArray(contract.methods); + const title = !contract.meta + ? "Contract" + : `${contract.meta.name} token`; + const icon = !contract.meta ? ( + + + + ) : contract.meta.logoUrl ? ( + + ) : ( + + + + ); + + return ( + + ); + })} + + {policies.messages?.map((message, index) => ( + + ))} +
+ ); +} diff --git a/packages/keychain/src/components/session/VerifiedSessionSummary.tsx b/packages/keychain/src/components/session/VerifiedSessionSummary.tsx new file mode 100644 index 000000000..f05a5e5f5 --- /dev/null +++ b/packages/keychain/src/components/session/VerifiedSessionSummary.tsx @@ -0,0 +1,20 @@ +import React from "react"; +import { ParsedSessionPolicies } from "hooks/session"; +import { CodeIcon } from "@cartridge/ui"; +import { AggregateCard } from "./AggregateCard"; + +export function VerifiedSessionSummary({ + game, + policies, +}: { + game: String; + policies: ParsedSessionPolicies; +}) { + return ( + } + policies={policies} + /> + ); +} diff --git a/packages/keychain/src/hooks/connection.ts b/packages/keychain/src/hooks/connection.ts index 72df762f9..3d91dfd0c 100644 --- a/packages/keychain/src/hooks/connection.ts +++ b/packages/keychain/src/hooks/connection.ts @@ -21,7 +21,13 @@ import { } from "components/Provider/connection"; import { UpgradeInterface, useUpgrade } from "./upgrade"; import posthog from "posthog-js"; -import { Policies, SessionPolicies } from "@cartridge/presets"; +import { Policies } from "@cartridge/presets"; +import { + defaultTheme, + controllerConfigs, + ControllerTheme, +} from "@cartridge/presets"; +import { ParsedSessionPolicies, parseSessionPolicies } from "./session"; const CHAIN_ID_TIMEOUT = 3000; @@ -33,7 +39,8 @@ export function useConnectionValue() { const [origin, setOrigin] = useState(); const [rpcUrl, setRpcUrl] = useState(); const [chainId, setChainId] = useState(); - const [policies, setPolicies] = useState({}); + const [policies, setPolicies] = useState(); + const [theme, setTheme] = useState(defaultTheme); const [controller, setControllerRaw] = useState(); const [prefunds, setPrefunds] = useState([]); const [hasPrefundRequest, setHasPrefundRequest] = useState(false); @@ -105,19 +112,69 @@ export function useConnectionValue() { ); } + // Handle prefunds const prefundParam = urlParams.get("prefunds"); const prefunds: Prefund[] = prefundParam ? JSON.parse(decodeURIComponent(prefundParam)) : []; setHasPrefundRequest(!!prefundParam); setPrefunds(mergeDefaultETHPrefund(prefunds)); - setPolicies(() => { - const param = urlParams.get("policies"); - if (!param) return {}; - const policies = JSON.parse(decodeURIComponent(param)) as Policies; - return toSessionPolicies(policies); - }); + // Handle theme and policies + const policiesParam = urlParams.get("policies"); + const themeParam = urlParams.get("theme"); + const presetParam = urlParams.get("preset"); + + // Provides backward compatability for Controler <= v0.5.1 + if (themeParam) { + const decodedPreset = decodeURIComponent(themeParam); + try { + const parsedTheme = JSON.parse(decodedPreset) as ControllerTheme; + setTheme(parsedTheme); + } catch (e) { + setTheme(controllerConfigs[decodedPreset].theme || defaultTheme); + } + } + + // URL policies take precedence over preset policies + if (policiesParam) { + try { + const parsedPolicies = JSON.parse( + decodeURIComponent(policiesParam), + ) as Policies; + setPolicies( + parseSessionPolicies({ + verified: false, + policies: toSessionPolicies(parsedPolicies), + }), + ); + } catch (e) { + console.error("Failed to parse policies:", e); + } + } + + // Application provided policies take precedence over preset policies. + if ( + presetParam && + presetParam in controllerConfigs + // TODO: Reenable + // && + // origin && + // (origin.startsWith("http://localhost") || + // toArray(controllerConfigs[presetParam].origin).includes(origin)) + ) { + setTheme(controllerConfigs[presetParam].theme || defaultTheme); + + // Set policies from preset if no URL policies + if (!policiesParam && controllerConfigs[presetParam].policies) { + setPolicies( + parseSessionPolicies({ + verified: true, + policies: controllerConfigs[presetParam].policies, + }), + ); + } + } const connection = connectToController({ setOrigin, @@ -131,6 +188,8 @@ export function useConnectionValue() { return () => { connection.destroy(); }; + + // `origin` intentionally omitted }, [setController]); useEffect(() => { @@ -189,6 +248,7 @@ export function useConnectionValue() { chainId, chainName, policies, + theme, prefunds, hasPrefundRequest, error, diff --git a/packages/keychain/src/hooks/session.ts b/packages/keychain/src/hooks/session.ts new file mode 100644 index 000000000..4395ffbe0 --- /dev/null +++ b/packages/keychain/src/hooks/session.ts @@ -0,0 +1,54 @@ +import { getChecksumAddress } from "starknet"; +import { + ContractPolicy, + erc20Metadata, + SessionPolicies, +} from "@cartridge/presets"; + +export type ERC20Metadata = { + name: string; + logoUrl?: string; + symbol: string; + decimals: number; +}; + +export type ParsedSessionPolicies = SessionPolicies & { + verified: boolean; + contracts?: Record< + string, + ContractPolicy & { + meta?: ERC20Metadata; + } + >; +}; + +export function parseSessionPolicies({ + policies, + verified = false, +}: { + policies: SessionPolicies; + verified: boolean; +}): ParsedSessionPolicies { + const summary: ParsedSessionPolicies = { + verified, + ...policies, + }; + + Object.entries(policies.contracts ?? []).forEach(([address]) => { + const meta = erc20Metadata.find( + (m) => + getChecksumAddress(m.l2_token_address) === getChecksumAddress(address), + ); + + if (meta) { + summary.contracts![address].meta = { + name: meta.name, + symbol: meta.symbol, + decimals: meta.decimals, + logoUrl: meta.logo_url, + }; + } + }); + + return summary; +} diff --git a/packages/keychain/src/pages/index.tsx b/packages/keychain/src/pages/index.tsx index cd082df65..ad0203a6f 100644 --- a/packages/keychain/src/pages/index.tsx +++ b/packages/keychain/src/pages/index.tsx @@ -55,23 +55,29 @@ function Home() { case "connect": { posthog?.capture("Call Connect"); - // TODO: show missing policies if mismatch - if ( - !( - Object.keys(policies?.contracts ?? {}).length + - (policies?.messages?.length ?? 0) - ) || - controller.session(policies) - ) { + if (!policies) { + context.resolve({ + code: ResponseCodes.SUCCESS, + address: controller.address, + }); + + return <>; + } + + if (controller.session(policies)) { context.resolve({ code: ResponseCodes.SUCCESS, address: controller.address, - policies, + policies: policies, }); + + return <>; } + // TODO: show missing policies if mismatch return ( { context.resolve({ code: ResponseCodes.SUCCESS, diff --git a/packages/keychain/src/pages/session.tsx b/packages/keychain/src/pages/session.tsx index 8e48bc6ca..23e37e158 100644 --- a/packages/keychain/src/pages/session.tsx +++ b/packages/keychain/src/pages/session.tsx @@ -119,7 +119,7 @@ export default function Session() { // If yes, check if the policies of the session are the same as the ones that are // currently being requested. Return existing session to the callback uri if policies match. useEffect(() => { - if (!controller || !origin) { + if (!controller || !origin || !policies) { return; } @@ -148,12 +148,20 @@ export default function Session() { return ; } + if (!policies) { + return <>No Session Policies; + } + return ( <> {queries.public_key ? ( - + ) : ( - + )} ); diff --git a/packages/keychain/src/utils/connection/connect.ts b/packages/keychain/src/utils/connection/connect.ts index 47146da8d..a5098e083 100644 --- a/packages/keychain/src/utils/connection/connect.ts +++ b/packages/keychain/src/utils/connection/connect.ts @@ -1,6 +1,7 @@ import { ConnectReply, toSessionPolicies } from "@cartridge/controller"; import { ConnectCtx, ConnectionCtx } from "./types"; -import { Policies, SessionPolicies } from "@cartridge/presets"; +import { Policies } from "@cartridge/presets"; +import { ParsedSessionPolicies, parseSessionPolicies } from "hooks/session"; export function connectFactory({ setOrigin, @@ -10,7 +11,7 @@ export function connectFactory({ }: { setOrigin: (origin: string) => void; setRpcUrl: (url: string) => void; - setPolicies: (policies: SessionPolicies) => void; + setPolicies: (policies: ParsedSessionPolicies) => void; setContext: (context: ConnectionCtx) => void; }) { return (origin: string) => { @@ -18,7 +19,12 @@ export function connectFactory({ return (policies: Policies, rpcUrl: string): Promise => { setRpcUrl(rpcUrl); - setPolicies(toSessionPolicies(policies)); + setPolicies( + parseSessionPolicies({ + verified: false, + policies: toSessionPolicies(policies), + }), + ); return new Promise((resolve, reject) => { setContext({ diff --git a/packages/keychain/src/utils/connection/index.ts b/packages/keychain/src/utils/connection/index.ts index 9eadf44ca..147893ece 100644 --- a/packages/keychain/src/utils/connection/index.ts +++ b/packages/keychain/src/utils/connection/index.ts @@ -12,7 +12,7 @@ import { ConnectionCtx } from "./types"; import { deployFactory } from "./deploy"; import { openSettingsFactory } from "./settings"; import { normalize } from "@cartridge/utils"; -import { SessionPolicies } from "@cartridge/presets"; +import { ParsedSessionPolicies } from "hooks/session"; export function connectToController({ setOrigin, @@ -23,7 +23,7 @@ export function connectToController({ }: { setOrigin: (origin: string) => void; setRpcUrl: (url: string) => void; - setPolicies: (policies: SessionPolicies) => void; + setPolicies: (policies: ParsedSessionPolicies) => void; setContext: (ctx: ConnectionCtx) => void; setController: (controller?: Controller) => void; }) { diff --git a/packages/presets/configs/eternum/config.json b/packages/presets/configs/eternum/config.json index c1f6d2110..4761af251 100644 --- a/packages/presets/configs/eternum/config.json +++ b/packages/presets/configs/eternum/config.json @@ -1,5 +1,398 @@ { - "origin": "", + "origin": "eternum.realms.world", + "policies": { + "contracts": { + "0x047d88C65A627b38d728a783382Af648D79AED80Bf396047F9E839e8501d7F6D": { + "name": "Pillage", + "description": "Allows you raid a structure and pillage resources", + "methods": [ + { + "name": "Battle Pillage", + "description": "Pillage a structure", + "entrypoint": "battle_pillage" + } + ] + }, + "0x001cE27792b23cE379398F5468b69739e89314b2657Cfa3A9c388BDFD33DcFbf": { + "name": "Battle contract", + "description": "Required to engage in battles", + "methods": [ + { + "name": "Battle Start", + "description": "Start a battle", + "entrypoint": "battle_start" + }, + { + "name": "Battle Force Start", + "description": "Force start a battle", + "entrypoint": "battle_force_start" + }, + { + "name": "Battle Join", + "description": "Join a battle", + "entrypoint": "battle_join" + }, + { + "name": "Battle Leave", + "description": "Leave a battle", + "entrypoint": "battle_leave" + }, + { + "name": "Battle Claim", + "description": "Claim a structure after a battle", + "entrypoint": "battle_claim" + } + ] + }, + "0x03c212B90cC4f236BE2C014e0EE0D870277b2cC313217a73D41387E255e806ED": { + "name": "Leave battle contract", + "description": "Allows armies to leave a battle", + "methods": [ + { + "name": "Leave Battle", + "description": "Leave a battle", + "entrypoint": "leave_battle" + }, + { + "name": "Leave Battle If Ended", + "description": "Leave a battle if its ended", + "entrypoint": "leave_battle_if_ended" + } + ] + }, + "0x036b82076142f07fbD8bF7B2CABF2e6B190082c0b242c6eCC5e14B2C96d1763c": { + "name": "Building contract", + "description": "Allows to manage buildings", + "methods": [ + { + "name": "Create", + "description": "Create a building", + "entrypoint": "create" + }, + { + "name": "Pause Production", + "description": "Pause the production of a building", + "entrypoint": "pause_production" + }, + { + "name": "Resume Production", + "description": "Resume production of a building", + "entrypoint": "resume_production" + }, + { + "name": "Destroy a building", + "description": "Destroy a building", + "entrypoint": "destroy" + } + ] + }, + "0x012A0ca4558518d6aF296b8F393a917Bb89b3e78Ba33544814B7D9138cE4816e": { + "name": "Guild contract", + "description": "Allows guild utilities", + "methods": [ + { + "name": "Create Guild", + "description": "Creates a new guild", + "entrypoint": "create_guild" + }, + { + "name": "Join Guild", + "description": "Join an existing guild", + "entrypoint": "join_guild" + }, + { + "name": "Whitelist Player", + "description": "Add a player to the guild's whitelist", + "entrypoint": "whitelist_player" + }, + { + "name": "Leave Guild", + "description": "Exit the current guild", + "entrypoint": "leave_guild" + }, + { + "name": "Transfer Guild Ownership", + "description": "Transfer ownership of the guild to another player", + "entrypoint": "transfer_guild_ownership" + }, + { + "name": "Remove Guild Member", + "description": "Remove a member from the guild", + "entrypoint": "remove_guild_member" + }, + { + "name": "Remove Player From Whitelist", + "description": "Remove a player from the guild's whitelist", + "entrypoint": "remove_player_from_whitelist" + } + ] + }, + "0x03BA22B088a94093F781A968E3f82a88B2Ab5047e9C309C93066f00E37334dE6": { + "name": "Hyperstructure contract", + "description": "Handles the creation and management of hyperstructures", + "methods": [ + { + "name": "Create", + "description": "Create a new hyperstructure", + "entrypoint": "create" + }, + { + "name": "Contribute To Construction", + "description": "Contribute resources to hyperstructure construction", + "entrypoint": "contribute_to_construction" + }, + { + "name": "Set Co Owners", + "description": "Set additional owners for the hyperstructure", + "entrypoint": "set_co_owners" + }, + { + "name": "End Game", + "description": "Terminates the current game season once you've reached enough points", + "entrypoint": "end_game" + }, + { + "name": "Set Access", + "description": "Configure access permissions for contributions to the hyperstructure", + "entrypoint": "set_access" + } + ] + }, + "0x07a5e4dFaBA7AcEd9ADD65913d44311D74E12F85C55503EBF903103B102847e5": { + "name": "AMM liquidity contract", + "description": "Manages liquidity for the Automated Market Maker", + "methods": [ + { + "name": "Add", + "description": "Add liquidity to the pool", + "entrypoint": "add" + }, + { + "name": "Remove", + "description": "Remove liquidity from the pool", + "entrypoint": "remove" + } + ] + }, + "0x00CC0C73458864B9e0e884DE532b7EBFbe757E7429fB2c736aBd5E129e5FB81A": { + "name": "Exploration contract", + "description": "Allows you to move to unexplored hexes on the map", + "methods": [ + { + "name": "Explore", + "description": "Explore an uncharted hex on the game map", + "entrypoint": "explore" + } + ] + }, + "0x027952F3C1C681790a168E5422C21278d925CD1BDD8DAf1CdE8E63aFDfD19E20": { + "name": "Naming contract", + "description": "Manages entity naming in the game", + "methods": [ + { + "name": "Set Entity Name", + "description": "Assign a custom name to a game entity", + "entrypoint": "set_entity_name" + } + ] + }, + "0x024A8AFd7523e933d37eA2c91aD629fCCde8Ce23cEFA3c324C6248Ca929e3862": { + "name": "Realms contract", + "description": "Manages realm-related actions", + "methods": [ + { + "name": "Upgrade Level", + "description": "Upgrade the level of a realm", + "entrypoint": "upgrade_level" + }, + { + "name": "Quest Claim", + "description": "Claim rewards from completed quests", + "entrypoint": "quest_claim" + } + ] + }, + "0x0161A4CF2e207359dC7Dbf912b21e9099B7729bedE2544F849F384fCb166a109": { + "name": "Resource bridge contract", + "description": "Manages bridge transfers between L2 and Eternum", + "methods": [ + { + "name": "Deposit Initial", + "description": "Initial deposit of resources for bridge transfer", + "entrypoint": "deposit_initial" + }, + { + "name": "Deposit", + "description": "Deposit additional resources for bridge transfer", + "entrypoint": "deposit" + }, + { + "name": "Start Withdraw", + "description": "Initiate a withdrawal process", + "entrypoint": "start_withdraw" + }, + { + "name": "Finish Withdraw", + "description": "Finalize a withdrawal process", + "entrypoint": "finish_withdraw" + } + ] + }, + "0x0763fa425503dB5D4bdfF040f6f7509E6eCd8e3F7E75450B9b28f8fc4cDD2877": { + "name": "Resource contract", + "description": "In-game resource management", + "methods": [ + { + "name": "Approve", + "description": "Approve resource transfer", + "entrypoint": "approve" + }, + { + "name": "Send", + "description": "Send resources to another entity", + "entrypoint": "send" + }, + { + "name": "Pickup", + "description": "Collect available resources after approval", + "entrypoint": "pickup" + } + ] + }, + "0x030a4A6472FF2BcFc68d709802E5A9F31F5AC01D04fa97e37D32CE7568741262": { + "name": "AMM swap contract", + "description": "Handles token swaps in the Automated Market Maker", + "methods": [ + { + "name": "Buy", + "description": "Purchase tokens from the liquidity pool", + "entrypoint": "buy" + }, + { + "name": "Sell", + "description": "Sell tokens to the liquidity pool", + "entrypoint": "sell" + } + ] + }, + "0x0003A6bBB82F9E670c99647F3B0C4AaF1Be82Be712E1A393336B79B9DAB44cc5": { + "name": "Market contract", + "description": "Manages trading orders in the in-game market", + "methods": [ + { + "name": "Create Order", + "description": "Create a new trading order", + "entrypoint": "create_order" + }, + { + "name": "Accept Order", + "description": "Accept a trading order", + "entrypoint": "accept_order" + }, + { + "name": "Accept Partial Order", + "description": "Accept a partial trading order", + "entrypoint": "accept_partial_order" + }, + { + "name": "Cancel Order", + "description": "Cancel a trading order", + "entrypoint": "cancel_order" + } + ] + }, + "0x0119Bf067E05955c0F17f1d4900977fAcBDc10e046E2319FD4d1320f5cc8Be38": { + "name": "Map travel contract", + "description": "Manages player movement across the game map", + "methods": [ + { + "name": "Travel Hex", + "description": "Move to a specific hex on the map", + "entrypoint": "travel_hex" + } + ] + }, + "0x046998418397972011E93D370c2f7ac06184AD7Ed9e0811C5c9f88C1feF9445F": { + "name": "Army contract", + "description": "Manages army-related actions", + "methods": [ + { + "name": "Army Create", + "description": "Create a new army", + "entrypoint": "army_create" + }, + { + "name": "Army Delete", + "description": "Delete an existing army", + "entrypoint": "army_delete" + }, + { + "name": "Army Buy Troops", + "description": "Buy troops for an army", + "entrypoint": "army_buy_troops" + }, + { + "name": "Army Merge Troops", + "description": "Merge troops from multiple armies", + "entrypoint": "army_merge_troops" + } + ] + } + }, + "messages": [ + { + "types": { + "StarknetDomain": [ + { + "name": "name", + "type": "shortstring" + }, + { + "name": "version", + "type": "shortstring" + }, + { + "name": "chainId", + "type": "shortstring" + }, + { + "name": "revision", + "type": "shortstring" + } + ], + "eternum-Message": [ + { + "name": "identity", + "type": "ContractAddress" + }, + { + "name": "channel", + "type": "shortstring" + }, + { + "name": "content", + "type": "string" + }, + { + "name": "timestamp", + "type": "felt" + }, + { + "name": "salt", + "type": "felt" + } + ] + }, + "primaryType": "eternum-Message", + "domain": { + "name": "Eternum", + "version": "1", + "chainId": "SN_SEPOLIA", + "revision": "1" + } + } + ] + }, "theme": { "name": "Eternum", "icon": "/whitelabel/eternum/icon.svg", @@ -8,4 +401,4 @@ "primary": "#dc8b07" } } -} +} \ No newline at end of file diff --git a/packages/presets/package.json b/packages/presets/package.json index 43e0cc646..dfbb1571b 100644 --- a/packages/presets/package.json +++ b/packages/presets/package.json @@ -37,6 +37,7 @@ "@cartridge/tsconfig": "workspace:*", "@types/node": "^20.11.0", "tsx": "^4.7.0", - "typescript": "^5.4.5" + "typescript": "^5.4.5", + "starknet": "6.11.0" } } diff --git a/packages/presets/scripts/to-session-policies.ts b/packages/presets/scripts/to-session-policies.ts new file mode 100644 index 000000000..8939823b3 --- /dev/null +++ b/packages/presets/scripts/to-session-policies.ts @@ -0,0 +1,92 @@ +import fs from "fs"; +import path from "path"; +import { getChecksumAddress } from "starknet"; +import { Policies, SessionPolicies } from "@cartridge/presets"; + +const inputPath = path.join(process.cwd(), "configs/eternum/Policies (1).tsx"); +const outputPath = path.join( + process.cwd(), + "src/generated/session-policies.ts", +); + +function humanizeString(str: string): string { + return str + .split("_") + .map((s) => s.charAt(0).toUpperCase() + s.slice(1)) + .join(" "); +} + +function toArray(val: T | T[]): T[] { + return Array.isArray(val) ? val : [val]; +} + +async function main() { + try { + // Read and parse input file + const fileContent = fs.readFileSync(inputPath, "utf-8"); + const policiesMatch = fileContent.match( + /export const policies = (\[[\s\S]*?\]);/, + ); + + if (!policiesMatch) { + throw new Error("Could not find policies array in input file"); + } + + const policies: Policies = eval(policiesMatch[1]); + + // Convert to session policies + const sessionPolicies = policies.reduce( + (prev, p) => { + if ("target" in p) { + const target = getChecksumAddress(p.target); + const entrypoint = p.method; + const item = { + name: humanizeString(entrypoint), + entrypoint: entrypoint, + }; + + if (target in prev.contracts) { + const methods = toArray(prev.contracts[target].methods); + prev.contracts[target] = { + methods: [...methods, item], + }; + } else { + prev.contracts[target] = { + methods: [item], + }; + } + } else { + prev.messages.push(p); + } + + return prev; + }, + { contracts: {}, messages: [] }, + ); + + // Ensure output directory exists + const outputDir = path.dirname(outputPath); + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } + + // Write output file + const output = `// This file is auto-generated. DO NOT EDIT IT MANUALLY. +import { SessionPolicies } from "@cartridge/presets"; + +export const sessionPolicies: SessionPolicies = ${JSON.stringify( + sessionPolicies, + null, + 2, + )}; +`; + + fs.writeFileSync(outputPath, output); + console.log("Successfully generated session policies at:", outputPath); + } catch (error) { + console.log(`Failed to generate session policies: ${error.message}`); + process.exit(1); + } +} + +main(); diff --git a/packages/presets/src/generated/controller-configs.ts b/packages/presets/src/generated/controller-configs.ts index 64df395d3..cf8cae6b9 100644 --- a/packages/presets/src/generated/controller-configs.ts +++ b/packages/presets/src/generated/controller-configs.ts @@ -47,7 +47,403 @@ export const configs: ControllerConfigs = { }, }, eternum: { - origin: "", + origin: "eternum.realms.world", + policies: { + contracts: { + "0x047d88C65A627b38d728a783382Af648D79AED80Bf396047F9E839e8501d7F6D": { + name: "Pillage", + description: "Allows you raid a structure and pillage resources", + methods: [ + { + name: "Battle Pillage", + description: "Pillage a structure", + entrypoint: "battle_pillage", + }, + ], + }, + "0x001cE27792b23cE379398F5468b69739e89314b2657Cfa3A9c388BDFD33DcFbf": { + name: "Battle contract", + description: "Required to engage in battles", + methods: [ + { + name: "Battle Start", + description: "Start a battle", + entrypoint: "battle_start", + }, + { + name: "Battle Force Start", + description: "Force start a battle", + entrypoint: "battle_force_start", + }, + { + name: "Battle Join", + description: "Join a battle", + entrypoint: "battle_join", + }, + { + name: "Battle Leave", + description: "Leave a battle", + entrypoint: "battle_leave", + }, + { + name: "Battle Claim", + description: "Claim a structure after a battle", + entrypoint: "battle_claim", + }, + ], + }, + "0x03c212B90cC4f236BE2C014e0EE0D870277b2cC313217a73D41387E255e806ED": { + name: "Leave battle contract", + description: "Allows armies to leave a battle", + methods: [ + { + name: "Leave Battle", + description: "Leave a battle", + entrypoint: "leave_battle", + }, + { + name: "Leave Battle If Ended", + description: "Leave a battle if its ended", + entrypoint: "leave_battle_if_ended", + }, + ], + }, + "0x036b82076142f07fbD8bF7B2CABF2e6B190082c0b242c6eCC5e14B2C96d1763c": { + name: "Building contract", + description: "Allows to manage buildings", + methods: [ + { + name: "Create", + description: "Create a building", + entrypoint: "create", + }, + { + name: "Pause Production", + description: "Pause the production of a building", + entrypoint: "pause_production", + }, + { + name: "Resume Production", + description: "Resume production of a building", + entrypoint: "resume_production", + }, + { + name: "Destroy a building", + description: "Destroy a building", + entrypoint: "destroy", + }, + ], + }, + "0x012A0ca4558518d6aF296b8F393a917Bb89b3e78Ba33544814B7D9138cE4816e": { + name: "Guild contract", + description: "Allows guild utilities", + methods: [ + { + name: "Create Guild", + description: "Creates a new guild", + entrypoint: "create_guild", + }, + { + name: "Join Guild", + description: "Join an existing guild", + entrypoint: "join_guild", + }, + { + name: "Whitelist Player", + description: "Add a player to the guild's whitelist", + entrypoint: "whitelist_player", + }, + { + name: "Leave Guild", + description: "Exit the current guild", + entrypoint: "leave_guild", + }, + { + name: "Transfer Guild Ownership", + description: "Transfer ownership of the guild to another player", + entrypoint: "transfer_guild_ownership", + }, + { + name: "Remove Guild Member", + description: "Remove a member from the guild", + entrypoint: "remove_guild_member", + }, + { + name: "Remove Player From Whitelist", + description: "Remove a player from the guild's whitelist", + entrypoint: "remove_player_from_whitelist", + }, + ], + }, + "0x03BA22B088a94093F781A968E3f82a88B2Ab5047e9C309C93066f00E37334dE6": { + name: "Hyperstructure contract", + description: "Handles the creation and management of hyperstructures", + methods: [ + { + name: "Create", + description: "Create a new hyperstructure", + entrypoint: "create", + }, + { + name: "Contribute To Construction", + description: + "Contribute resources to hyperstructure construction", + entrypoint: "contribute_to_construction", + }, + { + name: "Set Co Owners", + description: "Set additional owners for the hyperstructure", + entrypoint: "set_co_owners", + }, + { + name: "End Game", + description: + "Terminates the current game season once you've reached enough points", + entrypoint: "end_game", + }, + { + name: "Set Access", + description: + "Configure access permissions for contributions to the hyperstructure", + entrypoint: "set_access", + }, + ], + }, + "0x07a5e4dFaBA7AcEd9ADD65913d44311D74E12F85C55503EBF903103B102847e5": { + name: "AMM liquidity contract", + description: "Manages liquidity for the Automated Market Maker", + methods: [ + { + name: "Add", + description: "Add liquidity to the pool", + entrypoint: "add", + }, + { + name: "Remove", + description: "Remove liquidity from the pool", + entrypoint: "remove", + }, + ], + }, + "0x00CC0C73458864B9e0e884DE532b7EBFbe757E7429fB2c736aBd5E129e5FB81A": { + name: "Exploration contract", + description: "Allows you to move to unexplored hexes on the map", + methods: [ + { + name: "Explore", + description: "Explore an uncharted hex on the game map", + entrypoint: "explore", + }, + ], + }, + "0x027952F3C1C681790a168E5422C21278d925CD1BDD8DAf1CdE8E63aFDfD19E20": { + name: "Naming contract", + description: "Manages entity naming in the game", + methods: [ + { + name: "Set Entity Name", + description: "Assign a custom name to a game entity", + entrypoint: "set_entity_name", + }, + ], + }, + "0x024A8AFd7523e933d37eA2c91aD629fCCde8Ce23cEFA3c324C6248Ca929e3862": { + name: "Realms contract", + description: "Manages realm-related actions", + methods: [ + { + name: "Upgrade Level", + description: "Upgrade the level of a realm", + entrypoint: "upgrade_level", + }, + { + name: "Quest Claim", + description: "Claim rewards from completed quests", + entrypoint: "quest_claim", + }, + ], + }, + "0x0161A4CF2e207359dC7Dbf912b21e9099B7729bedE2544F849F384fCb166a109": { + name: "Resource bridge contract", + description: "Manages bridge transfers between L2 and Eternum", + methods: [ + { + name: "Deposit Initial", + description: "Initial deposit of resources for bridge transfer", + entrypoint: "deposit_initial", + }, + { + name: "Deposit", + description: "Deposit additional resources for bridge transfer", + entrypoint: "deposit", + }, + { + name: "Start Withdraw", + description: "Initiate a withdrawal process", + entrypoint: "start_withdraw", + }, + { + name: "Finish Withdraw", + description: "Finalize a withdrawal process", + entrypoint: "finish_withdraw", + }, + ], + }, + "0x0763fa425503dB5D4bdfF040f6f7509E6eCd8e3F7E75450B9b28f8fc4cDD2877": { + name: "Resource contract", + description: "In-game resource management", + methods: [ + { + name: "Approve", + description: "Approve resource transfer", + entrypoint: "approve", + }, + { + name: "Send", + description: "Send resources to another entity", + entrypoint: "send", + }, + { + name: "Pickup", + description: "Collect available resources after approval", + entrypoint: "pickup", + }, + ], + }, + "0x030a4A6472FF2BcFc68d709802E5A9F31F5AC01D04fa97e37D32CE7568741262": { + name: "AMM swap contract", + description: "Handles token swaps in the Automated Market Maker", + methods: [ + { + name: "Buy", + description: "Purchase tokens from the liquidity pool", + entrypoint: "buy", + }, + { + name: "Sell", + description: "Sell tokens to the liquidity pool", + entrypoint: "sell", + }, + ], + }, + "0x0003A6bBB82F9E670c99647F3B0C4AaF1Be82Be712E1A393336B79B9DAB44cc5": { + name: "Market contract", + description: "Manages trading orders in the in-game market", + methods: [ + { + name: "Create Order", + description: "Create a new trading order", + entrypoint: "create_order", + }, + { + name: "Accept Order", + description: "Accept a trading order", + entrypoint: "accept_order", + }, + { + name: "Accept Partial Order", + description: "Accept a partial trading order", + entrypoint: "accept_partial_order", + }, + { + name: "Cancel Order", + description: "Cancel a trading order", + entrypoint: "cancel_order", + }, + ], + }, + "0x0119Bf067E05955c0F17f1d4900977fAcBDc10e046E2319FD4d1320f5cc8Be38": { + name: "Map travel contract", + description: "Manages player movement across the game map", + methods: [ + { + name: "Travel Hex", + description: "Move to a specific hex on the map", + entrypoint: "travel_hex", + }, + ], + }, + "0x046998418397972011E93D370c2f7ac06184AD7Ed9e0811C5c9f88C1feF9445F": { + name: "Army contract", + description: "Manages army-related actions", + methods: [ + { + name: "Army Create", + description: "Create a new army", + entrypoint: "army_create", + }, + { + name: "Army Delete", + description: "Delete an existing army", + entrypoint: "army_delete", + }, + { + name: "Army Buy Troops", + description: "Buy troops for an army", + entrypoint: "army_buy_troops", + }, + { + name: "Army Merge Troops", + description: "Merge troops from multiple armies", + entrypoint: "army_merge_troops", + }, + ], + }, + }, + messages: [ + { + types: { + StarknetDomain: [ + { + name: "name", + type: "shortstring", + }, + { + name: "version", + type: "shortstring", + }, + { + name: "chainId", + type: "shortstring", + }, + { + name: "revision", + type: "shortstring", + }, + ], + "eternum-Message": [ + { + name: "identity", + type: "ContractAddress", + }, + { + name: "channel", + type: "shortstring", + }, + { + name: "content", + type: "string", + }, + { + name: "timestamp", + type: "felt", + }, + { + name: "salt", + type: "felt", + }, + ], + }, + primaryType: "eternum-Message", + domain: { + name: "Eternum", + version: "1", + chainId: "SN_SEPOLIA", + revision: "1", + }, + }, + ], + }, theme: { name: "Eternum", icon: "/whitelabel/eternum/icon.svg", diff --git a/packages/presets/src/index.ts b/packages/presets/src/index.ts index f45ae5853..095fef01b 100644 --- a/packages/presets/src/index.ts +++ b/packages/presets/src/index.ts @@ -39,18 +39,19 @@ export type SessionPolicies = { export type ContractPolicies = Record; export type ContractPolicy = { - methods: Method[]; + name?: string; description?: string; + methods: Method[]; }; export type Method = { name?: string; - entrypoint: string; description?: string; + entrypoint: string; }; export type SignMessagePolicy = TypedDataPolicy & { - name: string; + name?: string; description?: string; }; diff --git a/packages/ui-next/src/components/primitives/card.tsx b/packages/ui-next/src/components/primitives/card.tsx index e1c00cddb..1bdff474c 100644 --- a/packages/ui-next/src/components/primitives/card.tsx +++ b/packages/ui-next/src/components/primitives/card.tsx @@ -24,16 +24,11 @@ const CardHeader = React.forwardRef< icon ? (
{icon} -
+
+
) : (
(
{src ? ( ) : props.children ? ( diff --git a/packages/utils/src/hooks/contract.ts b/packages/utils/src/hooks/contract.ts deleted file mode 100644 index 40d77d443..000000000 --- a/packages/utils/src/hooks/contract.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { getChecksumAddress, Provider } from "starknet"; -import useSWR from "swr"; -import { useEkuboMetadata } from "./balance"; -import { ERC20Metadata } from "../erc20"; -import { stringFromByteArray } from "../contract"; -import { - ContractPolicies, - ContractPolicy, - SessionPolicies, - SignMessagePolicy, -} from "@cartridge/presets"; - -export type SessionSummary = { - default: ContractPolicies; - dojo: Record; - ERC20: Record< - string, - ContractPolicy & { meta?: Omit } - >; - ERC721: ContractPolicies; - messages: SignMessagePolicy[] | undefined; -}; - -type ContractType = keyof SessionSummary; - -export function useSessionSummary({ - policies, - provider, -}: { - policies: SessionPolicies; - provider?: Provider; -}) { - const ekuboMeta = useEkuboMetadata(); - - const res: SessionSummary = { - default: {}, - dojo: {}, - ERC20: {}, - ERC721: {}, - messages: policies.messages, - }; - const summary = useSWR( - ekuboMeta && provider ? `tx-summary` : null, - async () => { - if (!provider) return res; - - const promises = Object.entries(policies.contracts ?? []).map( - async ([contractAddress, policies]) => { - const contractType = await checkContractType( - provider, - contractAddress, - ); - switch (contractType) { - case "ERC20": - const meta = ekuboMeta.find( - (m) => - getChecksumAddress(m.l2_token_address) === - getChecksumAddress(contractAddress), - ); - - res.ERC20[contractAddress] = { - meta: meta && { - address: contractAddress, - name: meta.name, - symbol: meta.symbol, - decimals: meta.decimals, - }, - ...policies, - }; - return; - case "ERC721": - res.ERC721[contractAddress] = policies; - return; - case "default": - default: { - try { - const dojoNameRes = await provider.callContract({ - contractAddress, - entrypoint: "dojo_name", - }); - - res.dojo[contractAddress] = { - meta: { dojoName: stringFromByteArray(dojoNameRes) }, - ...policies, - }; - } catch { - res.default[contractAddress] = policies; - } - return; - } - } - }, - ); - await Promise.all(promises); - - return res; - }, - { - fallbackData: res, - }, - ); - - return summary; -} - -// TODO: What the id? -const IERC20_ID = ""; -const IERC721_ID = - "0x33eb2f84c309543403fd69f0d0f363781ef06ef6faeb0131ff16ea3175bd943"; - -async function checkContractType( - provider: Provider, - contractAddress: string, -): Promise { - try { - // SNIP-5: check with via `support_interface` method - const [erc20Res] = await provider.callContract({ - contractAddress, - entrypoint: "supports_interface", - calldata: [IERC20_ID], // ERC20 interface ID - }); - if (!!erc20Res) { - return "ERC20"; - } - - const [erc721Res] = await provider.callContract({ - contractAddress, - entrypoint: "supports_interface", - calldata: [IERC721_ID], // ERC721 interface ID - }); - if (!!erc721Res) { - return "ERC721"; - } - - return "default"; - } catch { - try { - await provider.callContract({ - contractAddress, - entrypoint: "balanceOf", - calldata: ["0x0"], // ERC721 interface ID - }); - - try { - await provider.callContract({ - contractAddress, - entrypoint: "decimals", - }); - - return "ERC20"; - } catch { - await provider.callContract({ - contractAddress, - entrypoint: "tokenId", - calldata: ["0x0"], - }); - - return "ERC721"; - } - } catch { - return "default"; - } - } -} diff --git a/packages/utils/src/hooks/index.ts b/packages/utils/src/hooks/index.ts index fcc5b674a..e3f8abf55 100644 --- a/packages/utils/src/hooks/index.ts +++ b/packages/utils/src/hooks/index.ts @@ -1,4 +1,3 @@ export * from "./api"; export * from "./balance"; -export * from "./contract"; export * from "./countervalue"; diff --git a/packages/utils/tsconfig.json b/packages/utils/tsconfig.json index 150dc0e07..3e4bbc588 100644 --- a/packages/utils/tsconfig.json +++ b/packages/utils/tsconfig.json @@ -6,7 +6,5 @@ "rootDir": "./src", "outDir": "./dist" }, - "include": [ - "src/**/*" - ] + "include": ["src/**/*"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9bab76ece..5ddfa8e79 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -467,6 +467,9 @@ importers: '@types/node': specifier: ^20.11.0 version: 20.16.11 + starknet: + specifier: 6.11.0 + version: 6.11.0 tsx: specifier: ^4.7.0 version: 4.19.2