From aec9d7eaad69e96e6d4b9923685fc980c3b87020 Mon Sep 17 00:00:00 2001 From: Maciej Trojan Date: Mon, 10 Nov 2025 16:34:43 +0100 Subject: [PATCH] feat: add SLA management features --- abi/BeneficiaryFactory.ts | 542 ++++++++++++++++ abi/SLARegistry.ts | 597 ++++++++++++++++++ app/(app)/page.tsx | 90 ++- .../beneficiary-contract-deploy-widget.tsx | 223 +++++++ .../sla/components/register-sla-widget.tsx | 336 ++++++++++ app/(app)/sla/page.tsx | 32 + config/chains.ts | 16 + config/contracts.ts | 15 +- config/wagmi.ts | 4 +- 9 files changed, 1820 insertions(+), 35 deletions(-) create mode 100644 abi/BeneficiaryFactory.ts create mode 100644 abi/SLARegistry.ts create mode 100644 app/(app)/sla/components/beneficiary-contract-deploy-widget.tsx create mode 100644 app/(app)/sla/components/register-sla-widget.tsx create mode 100644 app/(app)/sla/page.tsx create mode 100644 config/chains.ts diff --git a/abi/BeneficiaryFactory.ts b/abi/BeneficiaryFactory.ts new file mode 100644 index 0000000..9dfb43c --- /dev/null +++ b/abi/BeneficiaryFactory.ts @@ -0,0 +1,542 @@ +import { type Abi } from "viem"; + +const beneficiaryFactoryAbi = [ + { + type: "constructor", + inputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "DEFAULT_ADMIN_ROLE", + inputs: [], + outputs: [ + { + name: "", + type: "bytes32", + internalType: "bytes32", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "UPGRADER_ROLE", + inputs: [], + outputs: [ + { + name: "", + type: "bytes32", + internalType: "bytes32", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "UPGRADE_INTERFACE_VERSION", + inputs: [], + outputs: [ + { + name: "", + type: "string", + internalType: "string", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "beacon", + inputs: [], + outputs: [ + { + name: "", + type: "address", + internalType: "address", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "create", + inputs: [ + { + name: "admin", + type: "address", + internalType: "address", + }, + { + name: "withdrawer", + type: "address", + internalType: "address", + }, + { + name: "provider", + type: "uint64", + internalType: "CommonTypes.FilActorId", + }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "getRoleAdmin", + inputs: [ + { + name: "role", + type: "bytes32", + internalType: "bytes32", + }, + ], + outputs: [ + { + name: "", + type: "bytes32", + internalType: "bytes32", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "grantRole", + inputs: [ + { + name: "role", + type: "bytes32", + internalType: "bytes32", + }, + { + name: "account", + type: "address", + internalType: "address", + }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "hasRole", + inputs: [ + { + name: "role", + type: "bytes32", + internalType: "bytes32", + }, + { + name: "account", + type: "address", + internalType: "address", + }, + ], + outputs: [ + { + name: "", + type: "bool", + internalType: "bool", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "initialize", + inputs: [ + { + name: "admin", + type: "address", + internalType: "address", + }, + { + name: "implementation", + type: "address", + internalType: "address", + }, + { + name: "slaAllocator_", + type: "address", + internalType: "contract SLAAllocator", + }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "instances", + inputs: [ + { + name: "provider", + type: "uint64", + internalType: "CommonTypes.FilActorId", + }, + ], + outputs: [ + { + name: "contractAddress", + type: "address", + internalType: "address", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "nonce", + inputs: [ + { + name: "admin", + type: "address", + internalType: "address", + }, + { + name: "provider", + type: "uint64", + internalType: "CommonTypes.FilActorId", + }, + ], + outputs: [ + { + name: "deployCounter", + type: "uint256", + internalType: "uint256", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "proxiableUUID", + inputs: [], + outputs: [ + { + name: "", + type: "bytes32", + internalType: "bytes32", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "renounceRole", + inputs: [ + { + name: "role", + type: "bytes32", + internalType: "bytes32", + }, + { + name: "callerConfirmation", + type: "address", + internalType: "address", + }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "revokeRole", + inputs: [ + { + name: "role", + type: "bytes32", + internalType: "bytes32", + }, + { + name: "account", + type: "address", + internalType: "address", + }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "slaAllocator", + inputs: [], + outputs: [ + { + name: "", + type: "address", + internalType: "contract SLAAllocator", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "supportsInterface", + inputs: [ + { + name: "interfaceId", + type: "bytes4", + internalType: "bytes4", + }, + ], + outputs: [ + { + name: "", + type: "bool", + internalType: "bool", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "upgradeToAndCall", + inputs: [ + { + name: "newImplementation", + type: "address", + internalType: "address", + }, + { + name: "data", + type: "bytes", + internalType: "bytes", + }, + ], + outputs: [], + stateMutability: "payable", + }, + { + type: "event", + name: "Initialized", + inputs: [ + { + name: "version", + type: "uint64", + indexed: false, + internalType: "uint64", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "ProxyCreated", + inputs: [ + { + name: "proxy", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "provider", + type: "uint64", + indexed: true, + internalType: "CommonTypes.FilActorId", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "RoleAdminChanged", + inputs: [ + { + name: "role", + type: "bytes32", + indexed: true, + internalType: "bytes32", + }, + { + name: "previousAdminRole", + type: "bytes32", + indexed: true, + internalType: "bytes32", + }, + { + name: "newAdminRole", + type: "bytes32", + indexed: true, + internalType: "bytes32", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "RoleGranted", + inputs: [ + { + name: "role", + type: "bytes32", + indexed: true, + internalType: "bytes32", + }, + { + name: "account", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "sender", + type: "address", + indexed: true, + internalType: "address", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "RoleRevoked", + inputs: [ + { + name: "role", + type: "bytes32", + indexed: true, + internalType: "bytes32", + }, + { + name: "account", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "sender", + type: "address", + indexed: true, + internalType: "address", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "Upgraded", + inputs: [ + { + name: "implementation", + type: "address", + indexed: true, + internalType: "address", + }, + ], + anonymous: false, + }, + { + type: "error", + name: "AccessControlBadConfirmation", + inputs: [], + }, + { + type: "error", + name: "AccessControlUnauthorizedAccount", + inputs: [ + { + name: "account", + type: "address", + internalType: "address", + }, + { + name: "neededRole", + type: "bytes32", + internalType: "bytes32", + }, + ], + }, + { + type: "error", + name: "AddressEmptyCode", + inputs: [ + { + name: "target", + type: "address", + internalType: "address", + }, + ], + }, + { + type: "error", + name: "Create2EmptyBytecode", + inputs: [], + }, + { + type: "error", + name: "ERC1967InvalidImplementation", + inputs: [ + { + name: "implementation", + type: "address", + internalType: "address", + }, + ], + }, + { + type: "error", + name: "ERC1967NonPayable", + inputs: [], + }, + { + type: "error", + name: "FailedCall", + inputs: [], + }, + { + type: "error", + name: "FailedDeployment", + inputs: [], + }, + { + type: "error", + name: "InstanceAlreadyExists", + inputs: [], + }, + { + type: "error", + name: "InsufficientBalance", + inputs: [ + { + name: "balance", + type: "uint256", + internalType: "uint256", + }, + { + name: "needed", + type: "uint256", + internalType: "uint256", + }, + ], + }, + { + type: "error", + name: "InvalidInitialization", + inputs: [], + }, + { + type: "error", + name: "NotInitializing", + inputs: [], + }, + { + type: "error", + name: "UUPSUnauthorizedCallContext", + inputs: [], + }, + { + type: "error", + name: "UUPSUnsupportedProxiableUUID", + inputs: [ + { + name: "slot", + type: "bytes32", + internalType: "bytes32", + }, + ], + }, +] as const satisfies Abi; + +export default beneficiaryFactoryAbi; diff --git a/abi/SLARegistry.ts b/abi/SLARegistry.ts new file mode 100644 index 0000000..95f5d34 --- /dev/null +++ b/abi/SLARegistry.ts @@ -0,0 +1,597 @@ +import { type Abi } from "viem"; + +const SLARegistryAbi = [ + { + type: "constructor", + inputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "DEFAULT_ADMIN_ROLE", + inputs: [], + outputs: [ + { + name: "", + type: "bytes32", + internalType: "bytes32", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "UPGRADER_ROLE", + inputs: [], + outputs: [ + { + name: "", + type: "bytes32", + internalType: "bytes32", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "UPGRADE_INTERFACE_VERSION", + inputs: [], + outputs: [ + { + name: "", + type: "string", + internalType: "string", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "getRoleAdmin", + inputs: [ + { + name: "role", + type: "bytes32", + internalType: "bytes32", + }, + ], + outputs: [ + { + name: "", + type: "bytes32", + internalType: "bytes32", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "grantRole", + inputs: [ + { + name: "role", + type: "bytes32", + internalType: "bytes32", + }, + { + name: "account", + type: "address", + internalType: "address", + }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "hasRole", + inputs: [ + { + name: "role", + type: "bytes32", + internalType: "bytes32", + }, + { + name: "account", + type: "address", + internalType: "address", + }, + ], + outputs: [ + { + name: "", + type: "bool", + internalType: "bool", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "initialize", + inputs: [ + { + name: "admin", + type: "address", + internalType: "address", + }, + { + name: "oracle_", + type: "address", + internalType: "contract SLIOracle", + }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "oracle", + inputs: [], + outputs: [ + { + name: "", + type: "address", + internalType: "contract SLIOracle", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "proxiableUUID", + inputs: [], + outputs: [ + { + name: "", + type: "bytes32", + internalType: "bytes32", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "registerSLA", + inputs: [ + { + name: "client", + type: "address", + internalType: "address", + }, + { + name: "provider", + type: "uint64", + internalType: "CommonTypes.FilActorId", + }, + { + name: "slaParams", + type: "tuple", + internalType: "struct SLARegistry.SLAParams", + components: [ + { + name: "latency", + type: "uint32", + internalType: "uint32", + }, + { + name: "retention", + type: "uint16", + internalType: "uint16", + }, + { + name: "bandwidth", + type: "uint16", + internalType: "uint16", + }, + { + name: "stability", + type: "uint16", + internalType: "uint16", + }, + { + name: "availability", + type: "uint8", + internalType: "uint8", + }, + { + name: "indexing", + type: "uint8", + internalType: "uint8", + }, + { + name: "registered", + type: "bool", + internalType: "bool", + }, + ], + }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "renounceRole", + inputs: [ + { + name: "role", + type: "bytes32", + internalType: "bytes32", + }, + { + name: "callerConfirmation", + type: "address", + internalType: "address", + }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "revokeRole", + inputs: [ + { + name: "role", + type: "bytes32", + internalType: "bytes32", + }, + { + name: "account", + type: "address", + internalType: "address", + }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "score", + inputs: [ + { + name: "client", + type: "address", + internalType: "address", + }, + { + name: "provider", + type: "uint64", + internalType: "CommonTypes.FilActorId", + }, + ], + outputs: [ + { + name: "", + type: "uint256", + internalType: "uint256", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "slas", + inputs: [ + { + name: "client", + type: "address", + internalType: "address", + }, + { + name: "provider", + type: "uint64", + internalType: "CommonTypes.FilActorId", + }, + ], + outputs: [ + { + name: "latency", + type: "uint32", + internalType: "uint32", + }, + { + name: "retention", + type: "uint16", + internalType: "uint16", + }, + { + name: "bandwidth", + type: "uint16", + internalType: "uint16", + }, + { + name: "stability", + type: "uint16", + internalType: "uint16", + }, + { + name: "availability", + type: "uint8", + internalType: "uint8", + }, + { + name: "indexing", + type: "uint8", + internalType: "uint8", + }, + { + name: "registered", + type: "bool", + internalType: "bool", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "supportsInterface", + inputs: [ + { + name: "interfaceId", + type: "bytes4", + internalType: "bytes4", + }, + ], + outputs: [ + { + name: "", + type: "bool", + internalType: "bool", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "upgradeToAndCall", + inputs: [ + { + name: "newImplementation", + type: "address", + internalType: "address", + }, + { + name: "data", + type: "bytes", + internalType: "bytes", + }, + ], + outputs: [], + stateMutability: "payable", + }, + { + type: "event", + name: "Initialized", + inputs: [ + { + name: "version", + type: "uint64", + indexed: false, + internalType: "uint64", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "RoleAdminChanged", + inputs: [ + { + name: "role", + type: "bytes32", + indexed: true, + internalType: "bytes32", + }, + { + name: "previousAdminRole", + type: "bytes32", + indexed: true, + internalType: "bytes32", + }, + { + name: "newAdminRole", + type: "bytes32", + indexed: true, + internalType: "bytes32", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "RoleGranted", + inputs: [ + { + name: "role", + type: "bytes32", + indexed: true, + internalType: "bytes32", + }, + { + name: "account", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "sender", + type: "address", + indexed: true, + internalType: "address", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "RoleRevoked", + inputs: [ + { + name: "role", + type: "bytes32", + indexed: true, + internalType: "bytes32", + }, + { + name: "account", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "sender", + type: "address", + indexed: true, + internalType: "address", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "SLARegistered", + inputs: [ + { + name: "client", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "provider", + type: "uint64", + indexed: true, + internalType: "CommonTypes.FilActorId", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "Upgraded", + inputs: [ + { + name: "implementation", + type: "address", + indexed: true, + internalType: "address", + }, + ], + anonymous: false, + }, + { + type: "error", + name: "AccessControlBadConfirmation", + inputs: [], + }, + { + type: "error", + name: "AccessControlUnauthorizedAccount", + inputs: [ + { + name: "account", + type: "address", + internalType: "address", + }, + { + name: "neededRole", + type: "bytes32", + internalType: "bytes32", + }, + ], + }, + { + type: "error", + name: "AddressEmptyCode", + inputs: [ + { + name: "target", + type: "address", + internalType: "address", + }, + ], + }, + { + type: "error", + name: "ERC1967InvalidImplementation", + inputs: [ + { + name: "implementation", + type: "address", + internalType: "address", + }, + ], + }, + { + type: "error", + name: "ERC1967NonPayable", + inputs: [], + }, + { + type: "error", + name: "FailedCall", + inputs: [], + }, + { + type: "error", + name: "InvalidInitialization", + inputs: [], + }, + { + type: "error", + name: "NotInitializing", + inputs: [], + }, + { + type: "error", + name: "SLAAlreadyRegistered", + inputs: [ + { + name: "client", + type: "address", + internalType: "address", + }, + { + name: "provider", + type: "uint64", + internalType: "CommonTypes.FilActorId", + }, + ], + }, + { + type: "error", + name: "SLAUnknown", + inputs: [ + { + name: "client", + type: "address", + internalType: "address", + }, + { + name: "provider", + type: "uint64", + internalType: "CommonTypes.FilActorId", + }, + ], + }, + { + type: "error", + name: "UUPSUnauthorizedCallContext", + inputs: [], + }, + { + type: "error", + name: "UUPSUnsupportedProxiableUUID", + inputs: [ + { + name: "slot", + type: "bytes32", + internalType: "bytes32", + }, + ], + }, +] as const satisfies Abi; + +export default SLARegistryAbi; diff --git a/app/(app)/page.tsx b/app/(app)/page.tsx index 6a18b03..b0cff6e 100644 --- a/app/(app)/page.tsx +++ b/app/(app)/page.tsx @@ -1,3 +1,5 @@ +"use client"; + import ScreenBreadcrumbs from "@/components/ScreenBreadcrumbs"; import { Button } from "@/components/ui/button"; import { @@ -7,9 +9,13 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; +import { filecoinDevnet } from "@/config/chains"; import Link from "next/link"; +import { useChainId } from "wagmi"; export default function HomePage() { + const chainId = useChainId(); + return (
- - - Allocators - - -

- List and create new allocator contracts. Manage individual - allocators allowance. Manage contracts ownership and more. -

-
- - - -
+ {chainId !== filecoinDevnet.id && ( + + + Allocators + + +

+ List and create new allocator contracts. Manage individual + allocators allowance. Manage contracts ownership and more. +

+
+ + + +
+ )} - - - Clients - - -

- Create new Client contracts. Manage individual clients allowance. - Manage contracts ownership and more. -

-
- - - -
+ {chainId !== filecoinDevnet.id && ( + + + Clients + + +

+ Create new Client contracts. Manage individual clients + allowance. Manage contracts ownership and more. +

+
+ + + +
+ )} @@ -68,6 +78,22 @@ export default function HomePage() { + + {chainId === filecoinDevnet.id && ( + + + SLA + + +

Deploy new Beneficiary contracts and manage SLA registry.

+
+ + + +
+ )}
); diff --git a/app/(app)/sla/components/beneficiary-contract-deploy-widget.tsx b/app/(app)/sla/components/beneficiary-contract-deploy-widget.tsx new file mode 100644 index 0000000..43148ef --- /dev/null +++ b/app/(app)/sla/components/beneficiary-contract-deploy-widget.tsx @@ -0,0 +1,223 @@ +"use client"; + +import beneficiaryFactoryAbi from "@/abi/BeneficiaryFactory"; +import RegularTransactionButton from "@/components/RegularTransactionButton"; +import { + Button, + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, + Input, +} from "@fidlabs/common-react-ui"; +import { zodResolver } from "@hookform/resolvers/zod"; +import type { TransactionBase } from "@safe-global/types-kit"; +import { Check, Loader2 } from "lucide-react"; +import { useCallback, useMemo, useState } from "react"; +import { useForm, useWatch } from "react-hook-form"; +import { type Address, encodeFunctionData, isAddress } from "viem"; +import { useWatchContractEvent } from "wagmi"; +import { z } from "zod"; + +export interface BeneficiaryContractDeployWidgetProps { + factoryAddress: Address; +} + +type StorageProviderId = `${"f0" | "t0" | ""}${number}` | ""; + +const storageProviderIdPrefixRegex = /(?:f0|t0)/g; +const storageProviderIdRegex = /^(?:f0|t0){0,1}[0-9]+$/; + +const formSchema = z.object({ + admin: z.string().refine((value) => { + return isAddress(value); + }, "Invalid Admin address"), + withdrawer: z.string().refine((value) => { + return isAddress(value); + }, "Invalid Withdrawer address"), + provider: z.string().refine((value): value is StorageProviderId => { + return storageProviderIdRegex.test(value); + }, "Invalid Storage Provider ID"), +}); + +export function BeneficiaryContractDeployWidget({ + factoryAddress, +}: BeneficiaryContractDeployWidgetProps) { + const [transactionHash, setTransactionHash] = useState(); + const [createdContractAddress, setCreatedContractAddress] = + useState
(); + const shouldShowForm = !transactionHash && !createdContractAddress; + const shouldShowLoader = !!transactionHash && !createdContractAddress; + + const form = useForm({ + resolver: zodResolver(formSchema), + }); + + const { + reset: resetForm, + formState: { isValid: isFormValid }, + } = form; + + const [admin, provider, withdrawer] = useWatch({ + control: form.control, + name: ["admin", "provider", "withdrawer"], + }); + + const transaction = useMemo(() => { + if (!isFormValid) { + return null; + } + + const providerIdInteger = BigInt( + provider.replaceAll(storageProviderIdPrefixRegex, "") + ); + + return { + to: factoryAddress, + data: encodeFunctionData({ + abi: beneficiaryFactoryAbi, + functionName: "create", + args: [admin as Address, withdrawer as Address, providerIdInteger], + }), + value: "0", + }; + }, [admin, factoryAddress, isFormValid, provider, withdrawer]); + + const clear = useCallback(() => { + resetForm(); + setTransactionHash(undefined); + setCreatedContractAddress(undefined); + }, [resetForm]); + + useWatchContractEvent({ + abi: beneficiaryFactoryAbi, + address: factoryAddress, + eventName: "ProxyCreated", + onLogs(logs) { + console.log(logs, transactionHash); + const createdContactLog = logs.find((log) => { + return log.transactionHash === transactionHash && !!log.args.proxy; + }); + + if (createdContactLog) { + setCreatedContractAddress(createdContactLog.args.proxy); + } + }, + }); + + return ( + +
+ + Create Beneficiary contract + Factory address: {factoryAddress} + + + {shouldShowForm && ( + <> + +
+ ( + + Admin address + + + + + + )} + /> + + ( + + Withdrawer address + + + + + + )} + /> + + ( + + Provider ID + + + + + + )} + /> +
+
+ + + + Create + + + + )} + + {shouldShowLoader && ( + + +

+ You transaction is pending. Do not close this tab or refresh the + page. Once the transaction passes you will see the newly created + Beneficiary contract address here. +

+
+ )} + + {!!createdContractAddress && ( + +
+ +
+

+ A new Beneficiary contract was successfuly deployed at{" "} +

+
+              {createdContractAddress}
+            
+ +
+ )} +
+
+ ); +} diff --git a/app/(app)/sla/components/register-sla-widget.tsx b/app/(app)/sla/components/register-sla-widget.tsx new file mode 100644 index 0000000..dbe6a67 --- /dev/null +++ b/app/(app)/sla/components/register-sla-widget.tsx @@ -0,0 +1,336 @@ +"use client"; + +import SLARegistryAbi from "@/abi/SLARegistry"; +import LoaderButton from "@/components/LoaderButton"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, + Input, +} from "@fidlabs/common-react-ui"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useCallback, useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { type Address, isAddress } from "viem"; +import { useWaitForTransactionReceipt, useWriteContract } from "wagmi"; +import { z } from "zod"; + +export interface RegisterSLAWidgetProps { + slaRegistryAddress: Address; +} + +type StorageProviderId = `${"f0" | "t0" | ""}${number}` | ""; + +const storageProviderIdPrefixRegex = /(?:f0|t0)/g; +const storageProviderIdRegex = /^(?:f0|t0){0,1}[0-9]+$/; +const positiveIntegerMessage = "Must be an integer grater than or equal zero"; +const percentageMessage = "Must be an integer in a 0-100 range"; + +const formSchema = z.object({ + client: z.string().refine((value) => { + return isAddress(value); + }, "Client address must be a valid ETH address"), + provider: z.string().refine((value): value is StorageProviderId => { + return storageProviderIdRegex.test(value); + }, "Invalid Storage Provider ID"), + latency: z + .number({ coerce: true, message: positiveIntegerMessage }) + .min(0, positiveIntegerMessage), + retention: z + .number({ coerce: true, message: positiveIntegerMessage }) + .min(0, positiveIntegerMessage), + bandwidth: z + .number({ coerce: true, message: positiveIntegerMessage }) + .min(0, positiveIntegerMessage), + stability: z + .number({ coerce: true, message: positiveIntegerMessage }) + .min(0, positiveIntegerMessage), + availability: z + .number({ coerce: true, message: percentageMessage }) + .min(0, percentageMessage) + .max(100, percentageMessage), + indexing: z + .number({ coerce: true, message: percentageMessage }) + .min(0, percentageMessage) + .max(100, percentageMessage), +}); + +export function RegisterSLAWidget({ + slaRegistryAddress, +}: RegisterSLAWidgetProps) { + const { data: transactionHash, writeContractAsync } = useWriteContract(); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + retention: 0, + stability: 0, + }, + }); + + const { + reset: resetForm, + formState: { isSubmitting: isFormSubmitting }, + } = form; + + const handleFormSubmit = useCallback( + async (values: z.infer) => { + const { client, provider, ...restOfValues } = values; + const providerIdInteger = BigInt( + provider.replaceAll(storageProviderIdPrefixRegex, "") + ); + + await writeContractAsync({ + abi: SLARegistryAbi, + address: slaRegistryAddress, + functionName: "registerSLA", + args: [ + client as Address, + providerIdInteger, + { + ...restOfValues, + registered: true, + }, + ], + }); + }, + [slaRegistryAddress, writeContractAsync] + ); + + const { fetchStatus, status } = useWaitForTransactionReceipt({ + hash: transactionHash, + }); + + useEffect(() => { + if (status === "success") { + toast.success("SLA registered"); + resetForm(); + } + + if (status === "error") { + toast.error("Error occurred while registering SLA"); + } + }, [resetForm, status]); + + return ( + +
+ + + Register SLA + + Set SLA parameters for given Client - Provider pair + + + + +
+ ( + + Client address + + + + + + )} + /> + + ( + + Provider ID + + + + + + )} + /> + +
+

SLA Parameters:

+ +
+ ( + + Latency (ms) + + + + + Max TTB (time to first byte) in milliseconds + + + + )} + /> + + ( + + Bandwidth (Mbps) + + + + + Minimal bandwith required when accessing data from + Provider + + + + )} + /> + + ( + + Availability (%) + + + + + Minimal percentage of on-chain deals that should be + available for download from the Provider. + + + + )} + /> + + ( + + Indexing (%) + + + + + Minimal percentage of on-chain deals that should be + registered in IPNI. + + + + )} + /> + + ( + + Retention + + + + + + )} + /> + + ( + + Stability + + + + + + )} + /> +
+
+
+
+ + + + Register SLA + + +
+ +
+ ); +} diff --git a/app/(app)/sla/page.tsx b/app/(app)/sla/page.tsx new file mode 100644 index 0000000..327946d --- /dev/null +++ b/app/(app)/sla/page.tsx @@ -0,0 +1,32 @@ +import ScreenBreadcrumbs from "@/components/ScreenBreadcrumbs"; +import { BeneficiaryContractDeployWidget } from "./components/beneficiary-contract-deploy-widget"; +import { RegisterSLAWidget } from "./components/register-sla-widget"; +import { type Address } from "viem"; + +const factoryAddress: Address = "0x4d239cD2c62475BEa41e09BACBe59a9380C28220"; +const slaRegistryAddress: Address = + "0xadE4feEB2f4b6D829765B430c1a8674E1607762E"; + +export default function SLAPage() { + return ( +
+ + +
+ + +
+
+ ); +} diff --git a/config/chains.ts b/config/chains.ts new file mode 100644 index 0000000..9527402 --- /dev/null +++ b/config/chains.ts @@ -0,0 +1,16 @@ +import { defineChain } from "viem"; + +export const filecoinDevnet = defineChain({ + id: 31415926, + name: "Filecoin Devnet", + nativeCurrency: { + decimals: 18, + name: "devnet filecoin", + symbol: "dFIL", + }, + rpcUrls: { + default: { + http: ["http://fidlabs.servehttp.com:1234/rpc/v1"], + }, + }, +}); diff --git a/config/contracts.ts b/config/contracts.ts index 7184a55..56693a9 100644 --- a/config/contracts.ts +++ b/config/contracts.ts @@ -1,18 +1,29 @@ import { TESTNET_ENABLED } from "@/lib/constants"; -import { type Address, isAddress } from "viem"; +import { type Address, isAddress, zeroAddress } from "viem"; import { filecoin, filecoinCalibration } from "viem/chains"; +import { filecoinDevnet } from "./chains"; interface ContractsConfig { factoryAddress: Address; beaconProxyFactoryAddress: Address; } -type ChainId = typeof filecoin.id | typeof filecoinCalibration.id; +type ChainId = + | typeof filecoin.id + | typeof filecoinCalibration.id + | typeof filecoinDevnet.id; type ContractsConfigMap = Map; export const contractsConfigMap: ContractsConfigMap = new Map([ [filecoin.id, loadContractsConfig()], [filecoinCalibration.id, TESTNET_ENABLED ? loadContractsConfig(true) : null], + [ + filecoinDevnet.id, + { + factoryAddress: zeroAddress, + beaconProxyFactoryAddress: zeroAddress, + }, + ], ]); export default contractsConfigMap; diff --git a/config/wagmi.ts b/config/wagmi.ts index 080ba9a..66a23a1 100644 --- a/config/wagmi.ts +++ b/config/wagmi.ts @@ -8,6 +8,7 @@ import { extractChain, type HttpTransportConfig } from "viem"; import { createConfig, http } from "wagmi"; import { Chain, filecoin, filecoinCalibration } from "wagmi/chains"; import contractsConfigMap from "./contracts"; +import { filecoinDevnet } from "./chains"; interface AppInfo { appName: string; @@ -37,7 +38,7 @@ const chains = Array.from(contractsConfigMap.keys()) .filter((chainId) => contractsConfigMap.get(chainId) != null) .map((chainId) => { return extractChain({ - chains: [filecoin, filecoinCalibration], + chains: [filecoin, filecoinCalibration, filecoinDevnet], id: chainId, }); }); @@ -53,6 +54,7 @@ export const wagmiConfig = isNotEmptyChainsList(chains) transports: { [filecoin.id]: http(undefined, commonTransportConfig), [filecoinCalibration.id]: http(undefined, commonTransportConfig), + [filecoinDevnet.id]: http(undefined, commonTransportConfig), }, ssr: true, })