Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: tenderly simulation button in transaction builder #120

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
21 changes: 21 additions & 0 deletions src/assets/icons/tenderly.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 2 additions & 3 deletions src/plugins/oSnap/Create.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,10 @@ import {
Transaction
} from './types';
import {
allTransactionsValid,
getGnosisSafeBalances,
getGnosisSafeCollectibles,
getIsOsnapEnabled,
getModuleAddressForTreasury,
validateOsnapTransaction
getModuleAddressForTreasury
} from './utils';
import OsnapMarketingWidget from './components/OsnapMarketingWidget.vue';

Expand Down Expand Up @@ -286,6 +284,7 @@ onMounted(async () => {
:collectables="collectables"
:network="newPluginData.safe.network"
:transactions="newPluginData.safe.transactions"
:safe="newPluginData.safe"
@add-transaction="addTransaction"
@remove-transaction="removeTransaction"
@update-transaction="updateTransaction"
Expand Down
17 changes: 13 additions & 4 deletions src/plugins/oSnap/Proposal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import ReadOnly from './components/Input/ReadOnly.vue';
import SafeLinkWithAvatar from './components/SafeLinkWithAvatar.vue';
import { GnosisSafe, Transaction } from './types';
import OsnapMarketingWidget from './components/OsnapMarketingWidget.vue';
import TenderlySimulation from './components/TransactionBuilder/TenderlySimulation.vue';

const keyOrder = [
'to',
Expand Down Expand Up @@ -59,9 +60,11 @@ function enrichTransactionForDisplay(transaction: Transaction) {
...commonProperties,
type: 'Contract interaction',
'method name': method.name,
...Object.fromEntries(method.inputs.map((input,i)=>{
return [`${input.name} (param ${i+1}): `,parameters[i]]
}))
...Object.fromEntries(
method.inputs.map((input, i) => {
return [`${input.name} (param ${i + 1}): `, parameters[i]];
})
)
};
}
if (transaction.type === 'transferFunds') {
Expand Down Expand Up @@ -97,7 +100,7 @@ function enrichTransactionForDisplay(transaction: Transaction) {
<template>
<template v-if="safe.transactions.length > 0">
<div
class="flex w-full flex-col gap-4 rounded-2xl border border-gray-200 p-3 md:p-4 relative"
class="flex w-full flex-col gap-4 rounded-2xl border border-skin-border p-3 md:p-4 relative"
>
<OsnapMarketingWidget class="absolute top-[-16px] right-[16px]" />
<h2 class="text-lg">oSnap Transactions</h2>
Expand All @@ -120,6 +123,12 @@ function enrichTransactionForDisplay(transaction: Transaction) {
</ReadOnly>
</div>

<TenderlySimulation
:transactions="safe.transactions"
:safe="safe"
:network="safe.network"
/>

<HandleOutcome
v-if="!!results"
:space="space"
Expand Down
28 changes: 21 additions & 7 deletions src/plugins/oSnap/components/Input/Address.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { mustBeEthereumAddress } from '../../utils';
const props = defineProps<{
modelValue: string;
label: string;
error?: string;
disabled?: boolean;
}>();
const emit = defineEmits<{
Expand All @@ -12,12 +13,25 @@ const emit = defineEmits<{

const input = ref('');
const dirty = ref(false);
const error = computed(() => {
if (!dirty.value) return '';
if (input.value === '') return 'Address is required';
if (!mustBeEthereumAddress(input.value)) return 'Invalid address';
return '';
});
const error = ref('');

const validate = () => {
if (!dirty.value) {
error.value = '';
return;
}
if (input.value === '') {
error.value = 'Address is required';
return;
}
if (!mustBeEthereumAddress(input.value)) {
error.value = 'Invalid address';
return;
}
error.value = '';
};

watch(input, validate);

watch(
() => props.modelValue,
Expand All @@ -41,7 +55,7 @@ const handleInput = () => {
<UiInput
v-model="input"
:disabled="disabled"
:error="error !== '' && error"
:error="props.error ?? (error || '')"
@input="handleInput()"
@blur="dirty = true"
>
Expand Down
155 changes: 141 additions & 14 deletions src/plugins/oSnap/components/TransactionBuilder/ContractInteraction.vue
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
<script setup lang="ts">
import { FunctionFragment } from '@ethersproject/abi';
import { isAddress } from '@ethersproject/address';
import { asyncComputed, useDebounceFn } from '@vueuse/core';

import { ContractInteractionTransaction, Network } from '../../types';
import { ContractInteractionTransaction, Network, Status } from '../../types';
import {
checkIsContract,
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 @@ -22,12 +26,26 @@ const emit = defineEmits<{
updateTransaction: [transaction: ContractInteractionTransaction];
}>();

const to = ref(props.transaction.to ?? '');
const to = ref('');

const isToContract = asyncComputed(() => {
if (!isAddress(to.value)) {
return true;
}
return checkIsContract(to.value, props.network);
}, true);

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 abiError = ref<string>();
const value = ref(props.transaction.value ?? '0');
const isValueValid = ref(true);
const methods = ref<FunctionFragment[]>([]);
Expand Down Expand Up @@ -60,11 +78,20 @@ function updateTransaction() {
parameters: parameters.value
});
emit('updateTransaction', transaction);
} catch {
} catch (error) {
console.error(error);
props.setTransactionAsInvalid();
}
}

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 @@ -75,22 +102,80 @@ function updateMethod(methodName: string) {
}

function updateAbi(newAbi: string) {
abi.value = newAbi;
methods.value = [];
if (newAbi === abi.value) {
return;
}
try {
abi.value = newAbi;
methods.value = getABIWriteFunctions(abi.value);
isAbiValid.value = true;
updateMethod(methods.value[0].name);
parameters.value = [];
} catch (error) {
isAbiValid.value = false;
console.warn('error extracting useful methods', error);
handleFail();
abiError.value = 'Error extracting write methods.';
}
}

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;
if (!isToContract.value) {
throw new Error('Address provided is not a contract on this network');
}
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 +196,22 @@ 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"
:error="!isToContract ? 'Not Contract address' : undefined"
:network="network"
@update:model-value="debouncedUpdateAddress()"
/>

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

<UiInput
:error="!isAbiValid && $t('safeSnap.invalidAbi')"
:disabled="abiFetchStatus === Status.LOADING"
:error="!isAbiValid && (abiError ?? $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 +270,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>
Loading
Loading