Skip to content

Commit

Permalink
feat: support offchain space creation
Browse files Browse the repository at this point in the history
  • Loading branch information
wa0x6e committed Jan 21, 2025
1 parent 1b6147b commit b38f393
Show file tree
Hide file tree
Showing 18 changed files with 1,549 additions and 201 deletions.
157 changes: 157 additions & 0 deletions apps/ui/src/components/EnsConfiguratorOffchain.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
<script lang="ts" setup>
import { getNetwork } from '@/networks';
import { SNAPSHOT_URLS } from '@/networks/offchain';

Check warning on line 3 in apps/ui/src/components/EnsConfiguratorOffchain.vue

View workflow job for this annotation

GitHub Actions / lint-build-test

'SNAPSHOT_URLS' is defined but never used
import { NetworkID } from '@/types';
const spaceId = defineModel<string>();
const emit = defineEmits<{
(e: 'select');
}>();
const props = defineProps<{
networkId: NetworkID;
}>();
const {
MAX_ENS_NAME_LENGTH,
isRefreshing,
isLoading,
hasError,
names,
load,
refresh
} = useWalletEns(props.networkId);
const { resume: resumeEnsMonitoring } = useIntervalFn(() => refresh, 5000, {
immediate: false
});
const validNames = computed(() => {
return Object.values(names.value || {}).filter(d => d.status === 'AVAILABLE');
});
const invalidNames = computed(() => {
return Object.values(names.value || {}).filter(d => d.status !== 'AVAILABLE');
});
const isTestnet = computed(() => {
return getNetwork(props.networkId).name.includes('testnet');
});
function handleSelect(value: string) {
spaceId.value = value;
emit('select');
}
</script>

