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 16 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();
}
96 changes: 96 additions & 0 deletions src/plugins/oSnap/components/Input/FileInput/FileInput.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<script setup lang="ts">
import { ref, defineProps, watch } from 'vue';
import { getFileFromEvent, isFileOfType } from './utils';

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: 'IDLE' | 'ERROR' | 'INVALID_TYPE' | 'VALID'
): void;
}>();

const file = ref<File | null>(null);
const fileInputState = ref<'IDLE' | 'ERROR' | 'VALID' | 'INVALID_TYPE'>(
props.error ? 'ERROR' : 'IDLE'
);

const isDropping = ref(false);

const handleFileChange = (event: Event | DragEvent) => {
isDropping.value = false;
const _file = getFileFromEvent(event);
if (!_file) return;
if (!isFileOfType(_file, props.fileType)) {
fileInputState.value = 'INVALID_TYPE';
file.value = null;
} else {
file.value = _file;
fileInputState.value = 'VALID';
}
};

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

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

const toggleDropping = () => {
isDropping.value = !isDropping.value;
};
</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':
fileInputState === 'INVALID_TYPE' || props.error,
'bg-green/10 border-green/50 text-green/80': fileInputState === 'VALID'
}"
>
<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 === 'VALID' && file">{{
file.name
}}</template>
<template v-else="fileInputState === 'IDLE'"
>Click to select file, or drag n drop</template
>
</span>
</div>

<input
size=""
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 getFileFromEvent(event: DragEvent | Event) {
let _file: File | undefined;

if (event instanceof DragEvent) {
_file = event.dataTransfer?.files?.[0];
}

if (event.target && event.target instanceof HTMLInputElement) {
_file = (event?.currentTarget as HTMLInputElement)?.files?.[0];
}
if (!_file) return;
return _file;
}
65 changes: 54 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,49 @@ 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(() => {
// only suggest padding with zeros if too short but still valid bytes (even characters & starts with "0x")
const allowQuickFixForBytes32 = computed(() => {
if (errorMessageForDisplay?.value?.includes('short')) {
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 +92,23 @@ 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 (!isBytesLike(value)) {
throw new Error('Invalid bytes32');
}
return isBytesLike(value);
if (data.length < 64) {
validationErrorMessage.value = 'Value too short';
throw new Error('Less than 32 bytes');
}
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 +139,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 +167,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 +176,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 +185,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 +194,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>
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
<script setup lang="ts">
import { parseAmount } from '@/helpers/utils';
import { isAddress } from '@ethersproject/address';
import { isBigNumberish } from '@ethersproject/bignumber/lib/bignumber';
import { isHexString } from '@ethersproject/bytes';
import { RawTransaction } from '../../types';
import { createRawTransaction, parseValueInput } from '../../utils';
Expand Down Expand Up @@ -76,7 +74,7 @@ function updateValue(newValue: string) {
<AddressInput
v-model="to"
:input-props="{ required: false }"
:error="!isToValid && $t('safeSnap.invalidAddress')"
:error="!isToValid ? $t('safeSnap.invalidAddress') : undefined"
:label="$t('safeSnap.to')"
/>

Expand Down
Loading
Loading