diff --git a/.github/composite-actions/install/action.yml b/.github/composite-actions/install/action.yml index cf3f9047017..57aecf82ecf 100644 --- a/.github/composite-actions/install/action.yml +++ b/.github/composite-actions/install/action.yml @@ -12,7 +12,7 @@ runs: # pnpm for our dependencies - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 with: - version: 9 + version: 9.11.0 - name: Setup Node.js uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 with: diff --git a/apps/dashboard/src/app/account/settings/getAccount.ts b/apps/dashboard/src/app/account/settings/getAccount.ts index 5f1abaaf32c..77bda384969 100644 --- a/apps/dashboard/src/app/account/settings/getAccount.ts +++ b/apps/dashboard/src/app/account/settings/getAccount.ts @@ -22,6 +22,11 @@ export async function getRawAccount() { }, }); + if (!res.ok) { + console.error("Error fetching account", res.status, res.statusText); + return undefined; + } + const json = await res.json(); if (json.error) { diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/add-partner/page.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/add-partner/page.tsx new file mode 100644 index 00000000000..285ccb0a5fc --- /dev/null +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/add-partner/page.tsx @@ -0,0 +1,50 @@ +import {} from "@/components/ui/breadcrumb"; +import { getAuthToken } from "../../../../../../../../../api/lib/getAuthToken"; +import { loginRedirect } from "../../../../../../../../../login/loginRedirect"; +import { AddPartnerForm } from "../components/client/add-partner-form.client"; +import { fetchEcosystem } from "../hooks/fetchEcosystem"; + +export default async function AddPartnerPage({ + params, +}: { + params: Promise<{ slug: string; team_slug: string }>; +}) { + const { slug, team_slug } = await params; + const authToken = await getAuthToken(); + + if (!authToken) { + loginRedirect(`/team/${team_slug}/~/ecosystem/${slug}`); + } + + const teamSlug = team_slug; + const ecosystemSlug = slug; + + try { + const ecosystem = await fetchEcosystem({ + teamIdOrSlug: teamSlug, + slug: ecosystemSlug, + authToken, + }); + + return ( +
+
+

+ Add New Partner +

