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: import transactions from Safe builder #147

Closed
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
a49f928
feat: allow safe json upload into osnap tx builder
daywiss Mar 11, 2024
6217d74
parse batchfile & populate input fields with values
gsteenkamp89 Mar 18, 2024
2f48613
show value & to fields
gsteenkamp89 Mar 18, 2024
fad35f7
improve input
gsteenkamp89 Mar 19, 2024
81e396b
improve file input error handling and styling
gsteenkamp89 Mar 20, 2024
cff8986
refactor
gsteenkamp89 Mar 20, 2024
4dca2b7
refactor
gsteenkamp89 Mar 20, 2024
928fb22
minor bug fix
gsteenkamp89 Mar 20, 2024
22e1678
allow quick fix for short bytes32
gsteenkamp89 Mar 20, 2024
bc81130
align icon better
gsteenkamp89 Mar 20, 2024
395d4c0
make file input own component
gsteenkamp89 Mar 25, 2024
dfb2406
import file once and initialize multiple safeImport transactions
gsteenkamp89 Mar 25, 2024
7cb56ed
check if safe address & chainId match
gsteenkamp89 Mar 25, 2024
4dfee57
small fixes
gsteenkamp89 Mar 25, 2024
b3d41e7
clean up comments
gsteenkamp89 Mar 26, 2024
877053e
better border sie for file input
gsteenkamp89 Mar 26, 2024
75699bf
clear input element value to allow recurring upload
gsteenkamp89 Mar 26, 2024
83203a8
fix quick fix
gsteenkamp89 Mar 26, 2024
7246ebb
handle multiple files
gsteenkamp89 Mar 26, 2024
b81907f
better error message
gsteenkamp89 Mar 26, 2024
568289c
emit from watcher
gsteenkamp89 Mar 27, 2024
23091f1
Merge branch 'master' into gerhard/uma-2431-figure-out-how-to-get-saf…
gsteenkamp89 Mar 27, 2024
625fcf8
fix: use upstream state only
gsteenkamp89 Mar 29, 2024
da49507
clean up
gsteenkamp89 Mar 29, 2024
8bf005c
do byteslike check last
gsteenkamp89 Mar 29, 2024
7f5adef
fixes
gsteenkamp89 Mar 31, 2024
728e26e
add default label
gsteenkamp89 Apr 1, 2024
529e427
fix: remove component level tx state
gsteenkamp89 Apr 1, 2024
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
14 changes: 12 additions & 2 deletions src/components/Ui/UiInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const props = defineProps<{
additionalInputClass?: string;
focusOnMount?: boolean;
readonly?: boolean;
quickFix?(): void;
}>();

const emit = defineEmits(['update:modelValue', 'blur']);
Expand Down Expand Up @@ -64,13 +65,22 @@ onMounted(() => {
</div>
<div
:class="[
's-error relative z-0',
's-error relative z-0 flex justify-start',
!!error ? '-mt-[20px] opacity-100' : '-mt-[48px] opacity-0'
]"
>
<BaseIcon name="warning" class="text-red-500 mr-2" />
{{ error || '' }}
<!-- The fact that error can be bool or string makes this necessary -->
{{ error || '' }}
<!-- Allow parent to format value with action -->
<button
v-if="quickFix"
class="ml-auto flex items-center gap-1 h-full px-1 rounded-full"
@click="quickFix"
>
Quick Fix
<i-ho-sparkles class="inline" />
</button>
</div>
</div>
</template>
4 changes: 4 additions & 0 deletions src/helpers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,7 @@ export function toChecksumAddress(address: string) {
return address;
}
}

export function addressEqual(address1: string, address2: string) {
return address1.toLowerCase() === address2.toLowerCase();
}
125 changes: 125 additions & 0 deletions src/plugins/oSnap/components/Input/FileInput/FileInput.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<script setup lang="ts">
import { ref, defineProps, watch } from 'vue';
import { getFilesFromEvent, isFileOfType } from './utils';

type InputState = 'IDLE' | 'INVALID_TYPE' | 'VALID' | 'INVALID_QUANTITY';

const props = defineProps<{
fileType: File['type'];
error?: string | undefined;
multiple?: boolean;
}>();

const emit = defineEmits<{
(event: 'update:file', file: File | null): void;
(event: 'update:fileInputState', state: InputState): void;
}>();
const inputRef = ref<HTMLInputElement>();
const file = ref<File | null>();
const fileInputState = ref<InputState>('IDLE');
const isDropping = ref(false);

const isError = computed(() => {
return (
!!props.error ||
fileInputState.value === 'INVALID_TYPE' ||
fileInputState.value === 'INVALID_QUANTITY'
);
});