<template>
<UiAlert v-if="hasError" type="error">
<div>
An error happened while fetching the ENS names associated to your wallet.
Please try again
</div>
<UiButton
class="flex items-center justify-center gap-2"
:loading="isRefreshing"
@click="load"
>
<IH-refresh />
Retry
</UiButton>
</UiAlert>
<div v-else class="space-y-4">
<div class="space-y-2">
<div>
To create a space, you need an ENS name on
{{ isTestnet ? 'Sepolia testnet' : 'Ethereum mainnet' }}.
</div>
<UiMessage v-if="!isTestnet" type="info">
Still experimenting ?
<br />
You can also try
<AppLink to="https://testnet.snapshot.box/#/create">
testnet.snapshot.box
</AppLink>
- a Sepolia testnet playground dedicated to testing before creating your
space or proposals on Snapshot.
</UiMessage>
</div>
<div class="space-y-3">
<div class="flex justify-between items-center">
<h4 class="eyebrow">ENS names</h4>
<UiButton
v-if="names"
class="flex items-center gap-1 !text-skin-text !p-0 !border-0 !h-auto"
:disabled="isLoading"
:loading="isRefreshing"
@click="refresh"
>
<IH-refresh class="h-[16px]" />
Refresh
</UiButton>
</div>
<UiLoading v-if="(isLoading && !isRefreshing) || !names" class="block" />
<div v-else-if="Object.keys(names).length" class="space-y-2">
<UiSelector
v-for="name in validNames"
:key="name.name"
:is-active="spaceId === name.name"
class="w-full"
@click="() => handleSelect(name.name)"
>
<div class="flex gap-2 items-center text-skin-link">
<IH-Globe-alt class="shrink-0" />
{{ name.name }}
</div>
</UiSelector>
<UiSelector
v-for="name in invalidNames"
:key="name.name"
:disabled="true"
class="w-full"
>
<div class="flex gap-2 items-top">
<IH-Exclamation class="mt-[5px] text-skin-danger shrink-0" />
<div class="flex flex-col">
<div class="text-skin-danger" v-text="name.name" />
<div v-if="name.status === 'USED'">
ENS name already attached to a
<AppLink
:to="{
name: 'space',
params: { space: `${networkId}:${name.name}` }
}"
class="text-skin-link"
>
space
</AppLink>
</div>
<div v-else-if="name.status === 'TOO_LONG'">
ENS name is too long. It must be less than
{{ MAX_ENS_NAME_LENGTH }} characters
</div>
<div v-else-if="name.status === 'DELETED'">
ENS name was used by a previously deleted space and can not be
reused to create a new space.
<AppLink
to="https://docs.snapshot.box/faq/im-a-snapshot-user/space-settings#why-cant-i-create-a-new-space-with-my-previous-deleted-space-ens-name"
class="text-skin-link"
>
Learn more
<IH-arrow-sm-right class="-rotate-45 inline" />
</AppLink>
</div>
</div>
</div>
</UiSelector>
</div>
<UiMessage v-else type="danger">
No ENS names found for the current wallet.
</UiMessage>
</div>
<div class="space-y-3">
<h4 class="eyebrow">Register new ENS name</h4>
<FormEnsRegistration @submit="resumeEnsMonitoring" />
</div>
</div>
</template>
2 changes: 1 addition & 1 deletion apps/ui/src/components/FormController.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { validateForm } from '@/helpers/validation';
import { ChainId } from '@/types';
const model = defineModel<string>({ required: true });
const model = defineModel<string>();
const props = defineProps<{
title: string;
Expand Down
67 changes: 67 additions & 0 deletions apps/ui/src/components/FormEnsRegistration.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<script lang="ts" setup>
import { clone } from '@/helpers/utils';
import { getValidator } from '@/helpers/validation';
const VALID_EXTENSIONS = [
'eth',
'xyz',
'com',
'org',
'io',
'app',
'art',
'id'
] as const;
const DOMAIN_DEFINITION = {
type: 'string',
pattern: `^[a-zA-Z0-9\-\.]+\.(${VALID_EXTENSIONS.join('|')})$`,
title: 'ENS name',
examples: ['dao-name.eth'],
errorMessage: {
pattern: `Must be a valid domain ending with ${VALID_EXTENSIONS.join(', ')}`
}
} as const;
const definition = {
type: 'object',
additionalProperties: false,
required: [],
properties: {
domain: DOMAIN_DEFINITION
}
};
const emit = defineEmits<{
(e: 'submit');
}>();
const form = ref(clone({ domain: '' }));
const formErrors = computed(() => {
const validator = getValidator(definition);
return validator.validate(form.value, { skipEmptyOptionalFields: true });
});
const formValid = computed(() => {
return form.value.domain && Object.keys(formErrors.value).length === 0;
});
</script>

<template>
<div class="s-box">
<UiForm :model-value="form" :error="formErrors" :definition="definition" />
<UiButton
class="w-full"
:disabled="!formValid"
:to="
formValid
? `https://app.ens.domains/name/${form.domain}/register`
: undefined
"
@click="emit('submit')"
>
Register ENS name
</UiButton>
</div>
</template>
88 changes: 47 additions & 41 deletions apps/ui/src/components/FormSpaceStrategies.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { MAX_STRATEGIES } from '@/helpers/turbo';
import { StrategyConfig } from '@/networks/types';
import { NetworkID, Space } from '@/types';
import { NetworkID } from '@/types';
const snapshotChainId = defineModel<number>('snapshotChainId', {
required: true
Expand All @@ -10,11 +10,15 @@ const strategies = defineModel<StrategyConfig[]>('strategies', {
required: true
});
const props = defineProps<{
networkId: NetworkID;
isTicketValid: boolean;
space: Space;
}>();
const props = withDefaults(
defineProps<{
networkId: NetworkID;
isTicketValid: boolean;
space: { turbo: boolean; verified: boolean };
withNetworkSelector?: boolean;
}>(),
{ withNetworkSelector: true }
);
const strategiesLimit = computed(() => {
const spaceType = props.space.turbo
Expand All @@ -28,40 +32,42 @@ const strategiesLimit = computed(() => {
</script>

<template>
<h4 class="eyebrow mb-2 font-medium">Strategies</h4>
<div class="s-box mb-4">
<UiSelectorNetwork
v-model="snapshotChainId"
:definition="{
type: 'number',
title: 'Network',
tooltip:
'The default network used for this space. Networks can also be specified in individual strategies',
examples: ['Select network'],
networkId,
networksListKind: 'offchain'
}"
/>
</div>
<UiContainerSettings
:title="`Select up to ${strategiesLimit} strategies`"
description="(Voting power is cumulative)"
>
<UiMessage
v-if="!isTicketValid"
type="danger"
learn-more-link="https://snapshot.mirror.xyz/-uSylOUP82hGAyWUlVn4lCg9ESzKX9QCvsUgvv-ng84"
class="mb-3"
<div>
<h4 class="eyebrow mb-2 font-medium">Strategies</h4>
<div v-if="withNetworkSelector" class="s-box mb-4">
<UiSelectorNetwork
v-model="snapshotChainId"
:definition="{
type: 'number',
title: 'Network',
tooltip:
'The default network used for this space. Networks can also be specified in individual strategies',
examples: ['Select network'],
networkId,
networksListKind: 'offchain'
}"
/>
</div>
<UiContainerSettings
:title="`Select up to ${strategiesLimit} strategies`"
description="(Voting power is cumulative)"
>
In order to use the "ticket" strategy you are required to set a voting
validation strategy. This combination reduces the risk of spam and sybil
attacks.
</UiMessage>
<UiStrategiesConfiguratorOffchain
v-model:model-value="strategies"
:network-id="networkId"
:default-chain-id="snapshotChainId"
:limit="strategiesLimit"
/>
</UiContainerSettings>
<UiMessage
v-if="!isTicketValid"
type="danger"
learn-more-link="https://snapshot.mirror.xyz/-uSylOUP82hGAyWUlVn4lCg9ESzKX9QCvsUgvv-ng84"
class="mb-3"
>
In order to use the "ticket" strategy you are required to set a voting
validation strategy. This combination reduces the risk of spam and sybil
attacks.
</UiMessage>
<UiStrategiesConfiguratorOffchain
v-model:model-value="strategies"
:network-id="networkId"
:default-chain-id="snapshotChainId"
:limit="strategiesLimit"
/>
</UiContainerSettings>
</div>
</template>
Loading

0 comments on commit b38f393

Please sign in to comment.