Skip to content

Commit

Permalink
feat: fetch implementation abi in transaction builder for proxy contr…
Browse files Browse the repository at this point in the history
…acts (#124)
  • Loading branch information
gsteenkamp89 committed Feb 21, 2024
1 parent ea6595e commit 8bcf58b
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 9 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
<script setup lang="ts">
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,
parseValueInput
} 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;
Expand All @@ -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>(Status.IDLE);
const implementationAddress = ref('');
const showAbiChoiceModal = ref(false);
const isAbiValid = ref(true);
const value = ref(props.transaction.value ?? '0');
const isValueValid = ref(true);
Expand Down Expand Up @@ -65,6 +72,14 @@ function updateTransaction() {
}
}
async function handleFail() {
abiFetchStatus.value = Status.FAIL;
await sleep(3000);
if (abiFetchStatus.value === Status.FAIL) {
abiFetchStatus.value = Status.IDLE;
}
}
function updateParameter(index: number, value: string) {
parameters.value[index] = value;
}
Expand All @@ -82,15 +97,67 @@ 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);
}
}
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);
}
}
Expand All @@ -111,14 +178,20 @@ watch(abi, updateTransaction);
watch(selectedMethodName, updateTransaction);
watch(selectedMethod, updateTransaction);
watch(parameters, updateTransaction, { deep: true });
function handleDismissModal() {
abiFetchStatus.value = Status.IDLE;
showAbiChoiceModal.value = false;
}
</script>

<template>
<div class="space-y-2">
<AddressInput
v-model="to"
:label="$t('safeSnap.to')"
@update:model-value="updateAddress()"
:disabled="abiFetchStatus === Status.LOADING"
@update:model-value="debouncedUpdateAddress()"
/>

<UiInput
Expand All @@ -130,12 +203,28 @@ watch(parameters, updateTransaction, { deep: true });
</UiInput>

<UiInput
:disabled="abiFetchStatus === Status.LOADING"
:error="!isAbiValid && $t('safeSnap.invalidAbi')"
:model-value="abi"
@update:model-value="updateAbi($event)"
>
<template #label>ABI</template>
</UiInput>
<div
v-if="abiFetchStatus === Status.LOADING"
class="flex items-center justify-start gap-2 p-2"
>
<LoadingSpinner />
<p>Fetching ABI...</p>
</div>

<div
v-if="abiFetchStatus === Status.FAIL"
class="flex items-center justify-start gap-2 p-2 text-red"
>
<BaseIcon name="warning" class="text-inherit" />
<p>Failed to fetch ABI</p>
</div>

<div v-if="methods.length">
<UiSelect v-model="selectedMethodName" @change="updateMethod($event)">
Expand All @@ -161,4 +250,22 @@ watch(parameters, updateTransaction, { deep: true });
</div>
</div>
</div>

<BaseModal :open="showAbiChoiceModal" @close="handleDismissModal">
<template #header>
<h3 class="text-left px-3">Use Implementation ABI?</h3>
</template>
<div class="flex flex-col gap-4 p-3">
<p class="pr-8">
This contract looks like a proxy. Would you like to use the
implementation ABI?
</p>
<div class="flex gap-2 justify-center">
<TuneButton @click="handleUseProxyAbi"> Keep proxy ABI </TuneButton>
<TuneButton @click="handleUseImplementationAbi">
Use Implementation ABI
</TuneButton>
</div>
</div>
</BaseModal>
</template>
10 changes: 10 additions & 0 deletions src/plugins/oSnap/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -427,3 +427,13 @@ export type ErrorWithMessage = InstanceType<typeof Error> & {
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;
4 changes: 3 additions & 1 deletion src/plugins/oSnap/utils/abi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean> {
return ['tuple', 'array'].includes(parameter);
}

Expand Down
14 changes: 14 additions & 0 deletions src/plugins/oSnap/utils/getters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<string | undefined> {
try {
const provider = getProvider(network);
const requestFunc = ({ method, params }) => provider.send(method, params);
return (await detectProxyTarget(proxyAddress, requestFunc)) ?? undefined;
} catch (error) {
console.error(error);
}
}
1 change: 1 addition & 0 deletions src/plugins/oSnap/utils/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const mustBeEthereumContractAddress = memoize(
* Validates a transaction.
*/
export function validateTransaction(transaction: BaseTransaction) {
debugger;
const addressNotEmptyOrInvalid =
transaction.to !== '' && isAddress(transaction.to);
return (
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit 8bcf58b

Please sign in to comment.