const isAccepted = computed(() => {
return fileInputState.value === 'VALID' && !props?.error;
});

const handleFileChange = (event: Event | DragEvent) => {
isDropping.value = false;
const _files = getFilesFromEvent(event);
if (!_files?.length) return;

// enforce single drop based on props
if (!props.multiple) {
if (_files?.length && _files?.length > 1) {
fileInputState.value = 'INVALID_QUANTITY';
file.value = null;
clearInputValue();
return;
}
}
const _file = _files[0];
if (!isFileOfType(_file, props.fileType)) {
fileInputState.value = 'INVALID_TYPE';
file.value = null;
} else {
file.value = _file;
fileInputState.value = 'VALID';
}
clearInputValue();
};

function clearInputValue() {
if (inputRef?.value) {
inputRef.value.value = '';
}
}

function toggleDropping() {
isDropping.value = !isDropping.value;
}

watch(file, newFile => {
if (newFile) {
emit('update:file', newFile);
}
});

watch(fileInputState, newState => {
emit('update:fileInputState', newState);
});
</script>

<template>
<label
for="file_input"
@dragenter.prevent="toggleDropping"
@dragleave.prevent="toggleDropping"
@dragover.prevent
@drop.prevent="handleFileChange($event)"
class="my-2 w-full group hover:bg-transparent hover:border-skin-text hover:text-skin-link hover:cursor-pointer inline-block border-2 border-dashed py-2 px-4 rounded-xl"
:class="{
'border-solid border-skin-text text-skin-link bg-transparent': isDropping,
'bg-red/10 border-red/50 text-red/80': isError,
'bg-green/10 border-green/50 text-green/80': isAccepted
}"
>
<div
class="flex line-clamp-2 flex-col gap-1 items-center text-center justify-center"
>
<i-ho-upload />
<span class="line-clamp-2">
<template v-if="props.error">{{ props.error }}</template>
<template v-else-if="fileInputState === 'INVALID_TYPE'"
>File type must be <strong>{{ props.fileType }}</strong
>. Please choose another.</template
>
<template v-else-if="fileInputState === 'INVALID_QUANTITY'"
>Drop only <strong class="underline">one</strong> file at a time
</template>
<template v-else-if="fileInputState === 'VALID' && file">{{
file.name
}}</template>
<template v-else="fileInputState === 'IDLE'"
>Click to select file, or drag n drop</template
>
</span>
</div>

<input
ref="inputRef"
id="file_input"
class="hidden"
:accept="props.fileType"
type="file"
@change="handleFileChange"
/>
</label>
</template>
17 changes: 17 additions & 0 deletions src/plugins/oSnap/components/Input/FileInput/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export function isFileOfType(file: File, type: File['type']) {
return file.type === type;
}

export function getFilesFromEvent(event: DragEvent | Event) {
let _files: FileList | undefined | null;

if (event instanceof DragEvent) {
_files = event.dataTransfer?.files;
}

if (event.target && event.target instanceof HTMLInputElement) {
_files = (event?.currentTarget as HTMLInputElement)?.files;
}
if (!_files) return;
return _files;
}
66 changes: 55 additions & 11 deletions src/plugins/oSnap/components/Input/MethodParameter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { hexZeroPad, isBytesLike } from '@ethersproject/bytes';
const props = defineProps<{
parameter: ParamType;
value: string;
validateOnMount?: boolean;
}>();

