Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
542 changes: 542 additions & 0 deletions abi/BeneficiaryFactory.ts

Large diffs are not rendered by default.

597 changes: 597 additions & 0 deletions abi/SLARegistry.ts

Large diffs are not rendered by default.

90 changes: 58 additions & 32 deletions app/(app)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"use client";

import ScreenBreadcrumbs from "@/components/ScreenBreadcrumbs";
import { Button } from "@/components/ui/button";
import {
Expand All @@ -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 (
<div className="container mx-auto flex flex-col gap-6 pb-8">
<ScreenBreadcrumbs
Expand All @@ -21,39 +27,43 @@ export default function HomePage() {
/>

<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<Card>
<CardHeader>
<CardTitle>Allocators</CardTitle>
</CardHeader>
<CardContent>
<p>
List and create new allocator contracts. Manage individual
allocators allowance. Manage contracts ownership and more.
</p>
</CardContent>
<CardFooter className="justify-center">
<Button asChild>
<Link href="/allocator">Manage Allocators</Link>
</Button>
</CardFooter>
</Card>
{chainId !== filecoinDevnet.id && (
<Card>
<CardHeader>
<CardTitle>Allocators</CardTitle>
</CardHeader>
<CardContent>
<p>
List and create new allocator contracts. Manage individual
allocators allowance. Manage contracts ownership and more.
</p>
</CardContent>
<CardFooter className="justify-center">
<Button asChild>
<Link href="/allocator">Manage Allocators</Link>
</Button>
</CardFooter>
</Card>
)}

<Card>
<CardHeader>
<CardTitle>Clients</CardTitle>
</CardHeader>
<CardContent>
<p>
Create new Client contracts. Manage individual clients allowance.
Manage contracts ownership and more.
</p>
</CardContent>
<CardFooter className="justify-center">
<Button asChild>
<Link href="/beacon-proxy-factory">Manage Clients</Link>
</Button>
</CardFooter>
</Card>
{chainId !== filecoinDevnet.id && (
<Card>
<CardHeader>
<CardTitle>Clients</CardTitle>
</CardHeader>
<CardContent>
<p>
Create new Client contracts. Manage individual clients
allowance. Manage contracts ownership and more.
</p>
</CardContent>
<CardFooter className="justify-center">
<Button asChild>
<Link href="/beacon-proxy-factory">Manage Clients</Link>
</Button>
</CardFooter>
</Card>
)}

<Card>
<CardHeader>
Expand All @@ -68,6 +78,22 @@ export default function HomePage() {
</Button>
</CardFooter>
</Card>

{chainId === filecoinDevnet.id && (
<Card>
<CardHeader>
<CardTitle>SLA</CardTitle>
</CardHeader>
<CardContent>
<p>Deploy new Beneficiary contracts and manage SLA registry.</p>
</CardContent>
<CardFooter className="justify-center">
<Button asChild>
<Link href="/sla">Manage SLA</Link>
</Button>
</CardFooter>
</Card>
)}
</div>
</div>
);
Expand Down
223 changes: 223 additions & 0 deletions app/(app)/sla/components/beneficiary-contract-deploy-widget.tsx
Original file line number Diff line number Diff line change
@@ -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<string>((value) => {
return isAddress(value);
}, "Invalid Admin address"),
withdrawer: z.string().refine<string>((value) => {
return isAddress(value);
}, "Invalid Withdrawer address"),
provider: z.string().refine<string>((value): value is StorageProviderId => {
return storageProviderIdRegex.test(value);
}, "Invalid Storage Provider ID"),
});

export function BeneficiaryContractDeployWidget({
factoryAddress,
}: BeneficiaryContractDeployWidgetProps) {
const [transactionHash, setTransactionHash] = useState<string>();
const [createdContractAddress, setCreatedContractAddress] =
useState<Address>();
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<TransactionBase | null>(() => {
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 (
<Card>
<Form {...form}>
<CardHeader>
<CardTitle>Create Beneficiary contract</CardTitle>
<CardDescription>Factory address: {factoryAddress}</CardDescription>
</CardHeader>

{shouldShowForm && (
<>
<CardContent>
<div className="flex flex-col gap-4">
<FormField
control={form.control}
name="admin"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>Admin address</FormLabel>
<FormControl>
<Input
placeholder="0x"
{...field}
value={field.value?.toString() ?? ""}
/>
</FormControl>
<FormMessage className="text-xs" />
</FormItem>
)}
/>

<FormField
control={form.control}
name="withdrawer"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>Withdrawer address</FormLabel>
<FormControl>
<Input
placeholder="0x"
{...field}
value={field.value?.toString() ?? ""}
/>
</FormControl>
<FormMessage className="text-xs" />
</FormItem>
)}
/>

<FormField
control={form.control}
name="provider"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>Provider ID</FormLabel>
<FormControl>
<Input
placeholder="f0"
{...field}
value={field.value?.toString() ?? ""}
/>
</FormControl>
<FormMessage className="text-xs" />
</FormItem>
)}
/>
</div>
</CardContent>

<CardFooter className="justify-end">
<RegularTransactionButton
transaction={transaction}
onTransactionHash={setTransactionHash}
>
Create
</RegularTransactionButton>
</CardFooter>
</>
)}

{shouldShowLoader && (
<CardContent className="flex flex-col items-center gap-4">
<Loader2 size={48} className="animate-spin text-dodger-blue" />
<p className="text-center max-w-[640px] text-sm">
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.
</p>
</CardContent>
)}

{!!createdContractAddress && (
<CardContent className="flex flex-col items-center gap-4">
<div className="w-[48px] aspect-square bg-green-300 flex flex-col items-center justify-center rounded-full">
<Check size={28} />
</div>
<p className="text-center">
A new Beneficiary contract was successfuly deployed at{" "}
</p>
<pre className="font-mono text-center">
{createdContractAddress}
</pre>
<Button onClick={clear}>Create another</Button>
</CardContent>
)}
</Form>
</Card>
);
}
Loading