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: show warning if gas exceeds 500_000, refactor state #121

Merged
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
150 changes: 101 additions & 49 deletions src/plugins/oSnap/components/TransactionBuilder/TenderlySimulation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,40 +7,74 @@ import {
GnosisSafe,
isErrorWithMessage
} from '../../types';
import { computed } from 'vue';
import { ref } from 'vue';
import {
SIMULATION_ENDPOINT,
exceedsOsnapGasSubsidy,
prepareTenderlySimulationPayload,
validatePayload
validatePayload,
OSNAP_GAS_SUBSIDY
} from '../../utils/tenderly';

// ERROR => unable to simulate
// FAIL => tx Failed in simulation
// SUCCESS => tx Succeeded in simulation
type Status = 'SUCCESS' | 'FAIL' | 'ERROR' | 'LOADING' | 'IDLE';

const props = defineProps<{
transactions: TTransaction[];
safe: GnosisSafe | null;
network: Network;
}>();

const simulationState = ref<Status>('IDLE');
const simulationLink = ref<string>();
const simulationError = ref<string>();
type State =
| {
status: 'SUCCESS';
simulationLink: TenderlySimulationResult['resultUrl'];
gasUsed: TenderlySimulationResult['gasUsed'];
exceedsGasSubsidy: boolean;
}
| {
status: 'FAIL';
simulationLink: TenderlySimulationResult['resultUrl'];
gasUsed: TenderlySimulationResult['gasUsed'];
exceedsGasSubsidy: boolean;
}
| {
status: 'ERROR';
error: string;
}
| {
status: 'LOADING';
}
| {
status: 'IDLE';
};

const simulationState = ref<State>({ status: 'IDLE' });

const resetState = () => {
simulationState.value = { status: 'IDLE' };
};

function handleSimulationResult(res: TenderlySimulationResult) {
// if gas exceeds osnap gas subsidy, tx will not be automatically executed
const exceedsGasSubsidy = exceedsOsnapGasSubsidy(res);

if (res.status === true) {
simulationState.value = 'SUCCESS';
simulationState.value = {
status: 'SUCCESS',
simulationLink: res.resultUrl,
gasUsed: res.gasUsed,
exceedsGasSubsidy
};
} else {
simulationState.value = 'FAIL';
simulationState.value = {
status: 'FAIL',
simulationLink: res.resultUrl,
gasUsed: res.gasUsed,
exceedsGasSubsidy
};
}
simulationLink.value = res.resultUrl.url;
}