const emit = defineEmits<{
Expand Down Expand Up @@ -37,25 +38,48 @@ const inputType = computed(() => {

const label = `${props.parameter.name} (${props.parameter.type})`;
const arrayPlaceholder = `E.g. ["text", 123, 0x123]`;
const newValue = ref(props.value);

const validationState = ref(true);
const isInputValid = computed(() => validationState.value);

const validationErrorMessage = ref<string>();
const errorMessageForDisplay = computed(() => {
if (!isInputValid.value) {
return validationErrorMessage.value
? validationErrorMessage.value
: `Invalid ${props.parameter.baseType}`;
}
});

const isInputValid = computed(() => {
const allowQuickFixForBytes32 = computed(() => {
if (!errorMessageForDisplay?.value?.includes('long')) {
return true;
}
return false;
});

function validate() {
if (!isDirty.value) return true;
if (isAddressInput.value) return isAddress(newValue.value);
if (isArrayInput.value) return validateArrayInput(newValue.value);
if (isNumberInput.value) return validateNumberInput(newValue.value);
if (isBytes32Input.value) return validateBytes32Input(newValue.value);
if (isBytesInput.value) return validateBytesInput(newValue.value);
return true;
});

const newValue = ref(props.value);
}

watch(props.parameter, () => {
newValue.value = '';
isDirty.value = false;
});

watch(newValue, () => {
const valid = validate();
if (valid) {
validationErrorMessage.value = undefined;
}
validationState.value = valid;
emit('updateParameterValue', newValue.value);
});

Expand All @@ -67,12 +91,25 @@ function validateBytesInput(value: string) {
return isBytesLike(value);
}

// provide better feedback/validation messages for bytes32 inputs
function validateBytes32Input(value: string) {
try {
if (value.slice(2).length > 64) {
throw new Error('String too long');
const data = value?.slice(2) || '';

if (data.length < 64) {
validationErrorMessage.value = 'Value too short';
throw new Error('Less than 32 bytes');
}

if (!isBytesLike(value)) {
Copy link

Choose a reason for hiding this comment

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

this check should be done last. i think it should either be in this order, too short, too long, isBytesLIke

Copy link
Author

Choose a reason for hiding this comment

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

fixed

throw new Error('Invalid bytes32');
}
return isBytesLike(value);

if (data.length > 64) {
validationErrorMessage.value = 'Value too long';
throw new Error('More than 32 bytes');
}
return true;
} catch {
return false;
}
Expand Down Expand Up @@ -103,6 +140,12 @@ function formatBytes32() {
newValue.value = hexZeroPad(newValue.value, 32);
}
}
onMounted(() => {
if (props.validateOnMount) {
isDirty.value = true;
}
validationState.value = validate();
});
</script>

<template>
Expand All @@ -125,7 +168,7 @@ function formatBytes32() {
<UiInput
v-if="inputType === 'array'"
:placeholder="arrayPlaceholder"
:error="!isInputValid && `Invalid ${parameter.baseType}`"
:error="errorMessageForDisplay"
:model-value="value"
@update:model-value="onChange($event)"
>
Expand All @@ -134,7 +177,7 @@ function formatBytes32() {
<UiInput
v-if="inputType === 'number'"
placeholder="123456"
:error="!isInputValid && `Invalid ${parameter.baseType}`"
:error="errorMessageForDisplay"
:model-value="value"
@update:model-value="onChange($event)"
>
Expand All @@ -143,7 +186,7 @@ function formatBytes32() {
<UiInput
v-if="inputType === 'bytes'"
placeholder="0x123abc"
:error="!isInputValid && `Invalid ${parameter.baseType}`"
:error="errorMessageForDisplay"
:model-value="value"
@update:model-value="onChange($event)"
>
Expand All @@ -152,8 +195,9 @@ function formatBytes32() {
<UiInput
v-if="inputType === 'bytes32'"
placeholder="0x123abc"
:error="!isInputValid && `Invalid ${parameter.baseType}`"
:error="errorMessageForDisplay"
:model-value="value"
:quick-fix="allowQuickFixForBytes32 ? formatBytes32 : undefined"
@blur="formatBytes32"
@update:model-value="onChange($event)"
>
Expand Down
8 changes: 8 additions & 0 deletions src/plugins/oSnap/components/Input/TransactionType.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const transactionTypesWithDetails: {
type: TransactionType;
title: string;
description: string;
readOnly?: boolean;
}[] = [
{
type: 'transferFunds',
Expand All @@ -36,6 +37,13 @@ const transactionTypesWithDetails: {
type: 'raw',
title: 'Raw transaction',
description: 'Send a raw transaction'
},
{
type: 'safeImport',
title: 'Import Safe File',
description:
'Import JSON file exported from Gnosis Safe transaction builder',
readOnly: true
}
];
</script>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ defineProps<{
type: TransactionType;
title: string;
description: string;
readOnly?: boolean;
}[];
}>();

Expand All @@ -28,17 +29,19 @@ function select(type: TransactionType) {
<h3>Select transaction type</h3>
</template>
<div class="mx-0 my-4 flex flex-col space-y-3 md:mx-4">
<button
v-for="(typeAndDetails, key) in transactionTypesWithDetails"
:key="key"
@click="select(typeAndDetails.type)"
>
<BaseModalSelectItem
:selected="typeAndDetails.type === selected"
:title="typeAndDetails.title"
:description="typeAndDetails.description"
/>
</button>
<template v-for="(typeAndDetails, key) in transactionTypesWithDetails">
<button
v-if="!typeAndDetails.readOnly"
:key="key"
@click="select(typeAndDetails.type)"
>
<BaseModalSelectItem
:selected="typeAndDetails.type === selected"
:title="typeAndDetails.title"
:description="typeAndDetails.description"
/>
</button>
</template>
</div>
</BaseModal>
</template>
Loading
Loading