diff --git a/libs/moloch-v3-macro-ui/src/components/ProposalActionData/ProposalActionData.styles.ts b/libs/moloch-v3-macro-ui/src/components/ProposalActionData/ProposalActionData.styles.ts index 51dd74c6..fe5f03c1 100644 --- a/libs/moloch-v3-macro-ui/src/components/ProposalActionData/ProposalActionData.styles.ts +++ b/libs/moloch-v3-macro-ui/src/components/ProposalActionData/ProposalActionData.styles.ts @@ -19,6 +19,7 @@ export const TitleContainer = styled.div` display: flex; justify-content: space-between; align-items: center; + margin-bottom: 1rem; `; export const DisplayContainer = styled.div` @@ -27,6 +28,7 @@ export const DisplayContainer = styled.div` .display-segment { display: flex; flex-direction: column; + padding-bottom: 1.5rem; } .data { @@ -40,6 +42,9 @@ export const DisplayContainer = styled.div` .value-box { display: flex; } + .spaced-divider { + margin-bottom: 2rem; + } `; export const AlertContainer = styled.div` diff --git a/libs/moloch-v3-macro-ui/src/components/ProposalActionData/ProposalActionData.tsx b/libs/moloch-v3-macro-ui/src/components/ProposalActionData/ProposalActionData.tsx index e99a9348..b83bfb86 100644 --- a/libs/moloch-v3-macro-ui/src/components/ProposalActionData/ProposalActionData.tsx +++ b/libs/moloch-v3-macro-ui/src/components/ProposalActionData/ProposalActionData.tsx @@ -1,8 +1,10 @@ -import { useState } from 'react'; +import { ReactNode, useState } from 'react'; import { isValidNetwork } from '@daohaus/keychain-utils'; import { MolochV3Proposal } from '@daohaus/moloch-v3-data'; import { + ActionError, DeepDecodedMultiTX as DecodedMultiTX, + DeepDecodedAction, isActionError, } from '@daohaus/tx-builder'; import { @@ -15,6 +17,7 @@ import { Loading, useBreakpoint, widthQuery, + ParLg, } from '@daohaus/ui'; import { DAO_METHOD_TO_PROPOSAL_TYPE, @@ -63,125 +66,45 @@ export const ProposalActionData = ({ actionData, decodeError = false, }: ProposalActionDataProps) => { - const [open, setOpen] = useState(false); - - const network = isValidNetwork(daoChain) ? daoChain : undefined; - const isMobile = useBreakpoint(widthQuery.sm); - - const handleToggle = () => { - setOpen((prevState) => !prevState); - }; - return ( - Decoded Transaction Data + + All Actions + {!actionData && ( )} + + {actionData?.map((action, index) => { + return ( +
+ - {actionData && open && ( -
- -
- )} - {actionData && !open && ( -
- +
- )} - - {open && - actionData?.map((action, index) => { - if (isActionError(action)) { - return ( -
-

Action {index + 1}: Error

- {action.message} - - - HEX DATA: - - {action.data} -
- ); - } - return ( -
-
-

- Action {index + 1}: {action.name} -

- - - TARGET - - - - VALUE - - {action.value} - -
- {action.params?.map((arg, index) => { - return ( -
- - - PARAM - {index + 1}:{' '} - - {arg.name} - - - TYPE: - {arg.type} - - - VALUE: - - - -
- ); - })} - {action.decodedActions?.length ? ( - - ) : null} -
- ); - })} + ); + })} {decodeError && ( ); }; + +const SubActions = ({ + action, + index, + actionHeader, + daoChain, + daoId, + proposal, + proposalActionConfig, +}: { + action: DeepDecodedAction | ActionError; + index: number; + actionHeader: string; + daoChain: string; + daoId: string; + proposal: MolochV3Proposal; + proposalActionConfig?: ProposalActionConfig; +}) => { + if ( + isActionError(action) || + !action.decodedActions || + action.decodedActions.length === 0 + ) { + return null; + } + + return ( + <> + {action.decodedActions.map((subAction, i) => ( +
+ + + +
+ ))} + + ); +}; + +const ActionToggle = ({ + action, + actionHeader, + children, +}: { + action: DeepDecodedAction | ActionError; + actionHeader: string; + children: ReactNode; +}) => { + const [open, setOpen] = useState(false); + const handleToggle = () => { + setOpen((prevState) => !prevState); + }; + return ( + <> + + + {actionHeader} {'name' in action ? action.name : 'Decoding Error'} + + {open && ( +
+ +
+ )} + {!open && ( +
+ +
+ )} +
+ {open &&
{children}
} + + ); +}; + +const ActionSectionError = ({ + action, + index, +}: { + action: ActionError; + index: number; +}) => { + return ( +
+

Action {index + 1}: Error

+ {action.message} + + + HEX DATA: + + {action.data} +
+ ); +}; + +const ActionSection = ({ + action, + index, + actionHeader, + daoChain, + daoId, + proposal, + proposalActionConfig, +}: { + action: DeepDecodedAction | ActionError; + index: number; + actionHeader: string; + daoChain: string; + daoId: string; + proposal: MolochV3Proposal; + proposalActionConfig?: ProposalActionConfig; +}) => { + const network = isValidNetwork(daoChain) ? daoChain : undefined; + const isMobile = useBreakpoint(widthQuery.sm); + + if (isActionError(action)) { + return ; + } + + return ( +
+ + <> + + + TARGET + + + + VALUE + + {action.value} + + {action.params?.map((arg, index) => { + return ( +
+ + + PARAM + {index + 1}:{' '} + + {arg.name} + + + TYPE: + {arg.type} + + + VALUE: + + + +
+ ); + })} + +
+
+ ); +}; diff --git a/libs/moloch-v3-macro-ui/src/components/ProposalActionData/ProposalWarning.tsx b/libs/moloch-v3-macro-ui/src/components/ProposalActionData/ProposalWarning.tsx index 2be79861..4f967664 100644 --- a/libs/moloch-v3-macro-ui/src/components/ProposalActionData/ProposalWarning.tsx +++ b/libs/moloch-v3-macro-ui/src/components/ProposalActionData/ProposalWarning.tsx @@ -29,16 +29,16 @@ export const ProposalWarning = ({ proposalActionConfig, daoChain, }: ProposalWarningProps) => { - const warningMessage: string = useMemo(() => { + const warningMessage: string | undefined = useMemo(() => { if (decodeError) { return PROPOSAL_TYPE_WARNINGS.ERROR_CANNOT_DECODE; } else { return ( - (proposalType && - proposalActionConfig?.proposalTypeWarning?.[proposalType]) || - PROPOSAL_TYPE_WARNINGS.ERROR_UNKOWN + proposalType && + proposalActionConfig?.proposalTypeWarning?.[proposalType] ); } + return; }, [proposalType, decodeError, proposalActionConfig]); const hasWarning = @@ -50,19 +50,19 @@ export const ProposalWarning = ({ // TODO: activate this feature when errors use cases arise const hasError = false; + if (!hasWarning) return null; + return ( - {hasWarning && ( - - - - - - )} + + + + + {warningMessage} diff --git a/libs/moloch-v3-macro-ui/src/components/ProposalDetails/ProposalDetailsContainer.tsx b/libs/moloch-v3-macro-ui/src/components/ProposalDetails/ProposalDetailsContainer.tsx index 630d7e4f..9f0e21ee 100644 --- a/libs/moloch-v3-macro-ui/src/components/ProposalDetails/ProposalDetailsContainer.tsx +++ b/libs/moloch-v3-macro-ui/src/components/ProposalDetails/ProposalDetailsContainer.tsx @@ -67,6 +67,7 @@ export const ProposalDetailsContainer = ({ chainId, actionData, }); + if (shouldUpdate) { setActionData(proposalActions); setDecodeError( diff --git a/libs/tx-builder/src/utils/abi.ts b/libs/tx-builder/src/utils/abi.ts index 73028573..d6ab251b 100644 --- a/libs/tx-builder/src/utils/abi.ts +++ b/libs/tx-builder/src/utils/abi.ts @@ -246,9 +246,7 @@ export const fetchABI = async ({ throw new Error('Could generate explorer url with the given arguments'); } const scanResponse = await fetch(url); - console.log('scanResponse', scanResponse); const data = await scanResponse.json(); - console.log('data', data); if (data.message === 'OK' && isJSON(data.result)) { const abi = JSON.parse(data.result); cacheABI({ address: contractAddress, chainId, abi }); diff --git a/libs/tx-builder/src/utils/cache.ts b/libs/tx-builder/src/utils/cache.ts index de741591..4f724dcc 100644 --- a/libs/tx-builder/src/utils/cache.ts +++ b/libs/tx-builder/src/utils/cache.ts @@ -52,10 +52,6 @@ const addABI = ({ address: string; abi: ABI; }) => { - console.log('address', address); - console.log('abi', abi); - console.log('chainId', chainId); - console.log('abiStore', abiStore); return { ...abiStore, [chainId]: { @@ -75,7 +71,6 @@ export const cacheABI = async ({ abi: ABI; }) => { const abiStore = await getABIstore(); - console.log('abiStore', abiStore); const newStore = addABI({ abiStore, chainId, diff --git a/libs/tx-builder/src/utils/decoding.ts b/libs/tx-builder/src/utils/decoding.ts index 525dddc8..23b97e23 100644 --- a/libs/tx-builder/src/utils/decoding.ts +++ b/libs/tx-builder/src/utils/decoding.ts @@ -1,45 +1,6 @@ -import { - decodeFunctionData, - parseAbiParameters, - decodeAbiParameters, - getAbiItem, -} from 'viem'; -import { - ArgType, - ENCODED_0X0_DATA, - MulticallAction, - StringSearch, - ValidArgType, -} from '@daohaus/utils'; -import { - ABI_EXPLORER_KEYS, - CONTRACT_KEYCHAINS, - HAUS_NETWORK_DATA, - HAUS_RPC, - Keychain, - ValidNetwork, -} from '@daohaus/keychain-utils'; +import { ArgType } from '@daohaus/utils'; +import { OperationType } from 'ethers-multisend'; -import { LOCAL_ABI } from '@daohaus/abis'; -import { fetchABI, getCode } from './abi'; -import { isSearchArg } from './args'; - -const OPERATION_TYPE = 2; -const ADDRESS = 40; -const VALUE = 64; -const DATA_LENGTH = 64; - -type MultisendArgs = { - chainId: ValidNetwork; - actionData: string; - rpcs: Keychain; -}; -type EncodedAction = { - to: string; - value: string; - data: string; - operation: number; -}; export type DecodedAction = { to: string; name: string; @@ -59,215 +20,22 @@ export type ActionError = { export type DecodedMultiTX = (DecodedAction | ActionError)[]; -type ViemAbiFunction = { - inputs: { - internalType: string; +export type DeepDecodedAction = { + to: string; + operation: OperationType; + name: string; + value: string; + params: { name: string; type: string; + value: ArgType; }[]; + decodedActions?: DeepDecodedMultiTX; }; +export type DeepDecodedMultiTX = (DeepDecodedAction | ActionError)[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any export const isActionError = (action: any): action is ActionError => { return action.error; }; - -const getMultisendHex = ({ chainId, actionData, rpcs }: MultisendArgs) => { - const multisendAddr = CONTRACT_KEYCHAINS.GNOSIS_MULTISEND[chainId]; - if (!multisendAddr) throw new Error('Invalid chainId'); - - const decoded = decodeFunctionData({ - abi: LOCAL_ABI.GNOSIS_MULTISEND, - data: actionData as `0x${string}`, - }); - - const hexData = decoded?.args?.[0] as string; - return hexData.slice(2); -}; - -const processAction = (actionsHex: string, txLength: number): EncodedAction => { - return { - to: `0x${actionsHex.slice(OPERATION_TYPE, OPERATION_TYPE + ADDRESS)}`, - value: `0x${actionsHex.slice( - OPERATION_TYPE + ADDRESS, - OPERATION_TYPE + ADDRESS + VALUE - )}`, - data: `0x${actionsHex.slice( - OPERATION_TYPE + ADDRESS + VALUE + DATA_LENGTH, - OPERATION_TYPE + ADDRESS + VALUE + DATA_LENGTH + txLength * 2 - )}`, - operation: parseInt(actionsHex.slice(0, OPERATION_TYPE)), - }; -}; -const decodeMultisend = ({ chainId, actionData, rpcs }: MultisendArgs) => { - try { - let actionsHex = getMultisendHex({ chainId, actionData, rpcs }); - const transactions = []; - - while ( - actionsHex.length >= - OPERATION_TYPE + ADDRESS + VALUE + DATA_LENGTH - ) { - const thisTxLength = Number( - BigInt( - `0x${actionsHex.slice( - OPERATION_TYPE + ADDRESS + VALUE, - OPERATION_TYPE + ADDRESS + VALUE + DATA_LENGTH - )}` - ) - ); - - transactions.push(processAction(actionsHex, thisTxLength)); - actionsHex = actionsHex.slice( - OPERATION_TYPE + ADDRESS + VALUE + DATA_LENGTH + thisTxLength * 2 - ); - } - - return transactions; - } catch (error) { - console.error(error); - return []; - } -}; - -const isEthTransfer = async ( - chainId: ValidNetwork, - action: EncodedAction, - rpcs: Keychain -) => - action?.data?.slice(2)?.length === 0 || - action?.data === ENCODED_0X0_DATA || - (await getCode({ chainId, contractAddress: action.to, rpcs })) === '0x'; - -const buildEthTransferAction = ( - chainId: ValidNetwork, - action: EncodedAction -): DecodedAction => ({ - to: action.to, - name: `${HAUS_NETWORK_DATA[chainId]?.symbol} Transfer`, - value: BigInt(action.value).toString(), - params: [], -}); - -const decodeParam = ({ - argMeta, - value, -}: { - argMeta?: ValidArgType; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - value: any; -}) => { - if (!argMeta || isSearchArg(argMeta)) { - return value; - } - if (argMeta?.type === 'argEncode') { - const decodedValues = decodeAbiParameters( - parseAbiParameters(argMeta.solidityTypes.join(',')), - value - ); - return argMeta.args.map((arg, i) => { - const label = isSearchArg(arg) - ? (arg as StringSearch).trim().split('.').reverse()[0] - : `Param${i}`; - return [label, decodedValues[i]]; - }); - } - return value; -}; - -const decodeAction = async ({ - chainId, - action, - actionMeta, - rpcs, - explorerKeys, -}: { - chainId: ValidNetwork; - action: EncodedAction; - actionMeta?: MulticallAction; - rpcs: Keychain; - explorerKeys: Keychain; -}): Promise => { - if (await isEthTransfer(chainId, action, rpcs)) - return buildEthTransferAction(chainId, action); - - const { to, data, value } = action; - - const abi = await fetchABI({ - chainId, - contractAddress: to, - rpcs, - explorerKeys, - }); - if (!abi || !abi?.length) { - return { - error: true, - message: 'No ABI found for this contract', - data, - }; - } - - const decoded = decodeFunctionData({ - abi, - data: data as `0x${string}`, - }); - - if (!decoded) { - return { - error: true, - message: 'Could not decode action', - data: action.data, - }; - } - - const decodedArgs = decoded.args || []; - - return { - to, - name: decoded.functionName, - value: parseInt(value).toString(), - params: decodedArgs.map((arg, i) => { - const abiItem = getAbiItem({ - abi, - name: decoded.functionName, - }) as ViemAbiFunction; - return { - name: abiItem?.inputs?.[i]?.name || 'ERROR: Could not find name', - type: abiItem?.inputs?.[i]?.type || 'ERROR: Could not find type', - value: - abiItem?.inputs?.[i]?.type === 'bytes' - ? decodeParam({ - argMeta: actionMeta?.args?.[i], - value: arg, - }) - : arg, - }; - }), - }; -}; - -export const decodeProposalActions = async ({ - chainId, - actionData, - actionsMeta = [], - rpcs = HAUS_RPC, - explorerKeys = ABI_EXPLORER_KEYS, -}: { - chainId: ValidNetwork; - actionData: string; - actionsMeta?: MulticallAction[]; - rpcs?: Keychain; - explorerKeys?: Keychain; -}) => { - return Promise.all( - decodeMultisend({ chainId, actionData, rpcs })?.map(async (action, i) => { - return await decodeAction({ - chainId, - action, - actionMeta: actionsMeta[i], - rpcs, - explorerKeys, - }); - }) - ); -}; diff --git a/libs/tx-builder/src/utils/deepDecoding.ts b/libs/tx-builder/src/utils/deepDecoding.ts index 38986a24..f539d75e 100644 --- a/libs/tx-builder/src/utils/deepDecoding.ts +++ b/libs/tx-builder/src/utils/deepDecoding.ts @@ -1,5 +1,10 @@ -import { decodeFunctionData, fromHex, getAbiItem } from 'viem'; -import { ArgType, ENCODED_0X0_DATA } from '@daohaus/utils'; +import { + decodeAbiParameters, + decodeFunctionData, + fromHex, + getAbiItem, +} from 'viem'; +import { ENCODED_0X0_DATA } from '@daohaus/utils'; import { ABI_EXPLORER_KEYS, HAUS_NETWORK_DATA, @@ -13,24 +18,9 @@ import { whatsabi, loaders } from '@shazow/whatsabi'; import { providers } from 'ethers'; import { fetchABI, getCode } from './abi'; -import { ActionError } from './decoding'; +import { ActionError, DeepDecodedAction, DeepDecodedMultiTX } from './decoding'; const { MultiABILoader, SourcifyABILoader } = loaders; -export type DeepDecodedAction = { - to: string; - operation: OperationType; - name: string; - value: string; - params: { - name: string; - type: string; - value: ArgType; - }[]; - decodedActions?: DeepDecodedMultiTX; -}; - -export type DeepDecodedMultiTX = (DeepDecodedAction | ActionError)[]; - class EtherscanABILoader implements loaders.ABILoader { chainId: ValidNetwork; rpcs: Keychain; @@ -46,6 +36,7 @@ class EtherscanABILoader implements loaders.ABILoader { this.explorerKeys = options.explorerKeys; } + // eslint-disable-next-line @typescript-eslint/no-explicit-any async loadABI(address: string): Promise { const abi = await fetchABI({ chainId: this.chainId, @@ -149,6 +140,7 @@ const decodeValue = (value: unknown): string => { }; const decodeMethod = (options: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any abi: any[]; data: `0x${string}`; }): DecodedMethod => { @@ -161,6 +153,7 @@ const decodeMethod = (options: { const inputs = functionDetails['inputs'] || []; + // eslint-disable-next-line @typescript-eslint/no-explicit-any const inputsWithValues = (inputs as any[]).map((input, index) => ({ name: input.name, type: input.type, @@ -304,6 +297,38 @@ const actionDecoders: Record< decodedActions: [decodedAction], }; }, + // setGovernanceConfig(uint256,bytes) + '0xee4d88ed': async (options, action, decodedMethod) => { + const govTypes = [ + { name: 'voting', type: 'uint32' }, + { name: 'grace', type: 'uint32' }, + { name: 'newOffering', type: 'uint256' }, + { name: 'quorum', type: 'uint256' }, + { name: 'sponsor', type: 'uint256' }, + { name: 'minRetention', type: 'uint256' }, + ]; + + const govValues = decodeAbiParameters( + govTypes, + decodedMethod.inputs[0].value as `0x${string}` + ); + + const govParams = govTypes.map((govType, i) => { + return { + ...govType, + value: govValues[i] as string, + }; + }); + + return { + to: action.to, + operation: action.operation || OperationType.Call, + name: decodedMethod.functionName, + value: decodeValue(action.value), + params: govParams, + decodedActions: [], + }; + }, }; const decodeAction = async ( @@ -349,6 +374,7 @@ const decodeAction = async ( } const methodSignature = data.slice(0, 10); + const actionDecoder = actionDecoders[methodSignature]; if (actionDecoder) { return await actionDecoder(options, action, decodedMethod);