Skip to content

Commit

Permalink
feat(deployment): implement ato top up setting
Browse files Browse the repository at this point in the history
closes #412
  • Loading branch information
ygrishajev committed Nov 26, 2024
1 parent 8ad72fa commit 1301314
Show file tree
Hide file tree
Showing 19 changed files with 610 additions and 26 deletions.
6 changes: 3 additions & 3 deletions apps/api/test/seeders/deployment-grant.seeder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ export class DeploymentGrantSeeder {
spend_limit: {
denom: DenomSeeder.create(),
amount: faker.number.int({ min: 0, max: 10000000 }).toString()
},
expiration: faker.date.future().toISOString()
}
}
},
expiration: faker.date.future().toISOString()
},
input
);
Expand Down
2 changes: 1 addition & 1 deletion apps/deploy-web/next-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.
15 changes: 13 additions & 2 deletions apps/deploy-web/src/components/authorizations/Authorizations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Bank } from "iconoir-react";
import { NextSeo } from "next-seo";

import { Fieldset } from "@src/components/shared/Fieldset";
import { browserEnvConfig } from "@src/config/browser-env.config";
import { useWallet } from "@src/context/WalletProvider";
import { useAllowance } from "@src/hooks/useAllowance";
import { useAllowancesIssued, useGranteeGrants, useGranterGrants } from "@src/queries/useGrantsQuery";
Expand All @@ -26,6 +27,14 @@ type RefreshingType = "granterGrants" | "granteeGrants" | "allowancesIssued" | "
const defaultRefetchInterval = 30 * 1000;
const refreshingInterval = 1000;

const MASTER_WALLETS = new Set([
browserEnvConfig.NEXT_PUBLIC_USDC_TOP_UP_MASTER_WALLET_ADDRESS,
browserEnvConfig.NEXT_PUBLIC_UAKT_TOP_UP_MASTER_WALLET_ADDRESS
]);

const selectNonMaster = (records: Pick<GrantType, "grantee">[] | Pick<AllowanceType, "grantee">[]) =>
records.filter(({ grantee }) => !MASTER_WALLETS.has(grantee));

