Skip to content

Commit

Permalink
validate payload, handle request and update styles
Browse files Browse the repository at this point in the history
  • Loading branch information
gsteenkamp89 committed Jan 30, 2024
1 parent a43c673 commit b29827f
Show file tree
Hide file tree
Showing 6 changed files with 170 additions and 55 deletions.
1 change: 1 addition & 0 deletions src/plugins/oSnap/Create.vue
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,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
128 changes: 80 additions & 48 deletions src/plugins/oSnap/components/TransactionBuilder/TenderlySimulation.vue
Original file line number Diff line number Diff line change
@@ -1,73 +1,90 @@
<script setup lang="ts">
import { Transaction as TTransaction, Network } from '../../types';
import { sleep } from '@snapshot-labs/snapshot.js/src/utils';
import {
Transaction as TTransaction,
Network,
TenderlySimulationResult,
GnosisSafe,
isErrorWithMessage
} from '../../types';
import { computed } from 'vue';
import { ref } from 'vue';
import {
SIMULATION_ENDPOINT,
prepareTenderlySimulationPayload,
validatePayload
} 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[]; // simulate bundle https://docs.tenderly.co/web3-gateway/references/simulate-bundle-json-rpc
safeAddress: string;
moduleAddress: string;
transactions: TTransaction[];
safe: GnosisSafe | null;
network: Network;
}>();
// ERROR => unable to simulate
// FAIL => tx failed in simulation
// SUCCESS => tx Succeeded in simulation
type Status = 'SUCCESS' | 'FAIL' | 'ERROR' | 'LOADING' | 'IDLE';
const simulationState = ref<Status>('IDLE');
const simulationLink = ref<string>();
const simulationError = ref<string>();
// TODO: get interface/url when serverless func is implemented
type SimulationResponse = {
data?: unknown;
error?: unknown;
};
// TODO: get endpoint
const simulationEndpoint = 'https://jsonplaceholder.typicode.com/posts';
// TODO: remove sleep logic this is just for testing the UI
const sleep = async (ms: number) => new Promise(res => setTimeout(res, ms));
function handleSimulationResult(res: TenderlySimulationResult) {
if (res.status === true) {
simulationState.value = 'SUCCESS';
} else {
simulationState.value = 'FAIL';
}
simulationLink.value = res.resultUrl.url;
}
async function simulate() {
simulationState.value = 'LOADING';
try {
const response = await fetch(simulationEndpoint, {
const payload = prepareTenderlySimulationPayload(props);
// throws if invalid
validatePayload(payload);
const response = await fetch(SIMULATION_ENDPOINT, {
headers: new Headers({
'content-type': 'application/json'
}),
method: 'POST',
// TODO: edit payload where necessary
body: JSON.stringify(props)
body: JSON.stringify(payload)
});
const data: SimulationResponse = await response.json();
// TODO: parse Success response
if (!response.ok) {
throw new Error('Error running simulation');
}
// check if tx passed in simulation
simulationState.value = 'ERROR';
// simulationState.value = 'FAIL';
// set tender simulation link
simulationLink.value =
'https://docs.tenderly.co/web3-gateway/references/simulate-json-rpc';
const data: TenderlySimulationResult = await response.json();
handleSimulationResult(data);
} catch (error) {
// network error
console.error(error);
if (isErrorWithMessage(error)) {
simulationError.value = error.message;
}
simulationState.value = 'ERROR';
return { error };
// TODO: remove sleep logic this is just for testing the UI
} finally {
await sleep(2000);
await sleep(5_000);
simulationState.value = 'IDLE';
}
}
const errorMessage = simulationError ?? 'Failed to simulate!';
const showResult = computed(() => {
return (
simulationState.value === 'FAIL' || simulationState.value === 'SUCCESS'
);
});
// IDLE => run simulation
// FAIL || SUCCESS => re-run
// FAIL => show info on failure
// LOADING => hide simulate button, show spinner
const resetState = () => {
simulationState.value = 'IDLE';
simulationLink.value = undefined;
simulationError.value = undefined;
};
</script>

<template>
Expand All @@ -76,31 +93,46 @@ const showResult = computed(() => {
v-if="!showResult"
@click="simulate"
:disabled="simulationState !== '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"
: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'
}
]"
>
<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 v-if="simulationState === 'ERROR'">Failed to simulate!</span>
<span v-if="simulationState === 'ERROR'">{{ errorMessage }}</span>

<LoadingSpinner class="ml-auto" v-if="simulationState === 'LOADING'" />
</button>
<div
v-if="showResult"
:class="[
'flex w-full gap-2 justify-between h-[48px] px-[20px] items-center rounded-full',
'flex w-full justify-between h-[48px] px-[20px] items-center rounded-full',
{
'bg-green/20 border-green text-green': simulationState === 'SUCCESS',
'bg-red/20 border-red text-red': simulationState === 'FAIL'
'bg-green/20 text-green': simulationState === 'SUCCESS',
'bg-red/20 text-red': simulationState === 'FAIL'
}
]"
>
<IconTenderly class="inline h-[20px] w-[20px] text-inherit" />
<span v-if="simulationState === 'SUCCESS'">Transaction passed!</span>
<span v-if="simulationState === 'FAIL'">Transaction failed!</span>
<div class="flex items-center gap-2">
<IconTenderly class="inline h-[20px] w-[20px] text-inherit" />
<span v-if="simulationState === 'SUCCESS'">Transaction passed!</span>
<span v-if="simulationState === 'FAIL'">Transaction failed!</span>
</div>
<button
class="text-sm p-2 hover:cursor-pointer"
:tooltip="'Reset Simulation'"
v-if="showResult"
@click="resetState"
>
<IHoRefresh class="text-inherit inline w-[1.4em] h-[1.4em]" />
</button>
<a
target="_blank"
class="flex py-2 pl-2 items-center gap-1 ml-auto text-inherit hover:underline"
class="flex py-2 pl-2 items-center gap-1 text-inherit hover:underline"
:href="simulationLink"
>View on Tenderly
<IHoExternalLink class="text-inherit inline w-[1em] h-[1em]"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
<script setup lang="ts">
import { ExtendedSpace, Proposal, Results } from '@/helpers/interfaces';
import { shorten } from '@/helpers/utils';
import { NFT, Network, Transaction as TTransaction, Token } from '../../types';
import {
GnosisSafe,
NFT,
Network,
Transaction as TTransaction,
Token
} from '../../types';
import { getSafeAppLink } from '../../utils';
import Transaction from './Transaction.vue';
import TenderlySimulation from './TenderlySimulation.vue';
Expand All @@ -16,6 +22,7 @@ const props = defineProps<{
proposal?: Proposal;
space: ExtendedSpace;
results?: Results;
safe: GnosisSafe | null;
}>();
const emit = defineEmits<{
Expand Down Expand Up @@ -68,8 +75,7 @@ const safeLink = computed(() =>
<TenderlySimulation
v-if="transactions.length"
:transactions="transactions"
:safe-address="safeAddress"
:module-address="moduleAddress"
:safe="props.safe"
:network="props.network"
class="mt-4"
/>
Expand Down
23 changes: 22 additions & 1 deletion src/plugins/oSnap/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export type SafeNetworkPrefixes = typeof safePrefixes;
* One of the supported network prefixes as defined in EIP-3770 used by Safe apps.
* @see SafeNetworkPrefixes
*/
export type SafeNetworkPrefix = SafeNetworkPrefixes[Network];
export type SafeNetworkPrefix = SafeNetworkPrefixes[keyof SafeNetworkPrefixes];

/**
* Represents the four different types of transactions that oSnap supports.
Expand Down Expand Up @@ -405,3 +405,24 @@ export type OGProposalState =
| (AssertionTransactionDetails & {
status: 'transactions-executed';
});

interface ResultUrl {
url: string; // This is the URL to the simulation result page (public or private).
public: boolean; // This is false if the project is not publicly accessible.
}

export interface TenderlySimulationResult {
id: string;
status: boolean; // True if the simulation succeeded, false if it reverted.
gasUsed: number;
resultUrl: ResultUrl;
}

export type ErrorWithMessage = InstanceType<typeof Error> & {
message: string;
};

// predicate for better error handling
export function isErrorWithMessage(error: unknown): error is ErrorWithMessage {
return error !== null && typeof error === 'object' && 'message' in error;
}
55 changes: 55 additions & 0 deletions src/plugins/oSnap/utils/tenderly.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import {
OsnapPluginData,
Transaction as TTransaction,
GnosisSafe,
Network
} from '../types';
import {
validateModuleAddress,
validateTransaction
} from '../utils/validators';

export const SIMULATION_ENDPOINT =
'https://ethereum-api-read-prod-77jg7zf4ea-ue.a.run.app/osnap/simulate';

export function validatePayload(data: OsnapPluginData): void | never {
const { safe } = data;
if (!safe) {
throw new Error('No safe data');
}
if (!validateModuleAddress(safe.network, safe?.moduleAddress)) {
throw new Error('Module address is incorrect for this network');
}
if (!(safe.transactions.length > 0)) {
throw new Error('No transactions to simulate');
}
safe.transactions.forEach((tx, i) => {
if (!validateTransaction(tx)) {
throw new Error(`Transaction ${i + 1} has missing data`);
}
});
return;
}

export function prepareTenderlySimulationPayload(props: {
transactions: TTransaction[];
safe: GnosisSafe | null;
network: Network;
}): OsnapPluginData {
// this will not happen in this component
if (!props.safe) {
throw new Error('No safe selected');
}

const { safeAddress, safeName } = props.safe;

const payload: GnosisSafe = {
safeAddress,
safeName,
network: props.network,
moduleAddress: props.safe.moduleAddress,
transactions: props.transactions
};

return { safe: payload };
}
6 changes: 3 additions & 3 deletions src/plugins/oSnap/utils/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ export const mustBeEthereumContractAddress = memoize(
* Validates a transaction.
*/
export function validateTransaction(transaction: BaseTransaction) {
const addressEmptyOrValidate =
transaction.to === '' || isAddress(transaction.to);
const addressNotEmptyOrInvalid =
transaction.to !== '' && isAddress(transaction.to);
return (
isBigNumberish(transaction.value) &&
addressEmptyOrValidate &&
addressNotEmptyOrInvalid &&
(!transaction.data || isHexString(transaction.data))
);
}
Expand Down

0 comments on commit b29827f

Please sign in to comment.