diff --git a/apps/deploy-web/src/components/authorizations/AllowanceGrantedRow.tsx b/apps/deploy-web/src/components/authorizations/AllowanceGrantedRow.tsx deleted file mode 100644 index 33b8b78168..0000000000 --- a/apps/deploy-web/src/components/authorizations/AllowanceGrantedRow.tsx +++ /dev/null @@ -1,32 +0,0 @@ -"use client"; -import type { ReactNode } from "react"; -import React from "react"; -import { FormattedTime } from "react-intl"; -import { Address, Checkbox, TableCell, TableRow } from "@akashnetwork/ui/components"; - -import type { AllowanceType } from "@src/types/grant"; -import { getAllowanceTitleByType } from "@src/utils/grants"; -import { coinToUDenom } from "@src/utils/priceUtils"; -import { DenomAmount } from "../shared/DenomAmount/DenomAmount"; - -type Props = { - allowance: AllowanceType; - children?: ReactNode; - onSelect?: () => void; - selected?: boolean; -}; - -export const AllowanceGrantedRow: React.FunctionComponent = ({ allowance, selected, onSelect }) => { - const limit = allowance?.allowance?.spend_limit?.[0]; - return ( - - - checked && onSelect() : undefined} /> - - {getAllowanceTitleByType(allowance)} - {allowance.granter &&
} - {limit ? : Unlimited} - {} - - ); -}; diff --git a/apps/deploy-web/src/components/authorizations/AllowanceIssuedRow/AllowanceIssuedRow.spec.tsx b/apps/deploy-web/src/components/authorizations/AllowanceIssuedRow/AllowanceIssuedRow.spec.tsx deleted file mode 100644 index 27c5336111..0000000000 --- a/apps/deploy-web/src/components/authorizations/AllowanceIssuedRow/AllowanceIssuedRow.spec.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; - -import type { AllowanceType } from "@src/types/grant"; -import { AllowanceIssuedRow, DEPENDENCIES } from "./AllowanceIssuedRow"; - -import { render, screen } from "@testing-library/react"; -import { ComponentMock, MockComponents } from "@tests/unit/mocks"; - -describe(AllowanceIssuedRow.name, () => { - it("renders grantee address", () => { - const AddressMock = vi.fn(ComponentMock); - setup({ dependencies: { Address: AddressMock } }); - - expect(AddressMock).toHaveBeenCalledWith(expect.objectContaining({ address: "akash1grantee", isCopyable: true }), expect.anything()); - }); - - it("renders spend limit when present", () => { - const DenomAmountMock = vi.fn(ComponentMock); - setup({ dependencies: { DenomAmount: DenomAmountMock } }); - - expect(DenomAmountMock).toHaveBeenCalledWith(expect.objectContaining({ denom: "uakt" }), expect.anything()); - }); - - it("renders 'Unlimited' when spend_limit is empty", () => { - setup({ - allowance: createAllowance({ - allowance: { - "@type": "/cosmos.feegrant.v1beta1.BasicAllowance", - expiration: "2025-12-31T00:00:00Z", - spend_limit: [] - } - }) - }); - - expect(screen.getByText("Unlimited")).toBeInTheDocument(); - }); - - it("renders expiration date", () => { - const FormattedTimeMock = vi.fn(ComponentMock); - const expiration = "2025-12-31T00:00:00Z"; - setup({ - allowance: createAllowance({ - allowance: { - "@type": "/cosmos.feegrant.v1beta1.BasicAllowance", - expiration, - spend_limit: [{ denom: "uakt", amount: "1000000" }] - } - }), - dependencies: { FormattedTime: FormattedTimeMock } - }); - - expect(FormattedTimeMock).toHaveBeenCalledWith(expect.objectContaining({ value: expiration }), expect.anything()); - }); - - it("calls onEditAllowance when edit button is clicked", () => { - const ButtonMock = vi.fn(ComponentMock); - const allowance = createAllowance(); - const { onEditAllowance } = setup({ allowance, dependencies: { Button: ButtonMock } }); - - const editCall = ButtonMock.mock.calls.find(c => c[0]["aria-label"] === "Edit Authorization"); - editCall![0].onClick(); - - expect(onEditAllowance).toHaveBeenCalledWith(allowance); - }); - - it("calls setDeletingAllowance when revoke button is clicked", () => { - const ButtonMock = vi.fn(ComponentMock); - const allowance = createAllowance(); - const { setDeletingAllowance } = setup({ allowance, dependencies: { Button: ButtonMock } }); - - const revokeCall = ButtonMock.mock.calls.find(c => c[0]["aria-label"] === "Revoke Authorization"); - revokeCall![0].onClick(); - - expect(setDeletingAllowance).toHaveBeenCalledWith(allowance); - }); - - it("calls onSelectAllowance when checkbox is toggled", () => { - const CheckboxMock = vi.fn(ComponentMock); - const allowance = createAllowance(); - const { onSelectAllowance } = setup({ allowance, dependencies: { Checkbox: CheckboxMock } }); - - CheckboxMock.mock.calls[0][0].onCheckedChange(true); - - expect(onSelectAllowance).toHaveBeenCalledWith(true, allowance); - }); - - function setup(input: { allowance?: AllowanceType; checked?: boolean; dependencies?: Partial> } = {}) { - const onEditAllowance = vi.fn(); - const setDeletingAllowance = vi.fn(); - const onSelectAllowance = vi.fn(); - const allowance = input.allowance || createAllowance(); - - render( - - ); - - return { onEditAllowance, setDeletingAllowance, onSelectAllowance }; - } - - function createAllowance(overrides?: Partial): AllowanceType { - return { - granter: "akash1granter", - grantee: "akash1grantee", - allowance: { - "@type": "/cosmos.feegrant.v1beta1.BasicAllowance", - expiration: "2025-12-31T00:00:00Z", - spend_limit: [{ denom: "uakt", amount: "1000000" }] - }, - ...overrides - }; - } -}); diff --git a/apps/deploy-web/src/components/authorizations/AllowanceIssuedRow/AllowanceIssuedRow.tsx b/apps/deploy-web/src/components/authorizations/AllowanceIssuedRow/AllowanceIssuedRow.tsx deleted file mode 100644 index 7f119745bf..0000000000 --- a/apps/deploy-web/src/components/authorizations/AllowanceIssuedRow/AllowanceIssuedRow.tsx +++ /dev/null @@ -1,78 +0,0 @@ -"use client"; -import type { ReactNode } from "react"; -import React from "react"; -import { FormattedTime } from "react-intl"; -import { Address, Button, Checkbox, TableCell, TableRow } from "@akashnetwork/ui/components"; -import { Bin, Edit } from "iconoir-react"; - -import type { AllowanceType } from "@src/types/grant"; -import { getAllowanceTitleByType } from "@src/utils/grants"; -import { coinToUDenom } from "@src/utils/priceUtils"; -import { DenomAmount } from "../../shared/DenomAmount/DenomAmount"; - -export const DEPENDENCIES = { - FormattedTime, - Address, - Button, - Checkbox, - TableCell, - TableRow, - DenomAmount, - Bin, - Edit -}; - -type Props = { - allowance: AllowanceType; - children?: ReactNode; - checked?: boolean; - onEditAllowance: (allowance: AllowanceType) => void; - setDeletingAllowance: (grantallowance: AllowanceType) => void; - onSelectAllowance: (isChecked: boolean, allowance: AllowanceType) => void; - dependencies?: typeof DEPENDENCIES; -}; - -export const AllowanceIssuedRow: React.FunctionComponent = ({ - allowance, - checked, - onEditAllowance, - setDeletingAllowance, - onSelectAllowance, - dependencies: d = DEPENDENCIES -}) => { - const limit = allowance?.allowance?.spend_limit?.[0]; - - return ( - - {getAllowanceTitleByType(allowance)} - - - - {limit ? : Unlimited} - - - - -
-
- { - event.stopPropagation(); - }} - onCheckedChange={value => { - onSelectAllowance(value as boolean, allowance); - }} - /> -
- onEditAllowance(allowance)} aria-label="Edit Authorization"> - - - setDeletingAllowance(allowance)} aria-label="Revoke Authorization"> - - -
-
-
- ); -}; diff --git a/apps/deploy-web/src/components/authorizations/AllowanceModal.tsx b/apps/deploy-web/src/components/authorizations/AllowanceModal.tsx deleted file mode 100644 index e87c650c55..0000000000 --- a/apps/deploy-web/src/components/authorizations/AllowanceModal.tsx +++ /dev/null @@ -1,188 +0,0 @@ -"use client"; -import { useRef, useState } from "react"; -import { Controller, useForm } from "react-hook-form"; -import { FormattedDate } from "react-intl"; -import { Alert, Form, FormField, FormInput, Popup } from "@akashnetwork/ui/components"; -import type { EncodeObject } from "@cosmjs/proto-signing"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { addYears, format } from "date-fns"; -import { z } from "zod"; - -import { LinkTo } from "@src/components/shared/LinkTo"; -import { UAKT_DENOM } from "@src/config/denom.config"; -import { useServices } from "@src/context/ServicesProvider"; -import { useWallet } from "@src/context/WalletProvider"; -import { useDenomData } from "@src/hooks/useWalletBalance"; -import type { AllowanceType } from "@src/types/grant"; -import { aktToUakt, coinToDenom } from "@src/utils/priceUtils"; -import { TransactionMessageData } from "@src/utils/TransactionMessageData"; - -type Props = { - address: string; - editingAllowance?: AllowanceType | null; - onClose: () => void; -}; - -const formSchema = z.object({ - amount: z.coerce.number().min(0, { - message: "Amount must be greater than 0." - }), - expiration: z.string().min(1, "Expiration is required."), - granteeAddress: z.string().min(1, "Grantee address is required.") -}); - -export const AllowanceModal: React.FunctionComponent = ({ editingAllowance, address, onClose }) => { - const { analyticsService } = useServices(); - const formRef = useRef(null); - const [error, setError] = useState(""); - const { signAndBroadcastTx } = useWallet(); - const form = useForm>({ - defaultValues: { - amount: editingAllowance ? coinToDenom(editingAllowance.allowance.spend_limit[0]) : 0, - expiration: format(addYears(new Date(), 1), "yyyy-MM-dd'T'HH:mm"), - granteeAddress: editingAllowance?.grantee ?? "" - }, - resolver: zodResolver(formSchema) - }); - const { handleSubmit, control, watch, clearErrors, setValue } = form; - const { amount, granteeAddress, expiration } = watch(); - const denomData = useDenomData(UAKT_DENOM); - - const onDepositClick = (event: React.MouseEvent) => { - event.preventDefault(); - formRef.current?.dispatchEvent(new Event("submit", { cancelable: true, bubbles: true })); - }; - - const onSubmit = async ({ amount, expiration, granteeAddress }: z.infer) => { - setError(""); - clearErrors(); - - const messages: EncodeObject[] = []; - const spendLimit = aktToUakt(amount); - const expirationDate = new Date(expiration); - - if (editingAllowance) { - messages.push(TransactionMessageData.getRevokeAllowanceMsg(address, granteeAddress)); - } - messages.push(TransactionMessageData.getGrantBasicAllowanceMsg(address, granteeAddress, spendLimit, UAKT_DENOM, expirationDate)); - const response = await signAndBroadcastTx(messages); - - if (response) { - analyticsService.track("authorize_spend", { - category: "deployments", - label: "Authorize wallet to spend on deployment deposits" - }); - - onClose(); - } - }; - - function handleDocClick(ev: React.MouseEvent, url: string) { - ev.preventDefault(); - - window.open(url, "_blank"); - } - - const onBalanceClick = () => { - clearErrors(); - setValue("amount", denomData?.max || 0); - }; - - return ( - -
- - -

- handleDocClick(ev, "https://docs.cosmos.network/v0.46/modules/feegrant/")}>Authorized Fee Spend allows users to - authorize spend of a set number of tokens on fees from a source wallet to a destination, funded wallet. -

-
- -
- onBalanceClick()}> - Balance: {denomData?.balance} {denomData?.label} - -
- -
- { - return ( - {denomData?.label}} - /> - ); - }} - /> -
- -
- { - return ; - }} - /> -
- -
- { - return ; - }} - /> -
- - {!!amount && granteeAddress && ( - -

- This address will be able to spend up to {amount} AKT on transaction fees on your behalf ending on{" "} - . -

-
- )} - - {error && {error}} -
- -
- ); -}; diff --git a/apps/deploy-web/src/components/authorizations/Authorizations.tsx b/apps/deploy-web/src/components/authorizations/Authorizations.tsx deleted file mode 100644 index 64a6cbb6aa..0000000000 --- a/apps/deploy-web/src/components/authorizations/Authorizations.tsx +++ /dev/null @@ -1,475 +0,0 @@ -"use client"; -import { useEffect, useState } from "react"; -import React from "react"; -import { Button, Input, Popup, Spinner, Table, TableBody, TableHead, TableHeader, TableRow, useDebounce } from "@akashnetwork/ui/components"; -import { Bank, Refresh, Xmark } from "iconoir-react"; -import { NextSeo } from "next-seo"; - -import { Fieldset } from "@src/components/shared/Fieldset"; -import { browserEnvConfig } from "@src/config/browser-env.config"; -import { useSettings } from "@src/context/SettingsProvider"; -import { useWallet } from "@src/context/WalletProvider"; -import { useAllowance } from "@src/hooks/useAllowance"; -import { useExactDeploymentGrantsQuery } from "@src/queries/useExactDeploymentGrantsQuery"; -import { useAllowancesIssued, useGranteeGrants, useGranterGrants } from "@src/queries/useGrantsQuery"; -import type { AllowanceType, GrantType, PaginatedAllowanceType, PaginatedGrantType } from "@src/types/grant"; -import { isValidBech32Address } from "@src/utils/address"; -import { averageBlockTime } from "@src/utils/priceUtils"; -import { TransactionMessageData } from "@src/utils/TransactionMessageData"; -import Layout from "../layout/Layout"; -import { SettingsLayout, SettingsTabs } from "../settings/SettingsLayout"; -import { ConnectWallet } from "../shared/ConnectWallet"; -import { Title } from "../shared/Title"; -import { DeploymentGrantTable } from "./DeploymentGrantTable/DeploymentGrantTable"; -import { GranteeRow } from "./GranteeRow/GranteeRow"; -import { GrantModal } from "./GrantModal/GrantModal"; -import { AllowanceGrantedRow } from "./AllowanceGrantedRow"; -import { AllowanceModal } from "./AllowanceModal"; -import { FeeGrantTable } from "./FeeGrantTable"; - -type RefreshingType = "granterGrants" | "granteeGrants" | "allowancesIssued" | "allowancesGranted" | null; -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 selectNonMasterGrants = (data: PaginatedGrantType) => ({ - ...data, - grants: data.grants.filter(({ grantee }) => !MASTER_WALLETS.has(grantee)) -}); - -const selectNonMasterAllowances = (data: PaginatedAllowanceType) => ({ - ...data, - allowances: data.allowances.filter(({ grantee }) => !MASTER_WALLETS.has(grantee)) -}); - -export const Authorizations: React.FunctionComponent = () => { - const { settings } = useSettings(); - const { address, signAndBroadcastTx, isManaged } = useWallet(); - const { - fee: { all: allowancesGranted, isLoading: isLoadingAllowancesGranted, setDefault, default: defaultAllowance } - } = useAllowance(address, isManaged); - const [editingGrant, setEditingGrant] = useState(null); - const [editingAllowance, setEditingAllowance] = useState(null); - const [showGrantModal, setShowGrantModal] = useState(false); - const [showAllowanceModal, setShowAllowanceModal] = useState(false); - const [deletingGrants, setDeletingGrants] = useState(null); - const [deletingAllowances, setDeletingAllowances] = useState(null); - const [isRefreshing, setIsRefreshing] = useState(null); - const [selectedGrants, setSelectedGrants] = useState([]); - const [selectedAllowances, setSelectedAllowances] = useState([]); - const [searchGrantee, setSearchGrantee] = useState(""); - const [searchError, setSearchError] = useState(null); - const [pageIndex, setPageIndex] = useState({ deployment: 0, fee: 0 }); - const [pageSize, setPageSize] = useState({ deployment: 10, fee: 10 }); - const debouncedSearchGrantee = useDebounce(searchGrantee, 500); - const { data: granterGrants, isLoading: isLoadingGranterGrants } = useGranterGrants(address, pageIndex.deployment, pageSize.deployment, { - refetchInterval: isRefreshing === "granterGrants" ? refreshingInterval : defaultRefetchInterval, - select: selectNonMasterGrants, - enabled: !debouncedSearchGrantee - }); - const { data: granteeGrants, isLoading: isLoadingGranteeGrants } = useGranteeGrants(address, { - refetchInterval: isRefreshing === "granteeGrants" ? refreshingInterval : defaultRefetchInterval - }); - const { data: allowancesIssued, isLoading: isLoadingAllowancesIssued } = useAllowancesIssued(address, pageIndex.fee, pageSize.fee, { - refetchInterval: isRefreshing === "allowancesIssued" ? refreshingInterval : defaultRefetchInterval, - select: selectNonMasterAllowances - }); - const { - data: specificGranteeGrants, - isLoading: isLoadingGranterGranteeGrants, - refetch: refetchGranterGranteeGrants - } = useExactDeploymentGrantsQuery(address, searchGrantee, { - enabled: false - }); - const filteredGranterGrants = - !!debouncedSearchGrantee && !!specificGranteeGrants ? { grants: [specificGranteeGrants], pagination: { total: 1 } } : granterGrants; - const isLoading = - !!isRefreshing || - isLoadingAllowancesIssued || - isLoadingAllowancesGranted || - isLoadingGranteeGrants || - isLoadingGranterGrants || - isLoadingGranterGranteeGrants; - - useEffect(() => { - let timeout: NodeJS.Timeout; - if (isRefreshing) { - timeout = setTimeout(() => { - setIsRefreshing(null); - }, averageBlockTime * 1000); - } - - return () => { - if (timeout) { - clearTimeout(timeout); - } - }; - }, [isRefreshing]); - - useEffect(() => { - if (debouncedSearchGrantee && !searchError) { - refetchGranterGranteeGrants(); - } - }, [debouncedSearchGrantee, searchError, refetchGranterGranteeGrants]); - - async function onDeleteGrantsConfirmed() { - if (!deletingGrants) return; - - const messages = deletingGrants.map(grant => TransactionMessageData.getRevokeDepositMsg(address, grant.grantee)); - const response = await signAndBroadcastTx(messages); - - if (response) { - setIsRefreshing("granterGrants"); - setDeletingGrants(null); - setSelectedGrants([]); - } - } - - async function onDeleteAllowanceConfirmed() { - if (!deletingAllowances) return; - - const messages = deletingAllowances.map(allowance => TransactionMessageData.getRevokeAllowanceMsg(address, allowance.grantee)); - const response = await signAndBroadcastTx(messages); - - if (response) { - setIsRefreshing("allowancesIssued"); - setDeletingAllowances(null); - setSelectedAllowances([]); - } - } - - function onCreateNewGrant() { - setEditingGrant(null); - setShowGrantModal(true); - } - - function onEditGrant(grant: GrantType) { - setEditingGrant(grant); - setShowGrantModal(true); - } - - function onGrantClose() { - setIsRefreshing("granterGrants"); - setShowGrantModal(false); - } - - function onCreateNewAllowance() { - setEditingAllowance(null); - setShowAllowanceModal(true); - } - - function onAllowanceClose() { - setIsRefreshing("allowancesIssued"); - setShowAllowanceModal(false); - } - - function onEditAllowance(allowance: AllowanceType) { - setEditingAllowance(allowance); - setShowAllowanceModal(true); - } - - function onSearchGranteeChange(e: React.ChangeEvent) { - const value = e.target.value?.trim(); - setSearchGrantee(value); - - if (!value) { - setSearchError(null); - return; - } - - if (!isValidBech32Address(value, "akash")) { - setSearchError("Invalid Akash address"); - return; - } - - setSearchError(null); - } - - function onAllowancePageChange(newPageIndex: number, newPageSize: number) { - setPageIndex(prev => ({ ...prev, fee: newPageIndex })); - setPageSize(prev => ({ ...prev, fee: newPageSize })); - } - - function onDeploymentPageChange(newPageIndex: number, newPageSize: number) { - setPageIndex(prev => ({ ...prev, deployment: newPageIndex })); - setPageSize(prev => ({ ...prev, deployment: newPageSize })); - } - - function onRefreshSearchClick() { - if (!searchError && debouncedSearchGrantee) { - refetchGranterGranteeGrants(); - } - } - - return ( - - - - {settings.isBlockchainDown ? ( - - <> -

The blockchain is unavailable. Unable to create, list, or update authorizations.

- -
- ) : ( - <> - - - - ) - } - > -
- {!address ? ( - <> -
- -
- - ) : ( - <> -

- These authorizations allow you authorize other addresses to spend on deployments or deployment deposits using your funds. You can revoke - these authorizations at any time. -

-
- {(granterGrants?.grants?.length || searchGrantee) && ( -
- { - setSearchGrantee(""); - setSearchError(null); - }} - > - - - } - /> - -
- )} - {isLoadingGranterGrants || !filteredGranterGrants ? ( -
- -
- ) : ( - <> - {filteredGranterGrants?.grants?.length > 0 ? ( - - ) : ( -

- {searchGrantee - ? searchError - ? "Please enter a valid Akash address" - : "No matching authorizations found." - : "No authorizations given."} -

- )} - - )} -
- -
- {isLoadingGranteeGrants || !granteeGrants ? ( -
- -
- ) : ( - <> - {granteeGrants.length > 0 ? ( - - - - Granter - Spending Limit - Expiration - - - - - {granteeGrants.map(grant => ( - - ))} - -
- ) : ( -

No authorizations received.

- )} - - )} -
- - )} -
- -
-
- Tx Fee Authorizations - {address && ( - - )} -
- - {!address ? ( - <> -
- -
- - ) : ( - <> -

- These authorizations allow you authorize other addresses to spend on transaction fees using your funds. You can revoke these authorizations - at any time. -

- -
- {isLoadingAllowancesIssued || !allowancesIssued ? ( -
- -
- ) : ( - <> - {allowancesIssued.allowances.length > 0 ? ( - - ) : ( -

No allowances issued.

- )} - - )} -
- -
- {isLoadingAllowancesGranted || !allowancesGranted ? ( -
- -
- ) : ( - <> - {allowancesGranted.length > 0 ? ( - - - - Default - Type - Grantee - Spending Limit - Expiration - - - - - {!!allowancesGranted && ( - setDefault(undefined)} - selected={!defaultAllowance} - /> - )} - {allowancesGranted.map(allowance => ( - setDefault(allowance.granter)} - selected={defaultAllowance === allowance.granter} - /> - ))} - -
- ) : ( -

No allowances received.

- )} - - )} -
- - )} - - {!!deletingGrants && ( - setDeletingGrants(null)} - onCancel={() => setDeletingGrants(null)} - onValidate={onDeleteGrantsConfirmed} - enableCloseOnBackdropClick - > - Deleting grants will revoke their ability to spend your funds on deployments. - - )} - {!!deletingAllowances && ( - setDeletingAllowances(null)} - onCancel={() => setDeletingAllowances(null)} - onValidate={onDeleteAllowanceConfirmed} - enableCloseOnBackdropClick - > - Deleting allowance to will revoke their ability to fees on your behalf. - - )} - {showGrantModal && } - {showAllowanceModal && } -
-
- - )} -
- ); -}; diff --git a/apps/deploy-web/src/components/authorizations/DeploymentGrantTable/DeploymentGrantTable.spec.tsx b/apps/deploy-web/src/components/authorizations/DeploymentGrantTable/DeploymentGrantTable.spec.tsx deleted file mode 100644 index 95836e5dca..0000000000 --- a/apps/deploy-web/src/components/authorizations/DeploymentGrantTable/DeploymentGrantTable.spec.tsx +++ /dev/null @@ -1,180 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; - -import type { GrantType } from "@src/types/grant"; -import { DEPENDENCIES, DeploymentGrantTable } from "./DeploymentGrantTable"; - -import { render, screen } from "@testing-library/react"; -import { ComponentMock, MockComponents } from "@tests/unit/mocks"; - -describe(DeploymentGrantTable.name, () => { - it("calls onEditGrant when edit button is clicked", () => { - const ButtonMock = vi.fn(ComponentMock); - const grant = createGrant(); - const { onEditGrant } = setup({ - grants: [grant], - dependencies: { Button: ButtonMock } - }); - - const editCall = ButtonMock.mock.calls.find(c => c[0]["aria-label"] === "Edit Authorization"); - editCall![0].onClick(); - - expect(onEditGrant).toHaveBeenCalledWith(grant); - }); - - it("calls setDeletingGrants with the grant when revoke button is clicked", () => { - const ButtonMock = vi.fn(ComponentMock); - const grant = createGrant(); - const { setDeletingGrants } = setup({ - grants: [grant], - dependencies: { Button: ButtonMock } - }); - - const revokeCall = ButtonMock.mock.calls.find(c => c[0]["aria-label"] === "Revoke Authorization"); - revokeCall![0].onClick(); - - expect(setDeletingGrants).toHaveBeenCalledWith([grant]); - }); - - it("adds grant to selection when checkbox is checked", () => { - const CheckboxMock = vi.fn(ComponentMock); - const grant = createGrant(); - const { setSelectedGrants } = setup({ - grants: [grant], - dependencies: { Checkbox: CheckboxMock } - }); - - CheckboxMock.mock.calls[0][0].onCheckedChange(true); - - const updater = setSelectedGrants.mock.calls[0][0]; - expect(updater([])).toEqual([grant]); - }); - - it("removes grant from selection when checkbox is unchecked", () => { - const CheckboxMock = vi.fn(ComponentMock); - const grant = createGrant({ grantee: "akash1abc" }); - const { setSelectedGrants } = setup({ - grants: [grant], - selectedGrants: [grant], - dependencies: { Checkbox: CheckboxMock } - }); - - CheckboxMock.mock.calls[0][0].onCheckedChange(false); - - const updater = setSelectedGrants.mock.calls[0][0]; - expect(updater([grant])).toEqual([]); - }); - - it("shows 'Revoke all' button when grants exist and none selected", () => { - setup({ grants: [createGrant()] }); - - expect(screen.getByText("Revoke all")).toBeInTheDocument(); - }); - - it("calls setDeletingGrants with all grants when 'Revoke all' is clicked", () => { - const ButtonMock = vi.fn(ComponentMock); - const grants = [createGrant(), createGrant({ grantee: "akash1def" })]; - const { setDeletingGrants } = setup({ - grants, - dependencies: { Button: ButtonMock } - }); - - const revokeAllCall = ButtonMock.mock.calls.find(c => c[0].variant === "outline" && c[0].size === "sm"); - revokeAllCall![0].onClick(); - - expect(setDeletingGrants).toHaveBeenCalledWith(grants); - }); - - it("shows 'Revoke selected' count when grants are selected", () => { - const grant = createGrant(); - setup({ grants: [grant], selectedGrants: [grant] }); - - expect(screen.getByText(/Revoke selected/)).toBeInTheDocument(); - }); - - it("calls setDeletingGrants with selected grants when 'Revoke selected' is clicked", () => { - const ButtonMock = vi.fn(ComponentMock); - const grant = createGrant(); - const { setDeletingGrants } = setup({ - grants: [grant], - selectedGrants: [grant], - dependencies: { Button: ButtonMock } - }); - - const revokeSelectedCall = ButtonMock.mock.calls.find(c => c[0].variant === "outline" && c[0].size === "sm"); - revokeSelectedCall![0].onClick(); - - expect(setDeletingGrants).toHaveBeenCalledWith([grant]); - }); - - it("clears selection when 'Clear' link is clicked", () => { - const LinkToMock = vi.fn(ComponentMock); - const grant = createGrant(); - const { setSelectedGrants } = setup({ - grants: [grant], - selectedGrants: [grant], - dependencies: { LinkTo: LinkToMock } - }); - - LinkToMock.mock.calls[0][0].onClick(); - - expect(setSelectedGrants).toHaveBeenCalledWith([]); - }); - - function setup( - input: { - grants?: GrantType[]; - selectedGrants?: GrantType[]; - totalCount?: number; - pageIndex?: number; - pageSize?: number; - dependencies?: Partial>; - } = {} - ) { - const onEditGrant = vi.fn(); - const setDeletingGrants = vi.fn(); - const setSelectedGrants = vi.fn(); - const onPageChange = vi.fn(); - const grants = input.grants || []; - const selectedGrants = input.selectedGrants || []; - - render( - - ); - - return { onEditGrant, setDeletingGrants, setSelectedGrants, onPageChange }; - } - - function createGrant(overrides?: Partial): GrantType { - return { - granter: "akash1granter", - grantee: "akash1grantee", - expiration: "2025-12-31T00:00:00Z", - authorization: { - "@type": "/akash.escrow.v1.DepositAuthorization", - spend_limits: [ - { - denom: "uakt", - amount: "1000000" - } - ] - }, - ...overrides - }; - } -}); diff --git a/apps/deploy-web/src/components/authorizations/DeploymentGrantTable/DeploymentGrantTable.tsx b/apps/deploy-web/src/components/authorizations/DeploymentGrantTable/DeploymentGrantTable.tsx deleted file mode 100644 index 9671dbaca7..0000000000 --- a/apps/deploy-web/src/components/authorizations/DeploymentGrantTable/DeploymentGrantTable.tsx +++ /dev/null @@ -1,226 +0,0 @@ -import type { Dispatch, SetStateAction } from "react"; -import React from "react"; -import { FormattedTime } from "react-intl"; -import { LoggerService } from "@akashnetwork/logging"; -import { - Address, - Button, - Checkbox, - CustomPagination, - MIN_PAGE_SIZE, - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow -} from "@akashnetwork/ui/components"; -import { createColumnHelper, flexRender, getCoreRowModel, getPaginationRowModel, useReactTable } from "@tanstack/react-table"; -import { Bin, Edit } from "iconoir-react"; - -import { DenomAmount } from "@src/components/shared/DenomAmount/DenomAmount"; -import type { GrantType } from "@src/types/grant"; -import { coinToUDenom } from "@src/utils/priceUtils"; -import { LinkTo } from "../../shared/LinkTo"; - -export const DEPENDENCIES = { - FormattedTime, - Address, - Button, - Checkbox, - CustomPagination, - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, - DenomAmount, - LinkTo, - Bin, - Edit -}; - -interface Props { - grants: GrantType[]; - selectedGrants: GrantType[]; - totalCount: number; - onEditGrant: (grant: GrantType) => void; - setDeletingGrants: Dispatch>; - setSelectedGrants: Dispatch>; - onPageChange: (pageIndex: number, pageSize: number) => void; - pageIndex: number; - pageSize: number; - dependencies?: typeof DEPENDENCIES; -} - -const logger = LoggerService.forContext("DeploymentGrantTable"); - -export const DeploymentGrantTable: React.FC = ({ - grants, - totalCount, - onEditGrant, - onPageChange, - setDeletingGrants, - setSelectedGrants, - selectedGrants, - pageIndex, - pageSize, - dependencies: d = DEPENDENCIES -}) => { - const selectGrants = (checked: boolean, grant: GrantType) => { - setSelectedGrants(prev => { - return checked ? prev.concat([grant]) : prev.filter(x => x.grantee !== grant.grantee); - }); - }; - - const columnHelper = createColumnHelper(); - - const columns = [ - columnHelper.accessor("grantee", { - header: () =>
Grantee
, - cell: info => - }), - columnHelper.accessor( - row => { - return row.authorization.spend_limits; - }, - { - id: "spendingLimit", - cell: info => { - const value = info.getValue() ?? []; - - return ( -
- {value.map(limit => ( - - ))} -
- ); - }, - header: () =>
Spending Limit
- } - ), - columnHelper.accessor("expiration", { - header: () =>
Expiration
, - cell: info => ( -
- -
- ) - }), - columnHelper.display({ - id: "actions", - header: () => ( -
- {selectedGrants.length > 0 && ( -
- setSelectedGrants([])} className="text-xs"> - Clear - - setDeletingGrants(selectedGrants)} variant="outline" size="sm" className="h-6 p-2 text-xs"> - Revoke selected ({selectedGrants.length}) - -
- )} - {grants.length > 0 && selectedGrants.length === 0 && ( -
- setDeletingGrants(grants)} variant="outline" size="sm" className="h-6 p-2 text-xs"> - Revoke all - -
- )} -
- ), - cell: info => { - const grant = info.row.original; - - return ( -
-
- x.grantee === grant.grantee && x.granter === grant.granter)} - onClick={event => { - event.stopPropagation(); - }} - onCheckedChange={value => { - if (value !== "indeterminate") { - selectGrants(value, grant); - } else { - logger.warn("Unable to determinate checked state"); - } - }} - /> -
- onEditGrant(grant)} aria-label="Edit Authorization"> - - - setDeletingGrants([grant])} aria-label="Revoke Authorization"> - - -
- ); - } - }) - ]; - - const table = useReactTable({ - data: grants, - columns, - getCoreRowModel: getCoreRowModel(), - getPaginationRowModel: getPaginationRowModel(), - manualPagination: true, - onPaginationChange: updaterOrValue => { - const pagination = typeof updaterOrValue === "function" ? updaterOrValue(table.getState().pagination) : updaterOrValue; - onPageChange(pagination.pageIndex, pagination.pageSize); - }, - state: { - pagination: { - pageIndex, - pageSize - } - } - }); - const pagination = table.getState().pagination; - const pageCount = Math.ceil(totalCount / pagination.pageSize); - - return ( -
- - - {table.getHeaderGroups().map(headerGroup => ( - - {headerGroup.headers.map(header => ( - - {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} - - ))} - - ))} - - - - {table.getRowModel().rows.map(row => ( - - {row.getVisibleCells().map(cell => ( - {flexRender(cell.column.columnDef.cell, cell.getContext())} - ))} - - ))} - - - - {pageCount > MIN_PAGE_SIZE && ( -
- -
- )} -
- ); -}; diff --git a/apps/deploy-web/src/components/authorizations/FeeGrantTable.tsx b/apps/deploy-web/src/components/authorizations/FeeGrantTable.tsx deleted file mode 100644 index 99f2730a65..0000000000 --- a/apps/deploy-web/src/components/authorizations/FeeGrantTable.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import type { Dispatch, SetStateAction } from "react"; -import React from "react"; -import { Button, CustomPagination, Table, TableBody, TableHead, TableHeader, TableRow } from "@akashnetwork/ui/components"; - -import type { AllowanceType } from "@src/types/grant"; -import { LinkTo } from "../shared/LinkTo"; -import { AllowanceIssuedRow } from "./AllowanceIssuedRow/AllowanceIssuedRow"; - -interface Props { - allowances: AllowanceType[]; - selectedAllowances: AllowanceType[]; - pageIndex: number; - pageSize: number; - totalCount: number; - onEditAllowance: (feeAllowance: AllowanceType) => void; - setDeletingAllowances: Dispatch>; - setSelectedAllowances: Dispatch>; - onPageChange: (pageIndex: number, pageSize: number) => void; -} - -export const FeeGrantTable: React.FC = ({ - allowances, - selectedAllowances, - onEditAllowance, - setDeletingAllowances, - setSelectedAllowances, - pageIndex, - pageSize, - totalCount, - onPageChange -}) => { - const pageCount = Math.ceil(totalCount / pageSize); - - const onSelectGrant = (checked: boolean, grant: AllowanceType) => { - setSelectedAllowances(prev => { - return checked ? prev.concat([grant]) : prev.filter(x => x.grantee !== grant.grantee); - }); - }; - - const onGrantDelete = (grant: AllowanceType) => { - setDeletingAllowances([grant]); - }; - - const onDeleteGrants = () => { - setDeletingAllowances(selectedAllowances); - }; - - const onDeleteAll = () => { - setDeletingAllowances(allowances); - }; - - const onClearSelection = () => { - setSelectedAllowances([]); - }; - - const handleChangePage = (newPage: number) => { - onPageChange(newPage, pageSize); - }; - - const onPageSizeChange = (value: number) => { - onPageChange(0, value); - }; - - return ( -
- - - - Type - Grantee - Spending Limit - Expiration - - {selectedAllowances.length > 0 && ( -
- - Clear - - -
- )} - {allowances.length > 0 && selectedAllowances.length === 0 && ( -
- -
- )} -
-
-
- - - {allowances.map(grant => ( - x.grantee === grant.grantee && x.granter === grant.granter)} - /> - ))} - -
- - {pageCount > 1 && ( -
- -
- )} -
- ); -}; diff --git a/apps/deploy-web/src/components/authorizations/GrantModal/GrantModal.spec.tsx b/apps/deploy-web/src/components/authorizations/GrantModal/GrantModal.spec.tsx deleted file mode 100644 index 26db571773..0000000000 --- a/apps/deploy-web/src/components/authorizations/GrantModal/GrantModal.spec.tsx +++ /dev/null @@ -1,366 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; -import { mock } from "vitest-mock-extended"; - -import { UACT_DENOM, UAKT_DENOM } from "@src/config/denom.config"; -import type { AnalyticsService } from "@src/services/analytics/analytics.service"; -import type { GrantType } from "@src/types/grant"; -import { DEPENDENCIES, GrantModal } from "./GrantModal"; - -import { fireEvent, render, screen, waitFor } from "@testing-library/react"; -import { ComponentMock, MockComponents } from "@tests/unit/mocks"; - -describe(GrantModal.name, () => { - it("renders Popup with 'Authorize Spending' title", () => { - const PopupMock = vi.fn(ComponentMock); - setup({ dependencies: { Popup: PopupMock } }); - - expect(PopupMock.mock.calls[0][0].title).toBe("Authorize Spending"); - }); - - it("calls onClose when cancel action is clicked", () => { - const PopupMock = vi.fn(ComponentMock); - const { onClose } = setup({ dependencies: { Popup: PopupMock } }); - - const cancelAction = PopupMock.mock.calls[0][0].actions.find((a: { label: string }) => a.label === "Cancel"); - cancelAction.onClick(); - - expect(onClose).toHaveBeenCalled(); - }); - - it("disables Grant button when amount is 0", () => { - const PopupMock = vi.fn(ComponentMock); - setup({ dependencies: { Popup: PopupMock } }); - - const grantAction = PopupMock.mock.calls[0][0].actions.find((a: { label: string }) => a.label === "Grant"); - - expect(grantAction.disabled).toBe(true); - }); - - it("calls onClose when Popup onClose fires", () => { - const PopupMock = vi.fn(ComponentMock); - const { onClose } = setup({ dependencies: { Popup: PopupMock } }); - - PopupMock.mock.calls[0][0].onClose(); - - expect(onClose).toHaveBeenCalled(); - }); - - it("renders Popup as open", () => { - const PopupMock = vi.fn(ComponentMock); - setup({ dependencies: { Popup: PopupMock } }); - - expect(PopupMock.mock.calls[0][0].open).toBe(true); - }); - - it("sets grantee address field to editing grant's grantee", () => { - const FormFieldMock = vi.fn(ComponentMock); - setup({ - editingGrant: createGrant({ grantee: "akash1editgrantee" }), - dependencies: { FormField: FormFieldMock } - }); - - const granteeField = FormFieldMock.mock.calls.find(c => c[0].name === "granteeAddress"); - - expect(granteeField).toBeDefined(); - }); - - it("disables grantee address field when editing a grant", () => { - const FormInputMock = vi.fn(ComponentMock); - const FormFieldMock = vi.fn((props: Record) => { - const render = props.render as (args: Record) => React.ReactNode; - return <>{render({ field: { value: "", onChange: vi.fn() } })}; - }); - setup({ - editingGrant: createGrant({ grantee: "akash1editgrantee" }), - dependencies: { FormField: FormFieldMock, FormInput: FormInputMock } - }); - - const granteeCall = FormInputMock.mock.calls.find(c => c[0].label === "Grantee Address"); - - expect(granteeCall![0].disabled).toBe(true); - }); - - it("does not disable grantee address field when creating a new grant", () => { - const FormInputMock = vi.fn(ComponentMock); - const FormFieldMock = vi.fn((props: Record) => { - const render = props.render as (args: Record) => React.ReactNode; - return <>{render({ field: { value: "", onChange: vi.fn() } })}; - }); - setup({ - dependencies: { FormField: FormFieldMock, FormInput: FormInputMock } - }); - - const granteeCall = FormInputMock.mock.calls.find(c => c[0].label === "Grantee Address"); - - expect(granteeCall![0].disabled).toBe(false); - }); - - it("submits grant transaction and calls onClose on success", async () => { - const { signAndBroadcastTx, onClose, analyticsService } = setup({ - editingGrant: createGrant({ grantee: "akash1grantee" }) - }); - signAndBroadcastTx.mockResolvedValue({ transactionHash: "abc123" }); - - const form = document.querySelector("form"); - fireEvent.submit(form!); - - await waitFor(() => { - expect(signAndBroadcastTx).toHaveBeenCalled(); - }); - - expect(analyticsService.track).toHaveBeenCalledWith("authorize_spend", { - category: "deployments", - label: "Authorize wallet to spend on deployment deposits" - }); - expect(onClose).toHaveBeenCalled(); - }); - - it("does not call onClose when transaction fails", async () => { - const { signAndBroadcastTx, onClose } = setup({ - editingGrant: createGrant({ grantee: "akash1grantee" }) - }); - signAndBroadcastTx.mockResolvedValue(undefined); - - const form = document.querySelector("form"); - fireEvent.submit(form!); - - await waitFor(() => { - expect(signAndBroadcastTx).toHaveBeenCalled(); - }); - - expect(onClose).not.toHaveBeenCalled(); - }); - - it("sends spend limit as array when ACT is supported", async () => { - const { signAndBroadcastTx } = setup({ - editingGrant: createGrant({ grantee: "akash1grantee" }), - dependencies: { - useSupportedDenoms: () => ACT_SUPPORTED_TOKENS - } - }); - signAndBroadcastTx.mockResolvedValue({ transactionHash: "abc123" }); - - const form = document.querySelector("form"); - fireEvent.submit(form!); - - await waitFor(() => { - expect(signAndBroadcastTx).toHaveBeenCalled(); - }); - - const message = signAndBroadcastTx.mock.calls[0][0][0]; - expect(message).toBeDefined(); - }); - - it("triggers form submit when Grant button is clicked", async () => { - const PopupMock = vi.fn(ComponentMock); - const { signAndBroadcastTx } = setup({ - editingGrant: createGrant({ grantee: "akash1grantee" }), - dependencies: { Popup: PopupMock } - }); - signAndBroadcastTx.mockResolvedValue({ transactionHash: "abc123" }); - - const grantAction = PopupMock.mock.calls[0][0].actions.find((a: { label: string }) => a.label === "Grant"); - grantAction.onClick({ preventDefault: vi.fn() } as unknown as React.MouseEvent); - - await waitFor(() => { - expect(signAndBroadcastTx).toHaveBeenCalled(); - }); - }); - - it("renders info alert about authorized spend", () => { - const AlertMock = vi.fn(ComponentMock); - setup({ dependencies: { Alert: AlertMock } }); - - expect(AlertMock).toHaveBeenCalled(); - }); - - it("renders authorized spend doc link with click handler", () => { - const LinkToMock = vi.fn(ComponentMock); - setup({ dependencies: { LinkTo: LinkToMock } }); - - const docLinkCall = LinkToMock.mock.calls.find(c => { - const children = c[0].children; - return typeof children === "string" && children === "Authorized Spend"; - }); - - expect(docLinkCall).toBeDefined(); - expect(docLinkCall![0].onClick).toBeDefined(); - }); - - it("renders expiration field", () => { - const FormFieldMock = vi.fn(ComponentMock); - setup({ dependencies: { FormField: FormFieldMock } }); - - const expirationField = FormFieldMock.mock.calls.find(c => c[0].name === "expiration"); - - expect(expirationField).toBeDefined(); - }); - - it("shows 'Add AKT Grant' button when only one row exists", () => { - const ButtonMock = vi.fn(ComponentMock); - setup({ - dependencies: { - Button: ButtonMock, - useSupportedDenoms: () => ACT_SUPPORTED_TOKENS - } - }); - - const addButton = ButtonMock.mock.calls.find(c => c[0].children === "Add AKT Grant"); - - expect(addButton).toBeDefined(); - }); - - it("adds second SpendLimitRow when 'Add AKT Grant' button is clicked", () => { - const ButtonMock = vi.fn((props: Record) => ( - - )); - const SpendLimitRowMock = vi.fn(ComponentMock); - setup({ - dependencies: { - Button: ButtonMock, - SpendLimitRow: SpendLimitRowMock, - useSupportedDenoms: () => ACT_SUPPORTED_TOKENS - } - }); - - fireEvent.click(screen.getByText("Add AKT Grant")); - - const secondRow = SpendLimitRowMock.mock.calls.find(c => c[0].index === 1); - expect(secondRow).toBeDefined(); - }); - - it("passes isRemovable to second SpendLimitRow but not first", () => { - const SpendLimitRowMock = vi.fn(ComponentMock); - setup({ - editingGrant: createGrant({ - authorization: { - "@type": "/akash.escrow.v1.DepositAuthorization", - spend_limits: [ - { denom: UACT_DENOM, amount: "5000000" }, - { denom: UAKT_DENOM, amount: "3000000" } - ] - } - }), - dependencies: { - SpendLimitRow: SpendLimitRowMock, - useSupportedDenoms: () => ACT_SUPPORTED_TOKENS - } - }); - - const firstRow = SpendLimitRowMock.mock.calls.find(c => c[0].index === 0); - const secondRow = SpendLimitRowMock.mock.calls.find(c => c[0].index === 1); - expect(firstRow![0].isRemovable).toBe(false); - expect(secondRow![0].isRemovable).toBe(true); - }); - - it("renders SpendLimitRow for each spend limit when editing grant with multiple spend_limits", () => { - const SpendLimitRowMock = vi.fn(ComponentMock); - setup({ - editingGrant: createGrant({ - authorization: { - "@type": "/akash.escrow.v1.DepositAuthorization", - spend_limits: [ - { denom: UACT_DENOM, amount: "5000000" }, - { denom: UAKT_DENOM, amount: "3000000" } - ] - } - }), - dependencies: { - SpendLimitRow: SpendLimitRowMock, - useSupportedDenoms: () => ACT_SUPPORTED_TOKENS - } - }); - - const firstRow = SpendLimitRowMock.mock.calls.find(c => c[0].index === 0); - const secondRow = SpendLimitRowMock.mock.calls.find(c => c[0].index === 1); - - expect(firstRow).toBeDefined(); - expect(secondRow).toBeDefined(); - }); - - it("does not show add grant button when both rows exist", () => { - const ButtonMock = vi.fn(ComponentMock); - setup({ - editingGrant: createGrant({ - authorization: { - "@type": "/akash.escrow.v1.DepositAuthorization", - spend_limits: [ - { denom: UACT_DENOM, amount: "5000000" }, - { denom: UAKT_DENOM, amount: "3000000" } - ] - } - }), - dependencies: { - Button: ButtonMock, - useSupportedDenoms: () => ACT_SUPPORTED_TOKENS - } - }); - - const addButton = ButtonMock.mock.calls.find(c => c[0].children === "Add AKT Grant"); - - expect(addButton).toBeUndefined(); - }); - - function setup( - input: { - address?: string; - editingGrant?: GrantType | null; - dependencies?: Partial>; - } = {} - ) { - const onClose = vi.fn(); - const analyticsService = mock(); - const signAndBroadcastTx = vi.fn(); - - render( - ({ analyticsService }) as unknown as ReturnType, - useWallet: () => ({ signAndBroadcastTx }) as unknown as ReturnType, - useUsdcDenom: () => USDC_TEST_DENOM, - useDenomData: () => ({ min: 0, max: 1000, label: "AKT", balance: 500 }), - useSupportedDenoms: () => DEFAULT_SUPPORTED_TOKENS, - ...input.dependencies - } as typeof DEPENDENCIES - } - /> - ); - - return { onClose, analyticsService, signAndBroadcastTx }; - } - - function createGrant(overrides?: Partial): GrantType { - return { - granter: "akash1granter", - grantee: "akash1grantee", - expiration: "2025-12-31T00:00:00Z", - authorization: { - "@type": "/akash.escrow.v1.DepositAuthorization", - spend_limits: [ - { - denom: "uakt", - amount: "1000000" - } - ] - }, - ...overrides - } as GrantType; - } - - const USDC_TEST_DENOM = "ibc/170C677610AC31DF0904FFE09CD3B5C657492170E7E52372E48756B71E56F2F1" as const; - - const DEFAULT_SUPPORTED_TOKENS = [ - { id: UAKT_DENOM, label: "uAKT", tokenLabel: "AKT", value: UAKT_DENOM }, - { id: "uusdc", label: "uUSDC", tokenLabel: "USDC", value: USDC_TEST_DENOM } - ]; - - const ACT_SUPPORTED_TOKENS = [ - { id: UACT_DENOM, label: "uACT", tokenLabel: "ACT", value: UACT_DENOM }, - { id: UAKT_DENOM, label: "uAKT", tokenLabel: "AKT", value: UAKT_DENOM } - ]; -}); diff --git a/apps/deploy-web/src/components/authorizations/GrantModal/GrantModal.tsx b/apps/deploy-web/src/components/authorizations/GrantModal/GrantModal.tsx deleted file mode 100644 index 12294da42b..0000000000 --- a/apps/deploy-web/src/components/authorizations/GrantModal/GrantModal.tsx +++ /dev/null @@ -1,221 +0,0 @@ -"use client"; -import { useRef, useState } from "react"; -import { useFieldArray, useForm } from "react-hook-form"; -import { FormattedDate } from "react-intl"; -import { Alert, Button, Form, FormField, FormInput, FormLabel, FormMessage, Popup } from "@akashnetwork/ui/components"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { addYears, format } from "date-fns"; -import { z } from "zod"; - -import { LinkTo } from "@src/components/shared/LinkTo"; -import { UACT_DENOM, UAKT_DENOM } from "@src/config/denom.config"; -import { useServices } from "@src/context/ServicesProvider"; -import { useWallet } from "@src/context/WalletProvider"; -import { useSupportedDenoms, useUsdcDenom } from "@src/hooks/useDenom"; -import { useDenomData } from "@src/hooks/useWalletBalance"; -import type { GrantType } from "@src/types/grant"; -import { denomToUdenom } from "@src/utils/mathHelpers"; -import { coinToDenom } from "@src/utils/priceUtils"; -import { TransactionMessageData } from "@src/utils/TransactionMessageData"; -import { handleDocClick } from "@src/utils/urlUtils"; -import { SpendLimitRow } from "./SpendLimitRow"; - -export const DEPENDENCIES = { - Alert, - Button, - Form, - FormField, - FormInput, - FormLabel, - FormMessage, - FormattedDate, - Popup, - LinkTo, - SpendLimitRow, - useServices, - useWallet, - useUsdcDenom, - useDenomData, - useSupportedDenoms -}; - -type Props = { - address: string; - editingGrant?: GrantType | null; - onClose: () => void; - dependencies?: typeof DEPENDENCIES; -}; - -const spendLimitSchema = z.object({ - denom: z.string().min(1, "Token is required."), - amount: z.coerce.number().gt(0, "Amount must be greater than 0.") -}); - -const formSchema = z.object({ - spendLimits: z.array(spendLimitSchema).min(1), - expiration: z.string().min(1, "Expiration is required."), - granteeAddress: z.string().min(1, "Grantee address is required.") -}); - -export type GrantFormValues = z.infer; - -export const GrantModal: React.FunctionComponent = ({ editingGrant, address, onClose, dependencies: d = DEPENDENCIES }) => { - const { analyticsService } = d.useServices(); - const formRef = useRef(null); - const [error, setError] = useState(""); - const { signAndBroadcastTx } = d.useWallet(); - const usdcDenom = d.useUsdcDenom(); - const supportedTokens = d.useSupportedDenoms(); - - const defaultSpendLimits = editingGrant - ? editingGrant.authorization.spend_limits.map(sl => ({ - denom: sl.denom, - amount: coinToDenom(sl) - })) - : [{ denom: UACT_DENOM, amount: 0 }]; - - const form = useForm({ - defaultValues: { - spendLimits: defaultSpendLimits, - expiration: format(addYears(new Date(), 1), "yyyy-MM-dd'T'HH:mm"), - granteeAddress: editingGrant?.grantee ?? "" - }, - resolver: zodResolver(formSchema) - }); - const { handleSubmit, control, watch, clearErrors } = form; - const { fields: spendLimitFields, append: appendSpendLimit, remove: removeSpendLimitAt } = useFieldArray({ control, name: "spendLimits" }); - const watchedSpendLimits = watch("spendLimits"); - const granteeAddress = watch("granteeAddress"); - const expiration = watch("expiration"); - const hasAmount = watchedSpendLimits.some(sl => sl.amount > 0); - const canAddAkt = spendLimitFields.length < 2 && !spendLimitFields.some(f => f.denom === UAKT_DENOM); - - const onDepositClick = (event: React.MouseEvent) => { - event.preventDefault(); - formRef.current?.dispatchEvent(new Event("submit", { cancelable: true, bubbles: true })); - }; - - const authorizeSpending = async ({ spendLimits, expiration, granteeAddress }: GrantFormValues) => { - setError(""); - clearErrors(); - - const spendLimit = spendLimits.map(sl => ({ - amount: denomToUdenom(sl.amount).toString(), - denom: sl.denom === "usdc" ? usdcDenom : sl.denom - })); - - const expirationDate = new Date(expiration); - const message = TransactionMessageData.getGrantMsg(address, granteeAddress, spendLimit, expirationDate); - const response = await signAndBroadcastTx([message]); - - if (response) { - analyticsService.track("authorize_spend", { - category: "deployments", - label: "Authorize wallet to spend on deployment deposits" - }); - - onClose(); - } - }; - - const addAktGrant = () => { - appendSpendLimit({ denom: UAKT_DENOM, amount: 0 }); - }; - - return ( - - -
- -

- handleDocClick(ev, "https://akash.network/docs/network-features/authorized-spend/")}>Authorized Spend allows - users to authorize spending of a set number of tokens from a source wallet to a destination, funded wallet. The authorized spend is restricted to - Akash deployment activities and the recipient of the tokens would not have access to those tokens for other operations. -

-
- - {spendLimitFields.map((field, index) => ( - 0} - onRemove={() => removeSpendLimitAt(index)} - /> - ))} - - {canAddAkt && ( -
- - Add AKT Grant - -
- )} - -
- { - return ; - }} - /> -
- -
- { - return ; - }} - /> -
- - {hasAmount && granteeAddress && ( - -

- This address will be able to spend up to{" "} - {watchedSpendLimits - .filter(sl => sl.amount > 0) - .map(sl => { - const token = supportedTokens.find(t => t.id === sl.denom); - return `${sl.amount} ${token?.tokenLabel ?? sl.denom}`; - }) - .join(" and ")}{" "} - on your behalf ending on . -

-
- )} - - {error && {error}} - -
-
- ); -}; diff --git a/apps/deploy-web/src/components/authorizations/GrantModal/SpendLimitRow.spec.tsx b/apps/deploy-web/src/components/authorizations/GrantModal/SpendLimitRow.spec.tsx deleted file mode 100644 index bcbf8f0f78..0000000000 --- a/apps/deploy-web/src/components/authorizations/GrantModal/SpendLimitRow.spec.tsx +++ /dev/null @@ -1,193 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; - -import { UAKT_DENOM } from "@src/config/denom.config"; -import { DEPENDENCIES, SpendLimitRow } from "./SpendLimitRow"; - -import { fireEvent, render, screen } from "@testing-library/react"; -import { ComponentMock, MockComponents } from "@tests/unit/mocks"; - -describe("SpendLimitRow", () => { - it("renders FormField for amount", () => { - const FormFieldMock = vi.fn(ComponentMock); - setup({ dependencies: { FormField: FormFieldMock } }); - - const amountField = FormFieldMock.mock.calls.find(c => c[0].name === "spendLimits.0.amount"); - - expect(amountField).toBeDefined(); - }); - - it("resolves usdc denom before calling useDenomData", () => { - const useDenomData = vi.fn(() => DEFAULT_DENOM_DATA); - setup({ denom: "usdc", dependencies: { useDenomData } }); - - expect(useDenomData).toHaveBeenCalledWith(USDC_TEST_DENOM); - }); - - it("passes denom directly to useDenomData when not usdc", () => { - const useDenomData = vi.fn(() => DEFAULT_DENOM_DATA); - setup({ denom: UAKT_DENOM, dependencies: { useDenomData } }); - - expect(useDenomData).toHaveBeenCalledWith(UAKT_DENOM); - }); - - it("displays balance and label from denom data", () => { - const LinkToMock = vi.fn((props: Record) => {props.children as React.ReactNode}); - const FormFieldMock = vi.fn(RenderPropMock); - const FormInputMock = vi.fn(LabelRenderingMock); - setup({ dependencies: { LinkTo: LinkToMock, FormField: FormFieldMock, FormInput: FormInputMock } }); - - expect(screen.getByText(/Balance:/)).toBeInTheDocument(); - expect(screen.getByText(/500/)).toBeInTheDocument(); - expect(screen.getAllByText(/AKT/).length).toBeGreaterThanOrEqual(1); - }); - - it("calls clearErrors and field.onChange with max value when balance link is clicked", () => { - const onChange = vi.fn(); - const clearErrors = vi.fn(); - const FormFieldMock = vi.fn((props: Record) => { - const render = props.render as (args: Record) => React.ReactNode; - if (render) { - if ((props.name as string).includes("amount")) { - return <>{render({ field: { value: "", onChange } })}; - } - return <>{render({ field: { value: "", onChange: vi.fn() } })}; - } - return <>{(props as { children?: React.ReactNode }).children}; - }); - const FormInputMock = vi.fn(LabelRenderingMock); - const LinkToMock = vi.fn((props: Record) => {props.children as React.ReactNode}); - setup({ - dependencies: { - FormField: FormFieldMock, - FormInput: FormInputMock, - LinkTo: LinkToMock, - useFormContext: () => ({ control: {}, clearErrors }) - } - }); - - fireEvent.click(screen.getByText(/Balance:/)); - - expect(clearErrors).toHaveBeenCalled(); - expect(onChange).toHaveBeenCalledWith(1000); - }); - - it("passes endIcon with remove button to FormInput when isRemovable", () => { - const FormInputMock = vi.fn(ComponentMock); - const FormFieldMock = vi.fn(RenderPropMock); - setup({ - isRemovable: true, - dependencies: { FormInput: FormInputMock, FormField: FormFieldMock } - }); - - const inputWithEndIcon = FormInputMock.mock.calls.find(c => c[0].endIcon); - - expect(inputWithEndIcon).toBeDefined(); - }); - - it("does not pass endIcon to FormInput when not isRemovable", () => { - const FormInputMock = vi.fn(ComponentMock); - const FormFieldMock = vi.fn(RenderPropMock); - setup({ - isRemovable: false, - dependencies: { FormInput: FormInputMock, FormField: FormFieldMock } - }); - - const inputWithEndIcon = FormInputMock.mock.calls.find(c => c[0].endIcon); - - expect(inputWithEndIcon).toBeUndefined(); - }); - - it("calls onRemove when remove button is clicked", () => { - const FormFieldMock = vi.fn(RenderPropMock); - const FormInputMock = vi.fn((props: Record) => <>{props.endIcon as React.ReactNode}); - const onRemove = vi.fn(); - setup({ - isRemovable: true, - onRemove, - dependencies: { FormField: FormFieldMock, FormInput: FormInputMock } - }); - - fireEvent.click(screen.getByRole("button")); - - expect(onRemove).toHaveBeenCalled(); - }); - - it("displays token label in spending limit label", () => { - const FormFieldMock = vi.fn(RenderPropMock); - const FormInputMock = vi.fn(LabelRenderingMock); - setup({ - denom: UAKT_DENOM, - dependencies: { FormField: FormFieldMock, FormInput: FormInputMock } - }); - - expect(screen.getByText(/Spending Limit \(AKT\)/)).toBeInTheDocument(); - }); - - it("uses correct field name with provided index", () => { - const FormFieldMock = vi.fn(ComponentMock); - setup({ index: 2, dependencies: { FormField: FormFieldMock } }); - - const amountField = FormFieldMock.mock.calls.find(c => c[0].name === "spendLimits.2.amount"); - - expect(amountField).toBeDefined(); - }); - - function setup( - input: { - index?: number; - denom?: string; - isRemovable?: boolean; - onRemove?: () => void; - dependencies?: Partial>; - } = {} - ) { - const onRemove = input.onRemove ?? vi.fn(); - - render( - ({ control: {}, clearErrors: vi.fn() }), - useSupportedDenoms: () => DEFAULT_SUPPORTED_TOKENS, - useUsdcDenom: () => USDC_TEST_DENOM, - useDenomData: () => DEFAULT_DENOM_DATA, - ...input.dependencies - } as typeof DEPENDENCIES - } - /> - ); - - return { onRemove }; - } - - const USDC_TEST_DENOM = "ibc/170C677610AC31DF0904FFE09CD3B5C657492170E7E52372E48756B71E56F2F1" as const; - - const DEFAULT_DENOM_DATA = { min: 0, max: 1000, label: "AKT", balance: 500 }; - - const DEFAULT_SUPPORTED_TOKENS = [ - { id: UAKT_DENOM, label: "uAKT", tokenLabel: "AKT", value: UAKT_DENOM }, - { id: "uusdc", label: "uUSDC", tokenLabel: "USDC", value: USDC_TEST_DENOM } - ]; - - function LabelRenderingMock(props: Record) { - return ( - <> - {props.label as React.ReactNode} - {(props as { children?: React.ReactNode }).children} - - ); - } - - function RenderPropMock(props: Record) { - const render = props.render as ((args: Record) => React.ReactNode) | undefined; - if (render) { - return <>{render({ field: { value: "", onChange: vi.fn() } })}; - } - return <>{(props as { children?: React.ReactNode }).children}; - } -}); diff --git a/apps/deploy-web/src/components/authorizations/GrantModal/SpendLimitRow.tsx b/apps/deploy-web/src/components/authorizations/GrantModal/SpendLimitRow.tsx deleted file mode 100644 index 3cf91c0596..0000000000 --- a/apps/deploy-web/src/components/authorizations/GrantModal/SpendLimitRow.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { useFormContext } from "react-hook-form"; -import { FormField, FormInput, FormMessage } from "@akashnetwork/ui/components"; -import { Bin } from "iconoir-react"; - -import { LinkTo } from "@src/components/shared/LinkTo"; -import { useSupportedDenoms, useUsdcDenom } from "@src/hooks/useDenom"; -import { useDenomData } from "@src/hooks/useWalletBalance"; -import type { GrantFormValues } from "./GrantModal"; - -export const DEPENDENCIES = { - useSupportedDenoms, - useUsdcDenom, - useDenomData, - useFormContext, - FormField, - FormMessage, - FormInput, - Bin, - LinkTo -}; - -type Props = { - index: number; - denom: string; - isRemovable: boolean; - onRemove: () => void; - dependencies?: typeof DEPENDENCIES; -}; - -export const SpendLimitRow: React.FunctionComponent = ({ index, denom, isRemovable, onRemove, dependencies: d = DEPENDENCIES }) => { - const { control, clearErrors } = d.useFormContext(); - const supportedTokens = d.useSupportedDenoms(); - const usdcDenom = d.useUsdcDenom(); - const resolvedDenom = denom === "usdc" ? usdcDenom : denom; - const denomData = d.useDenomData(resolvedDenom); - const tokenInfo = supportedTokens.find(t => t.id === denom); - - return ( -
-
- { - const setMaxAmount = () => { - clearErrors(); - field.onChange(denomData?.max || 0); - }; - return ( - -
Spending Limit ({tokenInfo?.tokenLabel ?? denom})
- - Balance: {denomData?.balance} {denomData?.label} - -
- } - autoFocus={index === 0} - min={0} - step={0.000001} - max={denomData?.max} - startIcon={{denomData?.label}} - endIcon={ - isRemovable ? ( - - ) : undefined - } - className="flex-grow" - /> - ); - }} - /> -
- - ); -}; diff --git a/apps/deploy-web/src/components/authorizations/GranteeRow/GranteeRow.spec.tsx b/apps/deploy-web/src/components/authorizations/GranteeRow/GranteeRow.spec.tsx deleted file mode 100644 index 2e84679a91..0000000000 --- a/apps/deploy-web/src/components/authorizations/GranteeRow/GranteeRow.spec.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; - -import type { GrantType } from "@src/types/grant"; -import { DEPENDENCIES, GranteeRow } from "./GranteeRow"; - -import { render, screen } from "@testing-library/react"; -import { ComponentMock, MockComponents } from "@tests/unit/mocks"; - -describe(GranteeRow.name, () => { - it("renders granter address", () => { - const AddressMock = vi.fn(ComponentMock); - setup({ dependencies: { Address: AddressMock } }); - - expect(AddressMock).toHaveBeenCalledWith(expect.objectContaining({ address: "akash1granter", isCopyable: true }), expect.anything()); - }); - - it("renders spend limits when present", () => { - const DenomAmountMock = vi.fn(ComponentMock); - setup({ - grant: createGrant({ - authorization: { - "@type": "/akash.escrow.v1.DepositAuthorization", - spend_limits: [ - { denom: "uakt", amount: "1000000" }, - { denom: "ibc/170C677610AC31DF0904FFE09CD3B5C657492170E7E52372E48756B71E56F2F1", amount: "5000000" } - ] - } - }), - dependencies: { DenomAmount: DenomAmountMock } - }); - - expect(DenomAmountMock).toHaveBeenCalledTimes(2); - expect(DenomAmountMock).toHaveBeenCalledWith(expect.objectContaining({ denom: "uakt" }), expect.anything()); - expect(DenomAmountMock).toHaveBeenCalledWith( - expect.objectContaining({ denom: "ibc/170C677610AC31DF0904FFE09CD3B5C657492170E7E52372E48756B71E56F2F1" }), - expect.anything() - ); - }); - - it("renders 'Unlimited' when spend_limits is undefined", () => { - setup({ - grant: createGrant({ - authorization: { - "@type": "/akash.escrow.v1.DepositAuthorization", - spend_limits: undefined as never - } - }) - }); - - expect(screen.getByText("Unlimited")).toBeInTheDocument(); - }); - - it("renders expiration date", () => { - const FormattedTimeMock = vi.fn(ComponentMock); - const expiration = "2025-12-31T00:00:00Z"; - setup({ - grant: createGrant({ expiration }), - dependencies: { FormattedTime: FormattedTimeMock } - }); - - expect(FormattedTimeMock).toHaveBeenCalledWith( - expect.objectContaining({ value: expiration, year: "numeric", month: "numeric", day: "numeric" }), - expect.anything() - ); - }); - - function setup(input: { grant?: GrantType; dependencies?: Partial> } = {}) { - const grant = input.grant || createGrant(); - - render( - - ); - - return { grant }; - } - - function createGrant(overrides?: Partial): GrantType { - return { - granter: "akash1granter", - grantee: "akash1grantee", - expiration: "2025-12-31T00:00:00Z", - authorization: { - "@type": "/akash.escrow.v1.DepositAuthorization", - spend_limits: [{ denom: "uakt", amount: "1000000" }] - }, - ...overrides - }; - } -}); diff --git a/apps/deploy-web/src/components/authorizations/GranteeRow/GranteeRow.tsx b/apps/deploy-web/src/components/authorizations/GranteeRow/GranteeRow.tsx deleted file mode 100644 index 4edc581b19..0000000000 --- a/apps/deploy-web/src/components/authorizations/GranteeRow/GranteeRow.tsx +++ /dev/null @@ -1,49 +0,0 @@ -"use client"; -import type { ReactNode } from "react"; -import React from "react"; -import { FormattedTime } from "react-intl"; -import { Address, TableCell, TableRow } from "@akashnetwork/ui/components"; - -import type { GrantType } from "@src/types/grant"; -import { coinToUDenom } from "@src/utils/priceUtils"; -import { DenomAmount } from "../../shared/DenomAmount/DenomAmount"; - -export const DEPENDENCIES = { - FormattedTime, - Address, - TableCell, - TableRow, - DenomAmount -}; - -type Props = { - grant: GrantType; - children?: ReactNode; - dependencies?: typeof DEPENDENCIES; -}; - -export const GranteeRow: React.FunctionComponent = ({ grant, dependencies: d = DEPENDENCIES }) => { - const limits = grant?.authorization?.spend_limits; - - return ( - - - - - - {limits?.length ? ( -
- {limits.map((limit, index) => ( - - ))} -
- ) : ( - Unlimited - )} -
- - - -
- ); -}; diff --git a/apps/deploy-web/src/components/onboarding/OnboardingContainer/OnboardingContainer.spec.tsx b/apps/deploy-web/src/components/onboarding/OnboardingContainer/OnboardingContainer.spec.tsx index 0baea864a8..9b91d2733b 100644 --- a/apps/deploy-web/src/components/onboarding/OnboardingContainer/OnboardingContainer.spec.tsx +++ b/apps/deploy-web/src/components/onboarding/OnboardingContainer/OnboardingContainer.spec.tsx @@ -412,10 +412,6 @@ describe("OnboardingContainer", () => { getDepositDeploymentMsg: vi.fn(), getCloseDeploymentMsg: vi.fn(), getSendTokensMsg: vi.fn(), - getGrantMsg: vi.fn(), - getRevokeDepositMsg: vi.fn(), - getGrantBasicAllowanceMsg: vi.fn(), - getRevokeAllowanceMsg: vi.fn(), getUpdateProviderMsg: vi.fn() }; diff --git a/apps/deploy-web/src/components/settings/AutoTopUpSetting/AutoTopUpSetting.tsx b/apps/deploy-web/src/components/settings/AutoTopUpSetting/AutoTopUpSetting.tsx deleted file mode 100644 index 969979dc10..0000000000 --- a/apps/deploy-web/src/components/settings/AutoTopUpSetting/AutoTopUpSetting.tsx +++ /dev/null @@ -1,221 +0,0 @@ -import type { FC } from "react"; -import React, { useCallback, useEffect, useMemo } from "react"; -import type { SubmitHandler } from "react-hook-form"; -import { Controller, 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; - -type LimitFields = keyof Omit; - -type AutoTopUpSubmitHandler = (action: "revoke-all" | "update", next: FormValues) => Promise; - -export interface AutoTopUpSettingProps extends Partial> { - onSubmit: AutoTopUpSubmitHandler; - expiration?: Date; -} - -const fields: LimitFields[] = ["uaktFeeLimit", "usdcFeeLimit", "uaktDeploymentLimit", "usdcDeploymentLimit"]; - -export const AutoTopUpSetting: FC = ({ 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 - ); - }, [props]); - - const form = useForm>({ - 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 = useCallback( - async (next, event) => { - const nativeEvent = (event as React.BaseSyntheticEvent | undefined)?.nativeEvent; - const role = nativeEvent?.submitter?.getAttribute("data-role"); - await onSubmit(role as "revoke-all" | "update", convertToUakt(next)); - reset(next); - }, - [onSubmit, reset] - ); - - return ( -
-
- -
Deployments billed in AKT
-
-
- { - return ; - }} - /> -
- -
- { - return ; - }} - /> -
-
- -
Deployments billed in USDC
-
-
- { - return ; - }} - /> -
- -
- { - return ; - }} - /> -
-
- -
- { - return ; - }} - /> -
- - - - {hasAny && ( - - )} -
- -
- ); -}; - -function convertToUakt({ ...values }: FormValues) { - return fields.reduce((acc, field) => { - acc[field] = aktToUakt(values[field]); - return acc; - }, values); -} diff --git a/apps/deploy-web/src/components/settings/AutoTopUpSetting/AutoTopUpSettingContainer.tsx b/apps/deploy-web/src/components/settings/AutoTopUpSetting/AutoTopUpSettingContainer.tsx deleted file mode 100644 index 8a9a30b430..0000000000 --- a/apps/deploy-web/src/components/settings/AutoTopUpSetting/AutoTopUpSettingContainer.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import type { FC } from "react"; -import React, { useCallback, useEffect } from "react"; - -import type { AutoTopUpSettingProps } from "@src/components/settings/AutoTopUpSetting/AutoTopUpSetting"; -import { AutoTopUpSetting } 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 ( - - ); -}; diff --git a/apps/deploy-web/src/components/settings/SettingsContainer.tsx b/apps/deploy-web/src/components/settings/SettingsContainer.tsx index bb98949cdc..1ab707559c 100644 --- a/apps/deploy-web/src/components/settings/SettingsContainer.tsx +++ b/apps/deploy-web/src/components/settings/SettingsContainer.tsx @@ -5,13 +5,11 @@ import { Edit } from "iconoir-react"; import { useRouter } from "next/navigation"; import { NextSeo } from "next-seo"; -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"; import { useSettings } from "@src/context/SettingsProvider"; import { useWallet } from "@src/context/WalletProvider"; -import { useFlag } from "@src/hooks/useFlag"; import { useWhen } from "@src/hooks/useWhen"; import networkStore from "@src/store/networkStore"; import Layout from "../layout/Layout"; @@ -19,7 +17,7 @@ import { CertificateList } from "./CertificateList"; import { ColorModeSelect } from "./ColorModeSelect"; import { SelectNetworkModal } from "./SelectNetworkModal"; import { SettingsForm } from "./SettingsForm"; -import { SettingsLayout, SettingsTabs } from "./SettingsLayout"; +import { SettingsLayout } from "./SettingsLayout"; export const SettingsContainer: React.FunctionComponent = () => { const { settings } = useSettings(); @@ -27,7 +25,6 @@ export const SettingsContainer: React.FunctionComponent = () => { const selectedNetwork = networkStore.useSelectedNetwork(); const wallet = useWallet(); const router = useRouter(); - const isCustodialAutoTopupEnabled = useFlag("custodial_auto_topup"); useWhen(!wallet.isWalletConnected || wallet.isManaged, () => router.push("/")); @@ -39,7 +36,7 @@ export const SettingsContainer: React.FunctionComponent = () => { - + {isSelectingNetwork && }
@@ -62,12 +59,6 @@ export const SettingsContainer: React.FunctionComponent = () => {
- - {isCustodialAutoTopupEnabled && ( -
- -
- )}
{!settings.isBlockchainDown && ( diff --git a/apps/deploy-web/src/components/settings/SettingsLayout.spec.tsx b/apps/deploy-web/src/components/settings/SettingsLayout.spec.tsx new file mode 100644 index 0000000000..82c9c96f63 --- /dev/null +++ b/apps/deploy-web/src/components/settings/SettingsLayout.spec.tsx @@ -0,0 +1,27 @@ +import { describe, expect, it } from "vitest"; + +import { SettingsLayout } from "./SettingsLayout"; + +import { render, screen } from "@testing-library/react"; + +describe(SettingsLayout.name, () => { + it("renders the title, header actions, and children", () => { + setup({ + title: "Settings", + headerActions: , + children:

Body content

+ }); + + expect(screen.getByText("Settings")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Action" })).toBeInTheDocument(); + expect(screen.getByText("Body content")).toBeInTheDocument(); + }); + + function setup(input: { title: string; headerActions?: React.ReactNode; children?: React.ReactNode; titleId?: string }) { + return render( + + {input.children} + + ); + } +}); diff --git a/apps/deploy-web/src/components/settings/SettingsLayout.tsx b/apps/deploy-web/src/components/settings/SettingsLayout.tsx index 4a1ceaf868..49ebdff39e 100644 --- a/apps/deploy-web/src/components/settings/SettingsLayout.tsx +++ b/apps/deploy-web/src/components/settings/SettingsLayout.tsx @@ -2,58 +2,26 @@ import type { ReactNode } from "react"; import React from "react"; import { ErrorBoundary } from "react-error-boundary"; -import { ErrorFallback, Tabs, TabsList, TabsTrigger } from "@akashnetwork/ui/components"; -import { cn } from "@akashnetwork/ui/utils"; -import { useRouter } from "next/navigation"; +import { ErrorFallback } from "@akashnetwork/ui/components"; -import { UrlService } from "@src/utils/urlUtils"; import { Title } from "../shared/Title"; -export enum SettingsTabs { - GENERAL = "GENERAL", - AUTHORIZATIONS = "AUTHORIZATIONS" -} - type Props = { - page: SettingsTabs; children?: ReactNode; title: string; titleId?: string; headerActions?: ReactNode; }; -export const SettingsLayout: React.FunctionComponent = ({ children, page, title, titleId, headerActions }) => { - const router = useRouter(); - - const handleTabChange = (newValue: string) => { - switch (newValue as SettingsTabs) { - case SettingsTabs.AUTHORIZATIONS: - router.push(UrlService.settingsAuthorizations()); - break; - case SettingsTabs.GENERAL: - default: - router.push(UrlService.settings()); - break; - } - }; - +export const SettingsLayout: React.FunctionComponent = ({ children, title, titleId, headerActions }) => { return ( - - - - General - - - Authorizations - - - + <>
{title} {headerActions}
{children} -
+ ); }; diff --git a/apps/deploy-web/src/context/WalletProvider/WalletProvider.tsx b/apps/deploy-web/src/context/WalletProvider/WalletProvider.tsx index b36e06ab6b..acaa633e32 100644 --- a/apps/deploy-web/src/context/WalletProvider/WalletProvider.tsx +++ b/apps/deploy-web/src/context/WalletProvider/WalletProvider.tsx @@ -13,7 +13,6 @@ import { useSnackbar } from "notistack"; import type { LoadingState } from "@src/components/layout/TransactionModal"; import { TransactionModal } from "@src/components/layout/TransactionModal"; -import { useAllowance } from "@src/hooks/useAllowance"; import { useManagedWallet } from "@src/hooks/useManagedWallet"; import { useSelectedChain } from "@src/hooks/useSelectedChain/useSelectedChain"; import { useUser } from "@src/hooks/useUser"; @@ -116,9 +115,6 @@ export const WalletProvider: React.FC<{ children: React.ReactNode }> = ({ childr return { isManaged: false, denom: undefined }; }, [walletAddress, managedWallet]); const { isManaged } = managedMarker; - const { - fee: { default: feeGranter } - } = useAllowance(walletAddress as string, isManaged); const [selectedNetworkId, setSelectedNetworkId] = networkStore.useSelectedNetworkIdStore(); const isLoading = deriveWalletIsLoading({ hasAuthenticatedUserId: !!user?.userId, @@ -266,14 +262,7 @@ export const WalletProvider: React.FC<{ children: React.ReactNode }> = ({ childr }; setLoadingState("waitingForApproval"); const estimatedFees = await userWallet.estimateFee(msgs, undefined, CONSOLE_MEMO); - const txRaw = await userWallet.sign( - msgs, - { - ...estimatedFees, - granter: feeGranter - }, - CONSOLE_MEMO - ); + const txRaw = await userWallet.sign(msgs, estimatedFees, CONSOLE_MEMO); setLoadingState("broadcasting"); enqueueTxSnackbar(); diff --git a/apps/deploy-web/src/hooks/useAllowance.tsx b/apps/deploy-web/src/hooks/useAllowance.tsx deleted file mode 100644 index 77dbd5297d..0000000000 --- a/apps/deploy-web/src/hooks/useAllowance.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import type { FC } from "react"; -import React, { useMemo } from "react"; -import { Snackbar } from "@akashnetwork/ui/components"; -import isAfter from "date-fns/isAfter"; -import parseISO from "date-fns/parseISO"; -import { OpenNewWindow } from "iconoir-react"; -import difference from "lodash/difference"; -import Link from "next/link"; -import { useSnackbar } from "notistack"; -import { useLocalStorage } from "usehooks-ts"; - -import { useWhen } from "@src/hooks/useWhen"; -import { useAllowancesGranted } from "@src/queries/useGrantsQuery"; - -const persisted: Record = typeof window !== "undefined" ? JSON.parse(localStorage.getItem("fee-granters") || "{}") : {}; - -const AllowanceNotificationMessage: FC = () => ( - <> - You can update default fee granter in - - Authorizations Settings - - - -); - -export const useAllowance = (address: string, isManaged: boolean) => { - const [defaultFeeGranter, setDefaultFeeGranter] = useLocalStorage(`default-fee-granters/${address}`, undefined); - const { data: allFeeGranters, isLoading, isFetched } = useAllowancesGranted(address); - const { enqueueSnackbar } = useSnackbar(); - - const actualAllowanceAddresses = useMemo(() => { - if (!address || !allFeeGranters) { - return null; - } - - return allFeeGranters.reduce((acc, grant) => { - if (isAfter(parseISO(grant.allowance.expiration), new Date())) { - acc.push(grant.granter); - } - - return acc; - }, [] as string[]); - }, [allFeeGranters, address]); - - useWhen( - isFetched && address && !isManaged && !!actualAllowanceAddresses, - () => { - const _actualAllowanceAddresses = actualAllowanceAddresses as string[]; - const persistedAddresses = persisted[address] || []; - const added = difference(_actualAllowanceAddresses, persistedAddresses); - const removed = difference(persistedAddresses, _actualAllowanceAddresses); - - if (added.length || removed.length) { - persisted[address] = _actualAllowanceAddresses; - localStorage.setItem(`fee-granters`, JSON.stringify(persisted)); - } - - if (added.length) { - enqueueSnackbar(} />, { - variant: "info" - }); - } - - if (removed.length) { - enqueueSnackbar(} />, { - variant: "warning" - }); - } - - if (defaultFeeGranter && removed.includes(defaultFeeGranter)) { - setDefaultFeeGranter(undefined); - } else if (!defaultFeeGranter && _actualAllowanceAddresses.length) { - setDefaultFeeGranter(_actualAllowanceAddresses[0]); - } - }, - [actualAllowanceAddresses, persisted] - ); - - return useMemo( - () => ({ - fee: { - all: allFeeGranters, - default: defaultFeeGranter, - setDefault: setDefaultFeeGranter, - isLoading - } - }), - [defaultFeeGranter, setDefaultFeeGranter, allFeeGranters, isLoading] - ); -}; diff --git a/apps/deploy-web/src/hooks/useAutoTopUpLimits.tsx b/apps/deploy-web/src/hooks/useAutoTopUpLimits.tsx deleted file mode 100644 index 2d8453475c..0000000000 --- a/apps/deploy-web/src/hooks/useAutoTopUpLimits.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { useCallback, useMemo } from "react"; -import type { ExactDepositDeploymentGrant, FeeAllowance } from "@akashnetwork/http-sdk"; -import { isFuture } from "date-fns"; -import invokeMap from "lodash/invokeMap"; - -import { useServices } from "@src/context/ServicesProvider"; -import { useWallet } from "@src/context/WalletProvider"; -import { useExactDeploymentGrantsQuery } from "@src/queries/useExactDeploymentGrantsQuery"; -import { useExactFeeAllowanceQuery } from "@src/queries/useExactFeeAllowanceQuery"; - -export const useAutoTopUpLimits = () => { - const { publicConfig } = useServices(); - const { address } = useWallet(); - const uaktFeeAllowance = useExactFeeAllowanceQuery(address, publicConfig.NEXT_PUBLIC_UAKT_TOP_UP_MASTER_WALLET_ADDRESS, { enabled: false }); - const uaktDeploymentGrant = useExactDeploymentGrantsQuery(address, publicConfig.NEXT_PUBLIC_UAKT_TOP_UP_MASTER_WALLET_ADDRESS, { enabled: false }); - const usdcFeeAllowance = useExactFeeAllowanceQuery(address, publicConfig.NEXT_PUBLIC_USDC_TOP_UP_MASTER_WALLET_ADDRESS, { enabled: false }); - const usdcDeploymentGrant = useExactDeploymentGrantsQuery(address, publicConfig.NEXT_PUBLIC_USDC_TOP_UP_MASTER_WALLET_ADDRESS, { enabled: false }); - const uaktFeeLimit = useMemo(() => extractFeeLimit(uaktFeeAllowance.data), [uaktFeeAllowance.data]); - const usdcFeeLimit = useMemo(() => extractFeeLimit(usdcFeeAllowance.data), [usdcFeeAllowance.data]); - const uaktDeploymentLimit = useMemo(() => extractDeploymentLimit(uaktDeploymentGrant.data), [uaktDeploymentGrant.data]); - const usdcDeploymentLimit = useMemo(() => extractDeploymentLimit(usdcDeploymentGrant.data), [usdcDeploymentGrant.data]); - - const earliestExpiration = useMemo(() => { - const expirations = [ - uaktFeeAllowance.data?.allowance.expiration, - uaktDeploymentGrant.data?.expiration, - usdcFeeAllowance.data?.allowance.expiration, - usdcDeploymentGrant.data?.expiration - ] - .filter(Boolean) - .map(expiration => new Date(expiration!)); - - if (!expirations.length) { - return undefined; - } - - return expirations.reduce((acc, date) => { - if (date < acc) { - return date; - } - - return acc; - }); - }, [ - uaktDeploymentGrant.data?.expiration, - uaktFeeAllowance.data?.allowance.expiration, - usdcDeploymentGrant.data?.expiration, - usdcFeeAllowance.data?.allowance.expiration - ]); - - const fetch = useCallback( - async () => await Promise.all([invokeMap([uaktFeeAllowance, uaktDeploymentGrant, usdcFeeAllowance, usdcDeploymentGrant], "refetch")]), - [uaktFeeAllowance, uaktDeploymentGrant, usdcFeeAllowance, usdcDeploymentGrant] - ); - - return { - fetch, - uaktFeeLimit, - usdcFeeLimit, - uaktDeploymentLimit, - usdcDeploymentLimit, - expiration: earliestExpiration - }; -}; - -function extractDeploymentLimit(deploymentGrant?: (ExactDepositDeploymentGrant & { granter: string; grantee: string }) | null) { - if (!deploymentGrant) { - return undefined; - } - - const isExpired = !isFuture(new Date(deploymentGrant.expiration)); - - if (isExpired) { - return undefined; - } - - return parseFloat(deploymentGrant?.authorization.spend_limits[0].amount); -} - -function extractFeeLimit(feeLimit?: FeeAllowance) { - if (!feeLimit) { - return undefined; - } - - const isExpired = !isFuture(new Date(feeLimit.allowance.expiration)); - - if (isExpired) { - return undefined; - } - - return parseFloat(feeLimit.allowance.spend_limit[0].amount); -} diff --git a/apps/deploy-web/src/hooks/useAutoTopUpService.ts b/apps/deploy-web/src/hooks/useAutoTopUpService.ts deleted file mode 100644 index b3a5e0eb94..0000000000 --- a/apps/deploy-web/src/hooks/useAutoTopUpService.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { useMemo } from "react"; - -import { USDC_IBC_DENOMS } from "@src/config/denom.config"; -import { AutoTopUpMessageService } from "@src/services/auto-top-up-message/auto-top-up-message.service"; -import networkStore from "@src/store/networkStore"; - -export const useAutoTopUpService = () => { - const selectedNetworkId = networkStore.useSelectedNetworkId(); - // BUGALERT: there is no testnet network in USDC_IBC_DENOMS - const usdcDenom = USDC_IBC_DENOMS[selectedNetworkId as keyof typeof USDC_IBC_DENOMS] as string; - - return useMemo(() => new AutoTopUpMessageService(usdcDenom), [usdcDenom]); -}; diff --git a/apps/deploy-web/src/lib/nextjs/pageGuards/selfCustody.ts b/apps/deploy-web/src/lib/nextjs/pageGuards/selfCustody.ts index b725a94e6c..66d07ca7a3 100644 --- a/apps/deploy-web/src/lib/nextjs/pageGuards/selfCustody.ts +++ b/apps/deploy-web/src/lib/nextjs/pageGuards/selfCustody.ts @@ -2,7 +2,7 @@ import type { FeatureFlag } from "@src/types/feature-flags"; import type { AppTypedContext } from "../defineServerSideProps/defineServerSideProps"; import { isFeatureEnabled } from "./pageGuards"; -export const SELF_CUSTODY_ROUTES = ["/get-started/wallet", "/mint-burn", "/settings", "/settings/authorizations"]; +export const SELF_CUSTODY_ROUTES = ["/get-started/wallet", "/mint-burn", "/settings"]; export function isSelfCustodyEnabled(context: AppTypedContext): Promise { return isFeatureEnabled("self_custody" satisfies FeatureFlag, context); diff --git a/apps/deploy-web/src/pages/settings/authorizations/index.tsx b/apps/deploy-web/src/pages/settings/authorizations/index.tsx deleted file mode 100644 index ae0b3b46f3..0000000000 --- a/apps/deploy-web/src/pages/settings/authorizations/index.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { Authorizations } from "@src/components/authorizations/Authorizations"; -import { Guard } from "@src/hoc/guard/guard.hoc"; -import { useIsSelfCustodyAccessible } from "@src/hoc/guard/useIsSelfCustodyAccessible"; -import { defineServerSideProps } from "@src/lib/nextjs/defineServerSideProps/defineServerSideProps"; -import { isSelfCustodyEnabled } from "@src/lib/nextjs/pageGuards/selfCustody"; - -const AuthorizationsPage: React.FunctionComponent = () => { - return ; -}; - -export default Guard(AuthorizationsPage, useIsSelfCustodyAccessible); - -export const getServerSideProps = defineServerSideProps({ - route: "/settings/authorizations", - if: ctx => isSelfCustodyEnabled(ctx) -}); diff --git a/apps/deploy-web/src/queries/queryKeys.ts b/apps/deploy-web/src/queries/queryKeys.ts index c79298a8b4..f5ec3e88ad 100644 --- a/apps/deploy-web/src/queries/queryKeys.ts +++ b/apps/deploy-web/src/queries/queryKeys.ts @@ -20,11 +20,6 @@ export class QueryKeys { static getTemplateKey = (id: string) => ["SDL_TEMPLATES", id]; static getUserTemplatesKey = (username: string) => ["USER_TEMPLATES", username]; static getUserFavoriteTemplatesKey = (userId: string) => ["USER_FAVORITES_TEMPLATES", userId]; - static getGranterGrants = (address: string, page: number, offset: number) => ["GRANTER_GRANTS", address, page, offset]; - static getGranteeGrants = (address: string) => ["GRANTEE_GRANTS", address]; - static getAllowancesIssued = (address: string, page: number, offset: number) => ["ALLOWANCES_ISSUED", address, page, offset]; - static getAllowancesGranted = (address: string) => ["ALLOWANCES_GRANTED", address]; - // Deploy static getDeploymentListKey = (address: string) => ["DEPLOYMENT_LIST", address]; static getDeploymentDetailKey = (address: string, dseq?: string) => ["DEPLOYMENT_DETAIL", address, dseq].filter(Boolean); @@ -69,9 +64,6 @@ export class QueryKeys { static getPackageJsonKey = (repo?: string, branch?: string, subFolder?: string) => ["PACKAGE_JSON", repo, branch, subFolder]; static getSrcFoldersKey = (repo?: string, branch?: string) => ["SRC_FOLDERS", repo, branch]; - static getDeploymentGrantsKey = (granter: string, grantee: string) => ["DEPLOYMENT_GRANT", granter, grantee]; - static getFeeAllowancesKey = (granter: string, grantee: string) => ["FEE_ALLOWANCE", granter, grantee]; - static getFeatureFlagsKey = (networkId: string) => ["FEATURE_FLAGS", networkId]; static getPaymentMethodsKey = () => ["PAYMENT_METHODS"]; diff --git a/apps/deploy-web/src/queries/useExactDeploymentGrantsQuery.ts b/apps/deploy-web/src/queries/useExactDeploymentGrantsQuery.ts deleted file mode 100644 index 0ae7ed568e..0000000000 --- a/apps/deploy-web/src/queries/useExactDeploymentGrantsQuery.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; - -import { useServices } from "@src/context/ServicesProvider/ServicesProvider"; -import { QueryKeys } from "@src/queries/queryKeys"; - -export function useExactDeploymentGrantsQuery(granter: string, grantee: string, { enabled = true } = {}) { - const { authzHttpService } = useServices(); - return useQuery({ - queryKey: QueryKeys.getDeploymentGrantsKey(granter, grantee), - queryFn: async () => { - const grant = await authzHttpService.getValidDepositDeploymentGrantsForGranterAndGrantee(granter, grantee); - return grant ? { ...grant, granter, grantee } : null; - }, - enabled - }); -} diff --git a/apps/deploy-web/src/queries/useExactFeeAllowanceQuery.ts b/apps/deploy-web/src/queries/useExactFeeAllowanceQuery.ts deleted file mode 100644 index 903c765a77..0000000000 --- a/apps/deploy-web/src/queries/useExactFeeAllowanceQuery.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; - -import { useServices } from "@src/context/ServicesProvider"; -import { QueryKeys } from "@src/queries/queryKeys"; - -export function useExactFeeAllowanceQuery(granter: string, grantee: string, { enabled = true } = {}) { - const { authzHttpService } = useServices(); - return useQuery({ - queryKey: QueryKeys.getFeeAllowancesKey(granter, grantee), - queryFn: () => authzHttpService.getFeeAllowanceForGranterAndGrantee(granter, grantee), - enabled - }); -} diff --git a/apps/deploy-web/src/queries/useGrantsQuery.spec.tsx b/apps/deploy-web/src/queries/useGrantsQuery.spec.tsx deleted file mode 100644 index 321e4b0298..0000000000 --- a/apps/deploy-web/src/queries/useGrantsQuery.spec.tsx +++ /dev/null @@ -1,220 +0,0 @@ -import type { AuthzHttpService } from "@akashnetwork/http-sdk"; -import { faker } from "@faker-js/faker"; -import { describe, expect, it, vi } from "vitest"; -import { mock } from "vitest-mock-extended"; - -import type { SettingsContextType } from "@src/context/SettingsProvider/SettingsProviderContext"; -import { SettingsProviderContext } from "@src/context/SettingsProvider/SettingsProviderContext"; -import type { FallbackableHttpClient } from "@src/services/createFallbackableHttpClient/createFallbackableHttpClient"; -import { useAllowancesGranted, useAllowancesIssued, useGranteeGrants, useGranterGrants } from "./useGrantsQuery"; - -import {} from "@testing-library/react"; -import { setupQuery } from "@tests/unit/query-client"; - -const createMockSettingsContext = (): SettingsContextType => - mock({ - settings: { - apiEndpoint: "https://api.example.com", - rpcEndpoint: "https://rpc.example.com", - isCustomNode: false, - nodes: [], - selectedNode: null, - customNode: null, - isBlockchainDown: false - }, - setSettings: vi.fn(), - isLoadingSettings: false, - isSettingsInit: true, - refreshNodeStatuses: vi.fn(), - isRefreshingNodeStatus: false - }); - -const MockSettingsProvider = ({ children }: { children: React.ReactNode }) => { - const mockSettings = createMockSettingsContext(); - - return {children}; -}; - -describe("useGrantsQuery", () => { - describe(useGranterGrants.name, () => { - it("fetches granter grants when address is provided", async () => { - const mockData = { - grants: [ - { - authorization: { - "@type": "/akash.escrow.v1.DepositAuthorization" - } - } - ], - pagination: { total: 1 } - }; - - const authzHttpService = mock({ - isReady: true, - getPaginatedDepositDeploymentGrants: vi.fn().mockResolvedValue(mockData) - }); - const { result } = setupQuery(() => useGranterGrants("test-address", 0, 1000), { - services: { - authzHttpService: () => authzHttpService - }, - wrapper: ({ children }) => {children} - }); - - await vi.waitFor(() => { - expect(authzHttpService.getPaginatedDepositDeploymentGrants).toHaveBeenCalledWith({ granter: "test-address", limit: 1000, offset: 0 }); - expect(result.current.isSuccess).toBe(true); - expect(result.current.data).toEqual(mockData); - }); - }); - - it("does not fetch when address is not provided", () => { - const authzHttpService = mock({ - isReady: true, - getPaginatedDepositDeploymentGrants: vi.fn().mockResolvedValue([]) - }); - setupQuery(() => useGranterGrants("", 0, 1000), { - services: { - authzHttpService: () => authzHttpService - }, - wrapper: ({ children }) => {children} - }); - - expect(authzHttpService.getPaginatedDepositDeploymentGrants).not.toHaveBeenCalled(); - }); - }); - - describe(useGranteeGrants.name, () => { - it("fetches grantee grants when address is provided", async () => { - const mockData = [ - { - authorization: { - "@type": "/akash.escrow.v1.DepositAuthorization" - } - } - ]; - const authzHttpService = mock({ - isReady: true, - getAllDepositDeploymentGrants: vi.fn().mockResolvedValue(mockData) - }); - - const { result } = setupQuery(() => useGranteeGrants("test-address"), { - services: { - authzHttpService: () => authzHttpService - }, - wrapper: ({ children }) => {children} - }); - - await vi.waitFor(() => { - expect(authzHttpService.getAllDepositDeploymentGrants).toHaveBeenCalledWith({ grantee: "test-address", limit: 1000 }); - expect(result.current.isSuccess).toBe(true); - expect(result.current.data).toEqual(mockData); - }); - }); - - it("does not fetch when address is not provided", () => { - const authzHttpService = mock({ - isReady: true, - getAllDepositDeploymentGrants: vi.fn().mockResolvedValue([]) - }); - setupQuery(() => useGranteeGrants(""), { - services: { - authzHttpService: () => authzHttpService - }, - wrapper: ({ children }) => {children} - }); - - expect(authzHttpService.getAllDepositDeploymentGrants).not.toHaveBeenCalled(); - }); - }); - - describe(useAllowancesIssued.name, () => { - it("fetches allowances issued when address is provided", async () => { - const mockData = { - allowances: [{ id: faker.string.uuid() }], - pagination: { total: 1 } - }; - const authzHttpService = mock({ - isReady: true, - getPaginatedFeeAllowancesForGranter: vi.fn().mockResolvedValue(mockData) - }); - - const { result } = setupQuery(() => useAllowancesIssued("test-address", 0, 1000), { - services: { - authzHttpService: () => authzHttpService - }, - wrapper: ({ children }) => {children} - }); - - await vi.waitFor(() => { - expect(authzHttpService.getPaginatedFeeAllowancesForGranter).toHaveBeenCalledWith("test-address", 1000, 0); - expect(result.current.isSuccess).toBe(true); - expect(result.current.data).toEqual(mockData); - }); - }); - - it("does not fetch when address is not provided", () => { - const authzHttpService = mock({ - isReady: true, - getPaginatedFeeAllowancesForGranter: vi.fn().mockResolvedValue([]) - }); - setupQuery(() => useAllowancesIssued("", 0, 1000), { - services: { - authzHttpService: () => authzHttpService - }, - wrapper: ({ children }) => {children} - }); - - expect(authzHttpService.getPaginatedFeeAllowancesForGranter).not.toHaveBeenCalled(); - }); - }); - - describe(useAllowancesGranted.name, () => { - it("fetches allowances granted when address is provided", async () => { - const mockData = [{ id: faker.string.uuid() }]; - const chainApiHttpClient = mock({ - isFallbackEnabled: false, - get: vi.fn().mockResolvedValue({ - data: { - allowances: mockData, - pagination: { next_key: null, total: mockData.length } - } - }) - } as any); - - const { result } = setupQuery(() => useAllowancesGranted("test-address"), { - services: { - chainApiHttpClient: () => chainApiHttpClient - }, - wrapper: ({ children }) => {children} - }); - - await vi.waitFor(() => { - expect(chainApiHttpClient.get).toHaveBeenCalledWith( - expect.stringContaining("/cosmos/feegrant/v1beta1/allowances/test-address?pagination.limit=1000&pagination.count_total=true") - ); - expect(result.current.isSuccess).toBe(true); - expect(result.current.data).toEqual(mockData); - }); - }); - - it("does not fetch when address is not provided", () => { - const chainApiHttpClient = mock({ - isFallbackEnabled: false, - get: vi.fn().mockResolvedValue({ - data: { - allowances: [], - pagination: { next_key: null, total: 0 } - } - }) - } as any); - setupQuery(() => useAllowancesGranted(""), { - services: { - chainApiHttpClient: () => chainApiHttpClient - }, - wrapper: ({ children }) => {children} - }); - - expect(chainApiHttpClient.get).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/apps/deploy-web/src/queries/useGrantsQuery.ts b/apps/deploy-web/src/queries/useGrantsQuery.ts deleted file mode 100644 index cf08858e0b..0000000000 --- a/apps/deploy-web/src/queries/useGrantsQuery.ts +++ /dev/null @@ -1,69 +0,0 @@ -import type { DepositDeploymentGrant } from "@akashnetwork/http-sdk"; -import type { UseQueryOptions } from "@tanstack/react-query"; -import { useQuery } from "@tanstack/react-query"; -import type { AxiosInstance } from "axios"; - -import { useServices } from "@src/context/ServicesProvider"; -import type { AllowanceType, PaginatedAllowanceType, PaginatedGrantType } from "@src/types/grant"; -import { ApiUrlService, loadWithPagination } from "@src/utils/apiUtils"; -import { QueryKeys } from "./queryKeys"; - -export function useGranterGrants( - address: string, - page: number, - limit: number, - options: Omit, "queryKey" | "queryFn"> = {} -) { - const { authzHttpService, chainApiHttpClient } = useServices(); - const offset = page * limit; - - return useQuery({ - queryKey: QueryKeys.getGranterGrants(address, page, offset), - queryFn: () => authzHttpService.getPaginatedDepositDeploymentGrants({ granter: address, limit, offset }), - ...options, - enabled: options.enabled !== false && !!address && !chainApiHttpClient.isFallbackEnabled - }); -} - -export function useGranteeGrants(address: string, options: Omit, "queryKey" | "queryFn"> = {}) { - const { authzHttpService, chainApiHttpClient } = useServices(); - - return useQuery({ - queryKey: QueryKeys.getGranteeGrants(address || "UNDEFINED"), - queryFn: () => authzHttpService.getAllDepositDeploymentGrants({ grantee: address, limit: 1000 }), - ...options, - enabled: options.enabled !== false && !!address && !chainApiHttpClient.isFallbackEnabled - }); -} - -export function useAllowancesIssued( - address: string, - page: number, - limit: number, - options: Omit, "queryKey" | "queryFn"> = {} -) { - const { authzHttpService, chainApiHttpClient } = useServices(); - const offset = page * limit; - - return useQuery({ - queryKey: QueryKeys.getAllowancesIssued(address, page, offset), - queryFn: () => authzHttpService.getPaginatedFeeAllowancesForGranter(address, limit, offset), - ...options, - enabled: options.enabled !== false && !!address && !chainApiHttpClient.isFallbackEnabled - }); -} - -async function getAllowancesGranted(chainApiHttpClient: AxiosInstance, address: string) { - return await loadWithPagination(ApiUrlService.allowancesGranted("", address), "allowances", 1000, chainApiHttpClient); -} - -export function useAllowancesGranted(address: string, options: Omit, "queryKey" | "queryFn"> = {}) { - const { chainApiHttpClient } = useServices(); - - return useQuery({ - queryKey: address ? QueryKeys.getAllowancesGranted(address) : [], - queryFn: () => getAllowancesGranted(chainApiHttpClient, address), - ...options, - enabled: options.enabled !== false && !!address && !chainApiHttpClient.isFallbackEnabled - }); -} diff --git a/apps/deploy-web/src/services/auto-top-up-message/auto-top-up-message.service.ts b/apps/deploy-web/src/services/auto-top-up-message/auto-top-up-message.service.ts deleted file mode 100644 index 9e9b5890ea..0000000000 --- a/apps/deploy-web/src/services/auto-top-up-message/auto-top-up-message.service.ts +++ /dev/null @@ -1,141 +0,0 @@ -import type { EncodeObject } from "@cosmjs/proto-signing"; - -import { browserEnvConfig } from "@src/config/browser-env.config"; -import { TransactionMessageData } from "@src/utils/TransactionMessageData"; - -interface LimitCollectorInput { - granter: string; - grantee: string; - denom?: string; - prev?: { - limit?: number; - expiration?: Date; - }; - next?: { - limit: number; - expiration: Date; - }; -} - -interface LimitState { - uaktFeeLimit: number; - usdcFeeLimit: number; - uaktDeploymentLimit: number; - usdcDeploymentLimit: number; - expiration: Date; -} - -interface CollectionInput { - granter: string; - prev: Partial; - next?: LimitState; -} - -export class AutoTopUpMessageService { - constructor(private readonly usdcDenom: string) {} - - collectMessages(options: CollectionInput): EncodeObject[] { - const uaktSides = { - granter: options.granter, - grantee: browserEnvConfig.NEXT_PUBLIC_UAKT_TOP_UP_MASTER_WALLET_ADDRESS - }; - const usdcSides = { - granter: options.granter, - grantee: browserEnvConfig.NEXT_PUBLIC_USDC_TOP_UP_MASTER_WALLET_ADDRESS - }; - - return [ - ...this.collectFeeMessages({ - ...uaktSides, - prev: { - limit: options.prev.uaktFeeLimit, - expiration: options.prev.expiration - }, - next: options.next && { - limit: options.next.uaktFeeLimit, - expiration: options.next.expiration - } - }), - ...this.collectFeeMessages({ - ...usdcSides, - prev: { - limit: options.prev.usdcFeeLimit, - expiration: options.prev.expiration - }, - next: options.next && { - limit: options.next.usdcFeeLimit, - expiration: options.next.expiration - } - }), - ...this.collectDeploymentMessages({ - ...uaktSides, - denom: "uakt", - prev: { - limit: options.prev.uaktDeploymentLimit, - expiration: options.prev.expiration - }, - next: options.next && { - limit: options.next.uaktDeploymentLimit, - expiration: options.next.expiration - } - }), - ...this.collectDeploymentMessages({ - ...usdcSides, - denom: this.usdcDenom, - prev: { - limit: options.prev.usdcDeploymentLimit, - expiration: options.prev.expiration - }, - next: options.next && { - limit: options.next.usdcDeploymentLimit, - expiration: options.next.expiration - } - }) - ]; - } - - private collectFeeMessages(options: LimitCollectorInput): EncodeObject[] { - const messages: EncodeObject[] = []; - const isSameExpiration = options.prev?.expiration?.getTime() === options.next?.expiration.getTime(); - const isSameLimit = options.prev?.limit === options.next?.limit; - - if (isSameExpiration && isSameLimit) { - return messages; - } - - if (typeof options.prev?.limit !== "undefined") { - messages.push(TransactionMessageData.getRevokeAllowanceMsg(options.granter, options.grantee)); - } - - if (options.next?.limit) { - messages.push(TransactionMessageData.getGrantBasicAllowanceMsg(options.granter, options.grantee, options.next.limit, "uakt", options.next.expiration)); - } - - return messages; - } - - private collectDeploymentMessages(options: LimitCollectorInput): EncodeObject[] { - const messages: EncodeObject[] = []; - const isSameExpiration = options.prev?.expiration?.getTime() === options.next?.expiration.getTime(); - const isSameLimit = options.prev?.limit === options.next?.limit; - - if (isSameExpiration && isSameLimit) { - return messages; - } - - if (options.next?.limit) { - messages.push( - TransactionMessageData.getGrantMsg( - options.granter, - options.grantee, - { amount: options.next.limit.toString(), denom: options.denom || "uakt" }, - options.next.expiration - ) - ); - } else if (typeof options.prev?.limit !== "undefined") { - messages.push(TransactionMessageData.getRevokeDepositMsg(options.granter, options.grantee)); - } - - return messages; - } -} diff --git a/apps/deploy-web/src/types/feature-flags.ts b/apps/deploy-web/src/types/feature-flags.ts index 072966d181..9f2d701c1b 100644 --- a/apps/deploy-web/src/types/feature-flags.ts +++ b/apps/deploy-web/src/types/feature-flags.ts @@ -3,7 +3,6 @@ export type FeatureFlag = | "notifications_general_alerts_update" | "ui_deployment_closed_alert" | "billing_usage" - | "custodial_auto_topup" | "ui_sdl_log_collector_enabled" | "maintenance_banner" | "auto_credit_reload" diff --git a/apps/deploy-web/src/types/grant.ts b/apps/deploy-web/src/types/grant.ts deleted file mode 100644 index 746761b816..0000000000 --- a/apps/deploy-web/src/types/grant.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { DepositDeploymentGrant } from "@akashnetwork/http-sdk"; - -export type GrantType = DepositDeploymentGrant; - -export type AllowanceType = { - granter: string; - grantee: string; - allowance: { - "@type": string; - expiration: string; - spend_limit: { - denom: string; - amount: string; - }[]; - }; -}; - -export type PaginatedAllowanceType = { - allowances: AllowanceType[]; - pagination?: { - next_key: string | null; - total: number; - }; -}; - -export type PaginatedGrantType = { - grants: GrantType[]; - pagination?: { - next_key: string | null; - total: number; - }; -}; diff --git a/apps/deploy-web/src/utils/TransactionMessageData.ts b/apps/deploy-web/src/utils/TransactionMessageData.ts index 0bf5c6f465..6d12b23ab8 100644 --- a/apps/deploy-web/src/utils/TransactionMessageData.ts +++ b/apps/deploy-web/src/utils/TransactionMessageData.ts @@ -1,11 +1,9 @@ -import { DepositAuthorization, DepositAuthorization_Scope, MsgAccountDeposit, Scope, Source } from "@akashnetwork/chain-sdk/private-types/akash.v1"; +import { MsgAccountDeposit, Scope, Source } from "@akashnetwork/chain-sdk/private-types/akash.v1"; import { MsgBurnACT, MsgCreateCertificate, MsgMintACT, MsgRevokeCertificate } from "@akashnetwork/chain-sdk/private-types/akash.v1"; import { MsgCloseDeployment, MsgCreateDeployment, MsgUpdateDeployment } from "@akashnetwork/chain-sdk/private-types/akash.v1beta4"; import { MsgUpdateProvider } from "@akashnetwork/chain-sdk/private-types/akash.v1beta4"; import { MsgCreateLease } from "@akashnetwork/chain-sdk/private-types/akash.v1beta5"; -import type { Coin } from "@akashnetwork/chain-sdk/private-types/cosmos.v1beta1"; import { MsgSend } from "@akashnetwork/chain-sdk/private-types/cosmos.v1beta1"; -import { BasicAllowance, MsgGrant, MsgGrantAllowance, MsgRevoke, MsgRevokeAllowance } from "@akashnetwork/chain-sdk/private-types/cosmos.v1beta1"; import type { BidDto, NewDeploymentData } from "@src/types/deployment"; @@ -121,110 +119,6 @@ export class TransactionMessageData { }; } - static getGrantMsg(granter: string, grantee: string, spendLimit: Coin | Coin[], expiration: Date) { - const authorization = Array.isArray(spendLimit) - ? DepositAuthorization.fromPartial({ - spendLimit: { - denom: spendLimit[0]?.denom ?? "uakt", - amount: "0" - }, - spendLimits: spendLimit, - scopes: [DepositAuthorization_Scope.deployment] - }) - : DepositAuthorization.fromPartial({ - spendLimit, - scopes: [DepositAuthorization_Scope.deployment] - }); - return { - typeUrl: `/${MsgGrant.$type}`, - value: MsgGrant.fromPartial({ - granter, - grantee, - grant: { - authorization: { - typeUrl: `/${DepositAuthorization.$type}`, - value: DepositAuthorization.encode(authorization).finish() - }, - expiration - } - }) - }; - } - - static getRevokeDepositMsg(granter: string, grantee: string) { - return { - typeUrl: `/${MsgRevoke.$type}`, - value: MsgRevoke.fromPartial({ - granter: granter, - grantee: grantee, - msgTypeUrl: `/${MsgAccountDeposit.$type}` - }) - }; - } - - static getGrantBasicAllowanceMsg(granter: string, grantee: string, spendLimit: number, denom: string, expiration?: Date) { - const allowance = { - typeUrl: `/${BasicAllowance.$type}`, - value: Uint8Array.from( - BasicAllowance.encode({ - spendLimit: [ - { - denom: denom, - amount: spendLimit.toString() - } - ], - expiration - }).finish() - ) - }; - - return { - typeUrl: `/${MsgGrantAllowance.$type}`, - value: MsgGrantAllowance.fromPartial({ - granter: granter, - grantee: grantee, - allowance: allowance - }) - }; - } - - // static getGrantPeriodicAllowanceMsg(granter: string, grantee: string, spendLimit: number, denom: string, expiration?: Date) { - // const message = { - // typeUrl: TransactionMessageData.Types.MSG_GRANT_ALLOWANCE, - // value: { - // granter: granter, - // grantee: grantee, - // allowance: { - // typeUrl: "/cosmos.feegrant.v1beta1.PeriodicAllowance", - // value: { - // spend_limit: [{ denom: denom, amount: spendLimit.toString() }], - // // Can be undefined, the grant will be valid forever - // expiration: undefined - // } - // } - // } - // }; - - // if (expiration) { - // message.value.allowance.value.expiration = { - // seconds: Math.floor(expiration.getTime() / 1_000), // Convert milliseconds to seconds - // nanos: Math.floor((expiration.getTime() % 1_000) * 1_000_000) // Convert reminder into nanoseconds - // }; - // } - - // return message; - // } - - static getRevokeAllowanceMsg(granter: string, grantee: string) { - return { - typeUrl: `/${MsgRevokeAllowance.$type}`, - value: MsgRevokeAllowance.fromPartial({ - granter: granter, - grantee: grantee - }) - }; - } - static getMintACTMsg(owner: string, amount: number, denom: string) { return { typeUrl: `/${MsgMintACT.$type}`, diff --git a/apps/deploy-web/src/utils/address.ts b/apps/deploy-web/src/utils/address.ts deleted file mode 100644 index 8b00fd5ee0..0000000000 --- a/apps/deploy-web/src/utils/address.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { fromBech32 } from "@cosmjs/encoding"; - -export function isValidBech32Address(address: string, prefix?: string) { - const bech32 = parseBech32(address); - - return bech32 && (!prefix || bech32.prefix === prefix); -} - -export function parseBech32(str: string) { - try { - return fromBech32(str); - } catch { - return null; - } -} diff --git a/apps/deploy-web/src/utils/grants.ts b/apps/deploy-web/src/utils/grants.ts deleted file mode 100644 index 414ab06ffb..0000000000 --- a/apps/deploy-web/src/utils/grants.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { BasicAllowance, PeriodicAllowance } from "@akashnetwork/chain-sdk/private-types/cosmos.v1beta1"; - -import type { AllowanceType } from "@src/types/grant"; - -export const getAllowanceTitleByType = (allowance: AllowanceType) => { - switch (allowance.allowance["@type"]) { - case `/${BasicAllowance.$type}`: - return "Basic"; - case `/${PeriodicAllowance.$type}`: - return "Periodic"; - case "$CONNECTED_WALLET": - return "Connected Wallet"; - default: - return "Unknown"; - } -}; diff --git a/apps/deploy-web/src/utils/urlUtils.ts b/apps/deploy-web/src/utils/urlUtils.ts index 1529329d39..647a405558 100644 --- a/apps/deploy-web/src/utils/urlUtils.ts +++ b/apps/deploy-web/src/utils/urlUtils.ts @@ -97,7 +97,6 @@ export class UrlService { static newNotificationChannel = () => "/alerts/notification-channels/new"; static notificationChannelDetails = (id: string) => `/alerts/notification-channels/${id}`; static settings = () => "/settings"; - static settingsAuthorizations = () => "/settings/authorizations"; static newDeployment = (params: NewDeploymentParams = {}) => { const {