async function simulate() {
simulationState.value = 'LOADING';
simulationState.value = { status: 'LOADING' };
try {
const payload = prepareTenderlySimulationPayload(props);

Expand All @@ -64,84 +98,102 @@ async function simulate() {
} catch (error) {
console.error(error);
if (isErrorWithMessage(error)) {
simulationError.value = error.message;
simulationState.value = { status: 'ERROR', error: error.message };
} else {
simulationState.value = {
status: 'ERROR',
error: 'Failed to simulate!'
};
}
simulationState.value = 'ERROR';
await sleep(5_000);
simulationState.value = 'IDLE';
simulationState.value = {
status: 'IDLE'
};
}
}

const errorMessage = simulationError ?? 'Failed to simulate!';

const showResult = computed(() => {
return (
simulationState.value === 'FAIL' || simulationState.value === 'SUCCESS'
);
});

const resetState = () => {
simulationState.value = 'IDLE';
simulationLink.value = undefined;
simulationError.value = undefined;
};
</script>

<template>
<div>
<button
v-if="!showResult"
v-if="
!(
simulationState.status === 'SUCCESS' ||
simulationState.status === 'FAIL'
)
"
@click="simulate"
:disabled="simulationState !== 'IDLE'"
:disabled="simulationState.status !== 'IDLE'"
:class="[
'flex w-full enabled:hover:border-skin-text gap-2 justify-center h-[48px] px-[20px] items-center border disabled:cursor-not-allowed rounded-full border-skin-border',
{
'text-red': simulationState === 'ERROR'
'text-red': simulationState.status === 'ERROR'
}
]"
>
<IconTenderly class="text-skin-link inline h-[20px] w-[20px]" />
<span v-if="simulationState === 'IDLE'">Simulate Transaction</span>
<span v-if="simulationState === 'LOADING'">Checking transaction...</span>
<span class="text-xs" v-if="simulationState === 'ERROR'">{{
errorMessage
<span v-if="simulationState.status === 'IDLE'">Simulate Transaction</span>
<span v-if="simulationState.status === 'LOADING'"
>Checking transaction...</span
>
<span class="text-xs" v-if="simulationState.status === 'ERROR'">{{
simulationState.error
}}</span>

<LoadingSpinner class="ml-auto" v-if="simulationState === 'LOADING'" />
<LoadingSpinner
class="ml-auto"
v-if="simulationState.status === 'LOADING'"
/>
</button>
<div class="flex flex-col gap-2" v-if="showResult">
<div class="flex flex-col gap-2" v-else>
<div
:class="[
'flex w-full text-sm md:text-[18px] justify-between h-[48px] px-[20px] items-center rounded-full',
{
'bg-green/20 text-green': simulationState === 'SUCCESS',
'bg-red/20 text-red': simulationState === 'FAIL'
'bg-green/20 text-green': simulationState.status === 'SUCCESS',
'bg-red/20 text-red': simulationState.status === 'FAIL'
}
]"
>
<div class="flex items-center gap-2">
<IconTenderly class="inline h-[20px] w-[20px] text-inherit" />
<span v-if="simulationState === 'SUCCESS'">Success!</span>
<span v-if="simulationState === 'FAIL'">Transaction failed!</span>
<span v-if="simulationState.status === 'SUCCESS'">Success!</span>
<span v-if="simulationState.status === 'FAIL'"
>Transaction failed!</span
>
</div>
<a
v-if="simulationState.simulationLink.public"
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if link isn't public, don't share it

target="_blank"
class="flex items-center gap-1 text-inherit hover:underline"
:href="simulationLink"
:href="simulationState.simulationLink.url"
>
<span>View on Tenderly</span>
<IHoExternalLink class="text-inherit inline w-[1.2em] h-[1.2em]"
/></a>
<IHoExternalLink class="text-inherit inline w-[1.2em] h-[1.2em]" />
</a>
<div v-else class="text-inherit">Simulation not public</div>
</div>

<TuneButton
class="group text-sm md:text-[18px] hover:cursor-pointer justify-center w-full flex gap-2 mx-auto items-center"
:tooltip="'Reset Simulation'"
v-if="showResult"
@click="resetState"
>
Reset
Reset Simulation
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

more explicit copy

<IHoRefresh class="text-inherit w-[1em] h-[1em]" />
</TuneButton>

<p v-if="simulationState.exceedsGasSubsidy" class="text-sm text-left">
<strong class="text-skins text-base text-red">Warning:</strong>
This transaction will
<strong class="underline"
>not be automatically executed by oSnap.</strong
>
This transaction used
{{ simulationState.gasUsed.toLocaleString() }} gas, which exceeds
oSnap's maximum subsidized amount of
{{ OSNAP_GAS_SUBSIDY.toLocaleString() }}.
</p>
</div>
</div>
</template>
9 changes: 8 additions & 1 deletion src/plugins/oSnap/utils/tenderly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import {
OsnapPluginData,
Transaction as TTransaction,
GnosisSafe,
Network
Network,
TenderlySimulationResult
} from '../types';
import {
validateModuleAddress,
Expand All @@ -12,6 +13,8 @@ import {
export const SIMULATION_ENDPOINT =
'https://ethereum-api-read-prod-77jg7zf4ea-ue.a.run.app/osnap/simulate';

export const OSNAP_GAS_SUBSIDY = 500_000;

export function validatePayload(data: OsnapPluginData): void | never {
const { safe } = data;
if (!safe) {
Expand Down Expand Up @@ -53,3 +56,7 @@ export function prepareTenderlySimulationPayload(props: {

return { safe: payload };
}

export function exceedsOsnapGasSubsidy(res: TenderlySimulationResult): boolean {
return res.gasUsed > OSNAP_GAS_SUBSIDY;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that simulation gas usage might be lower than real one due to how state overrides short-circuit the OOv3 settlement logic. The execution bot uses ethers estimateGas on oSnap executeProposal call at the time when challenge window passed, so it might be worth checking this divergence in some test runs

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

interesting we might want to implement some sort or buffer percentage.

}
Loading