+ +
+
+ ); + } catch (error) { + console.error("Error fetching ecosystem:", error); + return ( +
+
+

Error

+

Could not load ecosystem. Please try again.

+
+
+ ); + } +} diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/AddPartnerDialogButton.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/AddPartnerDialogButton.tsx index 78b65c53b63..9eb504ca171 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/AddPartnerDialogButton.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/AddPartnerDialogButton.tsx @@ -1,43 +1,23 @@ "use client"; import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; import { PlusIcon } from "lucide-react"; -import { useState } from "react"; +import Link from "next/link"; import type { Ecosystem } from "../../../../../types"; -import { AddPartnerForm } from "./add-partner-form.client"; export function AddPartnerDialogButton(props: { + teamSlug: string; ecosystem: Ecosystem; authToken: string; }) { - const [open, setOpen] = useState(false); + const addPartnerUrl = `/team/${props.teamSlug}/~/ecosystem/${props.ecosystem.slug}/configuration/add-partner`; + return ( - - - - - - - - Add Partner - - - setOpen(false)} - authToken={props.authToken} - /> - - + + + ); } diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/EcosystemPermissionsPage.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/EcosystemPermissionsPage.tsx index ff34abd976f..a8cde6e9fc7 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/EcosystemPermissionsPage.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/EcosystemPermissionsPage.tsx @@ -21,7 +21,11 @@ export function EcosystemPermissionsPage({ /> {ecosystem?.permission === "PARTNER_WHITELIST" && ( - + )} ); diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/add-partner-form.client.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/add-partner-form.client.tsx index b50f8d01360..f8940546e56 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/add-partner-form.client.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/add-partner-form.client.tsx @@ -1,3 +1,6 @@ +"use client"; +import { useDashboardRouter } from "@/lib/DashboardRouter"; +import { useParams } from "next/navigation"; import { toast } from "sonner"; import type { Ecosystem, Partner } from "../../../../../types"; import { useAddPartner } from "../../hooks/use-add-partner"; @@ -5,20 +8,29 @@ import { PartnerForm, type PartnerFormValues } from "./partner-form.client"; export function AddPartnerForm({ ecosystem, - onPartnerAdded, authToken, }: { - authToken: string; ecosystem: Ecosystem; - onPartnerAdded: () => void; + authToken: string; }) { + const router = useDashboardRouter(); + const params = useParams(); + const teamSlug = params.team_slug as string; + const ecosystemSlug = params.slug as string; + const { mutateAsync: addPartner, isPending } = useAddPartner( { authToken, }, { onSuccess: () => { - onPartnerAdded(); + toast.success("Partner added successfully", { + description: "The partner has been added to your ecosystem.", + }); + + // Redirect to the redirect page that will take us back to the configuration page + const redirectPath = `/team/${teamSlug}/~/ecosystem/${ecosystemSlug}`; + router.push(redirectPath); }, onError: (error) => { const message = diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/allowed-operations-section.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/allowed-operations-section.tsx new file mode 100644 index 00000000000..568a52b3feb --- /dev/null +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/allowed-operations-section.tsx @@ -0,0 +1,677 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; +import { PlusIcon, Trash2Icon } from "lucide-react"; +import { type Control, useFieldArray } from "react-hook-form"; +import { SingleNetworkSelector } from "../../../../../../../../../../../@/components/blocks/NetworkSelectors"; +import type { PartnerFormValues } from "./partner-form.client"; + +type AllowedOperationsSectionProps = { + control: Control; + enabled: boolean; + onToggle: (checked: boolean) => void; +}; + +export function AllowedOperationsSection({ + control, + enabled, + onToggle, +}: AllowedOperationsSectionProps) { + // Setup field array for allowed operations + const allowedOperationsFields = useFieldArray({ + control, + name: "accessControl.allowedOperations", + }); + + return ( +
+
+
+ +

+ Configure which signing operations are allowed for this partner +

+
+ +
+ + {enabled && ( +
+
+ {allowedOperationsFields.fields.map((field, index) => ( +
+
+ ( + + Allowed Signature Method + + Select the types of signatures that are allowed for + this partner + + + + + )} + /> + +
+ + {/* Render different fields based on the signature method */} + {allowedOperationsFields.fields[index]?.signMethod === + "eth_signTransaction" && ( + + )} + + {allowedOperationsFields.fields[index]?.signMethod === + "eth_signTypedData_v4" && ( + + )} + + {allowedOperationsFields.fields[index]?.signMethod === + "personal_sign" && ( + + )} +
+ ))} + + +
+
+ )} +
+ ); +} + +// Component for eth_signTransaction restrictions +function TransactionRestrictions({ + control, + index, +}: { + control: Control; + index: number; +}) { + const transactionsArray = useFieldArray({ + control, + name: `accessControl.allowedOperations.${index}.allowedTransactions`, + }); + + return ( +
+ + + Specify the specific transactions patterns that are allowed for this + partner. Optional, default is all transactions. + + + Attention - 'eth_signTransaction' signatures are only for EOA + transactions. To specify allowed Smart Account transactions, you need to + select the 'eth_personalSign' signature method, with type 'userOp' (user + operations). + +
+ {transactionsArray.fields.map((field, txIndex) => ( +
+
+ ( + + Chain + + field.onChange(chainId)} + /> + + + + )} + /> + ( + + Contract Address (optional) + + + + + + )} + /> + ( + + Function Selector (optional) + + + + + + )} + /> +
+ ( + + Max Value in Wei (optional) +
+ + + +
+ +
+ )} + /> +
+
+ +
+ ))} + + +
+
+ ); +} + +// Component for eth_signTypedData_v4 restrictions +function TypedDataRestrictions({ + control, + index, +}: { + control: Control; + index: number; +}) { + const typedDataArray = useFieldArray({ + control, + name: `accessControl.allowedOperations.${index}.allowedTypedData`, + }); + + return ( +
+ + + Specify the specific typed data patterns that are allowed for this + partner. Optional, default is any typed data. + +
+ {typedDataArray.fields.map((field, dataIndex) => ( +
+
+ ( + + Domain + + + + + + )} + /> + ( + + Verifying Contract (optional) + + + + + + )} + /> + ( + + Chain (optional) + + field.onChange(chainId)} + /> + + + + )} + /> +
+ ( + + Primary Type (optional) +
+ + + +
+ +
+ )} + /> +
+
+ +
+ ))} + + +
+
+ ); +} + +// Component for personal_sign restrictions +function PersonalSignRestrictions({ + control, + index, +}: { + control: Control; + index: number; +}) { + const personalSignArray = useFieldArray({ + control, + name: `accessControl.allowedOperations.${index}.allowedPersonalSigns`, + }); + + return ( +
+ + + Specify the types of personal signatures that are allowed for this + partner. Optional, default is all personal signatures. + +
+ {personalSignArray.fields.map((field, signIndex) => ( +
+
+ ( + + Message Type + + + + )} + /> + +
+ + {personalSignArray.fields[signIndex]?.messageType === "userOp" ? ( + + ) : ( + ( + + Message (optional) + + + + + Specify a message pattern that is allowed + + + + )} + /> + )} +
+ ))} + + +
+
+ ); +} + +// Component for userOp transactions within personal_sign +function UserOpTransactions({ + control, + opIndex, + signIndex, +}: { + control: Control; + opIndex: number; + signIndex: number; +}) { + const userOpTransactionsArray = useFieldArray({ + control, + name: `accessControl.allowedOperations.${opIndex}.allowedPersonalSigns.${signIndex}.allowedTransactions`, + }); + + return ( +
+ + + Specify the specific User Operations that are allowed for this partner. + Optional, default is all User Operations. + +
+ {userOpTransactionsArray.fields.map((field, txIndex) => ( +
+
+ ( + + Chain + + field.onChange(chainId)} + /> + + + + )} + /> + ( + + Contract Address (optional) + + + + + + )} + /> + ( + + Function Selector (optional) + + + + + + )} + /> +
+ ( + + Max Value in Wei (optional) +
+ + + +
+ +
+ )} + /> +
+
+ +
+ ))} + + +
+
+ ); +} diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/partner-form.client.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/partner-form.client.tsx index 2811dad4fff..d3695860cf2 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/partner-form.client.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/partner-form.client.tsx @@ -20,6 +20,7 @@ import { useFieldArray, useForm } from "react-hook-form"; import type { z } from "zod"; import type { Partner } from "../../../../../types"; import { partnerFormSchema } from "../../constants"; +import { AllowedOperationsSection } from "./allowed-operations-section"; export type PartnerFormValues = z.infer; @@ -43,6 +44,8 @@ export function PartnerForm({ const hasAccessControl = partner ? !!partner.accessControl : false; const hasServerVerifier = hasAccessControl && !!partner?.accessControl?.serverVerifier; + const hasAllowedOperations = + hasAccessControl && !!partner?.accessControl?.allowedOperations?.length; const form = useForm({ resolver: zodResolver(partnerFormSchema), @@ -55,12 +58,15 @@ export function PartnerForm({ // Set the UI control properties based on existing data accessControlEnabled: hasAccessControl, serverVerifierEnabled: hasServerVerifier, + allowedOperationsEnabled: hasAllowedOperations, }, + mode: "onChange", // Validate on change for better user experience }); // Watch the boolean flags for UI state const accessControlEnabled = form.watch("accessControlEnabled"); const serverVerifierEnabled = form.watch("serverVerifierEnabled"); + const allowedOperationsEnabled = form.watch("allowedOperationsEnabled"); // Setup field array for headers const customHeaderFields = useFieldArray({ @@ -83,7 +89,10 @@ export function PartnerForm({ }; } - // TODO add signature policies here + if (finalAccessControl && values.allowedOperationsEnabled) { + finalAccessControl.allowedOperations = + values.accessControl?.allowedOperations || []; + } // if no values have been set, remove the accessControl object if ( @@ -196,133 +205,158 @@ export function PartnerForm({ checked={accessControlEnabled} onCheckedChange={(checked) => { form.setValue("accessControlEnabled", checked); - // If disabling access control, also disable server verifier + // If disabling access control, also disable server verifier and allowed operations if (!checked) { form.setValue("serverVerifierEnabled", false); + form.setValue("allowedOperationsEnabled", false); } }} /> {accessControlEnabled && ( -
-
-
- -

- Configure a server verifier for access control -

-
- { - form.setValue("serverVerifierEnabled", checked); - - // Initialize serverVerifier fields if enabling - if ( - checked && - !form.getValues("accessControl.serverVerifier") - ) { - form.setValue("accessControl.serverVerifier", { - url: "", - headers: [], - }); - } - }} - /> -
+ <> +
+
+
+ +

+ Configure a server verifier for access control +

+
+ { + form.setValue("serverVerifierEnabled", checked); - {serverVerifierEnabled && ( -
- ( - - Server Verifier URL - - - - - {form.formState.errors.accessControl?.serverVerifier - ?.url?.message || - "Enter the URL of your server where verification requests will be sent"} - - - )} + // Initialize serverVerifier fields if enabling + if ( + checked && + !form.getValues("accessControl.serverVerifier") + ) { + form.setValue("accessControl.serverVerifier", { + url: "", + headers: [], + }); + } + }} /> +
-
- -
- {customHeaderFields.fields.map((field, headerIdx) => { - return ( -
- + {serverVerifierEnabled && ( +
+ ( + + Server Verifier URL + - -
- ); - })} + + + {form.formState.errors.accessControl?.serverVerifier + ?.url?.message || + "Enter the URL of your server where verification requests will be sent"} + + + )} + /> - -
+
+ +
+ {customHeaderFields.fields.map((field, headerIdx) => { + return ( +
+ + + +
+ ); + })} -

- Set custom headers to be sent along with verification - requests -

+ +
+ +

+ Set custom headers to be sent along with verification + requests +

+
-
- )} -
+ )} +
+ + {/* Allowed Operations Section */} + { + form.setValue("allowedOperationsEnabled", checked); + + // Initialize allowedOperations array if enabling + if ( + checked && + !form.getValues("accessControl.allowedOperations") + ) { + form.setValue("accessControl.allowedOperations", []); + } + }} + /> + )}
diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/update-partner-form.client.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/update-partner-form.client.tsx index 15027b09443..97540af1e2e 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/update-partner-form.client.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/update-partner-form.client.tsx @@ -1,4 +1,6 @@ "use client"; +import { useDashboardRouter } from "@/lib/DashboardRouter"; +import { useParams } from "next/navigation"; import { toast } from "sonner"; import type { Ecosystem, Partner } from "../../../../../types"; import { useUpdatePartner } from "../../hooks/use-update-partner"; @@ -7,21 +9,30 @@ import { PartnerForm, type PartnerFormValues } from "./partner-form.client"; export function UpdatePartnerForm({ ecosystem, partner, - onSuccess, authToken, }: { ecosystem: Ecosystem; partner: Partner; - onSuccess: () => void; authToken: string; }) { + const router = useDashboardRouter(); + const params = useParams(); + const teamSlug = params.team_slug as string; + const ecosystemSlug = params.slug as string; + const { mutateAsync: updatePartner, isPending } = useUpdatePartner( { authToken, }, { onSuccess: () => { - onSuccess(); + toast.success("Partner updated successfully", { + description: "The partner details have been updated.", + }); + + // Redirect to the redirect page that will take us back to the configuration page + const redirectPath = `/team/${teamSlug}/~/ecosystem/${ecosystemSlug}`; + router.push(redirectPath); }, onError: (error) => { const message = diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/update-partner-modal.client.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/update-partner-modal.client.tsx deleted file mode 100644 index 71f4b7026ae..00000000000 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/update-partner-modal.client.tsx +++ /dev/null @@ -1,42 +0,0 @@ -"use client"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; -import { useState } from "react"; -import type { Ecosystem, Partner } from "../../../../../types"; -import { UpdatePartnerForm } from "./update-partner-form.client"; - -export function UpdatePartnerModal({ - children, - ecosystem, - partner, - authToken, -}: { - children: React.ReactNode; - ecosystem: Ecosystem; - partner: Partner; - authToken: string; -}) { - const [open, setOpen] = useState(false); - - return ( - - {children} - - - Update {partner.name} - - setOpen(false)} - authToken={authToken} - /> - - - ); -} diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/server/ecosystem-partners-section.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/server/ecosystem-partners-section.tsx index ec472671134..9a83f8d2034 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/server/ecosystem-partners-section.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/server/ecosystem-partners-section.tsx @@ -3,9 +3,10 @@ import { AddPartnerDialogButton } from "../client/AddPartnerDialogButton"; import { PartnersTable } from "./partners-table"; export function EcosystemPartnersSection({ + teamSlug, ecosystem, authToken, -}: { ecosystem: Ecosystem; authToken: string }) { +}: { teamSlug: string; ecosystem: Ecosystem; authToken: string }) { return (
@@ -21,10 +22,18 @@ export function EcosystemPartnersSection({ own app.

- +
- + ); } diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/server/partners-table.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/server/partners-table.tsx index d106ea802ac..d74d9d76e9a 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/server/partners-table.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/server/partners-table.tsx @@ -12,20 +12,28 @@ import { TableRow, } from "@/components/ui/table"; import { ToolTipLabel } from "@/components/ui/tooltip"; +import { useDashboardRouter } from "@/lib/DashboardRouter"; import { cn } from "@/lib/utils"; import { Pencil, Trash2 } from "lucide-react"; -import Link from "next/link"; import { toast } from "sonner"; +import { Link } from "../../../../../../../../../../../tw-components/link"; import type { Ecosystem, Partner } from "../../../../../types"; import { usePartners } from "../../../hooks/use-partners"; import { useDeletePartner } from "../../hooks/use-delete-partner"; -import { UpdatePartnerModal } from "../client/update-partner-modal.client"; export function PartnersTable({ ecosystem, authToken, -}: { ecosystem: Ecosystem; authToken: string }) { - const { partners, isPending } = usePartners({ ecosystem, authToken }); + teamSlug, +}: { + ecosystem: Ecosystem; + authToken: string; + teamSlug: string; +}) { + const { partners, isPending } = usePartners({ + ecosystem, + authToken, + }); if (isPending) { return ( @@ -59,6 +67,7 @@ export function PartnersTable({ partner={partner} ecosystem={ecosystem} authToken={authToken} + teamSlug={teamSlug} /> ))} @@ -70,8 +79,10 @@ export function PartnersTable({ function PartnerRow(props: { partner: Partner; ecosystem: Ecosystem; + teamSlug: string; authToken: string; }) { + const router = useDashboardRouter(); const { mutateAsync: deletePartner, isPending: isDeleting } = useDeletePartner( { @@ -117,22 +128,21 @@ function PartnerRow(props: {
- { + router.push( + `/team/${props.teamSlug}/~/ecosystem/${props.ecosystem.slug}/configuration/partners/${props.partner.id}/edit`, + ); + }} > - - + + Edit + { + if (data !== undefined) { + return data > 0; + } + return true; + }, + { + message: "Invalid chain ID", + }, + ), + contractAddress: z + .string() + .optional() + .transform((data) => (data?.length === 0 ? undefined : data)) + .refine( + (data) => { + if (data) { + return isAddress(data); + } + return true; + }, + { + message: "Invalid contract address", + }, + ), + selector: z + .string() + .optional() + .transform((data) => (data?.length === 0 ? undefined : data)) + .refine( + (data) => { + if (data) { + return isHex(data) && data.length === 10; // 0x + 4 bytes for the selector (8 chars) + } + return true; + }, + { + message: "Invalid function selector", + }, + ), + maxValue: z + .string() + .optional() + .transform((data) => (data?.length === 0 ? undefined : data)) + .refine( + (data) => { + if (data) { + return BigInt(data) >= 0; + } + return true; + }, + { + message: "Invalid max value", + }, + ), +}); + +const allowedTypedDataSchema = z + .object({ + domain: z.string().refine((data) => data.length > 0, { + message: "Domain is required", + }), + verifyingContract: z + .string() + .optional() + .transform((data) => (data?.length === 0 ? undefined : data)) + .refine( + (data) => { + if (data) { + return isAddress(data); + } + return true; + }, + { + message: "Invalid verifying contract address", + }, + ), + chainId: z + .number() + .optional() + .refine( + (data) => { + if (data !== undefined) { + return data > 0; + } + return true; + }, + { + message: "Invalid chain ID", + }, + ), + primaryType: z.string().optional(), + }) + .transform((data) => { + return { + ...data, + verifyingContract: + data.verifyingContract && data.verifyingContract.length > 0 + ? data.verifyingContract + : undefined, + primaryType: + data.primaryType && data.primaryType.length > 0 + ? data.primaryType + : undefined, + }; + }); + +const personalSignRestrictionSchema = z.discriminatedUnion("messageType", [ + z.object({ + messageType: z.literal("userOp"), + allowedTransactions: z + .array(allowedTransactionSchema) + .optional() + .transform((data) => (data?.length === 0 ? undefined : data)), + }), + z.object({ + messageType: z.literal("other"), + message: z.string().optional(), + }), +]); + +const allowedOperationsSchema = z.discriminatedUnion("signMethod", [ + z.object({ + signMethod: z.literal("eth_signTransaction"), + allowedTransactions: z + .array(allowedTransactionSchema) + .optional() + .transform((data) => (data?.length === 0 ? undefined : data)), + }), + z.object({ + signMethod: z.literal("eth_signTypedData_v4"), + allowedTypedData: z + .array(allowedTypedDataSchema) + .optional() + .transform((data) => (data?.length === 0 ? undefined : data)), + }), + z.object({ + signMethod: z.literal("personal_sign"), + allowedPersonalSigns: z + .array(personalSignRestrictionSchema) + .optional() + .transform((data) => (data?.length === 0 ? undefined : data)), + }), +]); + export const partnerFormSchema = z .object({ name: z @@ -33,6 +182,7 @@ export const partnerFormSchema = z ), accessControlEnabled: z.boolean().default(false), serverVerifierEnabled: z.boolean().default(false), + allowedOperationsEnabled: z.boolean().default(false), accessControl: z .object({ serverVerifier: z @@ -48,6 +198,10 @@ export const partnerFormSchema = z .optional(), }) .optional(), + allowedOperations: z + .array(allowedOperationsSchema) + .optional() + .transform((data) => (data?.length === 0 ? undefined : data)), }) .optional(), }) diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/hooks/fetchEcosystem.ts b/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/hooks/fetchEcosystem.ts new file mode 100644 index 00000000000..f51d70b2bf9 --- /dev/null +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/hooks/fetchEcosystem.ts @@ -0,0 +1,34 @@ +import { API_SERVER_URL } from "@/constants/env"; +import type { Ecosystem } from "../../../../types"; + +/** + * Fetches ecosystem data from the server + */ +export async function fetchEcosystem(args: { + teamIdOrSlug: string; + slug: string; + authToken: string; +}): Promise { + const { teamIdOrSlug, slug, authToken } = args; + const res = await fetch( + `${API_SERVER_URL}/v1/teams/${teamIdOrSlug}/ecosystem-wallet/${slug}`, + { + headers: { + Authorization: `Bearer ${authToken}`, + }, + next: { + revalidate: 0, + }, + }, + ); + + if (!res.ok) { + const data = await res.json(); + console.error(data); + throw new Error( + data?.message ?? data?.error?.message ?? "Failed to fetch ecosystem", + ); + } + + return (await res.json()).result as Ecosystem; +} diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/hooks/fetchPartnerDetails.ts b/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/hooks/fetchPartnerDetails.ts new file mode 100644 index 00000000000..395683a67ee --- /dev/null +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/hooks/fetchPartnerDetails.ts @@ -0,0 +1,32 @@ +import type { Ecosystem, Partner } from "../../../../types"; + +export async function fetchPartnerDetails(args: { + authToken: string; + ecosystem: Ecosystem; + partnerId: string; +}): Promise { + const { authToken, ecosystem, partnerId } = args; + + try { + const response = await fetch( + `${ecosystem.url}/${ecosystem.id}/partner/${partnerId}`, + { + method: "GET", + headers: { + Authorization: `Bearer ${authToken}`, + }, + }, + ); + + if (!response.ok) { + throw new Error( + `Failed to fetch partner details: ${response.status} - ${response.statusText}`, + ); + } + + return await response.json(); + } catch (error) { + console.error("Error fetching partner:", error); + throw error; + } +} diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/hooks/fetchPartners.ts b/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/hooks/fetchPartners.ts new file mode 100644 index 00000000000..32c6fa54b0c --- /dev/null +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/hooks/fetchPartners.ts @@ -0,0 +1,34 @@ +import type { Ecosystem, Partner } from "../../../../types"; + +/** + * Fetches partners for an ecosystem + */ +export async function fetchPartners({ + ecosystem, + authToken, +}: { + ecosystem: Ecosystem; + authToken: string; +}): Promise { + const res = await fetch(`${ecosystem.url}/${ecosystem.id}/partners`, { + headers: { + Authorization: `Bearer ${authToken}`, + }, + next: { + revalidate: 0, + }, + }); + + if (!res.ok) { + const data = await res.json(); + console.error(data); + throw new Error( + data?.message ?? data?.error?.message ?? "Failed to fetch partners", + ); + } + + const partners = (await res.json()) as Partner[]; + return partners.sort( + (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(), + ); +} diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/partners/[partner_id]/edit/page.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/partners/[partner_id]/edit/page.tsx new file mode 100644 index 00000000000..2905f97f030 --- /dev/null +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/partners/[partner_id]/edit/page.tsx @@ -0,0 +1,96 @@ +import {} from "@/components/ui/breadcrumb"; +import { getAuthToken } from "../../../../../../../../../../../api/lib/getAuthToken"; +import { loginRedirect } from "../../../../../../../../../../../login/loginRedirect"; +import { UpdatePartnerForm } from "../../../components/client/update-partner-form.client"; +import { fetchEcosystem } from "../../../hooks/fetchEcosystem"; +import { fetchPartnerDetails } from "../../../hooks/fetchPartnerDetails"; + +export default async function EditPartnerPage({ + params, +}: { + params: Promise<{ slug: string; team_slug: string; partner_id: string }>; +}) { + const { slug, team_slug, partner_id } = await params; + const authToken = await getAuthToken(); + + if (!authToken) { + loginRedirect(`/team/${team_slug}/~/ecosystem/${slug}/configuration`); + } + + const teamSlug = team_slug; + const ecosystemSlug = slug; + const partnerId = partner_id; + + try { + const ecosystem = await fetchEcosystem({ + teamIdOrSlug: teamSlug, + slug: ecosystemSlug, + authToken, + }); + + try { + // TODO re-enable this once IAW service is re deployed + const partner = await fetchPartnerDetails({ + ecosystem, + partnerId, + authToken, + }); + // const partners = await fetchPartners({ + // ecosystem, + // authToken, + // }); + + // const partner = partners.find((p) => p.id === partnerId); + + if (!partner) { + return ( +
+
+

+ Error +

+

Could not load partner details. Please try again.

+
+
+ ); + } + + return ( +
+
+

+ Edit Partner: {partner.name} +

+ +
+
+ ); + } catch (partnerError) { + console.error("Error fetching partner:", partnerError); + return ( +
+
+

+ Error +

+

Could not load partner details. Please try again.

+
+
+ ); + } + } catch (ecosystemError) { + console.error("Error fetching ecosystem:", ecosystemError); + return ( +
+
+

Error

+

Could not load ecosystem. Please try again.

+
+
+ ); + } +} diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/hooks/use-partners.ts b/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/hooks/use-partners.ts index 50f6372f045..6a947ac5fc8 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/hooks/use-partners.ts +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/hooks/use-partners.ts @@ -1,5 +1,6 @@ import { useQuery } from "@tanstack/react-query"; import type { Ecosystem, Partner } from "../../../types"; +import { fetchPartners } from "../configuration/hooks/fetchPartners"; export function usePartners({ ecosystem, @@ -8,25 +9,7 @@ export function usePartners({ const partnersQuery = useQuery({ queryKey: ["ecosystem", ecosystem.id, "partners"], queryFn: async () => { - const res = await fetch(`${ecosystem.url}/${ecosystem.id}/partners`, { - headers: { - Authorization: `Bearer ${authToken}`, - }, - }); - - if (!res.ok) { - const data = await res.json(); - console.error(data); - throw new Error( - data?.message ?? data?.error?.message ?? "Failed to fetch ecosystems", - ); - } - - const partners = (await res.json()) as Partner[]; - return partners.sort( - (a, b) => - new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(), - ); + return fetchPartners({ ecosystem, authToken }); }, retry: false, }); diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/types.ts b/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/types.ts index 610494417b6..f2d0f66d3be 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/types.ts +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/types.ts @@ -60,5 +60,52 @@ export type Partner = { url: string; headers?: { key: string; value: string }[]; }; + allowedOperations?: AllowedOperations[]; }; }; + +type AllowedArgument = { + offset: number; + type: "address" | "uint256" | "bytes32" | "bool" | "string"; + comparisonOperator: "eq" | "neq" | "gt" | "gte" | "lt" | "lte"; + value: string; +}; + +type AllowedTransaction = { + chainId: number; + contractAddress?: string; + selector?: string; + arguments?: AllowedArgument[]; + maxValue?: string; +}; + +type AllowedTypedData = { + domain: string; + verifyingContract?: string; + chainId?: number; + primaryType?: string; +}; + +type PersonalSignRestriction = + | { + messageType: "userOp"; + allowedTransactions?: AllowedTransaction[]; + } + | { + messageType: "other"; + message?: string; + }; + +type AllowedOperations = + | { + signMethod: "eth_signTransaction"; + allowedTransactions?: AllowedTransaction[]; + } + | { + signMethod: "eth_signTypedData_v4"; + allowedTypedData?: AllowedTypedData[]; + } + | { + signMethod: "personal_sign"; + allowedPersonalSigns?: PersonalSignRestriction[]; + };