diff --git a/package-lock.json b/package-lock.json index 8eaafac..cf2be2c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@fontsource/roboto": "^5.0.12", "@glif/filecoin-address": "^3.0.4", "@ledgerhq/hw-transport-webusb": "^6.27.19", + "@mui/icons-material": "^5.15.14", "@mui/material": "^5.15.14", "@radix-ui/react-avatar": "^1.0.3", "@radix-ui/react-dialog": "^1.0.5", @@ -3434,6 +3435,31 @@ "url": "https://opencollective.com/mui-org" } }, + "node_modules/@mui/icons-material": { + "version": "5.15.14", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.14.tgz", + "integrity": "sha512-vj/51k7MdFmt+XVw94sl30SCvGx6+wJLsNYjZRgxhS6y3UtnWnypMOsm3Kmg8TN+P0dqwsjy4/fX7B1HufJIhw==", + "dependencies": { + "@babel/runtime": "^7.23.9" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^5.0.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@mui/material": { "version": "5.15.14", "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.14.tgz", @@ -17776,6 +17802,14 @@ "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.14.tgz", "integrity": "sha512-on75VMd0XqZfaQW+9pGjSNiqW+ghc5E2ZSLRBXwcXl/C4YzjfyjrLPhrEpKnR9Uym9KXBvxrhoHfPcczYHweyA==" }, + "@mui/icons-material": { + "version": "5.15.14", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.14.tgz", + "integrity": "sha512-vj/51k7MdFmt+XVw94sl30SCvGx6+wJLsNYjZRgxhS6y3UtnWnypMOsm3Kmg8TN+P0dqwsjy4/fX7B1HufJIhw==", + "requires": { + "@babel/runtime": "^7.23.9" + } + }, "@mui/material": { "version": "5.15.14", "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.14.tgz", diff --git a/package.json b/package.json index fce966d..6f50c48 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@fontsource/roboto": "^5.0.12", "@glif/filecoin-address": "^3.0.4", "@ledgerhq/hw-transport-webusb": "^6.27.19", + "@mui/icons-material": "^5.15.14", "@mui/material": "^5.15.14", "@radix-ui/react-avatar": "^1.0.3", "@radix-ui/react-dialog": "^1.0.5", diff --git a/src/components/cards/AppInfoCard.tsx b/src/components/cards/AppInfoCard.tsx index 8e8b88e..8ac35fc 100644 --- a/src/components/cards/AppInfoCard.tsx +++ b/src/components/cards/AppInfoCard.tsx @@ -51,6 +51,7 @@ import { } from 'react' import { toast } from 'react-toastify' import AllocatorBalance from '../AllocatorBalance' +import AllowedSps from './dialogs/allowedSps' interface ComponentProps { application: Application @@ -100,7 +101,10 @@ const AppInfoCard: React.FC = ({ loadMoreAccounts, mutationRequestKyc, mutationRemovePendingAllocation, + mutationChangeAllowedSPs, + mutationChangeAllowedSPsApproval, } = useApplicationActions(initialApplication, repo, owner) + const [buttonText, setButtonText] = useState('') const [modalMessage, setModalMessage] = useState(null) const [error, setError] = useState(false) @@ -340,7 +344,7 @@ const AppInfoCard: React.FC = ({ return } - if (isApiCalling) { + if (isApiCalling && application.Lifecycle.State !== 'ChangingSp') { setButtonText('Processing...') return } @@ -813,6 +817,87 @@ const AppInfoCard: React.FC = ({ setApiCalling(false) } + const handleAllowedSPsSubmit = async ( + client: string, + clientContractAddress: string, + addedSPs: string[], + removedSPs: string[], + newAvailableResult: string[], + maxDeviation?: string, + ): Promise => { + try { + setApiCalling(true) + const requestId = application['Allocation Requests'].find( + (alloc) => alloc.Active, + )?.ID + + const userName = session.data?.user?.githubUsername + + if ( + application.Lifecycle.State === 'ReadyToSign' && + requestId && + userName + ) { + await mutationChangeAllowedSPs.mutateAsync({ + userName, + clientAddress: client, + contractAddress: clientContractAddress, + allowedSps: addedSPs, + disallowedSPs: removedSPs, + newAvailableResult, + maxDeviation, + }) + } + } catch (error) { + console.log(error) + handleMutationError(error as Error) + } finally { + setApiCalling(false) + } + } + + const handleApproveAllowedSPs = async (): Promise => { + try { + setApiCalling(true) + + const activeRequest = application[ + 'Storage Providers Change Requests' + ].find((requests) => requests.Active) + + const userName = session.data?.user?.githubUsername + + if (activeRequest?.ID != null && userName != null) { + const res = await mutationChangeAllowedSPsApproval.mutateAsync({ + activeRequest, + userName, + }) + + if (res) { + const lastDatacapAllocation = getLastDatacapAllocation(res) + if (lastDatacapAllocation === undefined) { + throw new Error('No datacap allocation found') + } + const queryParams = [ + `client=${encodeURIComponent(res?.Client.Name)}`, + `messageCID=${encodeURIComponent( + lastDatacapAllocation.Signers[1]['Message CID'], + )}`, + `amount=${encodeURIComponent( + lastDatacapAllocation['Allocation Amount'], + )}`, + `notification=true`, + ].join('&') + + router.push(`/?${queryParams}`) + } + } + } catch (error) { + handleMutationError(error as Error) + } finally { + setApiCalling(false) + } + } + return ( <> = ({ - {modalMessage != null && ( = ({ error={error} /> )} - {(isApiCalling || isWalletConnecting) && (
)} -
@@ -954,6 +1036,72 @@ const AppInfoCard: React.FC = ({
+ {LDNActorType.Verifier === currentActorType && + walletConnected && + session?.data?.user?.name !== undefined && + application?.Lifecycle?.['On Chain Address'] && + application?.['Client Contract Address'] && + ['ReadyToSign', 'Granted'].includes( + application?.Lifecycle?.State, + ) && ( +
+ +
+ )} + + {!walletConnected && + currentActorType === LDNActorType.Verifier && + ![ + 'KYCRequested', + 'Submitted', + 'ChangesRequested', + 'AdditionalInfoRequired', + 'AdditionalInfoSubmitted', + ].includes(application?.Lifecycle?.State) && ( + + )} + + {LDNActorType.Verifier === currentActorType && + walletConnected && + session?.data?.user?.name !== undefined && + application?.Lifecycle?.['On Chain Address'] && + application?.['Client Contract Address'] && + ['ChangingSP'].includes(application?.Lifecycle?.State) && ( +
+ +
+ )} + {LDNActorType.Verifier === currentActorType ? ( session?.data?.user?.name !== undefined && application?.Lifecycle?.State !== 'Granted' ? ( @@ -1048,30 +1196,6 @@ const AppInfoCard: React.FC = ({ )} - - {!walletConnected && - currentActorType === LDNActorType.Verifier && - ![ - 'KYCRequested', - 'Submitted', - 'ChangesRequested', - 'AdditionalInfoRequired', - 'AdditionalInfoSubmitted', - ].includes(application?.Lifecycle?.State) && ( - - )} ) : ( progress > 75 && diff --git a/src/components/cards/dialogs/allowedSps/AllowedSps.tsx b/src/components/cards/dialogs/allowedSps/AllowedSps.tsx new file mode 100644 index 0000000..9fac62e --- /dev/null +++ b/src/components/cards/dialogs/allowedSps/AllowedSps.tsx @@ -0,0 +1,257 @@ +import { Button } from '@/components/ui/button' +import { Spinner } from '@/components/ui/spinner' +import useWallet from '@/hooks/useWallet' +import { Add, Delete } from '@mui/icons-material' +import { + Dialog, + DialogActions, + DialogContent, + DialogTitle, + FormControl, + IconButton, + InputAdornment, + InputLabel, + OutlinedInput, + TextField, +} from '@mui/material' +import { useEffect, useState } from 'react' +import { useQuery } from 'react-query' + +interface ComponentProps { + onSubmit: ( + client: string, + clientContractAddress: string, + added: string[], + removed: string[], + newAvailableResult: string[], + maxDeviation?: string, + ) => Promise + initDeviation: string + client: string + clientContractAddress: string + isApiCalling: boolean + setApiCalling: (isApiCalling: boolean) => void +} + +export const AllowedSPs: React.FC = ({ + client, + clientContractAddress, + initDeviation, + onSubmit, + isApiCalling, + setApiCalling, +}) => { + const [isDirty, setIsDirty] = useState(false) + const [isDialogOpen, setIsDialogOpen] = useState(false) + const [maxDeviation, setMaxDeviation] = useState(initDeviation ?? '') + const [data, setData] = useState(['']) + const [initData, setInitData] = useState(['']) + + const { getClientSPs, getClientConfig } = useWallet() + + const { data: availableAllowedSPs } = useQuery({ + queryKey: ['allowedSps', client], + queryFn: async () => await getClientSPs(client, clientContractAddress), + enabled: !!(client && clientContractAddress), + }) + + const { data: clientConfig } = useQuery({ + queryKey: ['clientConfig', client], + queryFn: async () => await getClientConfig(client, clientContractAddress), + enabled: !!(client && clientContractAddress), + }) + + const checkIsDirty = (currentData: string[]): void => { + setIsDirty(false) + + const set1 = new Set(availableAllowedSPs ?? ['']) + const set2 = new Set(currentData) + + if (set1.size !== set2.size) { + setIsDirty(true) + return + } + + set1.forEach((item) => { + if (!set2.has(item)) { + setIsDirty(true) + } + }) + } + + const handleInputChange = (index: number, value: string): void => { + const newData = [...data] + newData[index] = value + + setData(newData) + checkIsDirty(newData) + } + + const handleAddItem = (): void => { + const newData = [...data, ''] + + setData(newData) + checkIsDirty(newData) + } + + const handleRemoveItem = (index: number): void => { + const newData = data.filter((_, i) => i !== index) + setData(newData) + checkIsDirty(newData) + } + + const handleSubmit = async (): Promise => { + try { + setApiCalling(true) + const added = data.filter( + (item) => !availableAllowedSPs?.includes(item), + ) ?? [''] + + const removed: string[] = availableAllowedSPs?.filter( + (item) => !data.includes(item), + ) ?? [''] + + const afterAdd = [...(availableAllowedSPs ?? ['']), ...added] + const newAvailableResult = afterAdd.filter( + (item) => !removed.includes(item), + ) + + let maxDeviationResult: string | undefined + + if (clientConfig && clientConfig !== maxDeviation) { + maxDeviationResult = maxDeviation + } + + if (!clientConfig) { + maxDeviationResult = maxDeviation + } + setIsDialogOpen(false) + await onSubmit( + client, + clientContractAddress, + added, + removed, + newAvailableResult, + maxDeviationResult, + ) + } catch (error) { + console.log(error) + } finally { + setData(['']) + setInitData(['']) + setApiCalling(false) + } + } + + useEffect(() => { + if (availableAllowedSPs?.length) { + setData(availableAllowedSPs) + setInitData(availableAllowedSPs) + } + }, [availableAllowedSPs]) + + return ( + <> + + + { + setIsDialogOpen(false) + setData(initData) + }} + fullWidth + > + Change allowed SPs + + {isApiCalling ? ( +
+ +
+ ) : null} + + Max Deviation + %} + disabled={true} // make it dynamically in the future + label="Max Deviation" + value={maxDeviation ?? ''} + onChange={(event: React.ChangeEvent) => { + setMaxDeviation(event.target.value) + }} + /> + +
+
SP count: {data.length}
+ {data.map((item, index) => ( +
+ { + handleInputChange(index, e.target.value) + }} + className="flex-1" + /> + { + handleRemoveItem(index) + }} + color="secondary" + > + + +
+ ))} +
+ + + +
+
+
+ + + + + +
+ + ) +} diff --git a/src/components/cards/dialogs/allowedSps/index.tsx b/src/components/cards/dialogs/allowedSps/index.tsx new file mode 100644 index 0000000..e8959ee --- /dev/null +++ b/src/components/cards/dialogs/allowedSps/index.tsx @@ -0,0 +1,3 @@ +import { AllowedSPs } from './AllowedSps' + +export default AllowedSPs diff --git a/src/hooks/useApplicationActions.ts b/src/hooks/useApplicationActions.ts index 8d5a13a..34d5ef9 100644 --- a/src/hooks/useApplicationActions.ts +++ b/src/hooks/useApplicationActions.ts @@ -7,13 +7,20 @@ import { postApplicationProposal, postApplicationTrigger, postApproveChanges, + postChangeAllowedSPs, + postChangeAllowedSPsApproval, postRemoveAlloc, postRequestKyc, postRevertApplicationToReadyToSign, triggerSSA, } from '@/lib/apiClient' import { getStateWaitMsg } from '@/lib/glifApi' -import { AllocatorTypeEnum, type Application, type RefillUnit } from '@/type' +import { + AllocatorTypeEnum, + type StorageProvidersChangeRequest, + type Application, + type RefillUnit, +} from '@/type' import { useMemo, useState } from 'react' import { useMutation, @@ -72,10 +79,18 @@ interface ApplicationActions { { userName: string }, unknown > - mutationProposal: UseMutationResult< + mutationChangeAllowedSPs: UseMutationResult< Application | undefined, unknown, - { requestId: string; userName: string; allocationAmount?: string }, + { + userName: string + clientAddress: string + contractAddress: string + allowedSps: string[] + disallowedSPs: string[] + newAvailableResult: string[] + maxDeviation?: string + }, unknown > mutationApproval: UseMutationResult< @@ -84,6 +99,18 @@ interface ApplicationActions { { requestId: string; userName: string }, unknown > + mutationChangeAllowedSPsApproval: UseMutationResult< + Application | undefined, + unknown, + { activeRequest: StorageProvidersChangeRequest; userName: string }, + unknown + > + mutationProposal: UseMutationResult< + Application | undefined, + unknown, + { requestId: string; userName: string; allocationAmount?: string }, + unknown + > walletError: Error | null initializeWallet: (multisigAddress?: string) => Promise setActiveAccountIndex: (index: number) => void @@ -121,10 +148,12 @@ const useApplicationActions = ( getProposalTx, sendProposal, sendApproval, - setMessage, message, accounts, loadMoreAccounts, + submitClientAllowedSpsAndMaxDeviation, + getChangeSpsProposalTxs, + setMessage, } = useWallet() const { selectedAllocator } = useAllocator() @@ -568,6 +597,187 @@ const useApplicationActions = ( }, ) + const mutationChangeAllowedSPs = useMutation< + Application | undefined, + Error, + { + userName: string + clientAddress: string + contractAddress: string + maxDeviation?: string + allowedSps: string[] + disallowedSPs: string[] + newAvailableResult: string[] + }, + unknown + >( + async ({ + userName, + clientAddress, + contractAddress, + maxDeviation, + allowedSps, + disallowedSPs, + newAvailableResult, + }) => { + const signatures = await submitClientAllowedSpsAndMaxDeviation( + clientAddress, + contractAddress, + allowedSps, + disallowedSPs, + maxDeviation, + ) + + return await postChangeAllowedSPs( + initialApplication.ID, + userName, + owner, + repo, + activeAddress, + signatures, + newAvailableResult, + maxDeviation, + ) + }, + { + onSuccess: (data) => { + setApiCalling(false) + if (data != null) updateCache(data) + }, + onError: () => { + setApiCalling(false) + setMessage(null) + }, + }, + ) + + const mutationChangeAllowedSPsApproval = useMutation< + Application | undefined, + unknown, + { activeRequest: StorageProvidersChangeRequest; userName: string }, + unknown + >( + async ({ activeRequest, userName }) => { + setMessage(`Searching the pending transactions...`) + + const clientAddress = getClientAddress() + + const addedProviders = activeRequest?.Signers.find( + (x) => x['Add Allowed Storage Providers CID'], + )?.['Add Allowed Storage Providers CID'] + + const removedProviders = activeRequest?.Signers.find( + (x) => x['Remove Allowed Storage Providers CID'], + )?.['Remove Allowed Storage Providers CID'] + + const maxDeviation = activeRequest['Max Deviation'] + ? activeRequest['Max Deviation'].split('%')[0] + : undefined + + const proposalTxs = await getChangeSpsProposalTxs( + clientAddress, + maxDeviation, + addedProviders, + removedProviders, + ) + + if (!proposalTxs) { + throw new Error( + 'Transactions not found. You may need to wait some time if the proposal was just sent.', + ) + } + + const signatures: { + maxDeviationCid?: string + allowedSpsCids?: { [key in string]: string[] } + removedSpsCids?: { [key in string]: string[] } + } = {} + + const wait = async (ms: number): Promise => { + await new Promise((resolve) => setTimeout(resolve, ms)) + } + + for (let index = 0; index < proposalTxs.length; index++) { + const proposalTx = proposalTxs[index] + + setMessage(`Preparing the '${proposalTx.cidName}' transaction...`) + + await wait(2000) + const messageCID = await sendApproval(proposalTx.tx) + + if (messageCID == null) { + throw new Error( + `Error sending the '${proposalTx.cidName}' transaction. Please try again or contact support.`, + ) + } + + setMessage( + `Checking the '${proposalTx.cidName}' transaction, It may take a few minutes, please wait... Do not close this window.`, + ) + + const response = await getStateWaitMsg(messageCID) + + if ( + typeof response.data === 'object' && + response.data.ReturnDec.Applied && + response.data.ReturnDec.Code !== 0 + ) { + throw new Error( + `Change allowed SPs transaction failed on chain. Error code: ${response.data.ReturnDec.Code}`, + ) + } + + switch (proposalTx.cidName) { + case 'max deviation': + signatures.maxDeviationCid = messageCID + break + + case 'add allowed Sps': { + if (!signatures.allowedSpsCids) { + signatures.allowedSpsCids = {} + } + + signatures.allowedSpsCids[messageCID] = proposalTx.decodedPacked + ? proposalTx.decodedPacked + : [''] + + break + } + case 'remove allowed Sps': { + if (!signatures.removedSpsCids) { + signatures.removedSpsCids = {} + } + + signatures.removedSpsCids[messageCID] = proposalTx.decodedPacked + ? proposalTx.decodedPacked + : [''] + + break + } + } + } + + return await postChangeAllowedSPsApproval( + initialApplication.ID, + activeRequest.ID, + userName, + owner, + repo, + activeAddress, + signatures, + ) + }, + { + onSuccess: (data) => { + setApiCalling(false) + if (data != null) updateCache(data) + }, + onError: () => { + setApiCalling(false) + }, + }, + ) + return { application, mutationRequestKyc, @@ -588,6 +798,8 @@ const useApplicationActions = ( loadMoreAccounts, mutationTriggerSSA, mutationRemovePendingAllocation, + mutationChangeAllowedSPs, + mutationChangeAllowedSPsApproval, } } diff --git a/src/hooks/useWallet.ts b/src/hooks/useWallet.ts index c0c6fb5..f4e211e 100644 --- a/src/hooks/useWallet.ts +++ b/src/hooks/useWallet.ts @@ -1,5 +1,6 @@ import { getEvmAddressFromFilecoinAddress, + getStateWaitMsg, makeStaticEthCall, } from '@/lib/glifApi' import { anyToBytes } from '@/lib/utils' @@ -8,7 +9,14 @@ import { LedgerWallet } from '@/lib/wallet/LedgerWallet' import { AllocatorTypeEnum, type IWallet, type SendProposalProps } from '@/type' import { newFromString } from '@glif/filecoin-address' import { useCallback, useState } from 'react' -import { decodeFunctionData, encodeFunctionData, fromHex, parseAbi } from 'viem' +import { + decodeFunctionData, + decodeFunctionResult, + encodeFunctionData, + encodePacked, + fromHex, + parseAbi, +} from 'viem' import { type Hex } from 'viem/types/misc' import { config } from '../config' @@ -53,6 +61,44 @@ interface WalletState { contractAddress: string, allocatorAddress: string, ) => Promise + getClientSPs: ( + clientAddress: string, + contractAddress: string, + ) => Promise + submitClientAllowedSpsAndMaxDeviation: ( + clientAddress: string, + contractAddress: string, + allowedSps: string[], + disallowedSPs: string[], + maxDeviation?: string, + ) => Promise<{ + maxDeviationCid?: string + allowedSpCids?: { + [key in string]: string[] + } + disallowedSpCids?: { + [key in string]: string[] + } + }> + getClientConfig: ( + clientAddress: string, + contractAddress: string, + ) => Promise + getChangeSpsProposalTxs: ( + clientAddress: string, + maxDeviation?: string, + allowedSpCids?: { + [key in string]: string[] + }, + disallowedSpCids?: { + [key in string]: string[] + }, + ) => Promise | null> } /** @@ -174,7 +220,6 @@ const useWallet = (): WalletState => { const sign = useCallback( async (message: string): Promise => { if (wallet == null) throw new Error('No wallet initialized.') - return await wallet.sign(message, activeAccountIndex) }, [wallet, activeAccountIndex], @@ -243,6 +288,144 @@ const useWallet = (): WalletState => { [wallet, multisigAddress], ) + const getChangeSpsProposalTxs = useCallback( + async ( + clientAddress: string, + maxDeviation?: string, + allowedSpCids?: { + [key in string]: string[] + }, + disallowedSpCids?: { + [key in string]: string[] + }, + ): Promise | null> => { + if (wallet == null) throw new Error('No wallet initialized.') + if (multisigAddress == null) throw new Error('Multisig address not set.') + + const searchTransactions: Array<{ + cidName: 'max deviation' | 'add allowed Sps' | 'remove allowed Sps' + abi: any + args: any + decodedPacked?: string[] + }> = [] + + const evmClientAddress = + await getEvmAddressFromFilecoinAddress(clientAddress) + + if (maxDeviation) { + searchTransactions.push({ + cidName: 'max deviation', + abi: parseAbi([ + 'function setClientMaxDeviationFromFairDistribution(address client, uint256 maxDeviation)', + ]), + args: [evmClientAddress.data, BigInt(maxDeviation)], + }) + } + + if (allowedSpCids) { + for (const aSps of Object.values(allowedSpCids)) { + const packed = encodePacked( + aSps.map(() => 'uint64'), + aSps, + ) + + searchTransactions.push({ + cidName: 'add allowed Sps', + abi: parseAbi([ + 'function addAllowedSPsForClientPacked(address client, bytes calldata allowedSPs_)', + ]), + args: [evmClientAddress.data, packed], + decodedPacked: aSps, + }) + } + } + + if (disallowedSpCids) { + for (const dSps of Object.values(disallowedSpCids)) { + const packed = encodePacked( + dSps.map(() => 'uint64'), + dSps, + ) + + searchTransactions.push({ + cidName: 'remove allowed Sps', + abi: parseAbi([ + 'function removeAllowedSPsForClientPacked(address client, bytes calldata disallowedSPs_)', + ]), + args: [evmClientAddress.data, packed], + decodedPacked: dSps, + }) + } + } + + if (!searchTransactions.length) return null + + let pendingTxs + + try { + pendingTxs = await wallet.api.pendingTransactions(multisigAddress) + } catch (error) { + console.log(error) + throw new Error( + 'An error with the lotus node occurred. Please reload. If the problem persists, contact support.', + ) + } + + const results: Array<{ + cidName: 'max deviation' | 'add allowed Sps' | 'remove allowed Sps' + tx: any + args: any[] + decodedPacked?: string[] + }> = [] + + for (let i = 0; i < pendingTxs.length; i++) { + const transaction = pendingTxs[i] + + for (let i = 0; i < searchTransactions.length; i++) { + const item = searchTransactions[i] + + const transactionParamsHex: string = + transaction.parsed.params.toString('hex') + const transactionDataHex: Hex = `0x${transactionParamsHex}` + + let decodedData + + try { + decodedData = decodeFunctionData({ + abi: item.abi, + data: transactionDataHex, + }) + } catch (err) { + console.error(err) + continue + } + + const [evmTransactionClientAddress, data] = decodedData.args + + if ( + evmTransactionClientAddress.toLowerCase() === item.args[0] && + data === item.args[1] + ) { + results.push({ + tx: transaction, + cidName: item.cidName, + args: decodedData.args, + decodedPacked: item.decodedPacked, + }) + } + } + } + + return results.length ? results : null + }, + [wallet, multisigAddress], + ) + const sendProposalDirect = useCallback( async (clientAddress: string, bytesDatacap: number) => { if (wallet == null) throw new Error('No wallet initialized.') @@ -270,12 +453,16 @@ const useWallet = (): WalletState => { ]) const address = newFromString(clientAddress) + const addressHex: Hex = `0x${Buffer.from(address.bytes).toString('hex')}` + const calldataHex: Hex = encodeFunctionData({ abi, args: [addressHex, BigInt(bytesDatacap)], }) + const calldata = Buffer.from(calldataHex.substring(2), 'hex') + return wallet.api.multisigEvmInvoke( multisigAddress, contractAddress, @@ -294,6 +481,7 @@ const useWallet = (): WalletState => { const abi = parseAbi([ 'function allowance(address allocator) view returns (uint256)', ]) + const evmAllocatorAddress = await getEvmAddressFromFilecoinAddress(allocatorAddress) @@ -390,6 +578,326 @@ const useWallet = (): WalletState => { [wallet, multisigAddress, activeAccountIndex], ) + const getClientSPs = useCallback( + async (client: string, contractAddress: string): Promise => { + const abi = parseAbi([ + 'function clientSPs(address client) external view returns (uint256[] memory providers)', + ]) + + const [evmClientAddress, evmContractAddress] = await Promise.all([ + getEvmAddressFromFilecoinAddress(client), + getEvmAddressFromFilecoinAddress(contractAddress), + ]) + + const calldataHex: Hex = encodeFunctionData({ + abi, + args: [evmClientAddress.data], + }) + + const response = await makeStaticEthCall( + evmContractAddress.data, + calldataHex, + ) + + if (response.error) { + return [''] + } + + const decodedData = decodeFunctionResult({ + abi, + data: response.data as `0x${string}`, + }) + + const result: string[] = decodedData.map((x) => x.toString()) + return result + }, + [], + ) + + const getClientConfig = useCallback( + async (client: string, contractAddress: string): Promise => { + const abi = parseAbi([ + 'function clientConfigs(address client) external view returns (uint256)', + ]) + + const [evmClientAddress, evmContractAddress] = await Promise.all([ + getEvmAddressFromFilecoinAddress(client), + getEvmAddressFromFilecoinAddress(contractAddress), + ]) + + const calldataHex: Hex = encodeFunctionData({ + abi, + args: [evmClientAddress.data], + }) + + const response = await makeStaticEthCall( + evmContractAddress.data, + calldataHex, + ) + + if (response.error) { + return null + } + + const decodedData = decodeFunctionResult({ + abi, + data: response.data as `0x${string}`, + }) + + return decodedData.toString() + }, + [], + ) + + const prepareClientMaxDeviation = ( + clientAddressHex: Hex, + maxDeviation: string, + ): { calldata: Buffer; abi: any } => { + const abi = parseAbi([ + 'function setClientMaxDeviationFromFairDistribution(address client, uint256 maxDeviation)', + ]) + + const calldataHex: Hex = encodeFunctionData({ + abi, + args: [clientAddressHex, BigInt(maxDeviation)], + }) + + const calldata = Buffer.from(calldataHex.substring(2), 'hex') + + return { calldata, abi } + } + + const prepareClientAddAllowedSps = ( + clientAddressHex: Hex, + allowedSps: string[], + ): { calldata: Buffer; abi: any } => { + const abi = parseAbi([ + 'function addAllowedSPsForClientPacked(address client, bytes calldata allowedSPs_)', + ]) + + const parsedSps = allowedSps.map((x) => BigInt(x)) + + const packed = encodePacked( + parsedSps.map(() => 'uint64'), + parsedSps, + ) + + const calldataHex: Hex = encodeFunctionData({ + abi, + args: [clientAddressHex, packed], + }) + + const calldata = Buffer.from(calldataHex.substring(2), 'hex') + + return { calldata, abi } + } + + const prepareClientRemoveAllowedSps = ( + clientAddressHex: Hex, + disallowedSPs: string[], + ): { calldata: Buffer; abi: any } => { + const abi = parseAbi([ + 'function removeAllowedSPsForClientPacked(address client, bytes calldata disallowedSPs_)', + ]) + + const parsedSps = disallowedSPs.map((x) => BigInt(x)) + + const packed = encodePacked( + parsedSps.map(() => 'uint64'), + parsedSps, + ) + + const calldataHex: Hex = encodeFunctionData({ + abi, + args: [clientAddressHex, packed], + }) + + const calldata = Buffer.from(calldataHex.substring(2), 'hex') + + return { calldata, abi } + } + + const checkTransactionState = async ( + transactionCid: string, + transactionName: string, + ): Promise => { + if (transactionCid == null) { + throw new Error( + `Error sending ${transactionName} transaction. Please try again or contact support.`, + ) + } + + const response = await getStateWaitMsg(transactionCid) + + if ( + typeof response.data === 'object' && + response.data.ReturnDec.Applied && + response.data.ReturnDec.Code !== 0 + ) { + throw new Error( + `Error sending ${transactionName} transaction. Please try again or contact support. Error code: ${response.data.ReturnDec.Code}`, + ) + } + } + + const submitClientAllowedSpsAndMaxDeviation = useCallback( + async ( + clientAddress: string, + contractAddress: string, + allowedSps?: string[], + disallowedSPs?: string[], + maxDeviation?: string, + ): Promise<{ + maxDeviationCid?: string + allowedSpCids?: { + [key in string]: string[] + } + disallowedSpCids?: { + [key in string]: string[] + } + }> => { + if (wallet == null) throw new Error('No wallet initialized.') + if (multisigAddress == null) throw new Error('Multisig address not set.') + + const evmClientAddress = + await getEvmAddressFromFilecoinAddress(clientAddress) + + const wait = async (ms: number): Promise => { + await new Promise((resolve) => setTimeout(resolve, ms)) + } + + const signatures: { + maxDeviationCid?: string + allowedSpsCids?: { [key in string]: string[] } + removedSpsCids?: { [key in string]: string[] } + } = {} + + function chunkArray(array: T[], size: number): T[][] { + const result: T[][] = [] + for (let i = 0; i < array.length; i += size) { + result.push(array.slice(i, i + size)) + } + return result + } + + if (maxDeviation) { + setMessage(`Preparing the 'max deviation' transaction...`) + + await wait(2000) + + const { calldata } = prepareClientMaxDeviation( + evmClientAddress.data, + maxDeviation, + ) + + const maxDeviationTransaction = await wallet.api.multisigEvmInvoke( + multisigAddress, + contractAddress, + calldata, + activeAccountIndex, + ) + + setMessage( + `Checking the 'max deviation' transaction, it may take a few minutes, please wait... Do not close this window.`, + ) + + await checkTransactionState(maxDeviationTransaction, 'max deviation') + + signatures.maxDeviationCid = maxDeviationTransaction + } + + if (allowedSps?.length) { + const allowedChunkedArray = chunkArray(allowedSps, 8) + + for (let i = 0; i < allowedChunkedArray.length; i++) { + const allowedSpsPart = allowedChunkedArray[i] + + const countMessage = + allowedChunkedArray.length === 1 + ? '...' + : `${i} / ${allowedChunkedArray.length}` + + setMessage( + `Preparing the 'add allowed SPs' transactions ${countMessage}`, + ) + + await wait(2000) + + const { calldata } = prepareClientAddAllowedSps( + evmClientAddress.data, + allowedSpsPart, + ) + + const allowedSpsTransaction = await wallet.api.multisigEvmInvoke( + multisigAddress, + contractAddress, + calldata, + activeAccountIndex, + ) + + setMessage( + `Checking the 'add allowed SPs' transaction, it may take a few minutes, please wait... Do not close this window.`, + ) + + await checkTransactionState(allowedSpsTransaction, 'add allowed SPs') + + if (!signatures.allowedSpsCids) { + signatures.allowedSpsCids = {} + } + + signatures.allowedSpsCids[allowedSpsTransaction] = allowedSpsPart + } + } + + if (disallowedSPs?.length) { + const disallowedChunkedArray = chunkArray(disallowedSPs, 8) + + for (let i = 0; i < disallowedChunkedArray.length; i++) { + const disallowedSpsPart = disallowedChunkedArray[i] + + const countMessage = + disallowedChunkedArray.length === 1 + ? '...' + : `${i} / ${disallowedChunkedArray.length}` + + setMessage( + `Preparing the 'remove allowed SPs' transactions ${countMessage}`, + ) + + await wait(2000) + + const { calldata } = prepareClientRemoveAllowedSps( + evmClientAddress.data, + disallowedSpsPart, + ) + + const disallowedSpsTransaction = await wallet.api.multisigEvmInvoke( + multisigAddress, + contractAddress, + calldata, + activeAccountIndex, + ) + + setMessage( + `Checking the 'remove allowed SPs' transaction, it may take a few minutes, please wait... Do not close this window.`, + ) + + await checkTransactionState(disallowedSpsTransaction, 'disallow SPs') + + if (!signatures.removedSpsCids) { + signatures.removedSpsCids = {} + } + + signatures.removedSpsCids[disallowedSpsTransaction] = + disallowedSpsPart + } + } + + return signatures + }, + [wallet, multisigAddress, activeAccountIndex], + ) + const activeAddress = accounts[activeAccountIndex] ?? '' return { @@ -406,6 +914,10 @@ const useWallet = (): WalletState => { accounts, loadMoreAccounts, getAllocatorAllowanceFromContract, + getClientSPs, + submitClientAllowedSpsAndMaxDeviation, + getClientConfig, + getChangeSpsProposalTxs, } } diff --git a/src/lib/apiClient.ts b/src/lib/apiClient.ts index 06636bc..c479dbc 100644 --- a/src/lib/apiClient.ts +++ b/src/lib/apiClient.ts @@ -558,3 +558,92 @@ export const cacheRenewal = async ( throw e } } + +export const postChangeAllowedSPs = async ( + id: string, + userName: string, + owner: string, + repo: string, + address: string, + signatures: { + maxDeviationCid?: string + allowedSpsCids?: { [key in string]: string[] } + removedSpsCids?: { [key in string]: string[] } + }, + availableAllowedSpsData: string[], + maxDeviationData?: string, +): Promise => { + try { + const { data } = await apiClient.post( + `verifier/application/propose_storage_providers`, + { + max_deviation: maxDeviationData ? `${maxDeviationData}%` : undefined, + allowed_sps: availableAllowedSpsData.map((x) => Number(x)), + owner, + repo, + signer: { + signing_address: address, + max_deviation_cid: signatures.maxDeviationCid, + allowed_sps_cids: signatures.allowedSpsCids, + removed_allowed_sps_cids: signatures.removedSpsCids, + }, + }, + { + params: { + repo, + owner, + id, + github_username: userName, + }, + }, + ) + + return data + } catch (error) { + console.error(error) + throw error + } +} + +export const postChangeAllowedSPsApproval = async ( + id: string, + requestId: string, + userName: string, + owner: string, + repo: string, + address: string, + signatures: { + maxDeviationCid?: string + allowedSpsCids?: { [key in string]: string[] } + removedSpsCids?: { [key in string]: string[] } + }, +): Promise => { + try { + const { data } = await apiClient.post( + `verifier/application/approve_storage_providers`, + { + request_id: requestId, + owner, + repo, + signer: { + signing_address: address, + max_deviation_cid: signatures.maxDeviationCid, + allowed_sps_cids: signatures.allowedSpsCids, + removed_allowed_sps_cids: signatures.removedSpsCids, + }, + }, + { + params: { + repo, + owner, + id, + github_username: userName, + }, + }, + ) + return data + } catch (error) { + console.error(error) + throw error + } +} diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 68c0765..77f396e 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -13,6 +13,7 @@ export const stateColor = { ReadyToSign: 'bg-orange-500 text-white', Granted: 'bg-green-400 text-white', KYCRequested: 'bg-lime-400 text-white', + ChangingSP: 'bg-amber-400 text-white', } export const allocationActiveColor = { @@ -27,4 +28,5 @@ export const stateMapping = { StartSignDatacap: 'Start sign datacap', Granted: 'Granted', KYCRequested: 'KYC requested', + ChangingSP: 'Changing SP', } diff --git a/src/lib/glifApi.ts b/src/lib/glifApi.ts index 4aeb793..4737118 100644 --- a/src/lib/glifApi.ts +++ b/src/lib/glifApi.ts @@ -166,6 +166,10 @@ export const getStateWaitMsg = async ( const errMessage = `Error accessing GLIF API Filecoin.StateWaitMsg: ${ (error as Error).message }` + + if (errMessage.includes('too long')) { + return await getStateWaitMsg(cid) + } return { data: '', error: errMessage, diff --git a/src/lib/publicClient.ts b/src/lib/publicClient.ts index c2a71e8..9573a1c 100644 --- a/src/lib/publicClient.ts +++ b/src/lib/publicClient.ts @@ -45,7 +45,7 @@ type FilecoinRpcSchema = [ }, { Method: 'Filecoin.FilecoinAddressToEthAddress' - Parameters: [string, null] + Parameters: [string, string | null] ReturnType: string | null }, { diff --git a/src/type.ts b/src/type.ts index 220b7b9..2a96f8b 100644 --- a/src/type.ts +++ b/src/type.ts @@ -23,6 +23,8 @@ export interface Application { repo: string owner: string fullSpan?: boolean + 'Client Contract Address': string | null + 'Storage Providers Change Requests': StorageProvidersChangeRequest[] } export interface Allocation { @@ -61,6 +63,7 @@ export interface Lifecycle { | 'Granted' | 'TotalDatacapReached' | 'Error' + | 'ChangingSp' 'Validated At': string 'Validated By': string Active: boolean @@ -85,6 +88,22 @@ export interface Signer { 'Signing Address': string 'Created At': string 'Github Username': string + 'Set Max Deviation CID': string | undefined + 'Add Allowed Storage Providers CID': { [key in string]: string[] } | undefined + 'Remove Allowed Storage Providers CID': + | { [key in string]: string[] } + | undefined +} + +export interface StorageProvidersChangeRequest { + ID: string + 'Created At': string + 'Updated At': string + Active: boolean + Signers: Signer[] + 'Allowed Storage Providers': string[] + 'Removed Storage Providers': string[] + 'Max Deviation': string } export interface IWallet { diff --git a/tailwind.config.js b/tailwind.config.js index 9507af7..34ccaf7 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -70,6 +70,9 @@ module.exports = { 'accordion-down': 'accordion-down 0.2s ease-out', 'accordion-up': 'accordion-up 0.2s ease-out', }, + zIndex: { + 1400: '1400', + }, }, }, plugins: [require('tailwindcss-animate')],