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: add support for offchain space creation #1103

Open
wants to merge 29 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
8580ceb
feat: add space creation support to sx.js OffchainEthereumSig
wa0x6e Jan 14, 2025
44c9e3d
feat(ui): add new step 0 to select onchain/offchain type when creatin…
wa0x6e Jan 14, 2025
80a9dd4
feat: delegate steps handling to Stepper component
wa0x6e Jan 15, 2025
1b6147b
refactor: use useStepper composable to handle space creation steps
wa0x6e Jan 15, 2025
b38f393
feat: support offchain space creation
wa0x6e Jan 17, 2025
0fe4514
Merge branch 'master' into feat-create-offchain-space
wa0x6e Jan 21, 2025
d4838db
refactor: better variable name
wa0x6e Jan 21, 2025
32771ba
Merge branch 'feat-create-offchain-space' of https://github.com/snaps…
wa0x6e Jan 21, 2025
db2c620
feat: Use v2 to create offchain space
wa0x6e Jan 22, 2025
d848296
refactor: remove unused import
wa0x6e Jan 28, 2025
299ceb3
fix: fix space in loading icon
wa0x6e Jan 28, 2025
1c63665
fix: rename help center to helpdesk
wa0x6e Jan 28, 2025
f7665be
fix: remove shielded voting section to simplify the flow
wa0x6e Jan 28, 2025
bcaf498
fix: skip the voting type selection
wa0x6e Jan 28, 2025
59063bc
fix: enforce default 'any' privacy
wa0x6e Jan 28, 2025
1557837
revert: revert unrelated changes
wa0x6e Jan 28, 2025
92499c2
Merge branch 'master' into feat-create-offchain-space
wa0x6e Jan 28, 2025
0876394
fix: ui improvement
wa0x6e Jan 28, 2025
285aaa0
fix: use strategies limit from constant
wa0x6e Jan 28, 2025
f3c32f5
fix: migrate from token standard to popular strategies
wa0x6e Jan 29, 2025
86c457a
fix: improve UI
wa0x6e Jan 29, 2025
6d3eaca
fix: fix text casing
wa0x6e Jan 29, 2025
983ff55
Update apps/ui/src/components/EnsConfiguratorOffchain.vue
wa0x6e Jan 29, 2025
7488307
refactor: add types
wa0x6e Jan 29, 2025
eeace45
Update apps/ui/src/components/FormEnsRegistration.vue
wa0x6e Jan 29, 2025
67e9754
Merge branch 'master' into feat-create-offchain-space
bonustrack Feb 4, 2025
16e6a1a
Merge branch 'master' into feat-create-offchain-space
bonustrack Feb 5, 2025
fd60ad8
fix: update code to use dynamic turbo limits
wa0x6e Feb 5, 2025
e1e7c02
fix: update code to use the new updated web3 composable
wa0x6e Feb 5, 2025
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
5 changes: 5 additions & 0 deletions .changeset/cool-cameras-smoke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@snapshot-labs/sx": patch
---

add space creation support to OffchainEthereumSig
156 changes: 156 additions & 0 deletions apps/ui/src/components/EnsConfiguratorOffchain.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
<script lang="ts" setup>
import { getNetwork } from '@/networks';
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 !w-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>
49 changes: 40 additions & 9 deletions apps/ui/src/components/FormVoting.vue
Original file line number Diff line number Diff line change
@@ -1,40 +1,71 @@
<script setup lang="ts">
import { validateForm } from '@/helpers/validation';
import { offchainNetworks } from '@/networks';
import { NetworkID } from '@/types';

const props = defineProps<{
form: any;
selectedNetworkId: NetworkID;
title: string;
title?: string;
description?: string;
}>();

const emit = defineEmits<{
(e: 'errors', value: any);
}>();

const isOffchainNetwork = computed(() =>
offchainNetworks.includes(props.selectedNetworkId)
);

const definition = computed(() => {
return {
type: 'object',
title: 'SpaceSettings',
additionalProperties: true,
required: ['votingDelay', 'minVotingDuration', 'maxVotingDuration'],
required: [
'votingDelay',
'minVotingDuration',
!isOffchainNetwork.value ? 'maxVotingDuration' : undefined
].filter(Boolean),
properties: {
votingDelay: {
type: 'number',
format: 'duration',
title: 'Voting delay'
title: 'Voting delay',
...(isOffchainNetwork.value
? {
maximum: 2592000,
errorMessage: {
maximum: 'Delay must be less than 30 days'
}
}
: {})
},
minVotingDuration: {
type: 'number',
format: 'duration',
title: 'Min. voting duration'
title: isOffchainNetwork.value
? 'Voting period'
: 'Min. voting duration',
...(isOffchainNetwork.value
? {
maximum: 15552000,
errorMessage: {
maximum: 'Period must be less than 180 days'
}
}
: {})
},
maxVotingDuration: {
type: 'number',
format: 'duration',
title: 'Max. voting duration'
}
...(isOffchainNetwork.value
? {}
: {
maxVotingDuration: {
type: 'number',
format: 'duration',
title: 'Max. voting duration'
}
})
}
};
});
Expand Down
33 changes: 23 additions & 10 deletions apps/ui/src/components/Modal/SelectValidation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,18 @@ const STRATEGIES_WITHOUT_PARAMS: ValidationDetails['key'][] = [
'only-members'
];

const props = defineProps<{
open: boolean;
networkId: NetworkID;
defaultChainId: ChainId;
space: Space;
type: 'voting' | 'proposal';
current?: Validation;
}>();
const props = withDefaults(
defineProps<{
open: boolean;
networkId: NetworkID;
defaultChainId: ChainId;
space?: Space;
type: 'voting' | 'proposal';
current?: Validation;
skipMenu?: boolean;
}>(),
{ skipMenu: false }
);

const emit = defineEmits<{
(e: 'save', type: Validation);
Expand Down Expand Up @@ -227,14 +231,23 @@ function handleApply() {

watch(
() => props.open,
value => {
async value => {
if (value) {
selectedValidation.value = null;
fetchValidations();
await fetchValidations();

if (props.current) {
form.value = clone(props.current.params);
rawParams.value = JSON.stringify(props.current.params, null, 2);

if (props.skipMenu) {
const selectedValidationDetail = filteredValidations.value.find(
v => v.key === props.current!.name
);
if (selectedValidationDetail) {
handleSelect(selectedValidationDetail);
}
}
}
}
},
Expand Down
Loading