diff --git a/package.json b/package.json index 603790094..be036d37f 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@vueuse/head": "^2.0.0", "autolinker": "^4.0.0", "bluebird": "^3.7.2", + "evm-proxy-detection": "^1.2.0", "graphql": "16.6.0", "graphql-tag": "^2.12.6", "js-sha256": "^0.10.1", diff --git a/src/plugins/oSnap/components/TransactionBuilder/ContractInteraction.vue b/src/plugins/oSnap/components/TransactionBuilder/ContractInteraction.vue index a787fdf48..c37c5b342 100644 --- a/src/plugins/oSnap/components/TransactionBuilder/ContractInteraction.vue +++ b/src/plugins/oSnap/components/TransactionBuilder/ContractInteraction.vue @@ -2,16 +2,19 @@ import { parseAmount } from '@/helpers/utils'; import { FunctionFragment } from '@ethersproject/abi'; import { isAddress } from '@ethersproject/address'; +import { useDebounceFn } from '@vueuse/core'; -import { ContractInteractionTransaction, Network } from '../../types'; +import { ContractInteractionTransaction, Network, Status } from '../../types'; import { createContractInteractionTransaction, + fetchImplementationAddress, getABIWriteFunctions, getContractABI, validateTransaction } from '../../utils'; import AddressInput from '../Input/Address.vue'; import MethodParameterInput from '../Input/MethodParameter.vue'; +import { sleep } from '@snapshot-labs/snapshot.js/src/utils'; const props = defineProps<{ network: Network; @@ -26,7 +29,11 @@ const to = ref(props.transaction.to ?? ''); const isToValid = computed(() => { return to.value === '' || isAddress(to.value); }); -const abi = ref(props.transaction.abi ?? ''); +const abi = ref(''); +const abiFetchStatus = ref(Status.IDLE); +const implementationAddress = ref(''); +const showAbiChoiceModal = ref(false); + const isAbiValid = ref(true); const value = ref(props.transaction.value ?? '0'); const isValueValid = ref(true); @@ -40,7 +47,7 @@ const selectedMethod = computed( const parameters = ref([]); function updateTransaction() { - if (!isValueValid || !isToValid || !isAbiValid) return; + if (!isValueValid || !isToValid || !isAbiValid || !abi.value) return; try { const transaction = createContractInteractionTransaction({ to: to.value, @@ -55,7 +62,15 @@ function updateTransaction() { return; } } catch (error) { - console.warn('ContractInteraction - Invalid Transaction:',error); + console.warn('ContractInteraction - Invalid Transaction:', error); + } +} + +async function handleFail() { + abiFetchStatus.value = Status.FAIL; + await sleep(3000); + if (abiFetchStatus.value === Status.FAIL) { + abiFetchStatus.value = Status.IDLE; } } @@ -79,18 +94,69 @@ function updateAbi(newAbi: string) { isAbiValid.value = true; updateMethod(methods.value[0].name); } catch (error) { - isAbiValid.value = false; + handleFail(); console.warn('error extracting useful methods', error); } updateTransaction(); } -async function updateAddress() { - const result = await getContractABI(props.network, to.value); - if (result && result !== abi.value) { - updateAbi(result); +const debouncedUpdateAddress = useDebounceFn(() => { + if (isAddress(to.value)) { + fetchABI(); + } +}, 300); + +async function handleUseProxyAbi() { + showAbiChoiceModal.value = false; + try { + const res = await getContractABI(props.network, to.value); + if (!res) { + throw new Error('Failed to fetch ABI.'); + } + updateAbi(res); + abiFetchStatus.value = Status.SUCCESS; + } catch (error) { + handleFail(); + console.error(error); + } +} + +async function handleUseImplementationAbi() { + showAbiChoiceModal.value = false; + try { + if (!implementationAddress.value) { + throw new Error(' No Implementation address'); + } + const res = await getContractABI( + props.network, + implementationAddress.value + ); + if (!res) { + throw new Error('Failed to fetch ABI.'); + } + abiFetchStatus.value = Status.SUCCESS; + updateAbi(res); + } catch (error) { + handleFail(); + console.error(error); + } +} + +async function fetchABI() { + try { + abiFetchStatus.value = Status.LOADING; + const res = await fetchImplementationAddress(to.value, props.network); + if (!res) { + handleUseProxyAbi(); + return; + } + // if proxy, let user decide which ABI we should fetch + implementationAddress.value = res; + showAbiChoiceModal.value = true; + } catch (error) { + handleFail(); + console.error(error); } - updateTransaction(); } function updateValue(newValue: string) { @@ -103,6 +169,11 @@ function updateValue(newValue: string) { } updateTransaction(); } + +function handleDismissModal() { + abiFetchStatus.value = Status.IDLE; + showAbiChoiceModal.value = false; +} diff --git a/src/plugins/oSnap/types.ts b/src/plugins/oSnap/types.ts index 76b541dca..f34efe36e 100644 --- a/src/plugins/oSnap/types.ts +++ b/src/plugins/oSnap/types.ts @@ -426,3 +426,13 @@ export type ErrorWithMessage = InstanceType & { export function isErrorWithMessage(error: unknown): error is ErrorWithMessage { return error !== null && typeof error === 'object' && 'message' in error; } + +export const Status = { + IDLE: 'IDLE', + LOADING: 'LOADING', + SUCCESS: 'SUCCESS', + FAIL: 'FAIL', + ERROR: 'ERROR' +} as const; + +export type Status = keyof typeof Status; diff --git a/src/plugins/oSnap/utils/abi.ts b/src/plugins/oSnap/utils/abi.ts index 57451a27c..3db06524d 100644 --- a/src/plugins/oSnap/utils/abi.ts +++ b/src/plugins/oSnap/utils/abi.ts @@ -6,13 +6,15 @@ import { mustBeEthereumAddress, mustBeEthereumContractAddress } from './validators'; +import { isErrorWithMessage } from '../types'; +import { fetchImplementationAddress } from './getters'; /** * Checks if the `parameter` of a contract method `method` takes an array or tuple as input, based on the `baseType` of the parameter. * * If this is the case, we must parse the value as JSON and verify that it is valid. */ -export function isArrayParameter(parameter: string): boolean { +export async function isArrayParameter(parameter: string): Promise { return ['tuple', 'array'].includes(parameter); } diff --git a/src/plugins/oSnap/utils/getters.ts b/src/plugins/oSnap/utils/getters.ts index b03912c3b..4f070acaa 100644 --- a/src/plugins/oSnap/utils/getters.ts +++ b/src/plugins/oSnap/utils/getters.ts @@ -9,6 +9,7 @@ import { toUtf8Bytes } from '@ethersproject/strings'; import { multicall } from '@snapshot-labs/snapshot.js/src/utils'; import getProvider from '@snapshot-labs/snapshot.js/src/utils/provider'; import memoize from 'lodash/memoize'; +import detectProxyTarget from 'evm-proxy-detection'; import { ERC20_ABI, GNOSIS_SAFE_TRANSACTION_API_URLS, @@ -753,3 +754,16 @@ export function getOracleUiLink( } return `https://testnet.oracle.uma.xyz?transactionHash=${txHash}&eventIndex=${logIndex}`; } + +export async function fetchImplementationAddress( + proxyAddress: string, + network: string +): Promise { + try { + const provider = getProvider(network); + const requestFunc = ({ method, params }) => provider.send(method, params); + return (await detectProxyTarget(proxyAddress, requestFunc)) ?? undefined; + } catch (error) { + console.error(error); + } +} diff --git a/src/plugins/oSnap/utils/validators.ts b/src/plugins/oSnap/utils/validators.ts index 7b3327b37..65709daf8 100644 --- a/src/plugins/oSnap/utils/validators.ts +++ b/src/plugins/oSnap/utils/validators.ts @@ -39,6 +39,7 @@ export const mustBeEthereumContractAddress = memoize( * Validates a transaction. */ export function validateTransaction(transaction: BaseTransaction) { + debugger; const addressNotEmptyOrInvalid = transaction.to !== '' && isAddress(transaction.to); return ( diff --git a/yarn.lock b/yarn.lock index 54d91a93c..e6a09676c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4326,6 +4326,11 @@ events@^3.0.0, events@^3.3.0: resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== +evm-proxy-detection@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/evm-proxy-detection/-/evm-proxy-detection-1.2.0.tgz#090a0812d09638b0ef2389d2de121704dc21fb86" + integrity sha512-pujpLG5JIiNWLRvjqI7qeT63dAQMMZvO8gaDWMfIsur1GYaJwtqrerXjXjbb0s/P3fIYu9nUJMJTd8/HudR3ow== + evp_bytestokey@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02"