export const Authorizations: React.FunctionComponent = () => {
const { address, signAndBroadcastTx, isManaged } = useWallet();
const {
Expand All @@ -41,13 +50,15 @@ export const Authorizations: React.FunctionComponent = () => {
const [selectedGrants, setSelectedGrants] = useState<GrantType[]>([]);
const [selectedAllowances, setSelectedAllowances] = useState<AllowanceType[]>([]);
const { data: granterGrants, isLoading: isLoadingGranterGrants } = useGranterGrants(address, {
refetchInterval: isRefreshing === "granterGrants" ? refreshingInterval : defaultRefetchInterval
refetchInterval: isRefreshing === "granterGrants" ? refreshingInterval : defaultRefetchInterval,
select: selectNonMaster
});
const { data: granteeGrants, isLoading: isLoadingGranteeGrants } = useGranteeGrants(address, {
refetchInterval: isRefreshing === "granteeGrants" ? refreshingInterval : defaultRefetchInterval
});
const { data: allowancesIssued, isLoading: isLoadingAllowancesIssued } = useAllowancesIssued(address, {
refetchInterval: isRefreshing === "allowancesIssued" ? refreshingInterval : defaultRefetchInterval
refetchInterval: isRefreshing === "allowancesIssued" ? refreshingInterval : defaultRefetchInterval,
select: selectNonMaster
});

useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
import React, { FC, useCallback, useEffect, useMemo } from "react";
import { Controller, SubmitHandler, useForm } from "react-hook-form";
import { Button, Form, FormField, FormInput } from "@akashnetwork/ui/components";
import { zodResolver } from "@hookform/resolvers/zod";
import addYears from "date-fns/addYears";
import format from "date-fns/format";
import { z } from "zod";

import { aktToUakt, uaktToAKT } from "@src/utils/priceUtils";

const positiveNumberSchema = z.coerce.number().min(0, {
message: "Amount must be greater or equal to 0."
});

const formSchema = z
.object({
uaktFeeLimit: positiveNumberSchema,
usdcFeeLimit: positiveNumberSchema,
uaktDeploymentLimit: positiveNumberSchema,
usdcDeploymentLimit: positiveNumberSchema,
expiration: z.string().min(1, "Expiration is required.")
})
.refine(
data => {
if (data.usdcDeploymentLimit > 0) {
return data.usdcFeeLimit > 0;
}
return true;
},
{
message: "Must be greater than 0 if `USDC Deployments Limit` is greater than 0",
path: ["usdcFeeLimit"]
}
)
.refine(
data => {
if (data.usdcFeeLimit > 0) {
return data.usdcDeploymentLimit > 0;
}
return true;
},
{
message: "Must be greater than 0 if `USDC Fees Limit` is greater than 0",
path: ["usdcDeploymentLimit"]
}
)
.refine(
data => {
if (data.uaktDeploymentLimit > 0) {
return data.uaktFeeLimit > 0;
}
return true;
},
{
message: "Must be greater than 0 if `AKT Deployments Limit` is greater than 0",
path: ["uaktFeeLimit"]
}
)
.refine(
data => {
if (data.uaktFeeLimit > 0) {
return data.uaktDeploymentLimit > 0;
}
return true;
},
{
message: "Must be greater than 0 if `AKT Fees Limit` is greater than 0",
path: ["uaktDeploymentLimit"]
}
);

type FormValues = z.infer<typeof formSchema>;

type LimitFields = keyof Omit<FormValues, "expiration">;

type AutoTopUpSubmitHandler = (action: "revoke-all" | "update", next: FormValues) => Promise<void>;

export interface AutoTopUpSettingProps extends Partial<Record<LimitFields, number>> {
onSubmit: AutoTopUpSubmitHandler;
expiration?: Date;
}

const fields: LimitFields[] = ["uaktFeeLimit", "usdcFeeLimit", "uaktDeploymentLimit", "usdcDeploymentLimit"];

export const AutoTopUpSetting: FC<AutoTopUpSettingProps> = ({ onSubmit, expiration, ...props }) => {
const hasAny = useMemo(() => fields.some(field => props[field]), [props]);

const defaultLimitValues = useMemo(() => {
return fields.reduce(
(acc, field) => {
acc[field] = uaktToAKT(props[field] || 0);
return acc;
},
{} as Record<LimitFields, number>
);
}, [props]);

const form = useForm<z.infer<typeof formSchema>>({
defaultValues: {
...defaultLimitValues,
expiration: format(expiration || addYears(new Date(), 1), "yyyy-MM-dd'T'HH:mm")
},
resolver: zodResolver(formSchema)
});
const { handleSubmit, control, setValue, reset } = form;

useEffect(() => {
setValue("uaktFeeLimit", uaktToAKT(props.uaktFeeLimit || 0));
}, [props.uaktFeeLimit]);

useEffect(() => {
setValue("usdcFeeLimit", uaktToAKT(props.usdcFeeLimit || 0));
}, [props.usdcFeeLimit]);

useEffect(() => {
setValue("uaktDeploymentLimit", uaktToAKT(props.uaktDeploymentLimit || 0));
}, [props.uaktDeploymentLimit]);

useEffect(() => {
setValue("usdcDeploymentLimit", uaktToAKT(props.usdcDeploymentLimit || 0));
}, [props.usdcDeploymentLimit]);

useEffect(() => {
if (expiration) {
setValue("expiration", format(expiration || addYears(new Date(), 1), "yyyy-MM-dd'T'HH:mm"));
}
}, [expiration]);

const execSubmitterRoleAction: SubmitHandler<FormValues> = useCallback(
async (next: FormValues, event: React.BaseSyntheticEvent<SubmitEvent>) => {
const role = event.nativeEvent.submitter?.getAttribute("data-role");
await onSubmit(role as "revoke-all" | "update", convertToUakt(next));
reset(next);
},
[onSubmit, reset]
);

return (
<div>
<Form {...form}>
<form onSubmit={handleSubmit(execSubmitterRoleAction)} noValidate>
<h5 className="space-y-1.5">Deployments billed in AKT</h5>
<div className="flex">
<div className="flex-1">
<FormField
control={control}
name="uaktDeploymentLimit"
render={({ field, fieldState }) => {
return <FormInput {...field} dirty={fieldState.isDirty} type="number" label="Deployments Limit" min={0} step={0.000001} />;
}}
/>
</div>

<div className="ml-3 flex-1">
<FormField
control={control}
name="uaktFeeLimit"
render={({ field, fieldState }) => {
return <FormInput {...field} dirty={fieldState.isDirty} type="number" label="Fees Limit, AKT" min={0} step={0.000001} />;
}}
/>
</div>
</div>

<h5 className="space-y-1.5 pt-4">Deployments billed in USDC</h5>
<div className="flex">
<div className="flex-1">
<FormField
control={control}
name="usdcDeploymentLimit"
render={({ field, fieldState }) => {
return <FormInput {...field} dirty={fieldState.isDirty} type="number" label="Deployments Limit" min={0} step={0.000001} />;
}}
/>
</div>

<div className="ml-3 flex-1">
<FormField
control={control}
name="usdcFeeLimit"
render={({ field, fieldState }) => {
return <FormInput {...field} dirty={fieldState.isDirty} type="number" label="Fees Limit, AKT" min={0} step={0.000001} />;
}}
/>
</div>
</div>

<div className="my-4 w-full">
<Controller
control={control}
name="expiration"
render={({ field, fieldState }) => {
return <FormInput {...field} dirty={fieldState.isDirty} type="datetime-local" label="Expiration" />;
}}
/>
</div>

<Button variant="default" size="sm" className="mr-2" data-role="update" disabled={!form.formState.isDirty}>
{hasAny ? "Update" : "Enable"}
</Button>

{hasAny && (
<Button variant="default" size="sm" data-role="revoke-all">
Disable
</Button>
)}
</form>
</Form>
</div>
);
};

function convertToUakt({ ...values }: FormValues) {
return fields.reduce((acc, field) => {
acc[field] = aktToUakt(values[field]);
return acc;
}, values);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React, { FC, useCallback, useEffect } from "react";

import { AutoTopUpSetting, AutoTopUpSettingProps } from "@src/components/settings/AutoTopUpSetting/AutoTopUpSetting";
import { useWallet } from "@src/context/WalletProvider";
import { useAutoTopUpLimits } from "@src/hooks/useAutoTopUpLimits";
import { useAutoTopUpService } from "@src/hooks/useAutoTopUpService";

export const AutoTopUpSettingContainer: FC = () => {
const { address, signAndBroadcastTx } = useWallet();
const { fetch, uaktFeeLimit, usdcFeeLimit, uaktDeploymentLimit, usdcDeploymentLimit, expiration } = useAutoTopUpLimits();
const autoTopUpMessageService = useAutoTopUpService();

useEffect(() => {
fetch();
}, []);

const updateAllowancesAndGrants: AutoTopUpSettingProps["onSubmit"] = useCallback(
async (action, next) => {
const prev = {
uaktFeeLimit,
usdcFeeLimit,
uaktDeploymentLimit,
usdcDeploymentLimit,
expiration
};

const messages = autoTopUpMessageService.collectMessages({
granter: address,
prev,
next: action === "revoke-all" ? undefined : { ...next, expiration: new Date(next.expiration) }
});

if (messages.length) {
await signAndBroadcastTx(messages);
}

await fetch();
},
[address, autoTopUpMessageService, expiration, fetch, signAndBroadcastTx, uaktDeploymentLimit, uaktFeeLimit, usdcDeploymentLimit, usdcFeeLimit]
);

return (
<AutoTopUpSetting
onSubmit={updateAllowancesAndGrants}
uaktFeeLimit={uaktFeeLimit}
usdcFeeLimit={usdcFeeLimit}
uaktDeploymentLimit={uaktDeploymentLimit}
usdcDeploymentLimit={usdcDeploymentLimit}
expiration={expiration}
/>
);
};
6 changes: 6 additions & 0 deletions apps/deploy-web/src/components/settings/SettingsContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { Edit } from "iconoir-react";
import { useRouter } from "next/navigation";
import { NextSeo } from "next-seo";

import { AutoTopUpSetting } from "@src/components/settings/AutoTopUpSetting/AutoTopUpSetting";
import { AutoTopUpSettingContainer } from "@src/components/settings/AutoTopUpSetting/AutoTopUpSettingContainer";
import { LocalDataManager } from "@src/components/settings/LocalDataManager";
import { Fieldset } from "@src/components/shared/Fieldset";
import { LabelValue } from "@src/components/shared/LabelValue";
Expand Down Expand Up @@ -58,6 +60,10 @@ export const SettingsContainer: React.FunctionComponent = () => {
<ColorModeSelect />
<LocalDataManager />
</Fieldset>

<Fieldset label="Auto Top Up">
<AutoTopUpSettingContainer />
</Fieldset>
</div>

<Fieldset label="Certificates" className="mb-4">
Expand Down
8 changes: 2 additions & 6 deletions apps/deploy-web/src/components/settings/SettingsForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,7 @@ export const SettingsForm: React.FunctionComponent = () => {
control={control}
name="apiEndpoint"
defaultValue={settings.apiEndpoint}
render={({ field }) => {
return <FormInput {...field} type="text" className="flex-1" />;
}}
render={({ field }) => <FormInput {...field} type="text" className="flex-1" />}
/>
) : (
<p className="flex-grow">{settings.apiEndpoint}</p>
Expand All @@ -111,9 +109,7 @@ export const SettingsForm: React.FunctionComponent = () => {
control={control}
name="rpcEndpoint"
defaultValue={settings.rpcEndpoint}
render={({ field }) => {
return <FormInput {...field} type="text" className="flex-1" />;
}}
render={({ field }) => <FormInput {...field} type="text" className="flex-1" />}
/>
) : (
<p className="flex-grow">{settings.rpcEndpoint}</p>
Expand Down
9 changes: 9 additions & 0 deletions apps/deploy-web/src/hooks/useAllowanceService.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { useMemo } from "react";
import { AllowanceHttpService } from "@akashnetwork/http-sdk";

import { useSettings } from "@src/context/SettingsProvider";

export const useAllowanceService = () => {
const { settings } = useSettings();
return useMemo(() => new AllowanceHttpService({ baseURL: settings.apiEndpoint }), [settings.apiEndpoint]);
};
Loading

0 comments on commit 1301314

Please sign in to comment.