Repository: snapshot-labs/snapshot-v1
Branch: master
Commit: 23ef14e9e821
Files: 634
Total size: 2.0 MB
Directory structure:
gitextract_yfac7sqe/
├── .browserslistrc
├── .dockerignore
├── .eslintignore
├── .eslintrc-auto-import.json
├── .github/
│ ├── CODE_OF_CONDUCT.md
│ ├── CONTRIBUTING.md
│ ├── ISSUE_TEMPLATE/
│ │ ├── BUG_REPORT.yml
│ │ └── FEATURE_REQUEST.yml
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── dependabot.yml
│ ├── stale.yml
│ └── workflows/
│ ├── codeql.yml
│ ├── deploy.yml
│ ├── nodejs.yml
│ ├── test.yaml
│ ├── update-snapshot-packages.yml
│ └── update-snapshot-submodules.yml
├── .gitignore
├── .gitmodules
├── .gitpod.yml
├── .husky/
│ ├── post-checkout
│ ├── post-merge
│ ├── pre-commit
│ └── pre-push
├── Dockerfile
├── FUNDING.json
├── LICENSE
├── README.md
├── babel.config.js
├── crowdin.yml
├── index.html
├── package.json
├── postcss.config.js
├── public/
│ ├── .well-known/
│ │ ├── assetlinks.json
│ │ └── did.json
│ ├── manifest.json
│ └── service-worker.js
├── src/
│ ├── App.vue
│ ├── assets/
│ │ ├── css/
│ │ │ ├── main.scss
│ │ │ ├── tippy.scss
│ │ │ └── tune.scss
│ │ └── fonts/
│ │ ├── iconfont.css
│ │ └── iconfont.json
│ ├── components/
│ │ ├── AboutMembersListItem.vue
│ │ ├── AvatarOverlayEdit.vue
│ │ ├── AvatarSpace.vue
│ │ ├── AvatarToken.vue
│ │ ├── AvatarUser.vue
│ │ ├── Banner.vue
│ │ ├── BaseAvatar.vue
│ │ ├── BaseBadge.vue
│ │ ├── BaseBlock.vue
│ │ ├── BaseBreadcrumbs.vue
│ │ ├── BaseButtonIcon.vue
│ │ ├── BaseButtonRound.vue
│ │ ├── BaseCalendar.vue
│ │ ├── BaseCombobox.vue
│ │ ├── BaseContainer.vue
│ │ ├── BaseCounter.vue
│ │ ├── BaseIcon.vue
│ │ ├── BaseIndicator.vue
│ │ ├── BaseInput.vue
│ │ ├── BaseInterpunct.vue
│ │ ├── BaseLink.vue
│ │ ├── BaseListbox.vue
│ │ ├── BaseLoading.vue
│ │ ├── BaseMarkdown.vue
│ │ ├── BaseMenu.vue
│ │ ├── BaseMessage.vue
│ │ ├── BaseMessageBlock.vue
│ │ ├── BaseModal.vue
│ │ ├── BaseModalSelectItem.vue
│ │ ├── BaseNetworkItem.vue
│ │ ├── BaseNoResults.vue
│ │ ├── BasePill.vue
│ │ ├── BasePluginItem.vue
│ │ ├── BasePopover.vue
│ │ ├── BasePopoverHover.vue
│ │ ├── BaseProgressBar.vue
│ │ ├── BaseProgressRadial.vue
│ │ ├── BaseSearch.vue
│ │ ├── BaseSidebarNavigationItem.vue
│ │ ├── BaseSkinItem.vue
│ │ ├── BaseStrategyItem.vue
│ │ ├── BaseUser.vue
│ │ ├── BlockLink.vue
│ │ ├── BlockSpacesList.vue
│ │ ├── BlockSpacesListButtonMore.vue
│ │ ├── BlockSpacesListItem.vue
│ │ ├── BlockSpacesListSkeleton.vue
│ │ ├── ButtonBack.vue
│ │ ├── ButtonCard.vue
│ │ ├── ButtonFollow.vue
│ │ ├── ButtonPlayground.vue
│ │ ├── ButtonShare.vue
│ │ ├── ButtonSwitch.vue
│ │ ├── ButtonTheme.vue
│ │ ├── ComboboxNetwork.vue
│ │ ├── ContainerParallelInput.vue
│ │ ├── ExploreMenuCategories.vue
│ │ ├── ExploreSkeletonLoading.vue
│ │ ├── ExploreSpaces.vue
│ │ ├── FooterLinks.vue
│ │ ├── FooterLinksItem.vue
│ │ ├── FooterSocials.vue
│ │ ├── FooterSocialsItem.vue
│ │ ├── FooterTitle.vue
│ │ ├── FormArrayStrategies.vue
│ │ ├── FormObjectStrategyParams.vue
│ │ ├── IconDiscord.vue
│ │ ├── IconInformationTooltip.vue
│ │ ├── IconVerifiedSpace.vue
│ │ ├── IndicatorAssetsChange.spec.js
│ │ ├── IndicatorAssetsChange.vue
│ │ ├── InputCheckbox.vue
│ │ ├── InputComboboxToken.vue
│ │ ├── InputDate.vue
│ │ ├── InputEmail.vue
│ │ ├── InputNewsletter.vue
│ │ ├── InputSelect.vue
│ │ ├── InputSelectPrivacy.vue
│ │ ├── InputSelectVoteType.vue
│ │ ├── InputSelectVoteValidation.vue
│ │ ├── InputSwitch.vue
│ │ ├── InputUploadAvatar.vue
│ │ ├── LabelInput.vue
│ │ ├── LabelProposalState.vue
│ │ ├── LabelProposalVoted.vue
│ │ ├── LinkSpace.vue
│ │ ├── ListboxNetwork.vue
│ │ ├── LoadingList.vue
│ │ ├── LoadingPage.vue
│ │ ├── LoadingRow.vue
│ │ ├── LoadingSpinner.vue
│ │ ├── MenuAccount.vue
│ │ ├── MenuLanguages.vue
│ │ ├── MessageWarningFlagged.vue
│ │ ├── MessageWarningGnosisNetwork.vue
│ │ ├── MessageWarningHibernated.vue
│ │ ├── MessageWarningTestnet.vue
│ │ ├── MessageWarningValidation.vue
│ │ ├── ModalAccount.vue
│ │ ├── ModalConfirmAction.vue
│ │ ├── ModalConfirmLeave.vue
│ │ ├── ModalControllerEdit.vue
│ │ ├── ModalDelegate.vue
│ │ ├── ModalEmail.vue
│ │ ├── ModalEmailManagement.vue
│ │ ├── ModalEmailResend.vue
│ │ ├── ModalEmailSubscription.vue
│ │ ├── ModalLinkPreview.vue
│ │ ├── ModalMessage.vue
│ │ ├── ModalNotice.vue
│ │ ├── ModalOsnap.vue
│ │ ├── ModalPendingTransactions.vue
│ │ ├── ModalPlugins.vue
│ │ ├── ModalPostPayment.vue
│ │ ├── ModalPostVote.vue
│ │ ├── ModalProfileForm.vue
│ │ ├── ModalRevokeDelegate.vue
│ │ ├── ModalSelectDate.vue
│ │ ├── ModalSkins.vue
│ │ ├── ModalSnapshotTerms.vue
│ │ ├── ModalSpaces.vue
│ │ ├── ModalSpacesListItem.vue
│ │ ├── ModalStrategies.vue
│ │ ├── ModalStrategy.vue
│ │ ├── ModalTerms.vue
│ │ ├── ModalTokens.vue
│ │ ├── ModalTokensItem.vue
│ │ ├── ModalTransactionStatus.vue
│ │ ├── ModalTreasury.vue
│ │ ├── ModalValidation.vue
│ │ ├── ModalVote.vue
│ │ ├── ModalVoteValidation.vue
│ │ ├── ModalVotingPrivacy.vue
│ │ ├── ModalVotingType.vue
│ │ ├── ModalWrongNetwork.vue
│ │ ├── NavbarAccount.vue
│ │ ├── NavbarExtras.vue
│ │ ├── NavbarNotifications.vue
│ │ ├── PopoverHoverProfile.vue
│ │ ├── ProfileAboutBiography.vue
│ │ ├── ProfileAboutDelegate.vue
│ │ ├── ProfileAboutDelegateListItem.vue
│ │ ├── ProfileActivityList.vue
│ │ ├── ProfileActivityListItem.vue
│ │ ├── ProfileAddressCopy.vue
│ │ ├── ProfileName.vue
│ │ ├── ProfileSidebar.vue
│ │ ├── ProfileSidebarHeader.vue
│ │ ├── ProfileSidebarHeaderSkeleton.vue
│ │ ├── ProfileSidebarNavigation.vue
│ │ ├── ProposalsItem.vue
│ │ ├── ProposalsItemBody.vue
│ │ ├── ProposalsItemFooter.vue
│ │ ├── ProposalsItemResults.vue
│ │ ├── ProposalsItemTitle.vue
│ │ ├── SettingsBoostBlock.vue
│ │ ├── SettingsDangerzoneBlock.vue
│ │ ├── SettingsDelegationBlock.vue
│ │ ├── SettingsDomainBlock.vue
│ │ ├── SettingsLinkBlock.vue
│ │ ├── SettingsMembersBlock.vue
│ │ ├── SettingsMembersPopoverContent.vue
│ │ ├── SettingsPluginsBlock.vue
│ │ ├── SettingsProfileBlock.vue
│ │ ├── SettingsProposalBlock.vue
│ │ ├── SettingsStrategiesBlock.vue
│ │ ├── SettingsSubSpacesBlock.vue
│ │ ├── SettingsTreasuriesBlock.vue
│ │ ├── SettingsTreasuriesBlockItem.vue
│ │ ├── SettingsTreasuriesBlockItemButton.vue
│ │ ├── SettingsTreasuryActivateOsnapButton.vue
│ │ ├── SettingsValidationBlock.vue
│ │ ├── SettingsVotingBlock.vue
│ │ ├── SetupButtonBack.vue
│ │ ├── SetupButtonCreate.vue
│ │ ├── SetupButtonNext.vue
│ │ ├── SetupDomain.vue
│ │ ├── SetupDomainRegister.vue
│ │ ├── SetupExtras.vue
│ │ ├── SetupIntro.vue
│ │ ├── SetupMessageHelp.vue
│ │ ├── SetupProfile.vue
│ │ ├── SetupSidebarStepper.vue
│ │ ├── SetupStrategy.vue
│ │ ├── SetupStrategyAdvanced.vue
│ │ ├── SetupStrategyBasic.vue
│ │ ├── SetupStrategyVote.vue
│ │ ├── SidebarSpacesSkeleton.vue
│ │ ├── SidebarUnreadIndicator.vue
│ │ ├── SpaceBoostCardProposal.vue
│ │ ├── SpaceBoostDeposit.vue
│ │ ├── SpaceBreadcrumbs.vue
│ │ ├── SpaceCreateContent.vue
│ │ ├── SpaceCreateLegacyOsnap.vue
│ │ ├── SpaceCreateOsnap.vue
│ │ ├── SpaceCreatePlugins.vue
│ │ ├── SpaceCreateVoting.vue
│ │ ├── SpaceCreateVotingDateEnd.vue
│ │ ├── SpaceCreateVotingDateStart.vue
│ │ ├── SpaceCreateWarnings.vue
│ │ ├── SpaceDelegateEdit.vue
│ │ ├── SpaceDelegatesAccount.vue
│ │ ├── SpaceDelegatesCard.vue
│ │ ├── SpaceDelegatesDelegateModal.vue
│ │ ├── SpaceDelegatesSkeleton.vue
│ │ ├── SpaceDelegatesSplitDelegationModal.vue
│ │ ├── SpaceProposalBoost.vue
│ │ ├── SpaceProposalBoostClaim.vue
│ │ ├── SpaceProposalBoostClaimModal.vue
│ │ ├── SpaceProposalBoostClaimModalItem.vue
│ │ ├── SpaceProposalBoostClaimModalSuccess.vue
│ │ ├── SpaceProposalBoostItem.vue
│ │ ├── SpaceProposalBoostItemMenu.vue
│ │ ├── SpaceProposalBoostModalCreate.vue
│ │ ├── SpaceProposalBoostWinnersModal.vue
│ │ ├── SpaceProposalContent.vue
│ │ ├── SpaceProposalHeader.vue
│ │ ├── SpaceProposalInformation.vue
│ │ ├── SpaceProposalPage.vue
│ │ ├── SpaceProposalPlugins.vue
│ │ ├── SpaceProposalPluginsSidebar.vue
│ │ ├── SpaceProposalResults.vue
│ │ ├── SpaceProposalResultsList.vue
│ │ ├── SpaceProposalResultsListItem.vue
│ │ ├── SpaceProposalResultsProgressBar.vue
│ │ ├── SpaceProposalResultsQuorum.vue
│ │ ├── SpaceProposalResultsShutter.vue
│ │ ├── SpaceProposalVote.vue
│ │ ├── SpaceProposalVoteApproval.vue
│ │ ├── SpaceProposalVoteQuadratic.vue
│ │ ├── SpaceProposalVoteRankedChoice.vue
│ │ ├── SpaceProposalVoteSingleChoice.vue
│ │ ├── SpaceProposalVotes.vue
│ │ ├── SpaceProposalVotesFilters.vue
│ │ ├── SpaceProposalVotesItem.vue
│ │ ├── SpaceProposalVotesListItemChoice.vue
│ │ ├── SpaceProposalVotesModal.vue
│ │ ├── SpaceProposalVotesModalDownload.vue
│ │ ├── SpaceProposalsNotice.vue
│ │ ├── SpaceProposalsSearch.vue
│ │ ├── SpaceProposalsSearchFilter.vue
│ │ ├── SpaceSettingsMessageHibernated.vue
│ │ ├── SpaceSidebar.vue
│ │ ├── SpaceSidebarFooter.vue
│ │ ├── SpaceSidebarHeader.vue
│ │ ├── SpaceSidebarMenuThreeDot.vue
│ │ ├── SpaceSidebarNavigation.vue
│ │ ├── SpaceSidebarSubspaces.vue
│ │ ├── SpaceSplitDelegationRow.vue
│ │ ├── StrategiesBlockWarning.vue
│ │ ├── StrategiesListItem.vue
│ │ ├── TextAutolinker.vue
│ │ ├── TextareaArray.vue
│ │ ├── TextareaAutosize.vue
│ │ ├── TextareaJson.vue
│ │ ├── TheActionbar.vue
│ │ ├── TheFlashNotification.vue
│ │ ├── TheFooter.vue
│ │ ├── TheLayout.vue
│ │ ├── TheModalNotification.vue
│ │ ├── TheNavbar.vue
│ │ ├── TheSearchBar.vue
│ │ ├── TheSidebar.vue
│ │ ├── TreasuryAssetsList.vue
│ │ ├── TreasuryAssetsListItem.spec.js
│ │ ├── TreasuryAssetsListItem.vue
│ │ ├── TreasuryWalletsList.spec.js
│ │ ├── TreasuryWalletsList.vue
│ │ ├── TreasuryWalletsListItem.spec.js
│ │ ├── TreasuryWalletsListItem.vue
│ │ ├── Tune/
│ │ │ ├── TuneBlock.vue
│ │ │ ├── TuneBlockFooter.vue
│ │ │ ├── TuneBlockHeader.vue
│ │ │ ├── TuneButton.story.vue
│ │ │ ├── TuneButton.vue
│ │ │ ├── TuneButtonSelect.story.vue
│ │ │ ├── TuneButtonSelect.vue
│ │ │ ├── TuneCheckbox.story.vue
│ │ │ ├── TuneCheckbox.vue
│ │ │ ├── TuneCombobox.story.vue
│ │ │ ├── TuneCombobox.vue
│ │ │ ├── TuneComboboxMultiple.story.vue
│ │ │ ├── TuneComboboxMultiple.vue
│ │ │ ├── TuneErrorInput.story.vue
│ │ │ ├── TuneErrorInput.vue
│ │ │ ├── TuneForm.story.vue
│ │ │ ├── TuneForm.vue
│ │ │ ├── TuneIconHint.story.vue
│ │ │ ├── TuneIconHint.vue
│ │ │ ├── TuneInput.story.vue
│ │ │ ├── TuneInput.vue
│ │ │ ├── TuneInputDuration.story.vue
│ │ │ ├── TuneInputDuration.vue
│ │ │ ├── TuneInputSocial.vue
│ │ │ ├── TuneInputUrl.vue
│ │ │ ├── TuneLabelInput.story.vue
│ │ │ ├── TuneLabelInput.vue
│ │ │ ├── TuneListbox.story.vue
│ │ │ ├── TuneListbox.vue
│ │ │ ├── TuneListboxMultiple.story.vue
│ │ │ ├── TuneListboxMultiple.vue
│ │ │ ├── TuneLoadingSpinner.story.vue
│ │ │ ├── TuneLoadingSpinner.vue
│ │ │ ├── TuneMenu.story.vue
│ │ │ ├── TuneMenu.vue
│ │ │ ├── TuneModal.story.vue
│ │ │ ├── TuneModal.vue
│ │ │ ├── TuneModalDescription.vue
│ │ │ ├── TuneModalIndicator.vue
│ │ │ ├── TuneModalTitle.vue
│ │ │ ├── TunePopover.story.vue
│ │ │ ├── TunePopover.vue
│ │ │ ├── TuneRadio.story.vue
│ │ │ ├── TuneRadio.vue
│ │ │ ├── TuneSelect.vue
│ │ │ ├── TuneSwitch.story.vue
│ │ │ ├── TuneSwitch.vue
│ │ │ ├── TuneTag.story.vue
│ │ │ ├── TuneTag.vue
│ │ │ ├── TuneTextarea.story.vue
│ │ │ ├── TuneTextarea.vue
│ │ │ ├── TuneTextareaArray.story.vue
│ │ │ ├── TuneTextareaArray.vue
│ │ │ ├── TuneTextareaJson.story.vue
│ │ │ ├── TuneTextareaJson.vue
│ │ │ └── _Form/
│ │ │ ├── FormArray.vue
│ │ │ ├── FormBoolean.vue
│ │ │ ├── FormNumber.vue
│ │ │ └── FormString.vue
│ │ └── Ui/
│ │ ├── UiCollapsible.vue
│ │ ├── UiCollapsibleContent.vue
│ │ ├── UiCollapsibleText.vue
│ │ ├── UiInput.vue
│ │ └── UiSelect.vue
│ ├── composables/
│ │ ├── useAccount.ts
│ │ ├── useAliasAction.ts
│ │ ├── useApolloQuery.ts
│ │ ├── useApp.ts
│ │ ├── useBalances.ts
│ │ ├── useBoost.ts
│ │ ├── useChangeNetwork.ts
│ │ ├── useClient.ts
│ │ ├── useCopy.ts
│ │ ├── useDelegate.ts
│ │ ├── useDelegates.ts
│ │ ├── useEmailFetchClient.ts
│ │ ├── useEmailSubscription.ts
│ │ ├── useEns.ts
│ │ ├── useExtendedSpaces.ts
│ │ ├── useFlaggedMessageStatus.ts
│ │ ├── useFlashNotification.ts
│ │ ├── useFollowSpace.ts
│ │ ├── useFormSpaceProposal.ts
│ │ ├── useFormSpaceSettings.ts
│ │ ├── useFormValidation.ts
│ │ ├── useGnosis.ts
│ │ ├── useI18n.ts
│ │ ├── useImageUpload.ts
│ │ ├── useInfiniteLoader.ts
│ │ ├── useIntl.ts
│ │ ├── useMeta.ts
│ │ ├── useModal.ts
│ │ ├── useModalNotification.ts
│ │ ├── useNetworksFilter.ts
│ │ ├── useNotifications.ts
│ │ ├── usePayment.ts
│ │ ├── usePlugins.ts
│ │ ├── useProfiles.ts
│ │ ├── useProposalVotes.ts
│ │ ├── useProposals.ts
│ │ ├── useQuorum.ts
│ │ ├── useReportDownload.ts
│ │ ├── useResolveName.ts
│ │ ├── useSafe.ts
│ │ ├── useSharing.ts
│ │ ├── useShortUrls.ts
│ │ ├── useSkin.ts
│ │ ├── useSkinsFilter.ts
│ │ ├── useSnapshot.ts
│ │ ├── useSpaceController.ts
│ │ ├── useSpaceSubscription.ts
│ │ ├── useSpaces.ts
│ │ ├── useStatement.ts
│ │ ├── useStrategies.ts
│ │ ├── useTerms.ts
│ │ ├── useTreasury.ts
│ │ ├── useTxStatus.ts
│ │ ├── useUnseenProposals.ts
│ │ ├── useUsername.ts
│ │ └── useWeb3.ts
│ ├── env.d.ts
│ ├── helpers/
│ │ ├── alchemy/
│ │ │ ├── index.ts
│ │ │ └── types.ts
│ │ ├── apollo.ts
│ │ ├── auth.ts
│ │ ├── b64.ts
│ │ ├── beams.ts
│ │ ├── boost/
│ │ │ ├── abi.json
│ │ │ ├── api.ts
│ │ │ ├── index.ts
│ │ │ ├── subgraph.ts
│ │ │ ├── tokens.ts
│ │ │ └── types.ts
│ │ ├── clientEIP712.ts
│ │ ├── connectors.ts
│ │ ├── constants.ts
│ │ ├── covalent.ts
│ │ ├── delegation.ts
│ │ ├── delegationV2/
│ │ │ ├── compound/
│ │ │ │ ├── index.ts
│ │ │ │ ├── queries.ts
│ │ │ │ ├── read.ts
│ │ │ │ └── write.ts
│ │ │ ├── index.ts
│ │ │ ├── splitDelegation/
│ │ │ │ ├── abi.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── read.ts
│ │ │ │ └── write.ts
│ │ │ └── types.ts
│ │ ├── ens.ts
│ │ ├── i18n.ts
│ │ ├── interfaces.ts
│ │ ├── pin.ts
│ │ ├── queries.ts
│ │ ├── shutter.ts
│ │ ├── sign.ts
│ │ ├── snapshot.ts
│ │ ├── transaction.ts
│ │ ├── utils.test.js
│ │ ├── utils.ts
│ │ ├── validation.ts
│ │ └── vitePlugins.ts
│ ├── locales/
│ │ ├── ar-SA.json
│ │ ├── de-DE.json
│ │ ├── default.json
│ │ ├── es-ES.json
│ │ ├── fil-PH.json
│ │ ├── fr-FR.json
│ │ ├── hi-IN.json
│ │ ├── id-ID.json
│ │ ├── it-IT.json
│ │ ├── ja-JP.json
│ │ ├── ko-KR.json
│ │ ├── languages.json
│ │ ├── pt-PT.json
│ │ ├── ro-RO.json
│ │ ├── ru-RU.json
│ │ ├── tr-TR.json
│ │ ├── uk-UA.json
│ │ ├── vi-VN.json
│ │ └── zh-CN.json
│ ├── main.ts
│ ├── plugins/
│ │ ├── README.md
│ │ ├── domino/
│ │ │ ├── ProposalSidebar.vue
│ │ │ ├── components/
│ │ │ │ └── CustomBlock.vue
│ │ │ └── plugin.json
│ │ ├── gnosis/
│ │ │ ├── Create.vue
│ │ │ ├── ProposalSidebar.vue
│ │ │ ├── components/
│ │ │ │ ├── Config.vue
│ │ │ │ └── CustomBlock.vue
│ │ │ ├── index.ts
│ │ │ └── plugin.json
│ │ ├── oSnap/
│ │ │ ├── Create.vue
│ │ │ ├── CreateSafe.vue
│ │ │ ├── Proposal.vue
│ │ │ ├── README.md
│ │ │ ├── components/
│ │ │ │ ├── BotSupportWarning.vue
│ │ │ │ ├── ExternalLink.vue
│ │ │ │ ├── HandleOutcome/
│ │ │ │ │ ├── HandleOutcome.vue
│ │ │ │ │ └── steps/
│ │ │ │ │ ├── CanProposeToOG.vue
│ │ │ │ │ ├── CanRequestTxExecution.vue
│ │ │ │ │ ├── InOOChallengePeriod.vue
│ │ │ │ │ ├── RejectedBySnapshotVote.vue
│ │ │ │ │ ├── TallyingSnapshotVotes.vue
│ │ │ │ │ └── TransactionsExecuted.vue
│ │ │ │ ├── Input/
│ │ │ │ │ ├── Address.vue
│ │ │ │ │ ├── Amount.vue
│ │ │ │ │ ├── FileInput/
│ │ │ │ │ │ ├── FileInput.vue
│ │ │ │ │ │ └── utils.ts
│ │ │ │ │ ├── MethodParameter/
│ │ │ │ │ │ ├── MethodParameter.vue
│ │ │ │ │ │ └── utils.ts
│ │ │ │ │ ├── ReadOnly.vue
│ │ │ │ │ ├── SelectSafe.vue
│ │ │ │ │ └── TransactionType.vue
│ │ │ │ ├── OsnapMarketingWidget.vue
│ │ │ │ ├── SafeLinkWithAvatar.vue
│ │ │ │ └── TransactionBuilder/
│ │ │ │ ├── ContractInteraction.vue
│ │ │ │ ├── ModalSafe.vue
│ │ │ │ ├── ModalTransactionType.vue
│ │ │ │ ├── RawTransaction.vue
│ │ │ │ ├── SafeImport.vue
│ │ │ │ ├── TenderlySimulation.vue
│ │ │ │ ├── TokensModal.vue
│ │ │ │ ├── TokensModalItem.vue
│ │ │ │ ├── Transaction.vue
│ │ │ │ ├── TransactionBuilder.vue
│ │ │ │ ├── TransactionImport.vue
│ │ │ │ ├── TransferFunds.vue
│ │ │ │ └── TransferNFT.vue
│ │ │ ├── constants.ts
│ │ │ ├── plugin.json
│ │ │ ├── types.ts
│ │ │ └── utils/
│ │ │ ├── abi.ts
│ │ │ ├── coins.ts
│ │ │ ├── events.ts
│ │ │ ├── getters.ts
│ │ │ ├── index.ts
│ │ │ ├── proposal.ts
│ │ │ ├── safeImport.ts
│ │ │ ├── tenderly.ts
│ │ │ ├── transactions.ts
│ │ │ └── validators.ts
│ │ ├── poap/
│ │ │ ├── ProposalSidebar.vue
│ │ │ ├── components/
│ │ │ │ └── CustomBlock.vue
│ │ │ ├── index.ts
│ │ │ └── plugin.json
│ │ ├── progress/
│ │ │ ├── ProposalSidebar.vue
│ │ │ ├── components/
│ │ │ │ └── CustomBlock.vue
│ │ │ ├── index.ts
│ │ │ ├── plugin.json
│ │ │ └── readme.md
│ │ ├── projectGalaxy/
│ │ │ ├── ProposalSidebar.vue
│ │ │ ├── README.md
│ │ │ ├── components/
│ │ │ │ └── CustomBlock.vue
│ │ │ ├── index.ts
│ │ │ └── plugin.json
│ │ ├── quorum/
│ │ │ ├── examples.json
│ │ │ └── plugin.json
│ │ └── safeSnap/
│ │ ├── Create.vue
│ │ ├── Proposal.vue
│ │ ├── components/
│ │ │ ├── Config.vue
│ │ │ ├── Form/
│ │ │ │ ├── ContractInteraction.vue
│ │ │ │ ├── ImportTransactionsButton.vue
│ │ │ │ ├── RawTransaction.vue
│ │ │ │ ├── SendAsset.vue
│ │ │ │ ├── TokensModal.vue
│ │ │ │ ├── TokensModalItem.vue
│ │ │ │ ├── Transaction.vue
│ │ │ │ ├── TransactionBatch.vue
│ │ │ │ └── TransferFunds.vue
│ │ │ ├── HandleOutcome.vue
│ │ │ ├── HandleOutcomeUma.vue
│ │ │ ├── Input/
│ │ │ │ ├── Address.vue
│ │ │ │ ├── Amount.vue
│ │ │ │ ├── ArrayType.vue
│ │ │ │ └── MethodParameter.vue
│ │ │ ├── Modal/
│ │ │ │ └── OptionApproval.vue
│ │ │ ├── SafeTransactions.vue
│ │ │ └── Tooltip.vue
│ │ ├── constants.ts
│ │ ├── index.ts
│ │ ├── plugin.json
│ │ ├── types/
│ │ │ └── index.ts
│ │ └── utils/
│ │ ├── abi.ts
│ │ ├── coins.ts
│ │ ├── decoder.ts
│ │ ├── events.ts
│ │ ├── index.ts
│ │ ├── multiSend.ts
│ │ ├── realityETH.ts
│ │ ├── realityModule.ts
│ │ ├── safe.ts
│ │ ├── transactions.ts
│ │ ├── umaModule.ts
│ │ └── validator.ts
│ ├── router/
│ │ └── index.ts
│ ├── sentry.ts
│ └── views/
│ ├── DelegateView.vue
│ ├── ExploreView.vue
│ ├── PlaygroundView.vue
│ ├── ProfileAbout.vue
│ ├── ProfileActivity.vue
│ ├── ProfileView.vue
│ ├── RankingView.vue
│ ├── SetupView.vue
│ ├── SpaceAbout.vue
│ ├── SpaceBoost.vue
│ ├── SpaceCreate.vue
│ ├── SpaceDelegate.vue
│ ├── SpaceDelegates.vue
│ ├── SpaceProposal.vue
│ ├── SpaceProposals.vue
│ ├── SpaceSettings.vue
│ ├── SpaceTreasury.vue
│ ├── SpaceView.vue
│ ├── StrategyView.vue
│ ├── TermsView.vue
│ └── TimelineView.vue
├── tailwind.config.js
├── tsconfig.json
├── vercel.json
└── vite.config.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .browserslistrc
================================================
> 1%
last 2 versions
not dead
================================================
FILE: .dockerignore
================================================
node_modules
================================================
FILE: .eslintignore
================================================
src/plugins/*
================================================
FILE: .eslintrc-auto-import.json
================================================
{
"globals": {
"Component": true,
"ComponentPublicInstance": true,
"ComputedRef": true,
"DARK": true,
"EffectScope": true,
"InjectionKey": true,
"LIGHT": true,
"PropType": true,
"Ref": true,
"VNode": true,
"computed": true,
"createApp": true,
"customRef": true,
"defineAsyncComponent": true,
"defineComponent": true,
"effectScope": true,
"getCurrentInstance": true,
"getCurrentScope": true,
"getRanking": true,
"h": true,
"inject": true,
"isProxy": true,
"isReactive": true,
"isReadonly": true,
"isRef": true,
"markRaw": true,
"nextTick": true,
"onActivated": true,
"onBeforeMount": true,
"onBeforeRouteLeave": true,
"onBeforeRouteUpdate": true,
"onBeforeUnmount": true,
"onBeforeUpdate": true,
"onDeactivated": true,
"onErrorCaptured": true,
"onMounted": true,
"onRenderTracked": true,
"onRenderTriggered": true,
"onScopeDispose": true,
"onServerPrefetch": true,
"onUnmounted": true,
"onUpdated": true,
"provide": true,
"reactive": true,
"readonly": true,
"ref": true,
"resolveComponent": true,
"shallowReactive": true,
"shallowReadonly": true,
"shallowRef": true,
"toRaw": true,
"toRef": true,
"toRefs": true,
"triggerRef": true,
"unref": true,
"useAliasAction": true,
"useApolloQuery": true,
"useApp": true,
"useAttrs": true,
"useClient": true,
"useCopy": true,
"useCssModule": true,
"useCssVars": true,
"useDelegate": true,
"useEns": true,
"useExtendedSpaces": true,
"useFlashNotification": true,
"useFollowSpace": true,
"useFormSpaceProposal": true,
"useFormSpaceSettings": true,
"useFormValidation": true,
"useGnosis": true,
"useI18n": true,
"useImageUpload": true,
"useInfiniteLoader": true,
"useIntl": true,
"useLink": true,
"useMeta": true,
"useModal": true,
"useModalNotification": true,
"useNetworksFilter": true,
"useNotifications": true,
"usePlugins": true,
"useProfiles": true,
"useProposalVotes": true,
"useProposals": true,
"useQuorum": true,
"useReportDownload": true,
"useRoute": true,
"useRouter": true,
"useSafe": true,
"useSharing": true,
"useShortUrls": true,
"useSkin": true,
"useSkinsFilter": true,
"useSlots": true,
"useSnapshot": true,
"useSpaceController": true,
"useSpaceSubscription": true,
"useSpaces": true,
"useStrategies": true,
"useTerms": true,
"useTreasury": true,
"useTxStatus": true,
"useUnseenProposals": true,
"useUsername": true,
"useWeb3": true,
"useDelegates": true,
"useResolveName": true,
"watch": true,
"watchEffect": true,
"watchPostEffect": true,
"watchSyncEffect": true,
"watchTxStatus": true,
"toValue": true,
"useFlaggedMessageStatus": true,
"useEmailSubscription": true,
"useEmailFetchClient": true,
"useStatement": true,
"useBalances": true,
"useAccount": true,
"usePayment": true,
"useChangeNetwork": true,
"useBoost": true
}
}
================================================
FILE: .github/CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at fabien@bonustrack.co. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/
================================================
FILE: .github/CONTRIBUTING.md
================================================
### Opening an issue
You should usually open an issue in the following situations:
* Report an error you can’t solve yourself
* Discuss a high-level topic or idea (for example, community, vision or policies)
* Propose a new feature or other project idea
Tips for communicating on issues:
* **If you see an open issue that you want to tackle,** comment on the issue to let people know you’re on it. That way, people are less likely to duplicate your work.
* **If an issue was opened a while ago,** it’s possible that it’s being addressed somewhere else, or has already been resolved, so comment to ask for confirmation before starting work.
* **If you opened an issue, but figured out the answer later on your own,** comment on the issue to let people know, then close the issue. Even documenting that outcome is a contribution to the project.
### Opening a pull request
You should usually open a pull request in the following situations:
* Submit trivial fixes (for example, a typo, a broken link or an obvious error)
* Start work on a contribution that was already asked for, or that you’ve already discussed, in an issue
A pull request doesn’t have to represent finished work. It’s usually better to open a pull request early on, so others can watch or give feedback on your progress. Just mark it as a “WIP” (Work in Progress) in the subject line. You can always add more commits later.
If the project is on GitHub, here’s how to submit a pull request:
* **Fork the repository** and clone it locally. Connect your local to the original repository by adding it as a remote. Pull in changes from this repository often so that you stay up to date so that when you submit your pull request, merge conflicts will be less likely.
* **Create a branch** for your edits.
* **Reference any relevant issues** or supporting documentation in your PR (for example, “Closes #37.”)
* **Include screenshots of the before and after** if your changes include differences in HTML/CSS. Drag and drop the images into the body of your pull request.
* **Test your changes!** Run your changes against any existing tests if they exist and create new ones when needed. Whether tests exist or not, make sure your changes don’t break the existing project.
* **Contribute in the style of the project** to the best of your abilities. This may mean using indents, semi-colons or comments differently than you would in your own repository, but makes it easier for the maintainer to merge, others to understand and maintain in the future.
================================================
FILE: .github/ISSUE_TEMPLATE/BUG_REPORT.yml
================================================
name: Bug report
description: File a bug report
title: "[BUG] - "
labels: ["bug-report"]
body:
- type: markdown
attributes:
value: Thanks for making Snapshot awesome for everyone.
- type: input
id: title
attributes:
label: Briefly describe the bug.
description: A clear and concise description of what the bug is.
placeholder: ex. Unable to vote using metamask wallet on chrome.
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: How can we reproduce the bug?
placeholder: Steps to reproduce the behaviour.
value: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
validations:
required: true
- type: textarea
id: expectation
attributes:
label: What is the expected behaviour?
description: A clear and concise description of what you expected to happen.
placeholder: ex. I should be able to vote using metamask wallet on chrome.
validations:
required: false
- type: textarea
id: screenshots
attributes:
label: Can you attach screenshots?
description: Please attach all the screenshots that can help us visually see the problem.
placeholder: You can drag-and-drop the image, copy paste the image in the field below.
validations:
required: false
- type: textarea
id: device
attributes:
label: Can you provide your device specific details below?
value: |
1. Device Type - [ex. Desktop or Mobile]
2. Device OS - [ex. Android, MacOS, Windows, iOS]
3. OS Version - [ex. iOS 11, Windows 10]
4. Browser - [ex. chrome, safari]
5. Browser Version - [ex. 101]
6. Wallet - [metamask, portis, walletconnect]
7. Space - [ex. ENS, Gitcoin]
8. Any additional information we should know -
validations:
required: false
================================================
FILE: .github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml
================================================
name: Feature request
description: Suggest an idea for this project
title: "[NEW FEATURE] - "
labels: ["feature-request"]
body:
- type: markdown
attributes:
value: Thanks for making Snapshot awesome for everyone.
- type: input
id: title
attributes:
label: Briefly describe the feature.
description: A clear and concise description of the feature you are requesting.
validations:
required: true
- type: textarea
id: problem
attributes:
label: Which problem is this feature trying to solve?
description: A clear description of the problem or frustration that will be solved with this feature.
placeholder: ex. I'm always frustrated when I am viewing the proposal metrics
validations:
required: true
- type: textarea
id: solution
attributes:
label: What is the expected solution?
description: A clear and concise description of what you expected to happen.
placeholder: ex. Having a chart to read the votes on the proposal will make it very easy to read the proposal metrics.
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: How do you currently handle this problem?
description: A clear and concise description of any alternative solutions or features you've considered.
validations:
required: false
- type: textarea
id: additional
attributes:
label: Anything else you'd like to mention?
description: Please mention any technical details, screenshots, mock-ups that you might have for us to better understand the feature.
validations:
required: false
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
### Summary
Closes: #
### How to test
1.
### To-Do
- [ ]
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
ignore:
- dependency-name: "@snapshot-labs/*"
groups:
production-dependencies:
dependency-type: "production"
development-dependencies:
dependency-type: "development"
================================================
FILE: .github/stale.yml
================================================
daysUntilStale: 120
daysUntilClose: 14
exemptLabels:
- pinned
- enhancement
- bug
staleLabel: stale
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
closeComment: false
================================================
FILE: .github/workflows/codeql.yml
================================================
name: "CodeQL"
on:
push:
branches: [ 'master' ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ 'master' ]
schedule:
- cron: '36 5 * * 6'
jobs:
analyze:
name: Analyze
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Use only 'java' to analyze code written in Java, Kotlin or both
# Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{matrix.language}}"
================================================
FILE: .github/workflows/deploy.yml
================================================
name: Deploy
on:
workflow_dispatch:
inputs:
target:
type: choice
description: Target
options:
- stable
jobs:
deploy:
runs-on: ubuntu-latest
steps:
# Checks-out repo on develop branch
- uses: actions/checkout@v3
with:
ref: 'master'
# Overwrite target
- run: |
git checkout -b ${{ github.event.inputs.target }}
git push --set-upstream origin ${{ github.event.inputs.target }} --force
================================================
FILE: .github/workflows/nodejs.yml
================================================
name: Node CI
on:
push:
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x]
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: yarn
- name: Build
run: yarn run build
- name: Lint
run: yarn run lint
================================================
FILE: .github/workflows/test.yaml
================================================
name: Tests
on: [push]
jobs:
test:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '16'
cache: 'yarn'
- run: yarn
- run: yarn test:unit:coverage
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
================================================
FILE: .github/workflows/update-snapshot-packages.yml
================================================
name: Update Snapshot packages
on: workflow_dispatch
jobs:
update-dep:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: '16.x'
- name: Update snapshot.js
run: |
yarn upgrade @snapshot-labs/snapshot.js --latest
- name: Update lock.js
run: |
yarn upgrade @snapshot-labs/lock --latest
- name: Create Pull Request
uses: peter-evans/create-pull-request@v3
with:
commit-message: Update Snapshot packages
title: Update Snapshot packages
body: |
- Updates from snapshot.js or lock packages
Auto-generated by Github Actions
branch: update-snapshot-packages
================================================
FILE: .github/workflows/update-snapshot-submodules.yml
================================================
name: Update Snapshot submodules
on: workflow_dispatch
jobs:
update-dep:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
submodules: 'recursive'
- uses: actions/setup-node@v1
with:
node-version: '16.x'
- name: Update submodules
run: |
git submodule update --remote
- name: Create Pull Request
uses: peter-evans/create-pull-request@v3
with:
title: Update Snapshot submodules
body: |
- Updates from submodules
Auto-generated by Github Actions
branch: update-snapshot-submodules
================================================
FILE: .gitignore
================================================
.DS_Store
node_modules
/dist
.yalc
components.d.ts
auto-imports.d.ts
# local env file
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Vitest
/coverage
# Sentry Auth Token
.env.sentry-build-plugin
================================================
FILE: .gitmodules
================================================
[submodule "snapshot-spaces"]
path = snapshot-spaces
url = https://github.com/snapshot-labs/snapshot-spaces
================================================
FILE: .gitpod.yml
================================================
tasks:
- name: Setup
init: |
yarn install --frozen-lockfile --silent --network-timeout 100000
command: yarn dev
ports:
- port: 3000
onOpen: open-preview
================================================
FILE: .husky/post-checkout
================================================
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
yarn run init-submodules
================================================
FILE: .husky/post-merge
================================================
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
yarn run init-submodules
================================================
FILE: .husky/pre-commit
================================================
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
================================================
FILE: .husky/pre-push
================================================
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
================================================
FILE: Dockerfile
================================================
FROM node:18-alpine
RUN apk update && apk upgrade && \
apk add --no-cache git py3-pip python3 gcc g++ make
WORKDIR /app
COPY . .
RUN yarn install --frozen-lockfile
RUN yarn install --frozen-lockfile
RUN yarn run build
EXPOSE 8080
CMD ["yarn", "run", "dev", "--host"]
================================================
FILE: FUNDING.json
================================================
{
"drips": {
"ethereum": {
"ownedBy": "0x8C28Cf33d9Fd3D0293f963b1cd27e3FF422B425c"
}
},
"opRetro": {
"projectId": "0x5e6c436e48e56d6d9622ba5d0be0035c314e2b29d2afc8f5f1ee8ac75cd42532"
}
}
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) Snapshot Labs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
Snapshot
Snapshot is an off-chain gasless multi-governance client with easy to verify and hard to contest results.
## Project setup
```
yarn
```
### Compiles and hot-reloads for development
```
yarn dev
```
### Compiles and minifies for production
```
yarn build
```
### Lints and fixes files
```
yarn run lint
```
### Development Guide
Use `http://localhost:8080/#/fabien.eth` for testing your code.
By default your instance will connect to the hub at `https://testnet.hub.snapshot.org`. To change that (or other values) you can create a `.env.local` and overwrite the values from `.env`.
## Running service locally with Docker
1. Run `docker build -t snapshot .` to build the image
2. Run `docker run --name snapshot -p 8080:8080 snapshot` to run the container
3. Go to `http://localhost:8080/#/fabien.eth` to test your code
## License
Snapshot is open-sourced software licensed under the © [MIT license](LICENSE).
================================================
FILE: babel.config.js
================================================
module.exports = {
presets: ['@vue/cli-plugin-babel/preset']
};
================================================
FILE: crowdin.yml
================================================
files:
- source: /src/locales/default.json
translation: /src/locales/%locale%.json
================================================
FILE: index.html
================================================
Snapshot
================================================
FILE: package.json
================================================
{
"name": "snapshot",
"version": "0.1.4",
"license": "MIT",
"repository": "snapshot-labs/snapshot",
"scripts": {
"dev": "vite --port=8080",
"build": "vite build",
"lint": "eslint \"*.{ts,js,vue,json}\" src/ --ext .ts,.js,.vue,.json",
"lint:fix": "yarn lint --fix",
"preinstall": "yarn run init-submodules",
"postinstall": "patch-package && husky install",
"init-submodules": "git submodule update --init",
"test:unit": "vitest run",
"test:unit:coverage": "vitest run --coverage"
},
"eslintConfig": {
"extends": [
"@snapshot-labs/vue",
"./.eslintrc-auto-import.json"
]
},
"prettier": "@snapshot-labs/prettier-config",
"dependencies": {
"@apollo/client": "^3.8.7",
"@braintree/sanitize-url": "^6.0.4",
"@ensdomains/eth-ens-namehash": "^2.0.15",
"@ethersproject/abi": "^5.7.0",
"@ethersproject/address": "^5.7.0",
"@ethersproject/bignumber": "^5.6.1",
"@ethersproject/bytes": "^5.7.0",
"@ethersproject/contracts": "^5.7.0",
"@ethersproject/providers": "^5.7.1",
"@ethersproject/random": "^5.7.0",
"@ethersproject/solidity": "^5.7.0",
"@ethersproject/strings": "^5.7.0",
"@ethersproject/units": "^5.7.0",
"@ethersproject/wallet": "^5.7.0",
"@headlessui-float/vue": "^0.13.0",
"@headlessui/vue": "^1.7.17",
"@pusher/push-notifications-web": "^1.1.0",
"@sentry/vite-plugin": "^2.10.1",
"@sentry/vue": "^7.80.1",
"@shutter-network/shutter-crypto": "1.0.1",
"@snapshot-labs/lock": "^0.2.11",
"@snapshot-labs/pineapple": "^1.1.0",
"@snapshot-labs/snapshot.js": "^0.14.18",
"@vue/apollo-composable": "4.0.0-beta.11",
"@vueuse/core": "^10.6.1",
"@vueuse/head": "^2.0.0",
"autolinker": "^4.0.0",
"bluebird": "^3.7.2",
"evm-proxy-detection": "^1.2.0",
"graphql": "16.6.0",
"graphql-tag": "^2.12.6",
"js-sha256": "^0.10.1",
"jsonexport": "^3.2.0",
"kubo-rpc-client": "^3.0.2",
"lodash": "^4.17.21",
"minisearch": "^6.2.0",
"remarkable": "^2.0.1",
"remove-markdown": "^0.5.0",
"typescript": "^5.2.2",
"v-viewer": "^3.0.11",
"vite-compatible-readable-stream": "^3.6.1",
"vue": "^3.3.8",
"vue-i18n": "^9.7.0",
"vue-router": "^4.2.5",
"vue-tippy": "^6.3.1",
"vuedraggable": "^4.0.2"
},
"devDependencies": {
"@iconify-json/heroicons-outline": "^1.1.7",
"@rushstack/eslint-patch": "^1.3.3",
"@snapshot-labs/eslint-config-vue": "^0.1.0-beta.15",
"@snapshot-labs/prettier-config": "0.1.0-beta.15",
"@tailwindcss/forms": "^0.5.6",
"@types/bluebird": "^3.5.38",
"@types/lodash": "^4.14.198",
"@types/node": "^20.5.0",
"@vitejs/plugin-vue": "^2.3.4",
"@vitest/coverage-v8": "^0.34.5",
"@vue/test-utils": "^2.4.1",
"autoprefixer": "^10.4.15",
"env-cmd": "^10.1.0",
"eslint": "^8.46.0",
"happy-dom": "^10.11.0",
"husky": "^8.0.3",
"patch-package": "^7.0.0",
"postcss": "^8.4.28",
"postinstall-postinstall": "^2.1.0",
"prettier": "^3.1.0",
"prettier-plugin-tailwindcss": "^0.5.7",
"rollup-plugin-visualizer": "^5.9.0",
"sass": "~1.62.1",
"sass-loader": "^13.2.2",
"start-server-and-test": "^2.0.0",
"tailwindcss": "^3.3.5",
"unplugin-auto-import": "^0.16.1",
"unplugin-icons": "^0.16.5",
"unplugin-vue-components": "^0.25.2",
"vite": "^2.9.14",
"vitest": "^0.33.0"
},
"resolutions": {
"ansi-regex": "^5.0.1",
"axios": "^0.21.1"
}
}
================================================
FILE: postcss.config.js
================================================
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};
================================================
FILE: public/.well-known/assetlinks.json
================================================
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "org.snapshot",
"sha256_cert_fingerprints": [
"44:5B:B1:EE:DF:5C:2A:90:7D:7A:10:DF:18:67:68:54:8E:8E:62:C1:DF:84:06:F7:8D:8E:AD:67:2E:B2:5C:E5"
]
}
}
]
================================================
FILE: public/.well-known/did.json
================================================
{
"@context": [
"https://www.w3.org/ns/did/v1",
"https://w3id.org/security/suites/jws-2020/v1"
],
"id": "did:web:snapshot.org",
"verificationMethod": [
{
"id": "did:web:snapshot.org#wc-notify-subscribe-key",
"type": "JsonWebKey2020",
"controller": "did:web:snapshot.org",
"publicKeyJwk": {
"kty": "OKP",
"crv": "X25519",
"x": "gL37Y5hbjW4__xM7n4JrtAqc1OAGUWKiASb1W1cReA0"
}
},
{
"id": "did:web:snapshot.org#wc-notify-authentication-key",
"type": "JsonWebKey2020",
"controller": "did:web:snapshot.org",
"publicKeyJwk": {
"kty": "OKP",
"crv": "Ed25519",
"x": "iCZGVaUtesmG6RinZVff-GlW5qzap8uAI1opXKmuPA4"
}
}
],
"keyAgreement": [
"did:web:snapshot.org#wc-notify-subscribe-key"
],
"authentication": [
"did:web:snapshot.org#wc-notify-authentication-key"
]
}
================================================
FILE: public/manifest.json
================================================
{
"short_name": "Snapshot",
"name": "Snapshot",
"description": "Where decisions get made",
"iconPath": "icon.svg",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "avatar.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "avatar.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
================================================
FILE: public/service-worker.js
================================================
// importScripts('https://js.pusher.com/beams/service-worker.js');
================================================
FILE: src/App.vue
================================================
================================================
FILE: src/assets/css/main.scss
================================================
@import '../fonts/iconfont.css';
@import 'viewerjs/dist/viewer.css';
@import './tippy.scss';
@import './tune.scss';
@import '../snapshot-spaces/skins';
@tailwind base;
@tailwind components;
@tailwind utilities;
[data-color-scheme='dark'] {
color-scheme: dark;
--primary-color: #ffffff;
--bg-color: #1c1b20;
--text-color: #8b949e;
--link-color: #ffffff;
--heading-color: #ffffff;
--border-color: #343434;
--header-bg: #1c1b20;
--block-bg: transparent;
--shadow-color: rgba(255, 255, 255, 0.036);
--border-color-soft: rgba(45, 45, 45, 0.8);
--border-color-subtle: rgba(45, 45, 45, 0.5);
--border-color-faint: rgba(45, 45, 45, 0.3);
--network-blocks: url('@/assets/images/blocks-dark.svg');
}
[data-color-scheme='light'] {
color-scheme: light;
--primary-color: #000000;
--bg-color: white;
--text-color: #57606a;
--link-color: #444444;
--heading-color: #111111;
--border-color: #e0e0e0;
--header-bg: white;
--block-bg: transparent;
--shadow-color: #0001;
--border-color-soft: rgb(224, 224, 224, 0.6);
--border-color-subtle: rgb(224, 224, 224, 0.32);
--border-color-faint: rgb(224, 224, 224, 0.12);
--network-blocks: url('@/assets/images/blocks-light.svg');
}
@layer base {
@font-face {
font-family: 'Calibre';
src: url('../fonts/Calibre-Medium-Custom.woff2') format('woff2');
font-weight: 500;
font-style: normal;
}
@font-face {
font-family: 'Calibre';
src: url('../fonts/Calibre-Semibold-Custom.woff2') format('woff2');
font-weight: 600;
font-style: normal;
}
@font-face {
font-family: 'SpaceMono';
src: url('../fonts/space-mono-v11-latin-700.woff2') format('woff2');
}
@font-face {
font-family: 'Space Mono';
font-style: normal;
font-weight: 700;
src: url('../fonts/SpaceMono-Bold.woff2') format('woff2');
}
.mono {
font-size: 44px;
font-family: 'Space Mono', Helvetica, Arial, sans-serif;
letter-spacing: -1px;
font-weight: 700;
line-height: 1.1em;
}
.eyebrow {
text-transform: uppercase;
letter-spacing: 1px;
font-size: 16px;
}
html {
scrollbar-gutter: stable;
overflow: visible !important;
}
*,
::before,
::after {
@apply border-skin-border;
}
body {
overflow-x: hidden;
}
h1 {
@apply text-2xl;
}
h2 {
@apply text-xl;
}
h3 {
@apply my-2 text-lg;
}
h4 {
@apply text-md;
}
h1,
h2,
h3,
h4,
h5,
h6 {
@apply font-semibold text-skin-heading;
}
a,
button,
input {
@apply transition-colors;
}
b {
@apply font-semibold;
}
select {
@apply bg-skin-bg;
}
}
@layer utilities {
// This allows us to use 'no-scrollbar' with Tailwind breakpoints like this 'md:no-scrollbar'
/* Chrome, Safari and Opera */
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
}
// Don't prevent overscroll on touch devices
@media (pointer: fine) {
body {
overscroll-behavior-y: none;
}
}
// Fade transition for Vue's transition and transition-group
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
a,
.a {
color: var(--link-color);
&:hover {
cursor: pointer;
text-decoration: none;
}
}
.input {
@apply bg-transparent text-skin-link outline-none;
}
::placeholder {
color: var(--text-color);
opacity: 0.6 !important;
}
input[type='number']::-webkit-inner-spin-button,
input[type='number']::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type='time']::-webkit-calendar-picker-indicator {
background: none;
display: none;
}
// Pointer cursor for file input buttons
input[type=file], /* FF, IE7+, chrome (except button) */
input[type=file]::-webkit-file-upload-button {
/* chromes and blink button */
cursor: pointer;
}
input[type='number'] {
-moz-appearance: textfield;
}
.lazy-loading {
@apply animate-pulse-fast bg-skin-border;
}
// Shot UI kit
.s-input {
@apply block w-full rounded-full border border-skin-border bg-skin-bg px-3 py-2 text-skin-link outline-none focus-within:border-skin-text;
}
.s-error {
@apply flex items-center rounded-b-3xl bg-red px-3 pb-2 pt-4 text-sm text-white transition-all duration-200;
}
// Fix for chrome. Overwrites tailwinds break-words class.
// For some reason chrome doesn't break links with just overflow-wrap.
// It also needs word-break.
.break-words {
overflow-wrap: break-word;
word-break: break-word;
}
// v-viewer package styling
.viewer-backdrop {
background-color: rgba(0, 0, 0, 80%) !important;
}
.viewer-close {
scale: 1.5 !important;
margin-top: 50px !important;
margin-right: 66px !important;
width: 50px !important;
height: 50px !important;
background-color: transparent !important;
box-shadow: none !important;
}
@media (max-width: 544px) {
.viewer-close {
margin-right: 50px !important;
}
}
.network-blocks {
background-repeat: repeat-x;
background-position: top -40px center;
background-image: var(--network-blocks);
}
================================================
FILE: src/assets/css/tippy.scss
================================================
// V-Tippy tooltip package styling - could be moved somewhere else?
.tippy-box[data-animation='fade'][data-state='hidden'] {
opacity: 0;
}
[data-tippy-root] {
max-width: calc(100vw - 10px);
}
.tippy-box {
position: relative;
background-color: var(--text-color);
color: var(--header-bg);
border-radius: 7px;
font-size: 16px;
line-height: 1.4;
outline: 0;
transition-property: transform, visibility, opacity;
padding: 2px 4px;
text-align: center;
font-weight: 500;
}
.tippy-box[data-placement^='top'] > .tippy-arrow {
bottom: 0;
}
.tippy-box[data-placement^='top'] > .tippy-arrow:before {
bottom: -7px;
left: 0;
border-width: 8px 8px 0;
border-top-color: var(--text-color);
transform-origin: center top;
}
.tippy-box[data-placement^='bottom'] > .tippy-arrow {
top: 0;
}
.tippy-box[data-placement^='bottom'] > .tippy-arrow:before {
top: -7px;
left: 0;
border-width: 0 8px 8px;
border-bottom-color: var(--text-color);
transform-origin: center bottom;
}
.tippy-box[data-placement^='left'] > .tippy-arrow {
right: 0;
}
.tippy-box[data-placement^='left'] > .tippy-arrow:before {
border-width: 8px 0 8px 8px;
border-left-color: var(--text-color);
right: -7px;
transform-origin: center left;
}
.tippy-box[data-placement^='right'] > .tippy-arrow {
left: 0;
}
.tippy-box[data-placement^='right'] > .tippy-arrow:before {
left: -7px;
border-width: 8px 8px 8px 0;
border-right-color: var(--text-color);
transform-origin: center right;
}
.tippy-box[data-inertia][data-state='visible'] {
transition-timing-function: cubic-bezier(0.54, 1.5, 0.38, 1.11);
}
.tippy-arrow {
width: 16px;
height: 16px;
color: #333;
}
.tippy-arrow:before {
content: '';
position: absolute;
border-color: transparent;
border-style: solid;
}
.tippy-content {
position: relative;
padding: 5px 9px;
z-index: 1;
overflow-wrap: break-word;
}
.tippy-box[data-theme~='urlified'] {
max-width: 200px;
white-space: pre-wrap;
color: var(--header-bg);
a {
color: var(--header-bg);
}
}
================================================
FILE: src/assets/css/tune.scss
================================================
.tune-input {
@apply form-input rounded-full border-skin-border bg-transparent text-base text-skin-link focus:border-skin-text focus:outline-none focus:ring-0;
}
.tune-textarea {
@extend .tune-input;
@apply no-scrollbar rounded-3xl px-3;
&.disabled {
@apply cursor-not-allowed;
}
}
.tune-textarea-json {
@apply no-scrollbar overflow-x-scroll whitespace-pre;
&.disabled {
@apply cursor-not-allowed;
}
}
.tune-switch {
@apply border-transparent;
&.switched-on-bg {
@apply bg-green;
}
&.switched-off-bg {
@apply bg-skin-border;
}
&.switched-on-text {
@apply text-green;
}
&.switched-off-text {
@apply text-skin-text;
}
}
.tune-switch-button {
@apply bg-skin-bg;
}
.tune-input-loading {
@apply rounded-r-full bg-skin-bg;
}
.tune-button {
@apply h-[46px] cursor-pointer text-skin-link rounded-full bg-transparent border border-skin-border px-[22px] hover:border-skin-text;
&.disabled {
@apply border-skin-border bg-skin-bg text-skin-border cursor-not-allowed;
}
&.primary {
@apply border-skin-primary bg-skin-primary text-white hover:brightness-95;
&:disabled {
@apply border-skin-primary bg-skin-primary text-white opacity-40;
}
}
&.white-border {
@apply border-white/30 hover:border-white;
&:disabled {
@apply border-white text-white opacity-40;
}
}
&.danger {
@apply text-red hover:border-red border-red/40;
&:disabled {
@apply text-skin-border hover:border-skin-border;
}
}
}
.tune-button-select {
@apply relative h-[42px] w-full justify-start truncate pl-3 pr-5 text-left text-base text-skin-link rounded-full;
&.disabled {
@apply cursor-not-allowed border-skin-border;
}
}
.tune-input-duration {
@apply rounded-full border border-skin-border px-3 text-base text-skin-link focus-within:border-skin-text;
&.disabled {
@apply cursor-not-allowed;
}
}
.tune-input-duration-label {
@apply text-skin-text;
}
.tune-select {
@apply form-select text-left px-3 border border-skin-border w-full rounded-full outline-none focus:ring-0 focus:border-skin-text text-skin-link bg-skin-bg text-base;
&.disabled {
@apply cursor-not-allowed;
}
}
.tune-listbox-button {
@apply rounded-full border border-skin-border text-base text-skin-link hover:border-skin-text;
&.disabled {
@apply hover:border-skin-border;
}
&.error {
@apply border-red;
}
}
.tune-listbox-options {
@apply rounded-md border border-skin-border bg-skin-bg shadow-lg;
}
.tune-listbox-item {
&.selected {
@apply text-skin-link;
}
&.disabled {
@apply text-skin-border;
}
&.active {
@apply bg-skin-border;
}
}
.tune-listbox-placeholder {
@apply text-skin-text opacity-60;
}
.tune-label {
@apply font-sans text-skin-text;
}
.tune-sublabel {
@apply font-sans text-skin-text opacity-60;
}
.tune-icon-hint {
@apply text-xs hover:text-skin-link;
}
.tune-error-text {
@apply font-sans text-sm text-red;
}
.tune-error-border {
@apply border-red;
}
.tune-tag {
@apply rounded-lg border border-[--border-color-soft] bg-[--border-color-subtle] px-[6px] py-[3px] text-sm text-skin-text;
}
.tune-menu-list {
@apply rounded-md border border-skin-border bg-skin-header-bg shadow-lg;
}
.tune-menu-list-item {
@apply bg-skin-header-bg text-skin-text;
&.active {
@apply bg-skin-border text-skin-link;
}
}
.tune-popover {
@apply rounded-md border bg-skin-header-bg shadow-lg;
}
.tune-input-checkbox {
@apply form-checkbox h-[20px] w-[20px] rounded-lg border-skin-text text-skin-text focus:ring-0 focus:ring-offset-0 focus-visible:ring-offset-1 focus-visible:ring-offset-skin-text;
}
.tune-input-radio {
@apply form-radio h-[20px] w-[20px] rounded-full border-skin-text text-skin-text focus:ring-0 focus:ring-offset-0 focus-visible:ring-offset-1 focus-visible:ring-offset-skin-text;
}
.tune-form-array-objects {
@apply rounded-xl border p-3;
}
================================================
FILE: src/assets/fonts/iconfont.css
================================================
@font-face {
font-family: 'iconfont'; /* Project id 1946815 */
src: url('iconfont.woff2?t=1648383247623') format('woff2'),
url('iconfont.woff?t=1648383247623') format('woff'),
url('iconfont.ttf?t=1648383247623') format('truetype');
}
.iconfont {
font-family: 'iconfont' !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.iconnotificationsnone:before {
content: '\e708';
}
.iconearth:before {
content: '\e70d';
}
.iconmarkdown:before {
content: '\ec0f';
}
.iconcoins:before {
content: '\e6c6';
}
.iconcheck3:before {
content: '\e605';
}
.iconskip:before {
content: '\e8c5';
}
.iconclose2:before {
content: '\e6c3';
}
.iconcalendar:before {
content: '\e71f';
}
.icondraggable:before {
content: '\e617';
}
.iconpreview:before {
content: '\e637';
}
.iconfilter:before {
content: '\e74f';
}
.iconhome:before {
content: '\e673';
}
.iconapps:before {
content: '\e613';
}
.iconcheck1:before {
content: '\e607';
}
.iconremind:before {
content: '\e69e';
}
.iconemoji:before {
content: '\e709';
}
.iconflag_fill:before {
content: '\e711';
}
.iconmine:before {
content: '\e732';
}
.iconmore:before {
content: '\e736';
}
.iconpeople:before {
content: '\e73d';
}
.iconstealth_fill:before {
content: '\e75f';
}
.iconnotifications-on:before {
content: '\ec64';
}
.iconnotifications:before {
content: '\e630';
}
.iconnotifications-off:before {
content: '\e6e4';
}
.iconstamp:before {
content: '\e6de';
}
.iconinsertlink:before {
content: '\e621';
}
.iconupload:before {
content: '\e735';
}
.iconplay:before {
content: '\e628';
}
.iconfeed:before {
content: '\e6f6';
}
.icongitbook:before {
content: '\e604';
}
.iconreload:before {
content: '\e603';
}
.iconrefresh:before {
content: '\e78f';
}
.iconwarning1:before {
content: '\e602';
}
.icontoggle_off:before {
content: '\e9ad';
}
.icontoggle-on:before {
content: '\e8b2';
}
.iconthreedots:before {
content: '\e730';
}
.iconmoon:before {
content: '\e780';
}
.iconsun:before {
content: '\e634';
}
.iconinfo:before {
content: '\e93b';
}
.iconlogin:before {
content: '\e66a';
}
.iconcopy:before {
content: '\e638';
}
.iconsnapshot:before {
content: '\e6da';
}
.iconstars:before {
content: '\e6a3';
}
.iconfavorite-off:before {
content: '\e6d7';
}
.iconfavorite-on:before {
content: '\e6d8';
}
.iconloveit:before {
content: '\e60a';
}
.iconbyteball:before {
content: '\e6bc';
}
.iconandroid:before {
content: '\e601';
}
.icontwitter:before {
content: '\ec9c';
}
.iconreddit:before {
content: '\ea3c';
}
.icongoogle:before {
content: '\e723';
}
.iconchrome:before {
content: '\e6c2';
}
.iconapple:before {
content: '\e6bd';
}
.iconfacebook:before {
content: '\e6be';
}
.iconwindows:before {
content: '\e649';
}
.icondiscord:before {
content: '\e6fe';
}
.icontelegram:before {
content: '\e731';
}
.iconmacos:before {
content: '\e6ba';
}
.iconline:before {
content: '\e6c0';
}
.iconmenu1:before {
content: '\e6c8';
}
.iconclose1:before {
content: '\e6c9';
}
.icongear:before {
content: '\e6ca';
}
.iconbullet:before {
content: '\e619';
}
.iconplus:before {
content: '\e727';
}
.iconwallet:before {
content: '\e97c';
}
.iconsignature:before {
content: '\e600';
}
.iconreceipt-outlined:before {
content: '\e8eb';
}
.iconcheck:before {
content: '\e679';
}
.iconwarning:before {
content: '\e62c';
}
.iconsearch:before {
content: '\e6f2';
}
.iconmenu:before {
content: '\e609';
}
.iconclose:before {
content: '\e63e';
}
.icongithub:before {
content: '\e667';
}
.iconexternal-link:before {
content: '\e636';
}
.icongo:before {
content: '\e6cb';
}
.iconback:before {
content: '\e6cc';
}
.iconuser:before {
content: '\e66f';
}
.iconarrow-up:before {
content: '\e75c';
}
.iconarrow-down:before {
content: '\e75d';
}
================================================
FILE: src/assets/fonts/iconfont.json
================================================
{
"id": "1946815",
"name": "snapshot",
"font_family": "iconfont",
"css_prefix_text": "icon",
"description": "",
"glyphs": [
{
"icon_id": "736871",
"name": "notifications-none",
"font_class": "notificationsnone",
"unicode": "e708",
"unicode_decimal": 59144
},
{
"icon_id": "1444429",
"name": "earth",
"font_class": "earth",
"unicode": "e70d",
"unicode_decimal": 59149
},
{
"icon_id": "27365167",
"name": "markdown",
"font_class": "markdown",
"unicode": "ec0f",
"unicode_decimal": 60431
},
{
"icon_id": "1444287",
"name": "coins",
"font_class": "coins",
"unicode": "e6c6",
"unicode_decimal": 59078
},
{
"icon_id": "11398722",
"name": "check",
"font_class": "check3",
"unicode": "e605",
"unicode_decimal": 58885
},
{
"icon_id": "15617307",
"name": "skip-next",
"font_class": "skip",
"unicode": "e8c5",
"unicode_decimal": 59589
},
{
"icon_id": "980400",
"name": "close",
"font_class": "close2",
"unicode": "e6c3",
"unicode_decimal": 59075
},
{
"icon_id": "7239657",
"name": "calendar",
"font_class": "calendar",
"unicode": "e71f",
"unicode_decimal": 59167
},
{
"icon_id": "27154983",
"name": "draggable",
"font_class": "draggable",
"unicode": "e617",
"unicode_decimal": 58903
},
{
"icon_id": "20997029",
"name": "preview",
"font_class": "preview",
"unicode": "e637",
"unicode_decimal": 58935
},
{
"icon_id": "8555524",
"name": "filter-3-fill",
"font_class": "filter",
"unicode": "e74f",
"unicode_decimal": 59215
},
{
"icon_id": "14401724",
"name": "home",
"font_class": "home",
"unicode": "e673",
"unicode_decimal": 58995
},
{
"icon_id": "7988769",
"name": "apps",
"font_class": "apps",
"unicode": "e613",
"unicode_decimal": 58899
},
{
"icon_id": "583318",
"name": "check",
"font_class": "check1",
"unicode": "e607",
"unicode_decimal": 58887
},
{
"icon_id": "1410439",
"name": "notifications-none",
"font_class": "remind",
"unicode": "e69e",
"unicode_decimal": 59038
},
{
"icon_id": "2045201",
"name": "emoji",
"font_class": "emoji",
"unicode": "e709",
"unicode_decimal": 59145
},
{
"icon_id": "2045216",
"name": "flag_fill",
"font_class": "flag_fill",
"unicode": "e711",
"unicode_decimal": 59153
},
{
"icon_id": "2045276",
"name": "mine",
"font_class": "mine",
"unicode": "e732",
"unicode_decimal": 59186
},
{
"icon_id": "2045280",
"name": "more",
"font_class": "more",
"unicode": "e736",
"unicode_decimal": 59190
},
{
"icon_id": "2045289",
"name": "people",
"font_class": "people",
"unicode": "e73d",
"unicode_decimal": 59197
},
{
"icon_id": "2045337",
"name": "stealth_fill",
"font_class": "stealth_fill",
"unicode": "e75f",
"unicode_decimal": 59231
},
{
"icon_id": "490399",
"name": "notifications_active",
"font_class": "notifications-on",
"unicode": "ec64",
"unicode_decimal": 60516
},
{
"icon_id": "5226577",
"name": "notifications",
"font_class": "notifications",
"unicode": "e630",
"unicode_decimal": 58928
},
{
"icon_id": "7371992",
"name": "round-notifications_",
"font_class": "notifications-off",
"unicode": "e6e4",
"unicode_decimal": 59108
},
{
"icon_id": "24125360",
"name": "stamp",
"font_class": "stamp",
"unicode": "e6de",
"unicode_decimal": 59102
},
{
"icon_id": "11931885",
"name": "link",
"font_class": "insertlink",
"unicode": "e621",
"unicode_decimal": 58913
},
{
"icon_id": "1393807",
"name": "upload",
"font_class": "upload",
"unicode": "e735",
"unicode_decimal": 59189
},
{
"icon_id": "7428691",
"name": "play",
"font_class": "play",
"unicode": "e628",
"unicode_decimal": 58920
},
{
"icon_id": "7685091",
"name": "feed",
"font_class": "feed",
"unicode": "e6f6",
"unicode_decimal": 59126
},
{
"icon_id": "20531430",
"name": "gitbook",
"font_class": "gitbook",
"unicode": "e604",
"unicode_decimal": 58884
},
{
"icon_id": "88076",
"name": "reload",
"font_class": "reload",
"unicode": "e603",
"unicode_decimal": 58883
},
{
"icon_id": "2958463",
"name": "refresh",
"font_class": "refresh",
"unicode": "e78f",
"unicode_decimal": 59279
},
{
"icon_id": "18442559",
"name": "warning",
"font_class": "warning1",
"unicode": "e602",
"unicode_decimal": 58882
},
{
"icon_id": "11982163",
"name": "toggle_off",
"font_class": "toggle_off",
"unicode": "e9ad",
"unicode_decimal": 59821
},
{
"icon_id": "15617211",
"name": "toggle-on",
"font_class": "toggle-on",
"unicode": "e8b2",
"unicode_decimal": 59570
},
{
"icon_id": "1393793",
"name": "threedots",
"font_class": "threedots",
"unicode": "e730",
"unicode_decimal": 59184
},
{
"icon_id": "13896012",
"name": "moon",
"font_class": "moon",
"unicode": "e780",
"unicode_decimal": 59264
},
{
"icon_id": "18019049",
"name": "sun",
"font_class": "sun",
"unicode": "e634",
"unicode_decimal": 58932
},
{
"icon_id": "8827141",
"name": "info",
"font_class": "info",
"unicode": "e93b",
"unicode_decimal": 59707
},
{
"icon_id": "1567557",
"name": "login",
"font_class": "login",
"unicode": "e66a",
"unicode_decimal": 58986
},
{
"icon_id": "11968691",
"name": "copy",
"font_class": "copy",
"unicode": "e638",
"unicode_decimal": 58936
},
{
"icon_id": "17540404",
"name": "snapshot",
"font_class": "snapshot",
"unicode": "e6da",
"unicode_decimal": 59098
},
{
"icon_id": "10331571",
"name": "stars",
"font_class": "stars",
"unicode": "e6a3",
"unicode_decimal": 59043
},
{
"icon_id": "17018087",
"name": "favorite-off",
"font_class": "favorite-off",
"unicode": "e6d7",
"unicode_decimal": 59095
},
{
"icon_id": "17018088",
"name": "favorite-on",
"font_class": "favorite-on",
"unicode": "e6d8",
"unicode_decimal": 59096
},
{
"icon_id": "562650",
"name": "love it",
"font_class": "loveit",
"unicode": "e60a",
"unicode_decimal": 58890
},
{
"icon_id": "248947",
"name": "Vignetting",
"font_class": "byteball",
"unicode": "e6bc",
"unicode_decimal": 59068
},
{
"icon_id": "261002",
"name": "android",
"font_class": "android",
"unicode": "e601",
"unicode_decimal": 58881
},
{
"icon_id": "348956",
"name": "twitter",
"font_class": "twitter",
"unicode": "ec9c",
"unicode_decimal": 60572
},
{
"icon_id": "703493",
"name": "reddit",
"font_class": "reddit",
"unicode": "ea3c",
"unicode_decimal": 59964
},
{
"icon_id": "836629",
"name": "google",
"font_class": "google",
"unicode": "e723",
"unicode_decimal": 59171
},
{
"icon_id": "929164",
"name": "chrome",
"font_class": "chrome",
"unicode": "e6c2",
"unicode_decimal": 59074
},
{
"icon_id": "944996",
"name": "mac",
"font_class": "apple",
"unicode": "e6bd",
"unicode_decimal": 59069
},
{
"icon_id": "1192366",
"name": "facebook",
"font_class": "facebook",
"unicode": "e6be",
"unicode_decimal": 59070
},
{
"icon_id": "1275498",
"name": "windows",
"font_class": "windows",
"unicode": "e649",
"unicode_decimal": 58953
},
{
"icon_id": "1444399",
"name": "discord",
"font_class": "discord",
"unicode": "e6fe",
"unicode_decimal": 59134
},
{
"icon_id": "2612016",
"name": "mac os的icon",
"font_class": "macos",
"unicode": "e6ba",
"unicode_decimal": 59066
},
{
"icon_id": "3876347",
"name": "line",
"font_class": "line",
"unicode": "e6c0",
"unicode_decimal": 59072
},
{
"icon_id": "11262564",
"name": "menu",
"font_class": "menu1",
"unicode": "e6c8",
"unicode_decimal": 59080
},
{
"icon_id": "11262644",
"name": "close",
"font_class": "close1",
"unicode": "e6c9",
"unicode_decimal": 59081
},
{
"icon_id": "11262645",
"name": "gear",
"font_class": "gear",
"unicode": "e6ca",
"unicode_decimal": 59082
},
{
"icon_id": "6145465",
"name": "bullet",
"font_class": "bullet",
"unicode": "e619",
"unicode_decimal": 58905
},
{
"icon_id": "1176968",
"name": "plus",
"font_class": "plus",
"unicode": "e727",
"unicode_decimal": 59175
},
{
"icon_id": "924591",
"name": "wallet",
"font_class": "wallet",
"unicode": "e97c",
"unicode_decimal": 59772
},
{
"icon_id": "11399282",
"name": "signature",
"font_class": "signature",
"unicode": "e600",
"unicode_decimal": 58880
},
{
"icon_id": "15617545",
"name": "receipt-outlined",
"font_class": "receipt-outlined",
"unicode": "e8eb",
"unicode_decimal": 59627
},
{
"icon_id": "10561798",
"name": "Check, label",
"font_class": "check",
"unicode": "e679",
"unicode_decimal": 59001
},
{
"icon_id": "59347",
"name": "warning",
"font_class": "warning",
"unicode": "e62c",
"unicode_decimal": 58924
},
{
"icon_id": "1262104",
"name": "search",
"font_class": "search",
"unicode": "e6f2",
"unicode_decimal": 59122
},
{
"icon_id": "1336076",
"name": "Menu",
"font_class": "menu",
"unicode": "e609",
"unicode_decimal": 58889
},
{
"icon_id": "2518398",
"name": "close",
"font_class": "close",
"unicode": "e63e",
"unicode_decimal": 58942
},
{
"icon_id": "5679219",
"name": "mark-github",
"font_class": "github",
"unicode": "e667",
"unicode_decimal": 58983
},
{
"icon_id": "7141712",
"name": "external-link",
"font_class": "external-link",
"unicode": "e636",
"unicode_decimal": 58934
},
{
"icon_id": "11262646",
"name": "go",
"font_class": "go",
"unicode": "e6cb",
"unicode_decimal": 59083
},
{
"icon_id": "11262647",
"name": "back",
"font_class": "back",
"unicode": "e6cc",
"unicode_decimal": 59084
},
{
"icon_id": "13519508",
"name": "user-solid",
"font_class": "user",
"unicode": "e66f",
"unicode_decimal": 58991
},
{
"icon_id": "13895880",
"name": "arrow-up",
"font_class": "arrow-up",
"unicode": "e75c",
"unicode_decimal": 59228
},
{
"icon_id": "14232782",
"name": "arrow-up",
"font_class": "arrow-down",
"unicode": "e75d",
"unicode_decimal": 59229
}
]
}
================================================
FILE: src/components/AboutMembersListItem.vue
================================================
================================================
FILE: src/components/AvatarOverlayEdit.vue
================================================
{{ avatar ? $t('edit') : $t('upload') }}
================================================
FILE: src/components/AvatarSpace.vue
================================================
================================================
FILE: src/components/AvatarToken.vue
================================================
================================================
FILE: src/components/AvatarUser.vue
================================================
================================================
FILE: src/components/Banner.vue
================================================
Switch to the new interface
Switch to v2
================================================
FILE: src/components/BaseAvatar.vue
================================================
================================================
FILE: src/components/BaseBadge.vue
================================================
{{ $t('isCore') }}
================================================
FILE: src/components/BaseBlock.vue
================================================
================================================
FILE: src/components/BaseBreadcrumbs.vue
================================================
================================================
FILE: src/components/BaseButtonIcon.vue
================================================
================================================
FILE: src/components/BaseButtonRound.vue
================================================
================================================
FILE: src/components/BaseCalendar.vue
================================================
{{ monthName }} {{ fullYear }}
================================================
FILE: src/components/BaseCombobox.vue
================================================
{{ label }}
================================================
FILE: src/components/BaseContainer.vue
================================================
================================================
FILE: src/components/BaseCounter.vue
================================================
================================================
FILE: src/components/BaseIcon.vue
================================================
================================================
FILE: src/components/BaseIndicator.vue
================================================
================================================
FILE: src/components/BaseInput.vue
================================================
{{ title ?? definition.title }}
{{ error?.message }}
================================================
FILE: src/components/BaseInterpunct.vue
================================================
================================================
FILE: src/components/BaseLink.vue
================================================
================================================
FILE: src/components/BaseListbox.vue
================================================
{{ label || definition?.title }}
{{ selectedItem?.title || selectedItem.value }}
{{ item?.title || item.value }}
================================================
FILE: src/components/BaseLoading.vue
================================================
================================================
FILE: src/components/BaseMarkdown.vue
================================================
================================================
FILE: src/components/BaseMenu.vue
================================================
{{ selected }}
================================================
FILE: src/components/BaseMessage.vue
================================================
================================================
FILE: src/components/BaseMessageBlock.vue
================================================
================================================
FILE: src/components/BaseModal.vue
================================================
================================================
FILE: src/components/BaseModalSelectItem.vue
================================================
================================================
FILE: src/components/BaseNetworkItem.vue
================================================
{{
$tc('inSpaces', [
formatCompactNumber(networksSpacesCount?.[network.key] ?? 0)
])
}}
================================================
FILE: src/components/BaseNoResults.vue
================================================
{{ $t('noResultsFound') }}
================================================
FILE: src/components/BasePill.vue
================================================
================================================
FILE: src/components/BasePluginItem.vue
================================================
{{ plugin.author }}
{{
$tc('inSpaces', [
formatCompactNumber(pluginsSpacesCount?.[plugin.key] ?? 0)
])
}}
{{ $t('learnMore') }}
================================================
FILE: src/components/BasePopover.vue
================================================
{{ label }}
================================================
FILE: src/components/BasePopoverHover.vue
================================================
================================================
FILE: src/components/BaseProgressBar.vue
================================================
================================================
FILE: src/components/BaseProgressRadial.vue
================================================
================================================
FILE: src/components/BaseSearch.vue
================================================
================================================
FILE: src/components/BaseSidebarNavigationItem.vue
================================================
================================================
FILE: src/components/BaseSkinItem.vue
================================================
{{
skin
}}
{{ $tc('inSpaces', [formatCompactNumber(skinsSpacesCount[skin] ?? 0)]) }}
================================================
FILE: src/components/BaseStrategyItem.vue
================================================
{{ strategy.id }}
v{{ strategy.version }}
{{ strategy.author }}
{{ $tc('inSpaces', [formatCompactNumber(strategy.spacesCount)]) }}
================================================
FILE: src/components/BaseUser.vue
================================================
{{ getUsername(address, profile) }}
================================================
FILE: src/components/BlockLink.vue
================================================
{{ preview?.meta.title }}
{{ preview?.meta.description }}
================================================
FILE: src/components/BlockSpacesList.vue
================================================
{{ message || $t('noResultsFound') }}
================================================
FILE: src/components/BlockSpacesListButtonMore.vue
================================================
{{ $t('seeAll') }}
================================================
FILE: src/components/BlockSpacesListItem.vue
================================================
================================================
FILE: src/components/BlockSpacesListSkeleton.vue
================================================
================================================
FILE: src/components/ButtonBack.vue
================================================
{{ name ? name : $t('back') }}
================================================
FILE: src/components/ButtonCard.vue
================================================
{{ title }}
================================================
FILE: src/components/ButtonFollow.vue
================================================
{{ $t('join') }}
{{ $t('joined') }}
{{ $t('leave') }}
================================================
FILE: src/components/ButtonPlayground.vue
================================================
{{ $t('settings.testInPlayground') }}
================================================
FILE: src/components/ButtonShare.vue
================================================
{{ $t('share') }}
================================================
FILE: src/components/ButtonSwitch.vue
================================================
{{ state1.name }}
{{ state2.name }}
================================================
FILE: src/components/ButtonTheme.vue
================================================
================================================
FILE: src/components/ComboboxNetwork.vue
================================================
emit('select', value)"
>
{{ item.name }}
#{{ item.id }}
================================================
FILE: src/components/ContainerParallelInput.vue
================================================
================================================
FILE: src/components/ExploreMenuCategories.vue
================================================
{{ $tc('explore.categories.' + selected) }}
{{ $tc('explore.category') }}
{{ item.text }}
================================================
FILE: src/components/ExploreSkeletonLoading.vue
================================================
================================================
FILE: src/components/ExploreSpaces.vue
================================================
{{ $tc('spaceCount', [formatCompactNumber(spacesHomeMetrics.total)]) }}
{{
$tc('members', space.followersCount, {
count: formatCompactNumber(space.followersCount)
})
}}
{{ $t('homeLoadmore') }}
================================================
FILE: src/components/FooterLinks.vue
================================================
================================================
FILE: src/components/FooterLinksItem.vue
================================================
================================================
FILE: src/components/FooterSocials.vue
================================================
================================================
FILE: src/components/FooterSocialsItem.vue
================================================
================================================
FILE: src/components/FooterTitle.vue
================================================
================================================
FILE: src/components/FormArrayStrategies.vue
================================================
{{ $t('strategies') }}
{{ i + 1 }}
(input[i].name = value)"
/>
(input[i].network = value)"
/>
{{ $t('addStrategy') }}
{{ $t('copyVotingStrategies') }}
================================================
FILE: src/components/FormObjectStrategyParams.vue
================================================
================================================
FILE: src/components/IconDiscord.vue
================================================
================================================
FILE: src/components/IconInformationTooltip.vue
================================================
================================================
FILE: src/components/IconVerifiedSpace.vue
================================================
================================================
FILE: src/components/IndicatorAssetsChange.spec.js
================================================
import { describe, expect, it, afterEach } from 'vitest';
import { shallowMount } from '@vue/test-utils';
import IndicatorAssetsChange from './IndicatorAssetsChange.vue';
describe('IndicatorAssetsChange', () => {
let wrapper;
const findAssetChange = () =>
wrapper.find('[data-testid="asset-quote-change"]');
function createComponent(params = {}) {
wrapper = shallowMount(IndicatorAssetsChange, params);
}
afterEach(() => {
wrapper.unmount();
});
it('should render red if quote_24h is greater than quote', () => {
createComponent({
props: {
quote: {
quote: 2700,
quote_24h: 2800
}
}
});
expect(findAssetChange().classes()).toContain('text-red');
});
it('should render green if quote_24h is lower than quote', async () => {
createComponent({
props: {
quote: {
quote: 2900,
quote_24h: 2800
}
}
});
expect(findAssetChange().classes()).toContain('text-green');
});
it('should render correct 24h % change', () => {
const quote = {
quote: 2800,
quote_24h: 2800
};
createComponent({
props: {
quote
}
});
expect(
wrapper.find('[data-testid="asset-quote-change-percent"]').text()
).toContain('0%');
});
it('should render no indicator when both are zero', () => {
const quote = {
quote: 0,
quote_24h: 0
};
createComponent({
props: {
quote
}
});
expect(wrapper.find('[asset-quote-change"]').exists()).toBe(false);
});
it('should render the change in $', async () => {
const quote = {
quote: 2800,
quote_24h: 2800
};
createComponent({
props: {
quote
}
});
expect(
wrapper.find('[data-testid="asset-quote-change-usd"]').text()
).toContain(`$0`);
});
});
================================================
FILE: src/components/IndicatorAssetsChange.vue
================================================
{{
`${quote.quote_24h > quote.quote ? '' : '+'}${formatPercentNumber(
(quote.quote - quote.quote_24h) / quote.quote_24h
)}`
}}
{{ `($${formatNumber(quote.quote - quote.quote_24h)})` }}
================================================
FILE: src/components/InputCheckbox.vue
================================================
================================================
FILE: src/components/InputComboboxToken.vue
================================================
{{
selectedToken.symbol
}}
Select token
================================================
FILE: src/components/InputDate.vue
================================================
{{ title }}
{{ dateString }}
================================================
FILE: src/components/InputEmail.vue
================================================
================================================
FILE: src/components/InputNewsletter.vue
================================================
================================================
FILE: src/components/InputSelect.vue
================================================
{{ title }}
{{ modelValue }}
================================================
FILE: src/components/InputSelectPrivacy.vue
================================================
================================================
FILE: src/components/InputSelectVoteType.vue
================================================
================================================
FILE: src/components/InputSelectVoteValidation.vue
================================================
================================================
FILE: src/components/InputSwitch.vue
================================================
emit('update:modelValue', value)"
>
{{ label }}
{{ textRight || definition?.title }}
================================================
FILE: src/components/InputUploadAvatar.vue
================================================
================================================
FILE: src/components/LabelInput.vue
================================================
================================================
FILE: src/components/LabelProposalState.vue
================================================
{{ $t(`proposals.states.${state}`) }}
================================================
FILE: src/components/LabelProposalVoted.vue
================================================
{{ $t('voted') }}
================================================
FILE: src/components/LinkSpace.vue
================================================
================================================
FILE: src/components/ListboxNetwork.vue
================================================
emit('update:modelValue', value)"
>
{{ item.name }}
#{{ item.value }}
================================================
FILE: src/components/LoadingList.vue
================================================
================================================
FILE: src/components/LoadingPage.vue
================================================
================================================
FILE: src/components/LoadingRow.vue
================================================
================================================
FILE: src/components/LoadingSpinner.vue
================================================
================================================
FILE: src/components/MenuAccount.vue
================================================
================================================
FILE: src/components/MenuLanguages.vue
================================================
{{
languages[$i18n.locale]?.nativeName ?? languages[$i18n.locale]?.name
}}
================================================
FILE: src/components/MessageWarningFlagged.vue
================================================
{{ warningText }}
{{ $t('warningFlaggedActionShow') }}
================================================
FILE: src/components/MessageWarningGnosisNetwork.vue
================================================
{{
$t('settings.gnosisWrongNetwork.base', {
network: networks?.[networkKey]?.name,
action: $t(`settings.gnosisWrongNetwork.${action}`)
})
}}
================================================
FILE: src/components/MessageWarningHibernated.vue
================================================
{{
isAuthorized
? $t('create.errorSpaceHibernatedAdmin')
: $t('create.errorSpaceHibernatedUsers')
}}
{{ $t('learnMore') }}
Go to Settings
================================================
FILE: src/components/MessageWarningTestnet.vue
================================================
{{ context }}
#{{ strategyTestnetErrors.map(e => Number(e[0]) + 1).join(', ') }}
is using a test network which is no longer supported. If you are looking to
setup a space for testing, please checkout
testnet.snapshot.org
================================================
FILE: src/components/MessageWarningValidation.vue
================================================
{{
$t(`${tPath}.basic.invalidMessage`, {
amount: formatCompactNumber(minScore),
symbol
})
}}
{{ $t('learnMore') }}
{{
$t(`${tPath}.passport-gated.invalidMessage`, {
scoreThreshold: validationParams?.scoreThreshold || 0
})
}}.
{{
$t(`${tPath}.passport-gated.invalidMessageStamps`, {
operator:
props.validationParams?.operator === 'AND' ? 'all' : 'one',
stamps:
validationParams.stamps && validationParams.stamps.join(', ')
})
}}
Gitcoin Passport
{{ $t(`${tPath}.notValidMessage`) }}
{{ $t('learnMore') }}
================================================
FILE: src/components/ModalAccount.vue
================================================
Connect to Snapshot
{{ injected.name }}
{{ connectors[cId].name }}
{{ $t('showMore') }}
================================================
FILE: src/components/ModalConfirmAction.vue
================================================
{{ title ? title : $t('confirmAction') }}
{{ $t('cancel') }}
{{ $t('confirm') }}
================================================
FILE: src/components/ModalConfirmLeave.vue
================================================
{{ title ? title : 'Unsaved changes' }}
You have unsaved changes. Would you like to save them before leaving?
Leave
Save
================================================
FILE: src/components/ModalControllerEdit.vue
================================================
{{ $t('settings.editController') }}
{{
$tc('settings.currentSpaceControllerIs', {
address: shorten(spaceController)
})
}}
{{ $t('setup.seeOnEns') }}
{{ $t('settings.set') }}
================================================
FILE: src/components/ModalDelegate.vue
================================================
{{ $t('profile.about.delegate') }}
{{ $t('delegate.to') }}
{{ $t('space') }}
{{ $t('confirm') }}
================================================
FILE: src/components/ModalEmail.vue
================================================
{{ $t('emailSubscription.title') }}
{{ $t('emailResend.title') }}
{{ $t('emailManagement.title') }}
================================================
FILE: src/components/ModalEmailManagement.vue
================================================
{{ t('emailManagement.subtitle') }}
================================================
FILE: src/components/ModalEmailResend.vue
================================================
{{ $t('emailResend.description') }}
{{ $t('close') }}
================================================
FILE: src/components/ModalEmailSubscription.vue
================================================
{{ $t('emailSubscription.description') }}
{{ $t('emailSubscription.postSubscribeMessage.successThanks') }}
{{ $t('emailSubscription.postSubscribeMessage.successConfirmation') }}
{{ $t('close') }}
================================================
FILE: src/components/ModalLinkPreview.vue
================================================
================================================
FILE: src/components/ModalMessage.vue
================================================
{{ title }}
{{ $t('continue') }}
================================================
FILE: src/components/ModalNotice.vue
================================================
{{ title }}
{{ $t('continue') }}
================================================
FILE: src/components/ModalOsnap.vue
================================================
oSnap seamlessly integrates with Snapshot and your treasury,
automatically executing governance votes on-chain. Bypass the need for
privileged signers to create a DAO that's more efficient and truly
decentralized.
Learn more
{{ isOsnapEnabled ? 'Deactivate' : 'Activate' }} oSnap
Note that the deactivation process takes place in the Safe app
================================================
FILE: src/components/ModalPendingTransactions.vue
================================================
{{ $t('setup.pendingTransactions') }}
{{ shorten(tx.hash as string) }}
================================================
FILE: src/components/ModalPlugins.vue
================================================
{{
usedPlugins.includes(selectedPlugin?.key || '')
? $t('settings.editPlugin')
: $t('settings.addPlugin')
}}
{{ Object.keys(plugin).length ? $t('applyChanges') : $t('add') }}
================================================
FILE: src/components/ModalPostPayment.vue
================================================
Transaction submitted
Copy the transaction hash below for use on the network request form as
proof of payment.
{{ tx.hash }}
View in explorer
Copy tx hash
================================================
FILE: src/components/ModalPostVote.vue
================================================
Thank you for your participation!
Votes can be changed while the proposal is active.
Share on X
{{ $t('shareOnHey') }}
{{ $t('proposal.postVoteModal.subscribe') }}
{{ $t('proposal.postVoteModal.seeQueue') }}
{{ $t('close') }}
================================================
FILE: src/components/ModalProfileForm.vue
================================================
{{ $t('profile.settings.header') }}
(form.avatar = url)"
@image-remove="form.avatar = ''"
>
{{ $t('profile.settings.biography') }}
{{ $t('save') }}
================================================
FILE: src/components/ModalRevokeDelegate.vue
================================================
{{ $t('removeDelegation') }}
================================================
FILE: src/components/ModalSelectDate.vue
================================================
{{ type === 'start' ? $t('create.startDate') : $t('create.endDate') }}
{{ type === 'start' ? $t('create.startTime') : $t('create.endTime') }}
{{ $t('cancel') }}
{{ $t('next') }}
{{ $t('select') }}
================================================
FILE: src/components/ModalSkins.vue
================================================
{{ $t('skins') }}
================================================
FILE: src/components/ModalSnapshotTerms.vue
================================================
Term of use
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Alias maxime
ipsum reiciendis delectus laboriosam consequuntur ipsa, exercitationem,
ex, quam quaerat nisi dignissimos aperiam quisquam odio totam aliquam
necessitatibus! Natus, itaque.
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Alias maxime
ipsum reiciendis delectus laboriosam consequuntur ipsa, exercitationem,
ex, quam quaerat nisi dignissimos aperiam quisquam odio totam aliquam
necessitatibus! Natus, itaque. Lorem ipsum dolor, sit amet consectetur
adipisicing elit. Alias maxime ipsum reiciendis delectus laboriosam
consequuntur ipsa, exercitationem, ex, quam quaerat nisi dignissimos
aperiam quisquam odio totam aliquam necessitatibus! Natus, itaque.
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Alias maxime
ipsum reiciendis delectus laboriosam consequuntur ipsa, exercitationem,
ex, quam quaerat nisi dignissimos aperiam quisquam odio totam aliquam
necessitatibus! Natus, itaque.
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Alias maxime
ipsum reiciendis delectus laboriosam consequuntur ipsa, exercitationem,
ex, quam quaerat nisi dignissimos aperiam quisquam odio totam aliquam
necessitatibus! Natus, itaque.
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Alias maxime
ipsum reiciendis delectus laboriosam consequuntur ipsa, exercitationem,
ex, quam quaerat nisi dignissimos aperiam quisquam odio totam aliquam
necessitatibus! Natus, itaque.
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Alias maxime
ipsum reiciendis delectus laboriosam consequuntur ipsa, exercitationem,
ex, quam quaerat nisi dignissimos aperiam quisquam odio totam aliquam
necessitatibus! Natus, itaque. Lorem ipsum dolor, sit amet consectetur
adipisicing elit. Alias maxime ipsum reiciendis delectus laboriosam
consequuntur ipsa, exercitationem, ex, quam quaerat nisi dignissimos
aperiam quisquam odio totam aliquam necessitatibus! Natus, itaque.
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Alias maxime
ipsum reiciendis delectus laboriosam consequuntur ipsa, exercitationem,
ex, quam quaerat nisi dignissimos aperiam quisquam odio totam aliquam
necessitatibus! Natus, itaque.
Close
================================================
FILE: src/components/ModalSpaces.vue
================================================
{{ $t('spaces') }}
================================================
FILE: src/components/ModalSpacesListItem.vue
================================================
{{
$tc('members', {
count: formatCompactNumber(space.followersCount || 0)
})
}}
================================================
FILE: src/components/ModalStrategies.vue
================================================
{{ $t('strategiesPage') }}
================================================
FILE: src/components/ModalStrategy.vue
================================================
(input.network = value)"
/>
(isValidJson = value)"
/>
{{ strategy.name ? $t('save') : $t('add') }}
================================================
FILE: src/components/ModalTerms.vue
================================================
{{ $t('settings.terms.label') }}
{{
$tc('modalTerms.mustAgreeTo', {
action,
spaceName: space.name || 'spaces'
})
}}
{{ $t('cancel') }}
{{ $t('agree') }}
================================================
FILE: src/components/ModalTokens.vue
================================================
Assets
{{
$t('noResultsFound')
}}
No tokens found.
Add a token by searching the contract address.
================================================
FILE: src/components/ModalTokensItem.vue
================================================
{{
formatNumber(
Number(formatUnits(token.tokenBalance, token.decimals)),
getNumberFormatter({ maximumFractionDigits: 6 }).value
)
}}
{{ shorten(token.contractAddress) }}
================================================
FILE: src/components/ModalTransactionStatus.vue
================================================
{{ title }}
View on Etherscan
{{ subtitle }}
{{ closeButtonText }}
Try again
================================================
FILE: src/components/ModalTreasury.vue
================================================
(input.network = value)"
/>
{{ treasury.name ? $t('applyChanges') : $t('add') }}
================================================
FILE: src/components/ModalValidation.vue
================================================
{{
input.name
? $t('proposalValidation.settingsTitle')
: $t('proposalValidation.title')
}}
(isValidParams = value)"
/>
(isValidParams = value)"
/>
{{ $t('applyChanges') }}
================================================
FILE: src/components/ModalVote.vue
================================================
{{ $tc('proposal.castVote') }}
Blank vote
{{ format(proposal, selectedChoices) }}
{{ formatNumber(Number(proposal.snapshot)) }}
{{ $t('failed') }}
{{ $t(`votingValidation.${proposal.validation.name}.label`) }}
{{ $t('failed') }}
{{ formatCompactNumber(votingPower) }}
{{ shorten(proposal.symbol || space.symbol, 'symbol') }}
Help Center
Help Center
{{
$t('noVotingPower', {
blockNumber: formatNumber(Number(proposal.snapshot))
})
}}
{{ $t('learnMore') }}
{{ $t('cancel') }}
{{ $t('confirm') }}
================================================
FILE: src/components/ModalVoteValidation.vue
================================================
{{
input.name
? $t('votingValidation.settingsTitle')
: $t('votingValidation.title')
}}
(isValidParams = value)"
/>
(isValidParams = value)"
/>
{{ $t('applyChanges') }}
================================================
FILE: src/components/ModalVotingPrivacy.vue
================================================
{{ $t('privacy.title') }}
================================================
FILE: src/components/ModalVotingType.vue
================================================
{{ $t('voting.selectVoting') }}
================================================
FILE: src/components/ModalWrongNetwork.vue
================================================
Wrong network
To continue, you need to change the network in your wallet to
{{
networks[desiredNetwork].name
}} .
{{
$t('unsupportedNetwork.switchToNetwork', {
network: networks[desiredNetwork].name
})
}}
{{ $t('unsupportedNetwork.goToDemoSite') }}
Close
================================================
FILE: src/components/NavbarAccount.vue
================================================
================================================
FILE: src/components/NavbarExtras.vue
================================================
v{{ pkg.version }}#{{ commitSha.slice(0, 7) }}
================================================
FILE: src/components/NavbarNotifications.vue
================================================
================================================
FILE: src/components/PopoverHoverProfile.vue
================================================
{{ profile.about }}
{{ $t('profile.viewProfile') }}
{{ $t('seeInExplorer') }}
================================================
FILE: src/components/ProfileAboutBiography.vue
================================================
================================================
FILE: src/components/ProfileAboutDelegate.vue
================================================
{{
loadingDelegators === 'notSupportedNetwork'
? $t('profile.about.notSupportedNetwork', {
network: networkString
})
: $t('profile.about.noDelegatorsMessage', {
network: networkString
})
}}
================================================
FILE: src/components/ProfileAboutDelegateListItem.vue
================================================
{{ space.name }}
{{ $t('profile.about.delegated') }}
{{ $t('profile.about.delegate') }}
================================================
FILE: src/components/ProfileActivityList.vue
================================================
{{
title.toUpperCase()
}}
================================================
FILE: src/components/ProfileActivityListItem.vue
================================================
{{
$t('profile.activity.votedFor', {
choice: activity.vote?.choice
? `"${activity.vote?.choice}"`
: ''
})
}}
{{
formatRelativeTime(activity.created, longRelativeTimeFormatter)
}}
{{ activity.title }}
================================================
FILE: src/components/ProfileAddressCopy.vue
================================================
{{ profile.ens }}
{{ shorten(userAddress) }}
================================================
FILE: src/components/ProfileName.vue
================================================
{{ profile?.name || profile?.ens || shorten(address) }}
================================================
FILE: src/components/ProfileSidebar.vue
================================================
{{ $t('profile.buttonEdit') }}
================================================
FILE: src/components/ProfileSidebarHeader.vue
================================================
================================================
FILE: src/components/ProfileSidebarHeaderSkeleton.vue
================================================
================================================
FILE: src/components/ProfileSidebarNavigation.vue
================================================
{{ $t('profile.activity.header') }}
{{ $t('profile.about.header') }}
================================================
FILE: src/components/ProposalsItem.vue
================================================
{{ body }}
{{ boostsCount }}
boosts
active
================================================
FILE: src/components/ProposalsItemBody.vue
================================================
================================================
FILE: src/components/ProposalsItemFooter.vue
================================================
{{
capitalize(
getRelativeProposalPeriod(
proposal.state,
proposal.start,
proposal.end
)
)
}}
-
{{ quorumText }}
================================================
FILE: src/components/ProposalsItemResults.vue
================================================
{{ shorten(choice, 32) }}
{{ formatCompactNumber(proposal.scores[i]) }}
{{ proposal.symbol || proposal.space.symbol }}
================================================
FILE: src/components/ProposalsItemTitle.vue
================================================
{{ proposal.title }}
================================================
FILE: src/components/SettingsBoostBlock.vue
================================================
================================================
FILE: src/components/SettingsDangerzoneBlock.vue
================================================
{{ item.title }}
{{ item.description }}
{{ item.button }}
================================================
FILE: src/components/SettingsDelegationBlock.vue
================================================
{{ $t('settings.delegationPortal.information') }}
(form.delegationPortal.delegationType = value)
"
/>
(form.delegationPortal.delegationNetwork = value)"
/>
================================================
FILE: src/components/SettingsDomainBlock.vue
================================================
{{ $t('learnMore') }}
================================================
FILE: src/components/SettingsLinkBlock.vue
================================================
================================================
FILE: src/components/SettingsMembersBlock.vue
================================================
{{
validationErrors?.admins ||
validationErrors?.moderators ||
validationErrors?.members
}}
================================================
FILE: src/components/SettingsMembersPopoverContent.vue
================================================
{{ capitalize(role) }}
{{ $t(`settings.members.${role}.description`) }}
================================================
FILE: src/components/SettingsPluginsBlock.vue
================================================
{{ pluginIndex[name].name }}
{{ $t('settings.addPlugin') }}
================================================
FILE: src/components/SettingsProfileBlock.vue
================================================
{{ $t('settings.avatar') }}
(form.avatar = url)"
@image-remove="() => (form.avatar = '')"
>
================================================
FILE: src/components/SettingsProposalBlock.vue
================================================
================================================
FILE: src/components/SettingsStrategiesBlock.vue
================================================
(form.network = value)"
/>
{{ $tc('settings.strategiesList', [strategiesLimit]) }}
({{ $t('settings.votingPowerIsCumulative') }})
{{ $t('add') }}
================================================
FILE: src/components/SettingsSubSpacesBlock.vue
================================================
{{ $t('learnMore') }}
{{ child }}
================================================
FILE: src/components/SettingsTreasuriesBlock.vue
================================================
Warning: no treasuries
You have installed the oSnap plugin, but you don't have any treasuries.
Please add a Safe as a treasury and enable oSnap on it to use the oSnap
plugin.
handleEditTreasury(i)"
@remove-treasury="i => handleRemoveTreasury(i)"
@configure-osnap="handleOpenConfigureOsnapModal"
/>
{{ $t('settings.treasuries.add') }}
================================================
FILE: src/components/SettingsTreasuriesBlockItem.vue
================================================
emit('editTreasury', i)"
@remove-treasury="i => emit('removeTreasury', i)"
@configure-osnap="(i, isEnabled) => emit('configureOsnap', i, isEnabled)"
/>
================================================
FILE: src/components/SettingsTreasuriesBlockItemButton.vue
================================================
{{ treasury.name }}
================================================
FILE: src/components/SettingsTreasuryActivateOsnapButton.vue
================================================
oSnap activated
Activate oSnap
================================================
FILE: src/components/SettingsValidationBlock.vue
================================================
{{ $t('missingProposalValidationError') }}
================================================
FILE: src/components/SettingsVotingBlock.vue
================================================
(form.voting.quorum = Number(value))"
/>
(form.voting.type = value)"
/>
(form.voting.privacy = value)"
/>
(form.voteValidation = value)"
/>
================================================
FILE: src/components/SetupButtonBack.vue
================================================
{{ $t('back') }}
================================================
FILE: src/components/SetupButtonCreate.vue
================================================
{{ $t('createButton') }}
================================================
FILE: src/components/SetupButtonNext.vue
================================================
{{ $t('next') }}
================================================
FILE: src/components/SetupDomain.vue
================================================
{{ $t('setup.domain.title') }}
{{ $t('setup.domain.ensMessage') }}
{{ $t('setup.domain.tryDemo') }}
{{
$t('setup.demoTestnetEnsMessage', {
network: networks[defaultNetwork].name
})
}}
{{
$t(
availableDomains.length > 1
? 'setup.chooseExistingEns'
: 'setup.useSingleExistingEns'
)
}}
{{ ens.name }}
Unavailable ENS domains:
{{ ens.name }}
{{ shortenInvalidEns(ens.name) }}
{{ shortenInvalidEns(ens.name) }}
{{ $t('setup.orRegisterNewEns') }}
{{ $t('setup.toCreateASpace') }}
================================================
FILE: src/components/SetupDomainRegister.vue
================================================
{{ $t('setup.registerEnsButton') }}
================================================
FILE: src/components/SetupExtras.vue
================================================
{{ $t('setup.validationTitle') }}
================================================
FILE: src/components/SetupIntro.vue
================================================
{{ $t('header.description') }}
{{ $t('getStarted') }}
================================================
FILE: src/components/SetupMessageHelp.vue
================================================
documentation
Help Center
================================================
FILE: src/components/SetupProfile.vue
================================================
================================================
FILE: src/components/SetupSidebarStepper.vue
================================================
{{ step.name }}
{{ step.name }}
================================================
FILE: src/components/SetupStrategy.vue
================================================
{{ $t('setup.strategy.title') }}
{{ $t('setup.strategy.subtitle') }}
{{ $t('setup.strategy.tokenVoting.description') }}
{{ $t('setup.strategy.onePersonOneVote.description') }}
{{ $t('setup.strategy.advanced.description') }}
================================================
FILE: src/components/SetupStrategyAdvanced.vue
================================================
================================================
FILE: src/components/SetupStrategyBasic.vue
================================================
{{ token.name }}
{{ token.symbol }}
{{ shorten(contract) }}
================================================
FILE: src/components/SetupStrategyVote.vue
================================================
{{ t('setup.strategy.onePersonOneVote.votesEqualInfo') }}
{{
selectedItem.value === 'whitelist'
? 'Whitelist voting'
: 'Ticket voting'
}}
{{
item.value === 'whitelist'
? 'Whitelist voting'
: 'Ticket voting'
}}
{{ $t('learnMore') }}
================================================
FILE: src/components/SidebarSpacesSkeleton.vue
================================================
================================================
FILE: src/components/SidebarUnreadIndicator.vue
================================================
================================================
FILE: src/components/SpaceBoostCardProposal.vue
================================================
{{ proposal.title }}
{{ proposal.body }}
================================================
FILE: src/components/SpaceBoostDeposit.vue
================================================
Token fee
{{ tokenFeeFormatted }}
{{ formToken?.symbol }}
({{ tokenFeePercent }}%)
Final cost
{{ amountWithTokenFeeFormatted }}
{{ formToken?.symbol }}
+
{{ ethFee }}
ETH
================================================
FILE: src/components/SpaceBreadcrumbs.vue
================================================
================================================
FILE: src/components/SpaceCreateContent.vue
================================================
{{ $t('settings.proposal.guidelines.title') }}
{{ $t('create.proposalDescription') }}
{{ formatNumber(form.body.length) }} /
{{ formatNumber(bodyLimit) }}
================================================
FILE: src/components/SpaceCreateLegacyOsnap.vue
================================================
Where is oSnap?
oSnap is currently disabled because your space's voting settings
disallow the basic voting type which is a requirement for oSnap to work
properly.
Have your admin visit your
settings page under Voting ->
Type, and make sure either "Any" or "Basic Voting" is selected. This
will allow you to create oSnap proposals.
================================================
FILE: src/components/SpaceCreateOsnap.vue
================================================
oSnap Warning
You currently have both the oSnap plugin and the SafeSnap plugin
installed in your space. You can continue using oSnap with SafeSnap
without any changes to your space. If you would like to use the oSnap
plugin instead, please see the
oSnap plugin migration docs.
Otherwise please remove the oSnap plugin from your space settings for
the best experience with SafeSnap.
oSnap Proposal
Are you planning for this proposal to initiate a transaction that your
organizations Safe will execute if approved? (Remember, oSnap enables
trustless and permissionless execution)
================================================
FILE: src/components/SpaceCreatePlugins.vue
================================================
================================================
FILE: src/components/SpaceCreateVoting.vue
================================================
(form.type = value)"
/>
Note that Boost is not available for this voting type.
>
{{ $tc('create.choice', [index + 1]) }}
{{ `${element.text.length}/32` }}
setDateStart(value)"
/>
setDateEnd(value)"
/>
{{ $t('snapshot') }}
================================================
FILE: src/components/SpaceCreateVotingDateEnd.vue
================================================
================================================
FILE: src/components/SpaceCreateVotingDateStart.vue
================================================
================================================
FILE: src/components/SpaceCreateWarnings.vue
================================================
Proposal creation is blocked due to invalid space settings. Please contact
a space admin or if you are an admin head over to the settings page and
save them again.
{{ $t('create.errorGettingSnapshot') }}
{{ $t('learnMore') }}
{{ $t('proposalValidation.notConnectedMessage') }}
{{
$t('learnMore')
}}
{{ $t('proposalValidation.executionError') }}
{{ $t('proposalValidation.onlyMemberMessage') }}
{{ $t('warningShortUrl') }}
================================================
FILE: src/components/SpaceDelegateEdit.vue
================================================
Save changes
You can always come back and edit your profile at any time.
Save changes
================================================
FILE: src/components/SpaceDelegatesAccount.vue
================================================
view and edit your delegator profile
================================================
FILE: src/components/SpaceDelegatesCard.vue
================================================
{{ getUsername(delegate.id, profiles[delegate.id]) }}
{{ item.text }}
{{ formatCompactNumber(Number(delegate.delegatedVotes)) }}
{{ space.symbol }}
·
{{
formatCompactNumber(
Number(delegate.tokenHoldersRepresentedAmount)
)
}}
delegators
{{ about }}
{{ formatCompactNumber(stats?.votes || 0) }}
votes
·
{{ formatCompactNumber(stats?.proposals || 0) }}
proposals
Delegate
================================================
FILE: src/components/SpaceDelegatesDelegateModal.vue
================================================
{{ $t('delegates.delegateModal.title') }}
{{ $t('delegates.delegateModal.sub') }}
Voting power
{{ formatCompactNumber(Number(accountBalance)) }}
{{ space.symbol }}
{{ $t('confirm') }}
================================================
FILE: src/components/SpaceDelegatesSkeleton.vue
================================================
================================================
FILE: src/components/SpaceDelegatesSplitDelegationModal.vue
================================================
Delegate your voting power to multiple addresses.
Any unallocated power (100% - any delegations) will remain with you.
Set expiration
{{ 'Delegations' }}
Divide equally
updateDelegate(index, form)"
/>
Add delegate
Clear all delegations
{{ $t('confirm') }}
================================================
FILE: src/components/SpaceProposalBoost.vue
================================================
{{ boosts.length }} boost active
Try out this feature!
Get rewards by voting on this proposal
New boost
Rewards
You're eligible to {{ eligibleBoosts.length }} boosts
Claim
{{ relativeEndTime }}
Boost proposal
Incentivize people to vote with rewards.
Boost
================================================
FILE: src/components/SpaceProposalBoostClaim.vue
================================================
{{ unclaimedBoostsWithReward.length }}
Claim rewards
You can now claim your rewards!
0 && (claimModalOpen = true)
"
>
Claim
{{ unclaimedBoostsWithReward.length }} rewards
{{ formatDuration(timeLeftToClaim) }}
left
Claiming period ended
================================================
FILE: src/components/SpaceProposalBoostClaimModal.vue
================================================
Claim your rewards
You can now claim your rewards!
Claim all
================================================
FILE: src/components/SpaceProposalBoostClaimModalItem.vue
================================================
{{ hasClaimed ? 'Claimed' : 'Reward' }}
{{
formatNumber(
Number(reward),
getNumberFormatter({ maximumFractionDigits: 8 }).value
)
}}
{{ props.boost.token.symbol }}
Boost #{{ boost.id }}
{{ boostNetworkInfo?.name }}
Claim
View
================================================
FILE: src/components/SpaceProposalBoostClaimModalSuccess.vue
================================================
Congratulations!
Your rewards have been claimed.
Tell the world!
Share your victory with friends.
{{ moreToClaim ? 'Claim more' : 'Close' }}
================================================
FILE: src/components/SpaceProposalBoostItem.vue
================================================
Anyone who votes for the winning choice
Who votes
Anyone who votes
with a max
chance of
reward of
Boost #{{ boost.id }}
{{ boostNetworkInfo?.name }}
Claimed {{ claimedAmount }} {{ boost.token.symbol }}
Finalizing winners. Please check back
in {{ minutesUntilEpochEnd }} minutes!
shortly!
Something went wrong. Please try again later.
Oops, you didn't win this time!
View winners
Reward
{{ `${rewardFormatted} ${boost.token.symbol}` }}
View winners
You are eligible
The reward increased due to not enough eligible voters to hit the
{{ boost.strategy.distribution.numWinners }} winner cap for this
boost.
About your boost
Withdraw
You have {{ withdrawalAmount }} {{ boost.token.symbol }} to
withdraw
Withdraw {{ withdrawalAmount }} {{ boost.token.symbol }}
Amount remaining:
{{ withdrawalAmount }} {{ boost.token.symbol }}
Withdrawable in:
{{
formatDuration(
Number(boost.end) - Math.floor(Date.now() / 1000)
)
}}
================================================
FILE: src/components/SpaceProposalBoostItemMenu.vue
================================================
{{ item.text }}
================================================
FILE: src/components/SpaceProposalBoostModalCreate.vue
================================================
Welcome to Boost!
Welcome to Boost!
Boosts are a powerful way to incentivize voting on proposals by
rewarding active participation or specific actions.
{{ c.title }}
{{ c.description }}
Learn more
Get started
================================================
FILE: src/components/SpaceProposalBoostWinnersModal.vue
================================================
Winners
No winners due to no eligible votes.
{{ formattedPrize }}
{{ props.boost.token.symbol }}
Wondering how the winners are chosen? Check out
the docs
Close
================================================
FILE: src/components/SpaceProposalContent.vue
================================================
{{ showFullMarkdownBody ? 'View less' : 'View more' }}
================================================
FILE: src/components/SpaceProposalHeader.vue
================================================
{{ item.text }}
{{ item.text }}
================================================
FILE: src/components/SpaceProposalInformation.vue
================================================
IPFS
#{{ proposal.ipfs.slice(0, 7) }}
{{ $t('proposal.votingSystem') }}
{{ $t(`voting.${proposal.type}.label`) }}
{{ $t('proposal.privacy') }}
{{ $t(`privacy.${proposal.privacy}.label`) }}
{{ $t('proposal.startDate') }}
{{ $t('proposal.endDate') }}
{{ $t('snapshot') }}
{{ formatNumber(Number(proposal.snapshot)) }}
================================================
FILE: src/components/SpaceProposalPage.vue
================================================
================================================
FILE: src/components/SpaceProposalPlugins.vue
================================================
================================================
FILE: src/components/SpaceProposalPluginsSidebar.vue
================================================
================================================
FILE: src/components/SpaceProposalResults.vue
================================================
Finalizing results…
{{ $t('resultsCalculating') }}
{{ $t('resultsError') }}
{{ $t('getHelp') }}
================================================
FILE: src/components/SpaceProposalResultsList.vue
================================================
================================================
FILE: src/components/SpaceProposalResultsListItem.vue
================================================
{{ formatCompactNumber(results.scores[choice.i]) }}
{{ shorten(proposal.symbol || space.symbol, 'symbol') }}
================================================
FILE: src/components/SpaceProposalResultsProgressBar.vue
================================================
================================================
FILE: src/components/SpaceProposalResultsQuorum.vue
================================================
{{ quorumType === 'rejection' ? 'Quorum of rejection' : 'Quorum' }}
{{ formatCompactNumber(totalQuorumScore) }}
/
{{ formatCompactNumber(quorum) }}
================================================
FILE: src/components/SpaceProposalResultsShutter.vue
================================================
================================================
FILE: src/components/SpaceProposalVote.vue
================================================
Your vote is encrypted with Shutter privacy until the proposal ends and
the final score is calculated. You can still change your vote until then.
Oops, we were unable to validate your vote. Please try voting again or
consider contacting our support team on
Help Center
{{ $t('proposal.vote') }}
================================================
FILE: src/components/SpaceProposalVoteApproval.vue
================================================
You voted without selecting a choice. Your vote will be counted as a
blank vote .
{{ shorten(choice, 32) }}
================================================
FILE: src/components/SpaceProposalVoteQuadratic.vue
================================================
================================================
FILE: src/components/SpaceProposalVoteRankedChoice.vue
================================================
#{{ index + 1 }}
{{ proposal.choices[element - 1] }}
================================================
FILE: src/components/SpaceProposalVoteSingleChoice.vue
================================================
{{ shorten(choice, 32) }}
================================================
FILE: src/components/SpaceProposalVotes.vue
================================================
View all
================================================
FILE: src/components/SpaceProposalVotesFilters.vue
================================================
{{ $t('proposal.votesModal.filtersPopover.title') }}
{{ $t('proposal.votesModal.filtersPopover.votingPower') }}
{{ $t('proposal.votesModal.filtersPopover.more') }}
================================================
FILE: src/components/SpaceProposalVotesItem.vue
================================================
{{
`${balanceFormatted} ${shorten(
proposal.symbol || space.symbol,
'symbol'
)}`
}}
{{ $t('receipt') }}
#{{ vote.ipfs.slice(0, 7) }}
#{{ relayerIpfsHash.slice(0, 7) }}
{{ $t('verifyOnSignatorio') }}
================================================
FILE: src/components/SpaceProposalVotesListItemChoice.vue
================================================
Encrypted choice
Invalid choice
{{ format(proposal, vote.choice) }}
================================================
FILE: src/components/SpaceProposalVotesModal.vue
================================================
{{ $t('proposal.votesModal.title') }}
================================================
FILE: src/components/SpaceProposalVotesModalDownload.vue
================================================
{{ $t('proposal.downloadCsvVotes.postDownloadModal.title') }}
{{ $t(`${errorMessageKeyPrefix}.title`) }}
{{ $t(`${errorMessageKeyPrefix}.description`) }}
{{ $t('close') }}
================================================
FILE: src/components/SpaceProposalsNotice.vue
================================================
{{ $t('newSpaceNotice.header') }}
settings
documentation
Help Center
{{ $t('newSpaceNotice.gotIt') }}
================================================
FILE: src/components/SpaceProposalsSearch.vue
================================================
================================================
FILE: src/components/SpaceProposalsSearchFilter.vue
================================================
================================================
FILE: src/components/SpaceSettingsMessageHibernated.vue
================================================
{{ $t('settings.reactivatingHibernatedSpace.information') }}
{{ $t('settings.reactivatingHibernatedSpace.disabledInformation') }}
{{ $t('reactivateSpace') }}
================================================
FILE: src/components/SpaceSidebar.vue
================================================
================================================
FILE: src/components/SpaceSidebarFooter.vue
================================================
================================================
FILE: src/components/SpaceSidebarHeader.vue
================================================
{{ space.name }}
{{
$tc('members', space.followersCount, {
count: formatCompactNumber(space.followersCount)
})
}}
================================================
FILE: src/components/SpaceSidebarMenuThreeDot.vue
================================================
================================================
FILE: src/components/SpaceSidebarNavigation.vue
================================================
{{ $t('proposals.header') }}
{{ $t('delegate.header') }}
{{ $t('delegates.header') }}
{{ $t('treasury.title') }}
{{ $t('about') }}
{{ $t('settings.header') }}
================================================
FILE: src/components/SpaceSidebarSubspaces.vue
================================================
{{ $t('mainspace') }}
{{ mainSpace.name }}
{{ $t('subspaces') }}
{{ subSpace.name }}
================================================
FILE: src/components/SpaceSplitDelegationRow.vue
================================================
updateFormValue(event, 'to')"
/>
updateFormValue(parseFloat(event), 'weight')
"
>
%
$emit('deleteDelegate')"
>
================================================
FILE: src/components/StrategiesBlockWarning.vue
================================================
{{ $t('learnMore') }}
{{ error }}
================================================
FILE: src/components/StrategiesListItem.vue
================================================
================================================
FILE: src/components/TextAutolinker.vue
================================================
================================================
FILE: src/components/TextareaArray.vue
================================================
{{ title || '' }}
================================================
FILE: src/components/TextareaAutosize.vue
================================================
{{ title ?? definition.title }}
================================================
FILE: src/components/TextareaJson.vue
================================================
================================================
FILE: src/components/TheActionbar.vue
================================================
================================================
FILE: src/components/TheFlashNotification.vue
================================================
================================================
FILE: src/components/TheFooter.vue
================================================
{{ $t('newsletter.title') }}
Snapshot
{{ $t(`footerView.${item.text}`) }}
{{ $t('footerView.hiring') }}
{{ $t('footerView.resources') }}
{{ $t(`footerView.${item.text}`) }}
{{ $t('joinCommunity') }}
© {{ yearNow }} Snapshot Labs.
================================================
FILE: src/components/TheLayout.vue
================================================
================================================
FILE: src/components/TheModalNotification.vue
================================================
Discord
================================================
FILE: src/components/TheNavbar.vue
================================================
{{ $t('setup.pendingTransactions') }}:
{{ pendingTransactions.length }}
================================================
FILE: src/components/TheSearchBar.vue
================================================
================================================
FILE: src/components/TheSidebar.vue
================================================
================================================
FILE: src/components/TreasuryAssetsList.vue
================================================
{{ wallet.name }}
{{ $t('treasury.assets.empty') }}
================================================
FILE: src/components/TreasuryAssetsListItem.spec.js
================================================
import { shallowMount } from '@vue/test-utils';
import { describe, afterEach, it, expect } from 'vitest';
import TreasuryAssetsListItem from '@/components/TreasuryAssetsListItem.vue';
import { formatUnits } from '@ethersproject/units';
const { formatCompactNumber, formatNumber } = useIntl();
describe('TreasuryAssetsListItem', () => {
let wrapper;
const findAssetName = () => wrapper.find('[data-testid="asset-name"]');
const findAssetBalance = () => wrapper.find('[data-testid="asset-balance"]');
const findAssetSymbol = () => wrapper.find('[data-testid="asset-symbol"]');
const findAssetQuote = () => wrapper.find('[data-testid="asset-quote"]');
const asset = {
contract_name: 'Wrapped Ether',
contract_ticker_symbol: 'WETH',
contract_address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
contract_decimals: 18,
logo_url: 'https://logos.covalenthq.com/',
balance: '12000045317566025999',
balance_24h: '12000045317566025999',
quote: 2700,
quote_24h: 2800
};
function createComponent(params = {}) {
wrapper = shallowMount(TreasuryAssetsListItem, {
...params,
props: { asset }
});
}
afterEach(() => {
wrapper.unmount();
});
it('should render the correct name', () => {
createComponent();
expect(findAssetName().text()).toContain(asset.contract_name);
});
it('should render the correct asset balance', () => {
createComponent();
expect(findAssetBalance().text()).toContain(
formatCompactNumber(
Number(formatUnits(asset.balance, asset.contract_decimals))
)
);
});
it('should render the correct asset symbol', () => {
createComponent();
expect(findAssetSymbol().text()).toContain(asset.contract_ticker_symbol);
});
it('should render the correct quote', () => {
createComponent();
expect(findAssetQuote().text()).toContain(`$${formatNumber(asset.quote)}`);
});
});
================================================
FILE: src/components/TreasuryAssetsListItem.vue
================================================
{{ asset.contract_name }}
{{
formatCompactNumber(
Number(formatUnits(asset.balance, asset.contract_decimals))
)
}}
{{ asset.contract_ticker_symbol }}
${{ formatNumber(asset.quote) }}
================================================
FILE: src/components/TreasuryWalletsList.spec.js
================================================
import { shallowMount } from '@vue/test-utils';
import { describe, afterEach, it, expect } from 'vitest';
import TreasuryWalletsList from '@/components/TreasuryWalletsList.vue';
import i18n from '@/helpers/i18n';
describe('TreasuryWalletsList', () => {
let wrapper;
const findComponentWalletsBlock = () =>
wrapper.findComponent('[data-testid="treasury-wallets-block"]');
const findComponentWalletsMessageBlock = () =>
wrapper.findComponent('[data-testid="treasury-wallets-message-block"]');
function createComponent(params = {}) {
wrapper = shallowMount(TreasuryWalletsList, {
...params,
global: {
plugins: [i18n]
}
});
}
afterEach(() => {
wrapper.unmount();
});
it('should render wallets block when wallets is not empty', () => {
createComponent({
props: {
wallets: [
{
address: '0x10A19e7eE7d7F8a52822f6817de8ea18204F2e4f',
name: 'Test wallet',
network: '1'
}
]
}
});
expect(findComponentWalletsBlock().exists()).toBe(true);
});
it('should not render wallets block when wallets is empty', () => {
createComponent({
props: {
wallets: []
}
});
expect(findComponentWalletsBlock().exists()).toBe(false);
});
it('should not render message block when wallets is not empty', () => {
createComponent({
props: {
wallets: [
{
address: '0x10A19e7eE7d7F8a52822f6817de8ea18204F2e4f',
name: 'Test wallet',
network: '1'
}
]
}
});
expect(findComponentWalletsMessageBlock().exists()).toBe(false);
});
it('should render message block when wallets is empty', () => {
createComponent({
props: {
wallets: []
}
});
expect(findComponentWalletsMessageBlock().exists()).toBe(true);
});
});
================================================
FILE: src/components/TreasuryWalletsList.vue
================================================
{{ $t('treasury.title') }}
{{ $t('treasury.wallets.empty') }}
{{ $t('treasury.wallets.addTreasury') }}
================================================
FILE: src/components/TreasuryWalletsListItem.spec.js
================================================
import { mount, RouterLinkStub } from '@vue/test-utils';
import { describe, afterEach, it, expect } from 'vitest';
import TreasuryWalletsListItem from '@/components/TreasuryWalletsListItem.vue';
import AvatarUser from './AvatarUser.vue';
import BaseLink from './BaseLink.vue';
import { shorten, explorerUrl } from '@/helpers/utils';
describe('TreasuryWalletsListItem', () => {
let wrapper;
const findWalletName = () => wrapper.find('[data-testid="wallet-name"]');
const findWalletEnsAddress = () =>
wrapper.find('[data-testid="wallet-ens-address"]');
const findComponentBaseAvatar = () => wrapper.findComponent(AvatarUser);
const findComponentBaseLinkOne = () => wrapper.findAllComponents(BaseLink)[0];
const findComponentBaseLinkTwo = () => wrapper.findAllComponents(BaseLink)[1];
const wallet = {
name: 'Test Wallet',
address: '0x0000000000000000000000000000000000000000',
network: 1
};
function createComponent(params = {}) {
wrapper = mount(TreasuryWalletsListItem, {
...params,
global: {
stubs: { AvatarUser, BaseLink, RouterLink: RouterLinkStub }
},
props: {
wallet,
ensAddress: 'test.eth'
}
});
}
afterEach(() => {
wrapper.unmount();
});
it('should render base avatar using wallet address', () => {
createComponent();
expect(findComponentBaseAvatar().props('address')).toBe(wallet.address);
});
it('should render wallet name', () => {
createComponent();
expect(findWalletName().text()).toBe(wallet.name);
});
it('should render shortened wallet address', () => {
createComponent();
expect(findComponentBaseLinkOne().text()).toContain(
shorten(wallet.address)
);
});
it('should render external link pointing to explorer', () => {
createComponent();
expect(findComponentBaseLinkTwo().props('link')).toBe(
explorerUrl(wallet.network, wallet.address)
);
});
it('should render wallet ens address', () => {
createComponent();
expect(findWalletEnsAddress().text()).toBe('test.eth');
});
it('should not render ens address when not set', async () => {
createComponent();
await wrapper.setProps({ ensAddress: undefined });
expect(findWalletEnsAddress().exists()).toBe(false);
});
it('should render link to the wallet route', () => {
createComponent();
expect(findComponentBaseLinkOne().props('link')).toMatchObject({
name: 'spaceTreasury',
params: { wallet: wallet.address }
});
});
});
================================================
FILE: src/components/TreasuryWalletsListItem.vue
================================================
{{ wallet.name }}
{{ ensAddress }}
{{ shorten(wallet.address) }}
${{ formatNumber(walletQuote.quote) }}
================================================
FILE: src/components/Tune/TuneBlock.vue
================================================
================================================
FILE: src/components/Tune/TuneBlockFooter.vue
================================================
================================================
FILE: src/components/Tune/TuneBlockHeader.vue
================================================
{{ title }}
{{ subTitle }}
================================================
FILE: src/components/Tune/TuneButton.story.vue
================================================
Confirm
================================================
FILE: src/components/Tune/TuneButton.vue
================================================
================================================
FILE: src/components/Tune/TuneButtonSelect.story.vue
================================================
================================================
FILE: src/components/Tune/TuneButtonSelect.vue
================================================
{{ label || definition.title }}
{{ modelValue }}
================================================
FILE: src/components/Tune/TuneCheckbox.story.vue
================================================
================================================
FILE: src/components/Tune/TuneCheckbox.vue
================================================
================================================
FILE: src/components/Tune/TuneCombobox.story.vue
================================================
================================================
FILE: src/components/Tune/TuneCombobox.vue
================================================
{{ label || definition.title }}
================================================
FILE: src/components/Tune/TuneComboboxMultiple.story.vue
================================================
================================================
FILE: src/components/Tune/TuneComboboxMultiple.vue
================================================
{{ label || definition.title }}
================================================
FILE: src/components/Tune/TuneErrorInput.story.vue
================================================
================================================
FILE: src/components/Tune/TuneErrorInput.vue
================================================
{{ error }}
================================================
FILE: src/components/Tune/TuneForm.story.vue
================================================
Show errors
================================================
FILE: src/components/Tune/TuneForm.vue
================================================
================================================
FILE: src/components/Tune/TuneIconHint.story.vue
================================================
================================================
FILE: src/components/Tune/TuneIconHint.vue
================================================
================================================
FILE: src/components/Tune/TuneInput.story.vue
================================================
================================================
FILE: src/components/Tune/TuneInput.vue
================================================
{{ label || definition.title }}
================================================
FILE: src/components/Tune/TuneInputDuration.story.vue
================================================
================================================
FILE: src/components/Tune/TuneInputDuration.vue
================================================
{{ label || definition?.title }}
================================================
FILE: src/components/Tune/TuneInputSocial.vue
================================================
emit('update:modelValue', value)"
>
================================================
FILE: src/components/Tune/TuneInputUrl.vue
================================================
emit('update:modelValue', input)"
>
================================================
FILE: src/components/Tune/TuneLabelInput.story.vue
================================================
Label
================================================
FILE: src/components/Tune/TuneLabelInput.vue
================================================
================================================
FILE: src/components/Tune/TuneListbox.story.vue
================================================
================================================
FILE: src/components/Tune/TuneListbox.vue
================================================
{{ label || definition?.title }}
{{ selectedItem?.name || selectedItem?.value }}
Select
{{ item?.name || item.value }}
================================================
FILE: src/components/Tune/TuneListboxMultiple.story.vue
================================================
================================================
FILE: src/components/Tune/TuneListboxMultiple.vue
================================================
{{ label || definition?.title }}
{{ placeholder || definition?.examples?.[0] }}
{{ selectedItems.map(item => item?.name || item.value).join(', ') }}
{{ item?.name || item.value }}
================================================
FILE: src/components/Tune/TuneLoadingSpinner.story.vue
================================================
================================================
FILE: src/components/Tune/TuneLoadingSpinner.vue
================================================
================================================
FILE: src/components/Tune/TuneMenu.story.vue
================================================
================================================
FILE: src/components/Tune/TuneMenu.vue
================================================
{{ selected }}
================================================
FILE: src/components/Tune/TuneModal.story.vue
================================================
Open modal
Lorem ipsum dolor sit amet consectetur adipisicing elit. Distinctio illo
expedita libero, eaque iste aliquam praesentium necessitatibus ipsum
impedit hic temporibus officia omnis deleniti, at sint, doloremque atque
sit id?
# TuneModal
The TuneModal component provides a modal dialog box that appears on top of the current page, dimming the background content and prompting the user for an action. This component uses the @headlessui/vue package for creating accessible dialogs and transitions.
### Props
- **open**: a boolean value that determines whether the modal should be shown or hidden. When true, the modal is displayed; otherwise, it is hidden. This property is required.
- **title**: a string value that specifies the title of the modal dialog box.
### Slots
The TuneModal component provides two slots for inserting content:
- **Default slot**: Use this slot to add the main content of the modal dialog box. You can include any valid HTML markup, such as text, images, forms, or other components.
- **Footer slot**: Use this slot to add any additional buttons, links, or other UI elements to the bottom of the modal dialog box.
### Emits
- **close**: an event emitted when the user closes the modal dialog box. This event does not contain any data.
================================================
FILE: src/components/Tune/TuneModal.vue
================================================
================================================
FILE: src/components/Tune/TuneModalDescription.vue
================================================
================================================
FILE: src/components/Tune/TuneModalIndicator.vue
================================================
================================================
FILE: src/components/Tune/TuneModalTitle.vue
================================================
================================================
FILE: src/components/Tune/TunePopover.story.vue
================================================
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet
aperiam vitae eveniet vel ex praesentium nam quis deserunt porro
harum. Magni maiores ipsa ea? Rem, quidem. Quas sunt ducimus rerum.
================================================
FILE: src/components/Tune/TunePopover.vue
================================================
{{ label }}
================================================
FILE: src/components/Tune/TuneRadio.story.vue
================================================
================================================
FILE: src/components/Tune/TuneRadio.vue
================================================
{{ hint || definition.description }}
================================================
FILE: src/components/Tune/TuneSelect.vue
================================================
{{ item.label }}
================================================
FILE: src/components/Tune/TuneSwitch.story.vue
================================================
================================================
FILE: src/components/Tune/TuneSwitch.vue
================================================
emit('update:modelValue', value)"
>
{{ label || definition?.title }}
{{ label || definition?.title }}
================================================
FILE: src/components/Tune/TuneTag.story.vue
================================================
================================================
FILE: src/components/Tune/TuneTag.vue
================================================
{{ label }}
================================================
FILE: src/components/Tune/TuneTextarea.story.vue
================================================
================================================
FILE: src/components/Tune/TuneTextarea.vue
================================================
{{ label || definition?.title }}
================================================
FILE: src/components/Tune/TuneTextareaArray.story.vue
================================================
================================================
FILE: src/components/Tune/TuneTextareaArray.vue
================================================
================================================
FILE: src/components/Tune/TuneTextareaJson.story.vue
================================================
================================================
FILE: src/components/Tune/TuneTextareaJson.vue
================================================
================================================
FILE: src/components/Tune/_Form/FormArray.vue
================================================
{{ definition?.title }}
Add
================================================
FILE: src/components/Tune/_Form/FormBoolean.vue
================================================
================================================
FILE: src/components/Tune/_Form/FormNumber.vue
================================================
================================================
FILE: src/components/Tune/_Form/FormString.vue
================================================
================================================
FILE: src/components/Ui/UiCollapsible.vue
================================================
================================================
FILE: src/components/Ui/UiCollapsibleContent.vue
================================================
================================================
FILE: src/components/Ui/UiCollapsibleText.vue
================================================
{{ text }}
================================================
FILE: src/components/Ui/UiInput.vue
================================================
{{ error || '' }}
Quick Fix
================================================
FILE: src/components/Ui/UiSelect.vue
================================================
================================================
FILE: src/composables/useAccount.ts
================================================
import snapshot from '@snapshot-labs/snapshot.js';
import { ERC20ABI } from '@/helpers/constants';
import getProvider from '@snapshot-labs/snapshot.js/src/utils/provider';
async function getERC20Account(
provider: any,
account: string,
token: string,
chainId: string,
contract: string
) {
const multi = new snapshot.utils.Multicaller(chainId, provider, ERC20ABI, {});
multi.call('balance', token, 'balanceOf', [account]);
multi.call('allowance', token, 'allowance', [account, contract]);
return await multi.execute();
}
export function useAccount() {
const { web3Account } = useWeb3();
const account = ref<{
balance?: string;
allowance?: string;
}>({});
const updatingAccount = ref(false);
async function updateAccount(
token: string,
chainId: string,
contract: string
) {
account.value = {};
updatingAccount.value = true;
const broviderUrl = import.meta.env.VITE_BROVIDER_URL;
const provider = getProvider(chainId, { broviderUrl });
try {
account.value = await getERC20Account(
provider,
web3Account.value,
token,
chainId,
contract
);
} catch (e) {
console.log('Error getting account', e);
} finally {
updatingAccount.value = false;
}
}
return {
account,
updatingAccount,
updateAccount
};
}
================================================
FILE: src/composables/useAliasAction.ts
================================================
/**
* Alias addresses/wallets are used to reduce the need for signing messages manually, e.g. each time a user wants to join a space.
* An alias is a randomly generated wallet, of which the private key is stored in the browser's local storage.
* The user only needs to sign a message once, to "register" the respective alias address on the hub. All following messages can be signed
* by the alias wallet, without requiring the user's approval. This leads to much better UX, at the cost of less security.
* If the private key is removed from local storage, a new one will be created and registered.
*/
import { lsGet, lsSet } from '@/helpers/utils';
import { Wallet } from '@ethersproject/wallet';
import { getInstance } from '@snapshot-labs/lock/plugins/vue3';
import { getDefaultProvider, Provider } from '@ethersproject/providers';
import { ALIASES_QUERY } from '@/helpers/queries';
import client from '@/helpers/clientEIP712';
const aliases = ref(lsGet('aliases') || {});
const isValidAlias = ref(false);
export function useAliasAction() {
const { notify } = useFlashNotification();
const { t } = useI18n();
const { web3 } = useWeb3();
const auth = getInstance();
const { apolloQuery } = useApolloQuery();
const userAlias = computed(() => {
return aliases.value?.[web3.value.account];
});
const aliasWallet: any = computed(() => {
const provider: Provider = getDefaultProvider();
return userAlias.value ? new Wallet(userAlias.value, provider) : null;
});
async function checkAlias() {
if (aliasWallet.value?.address && web3.value?.account) {
const alias = await apolloQuery(
{
query: ALIASES_QUERY,
variables: {
address: web3.value.account,
alias: aliasWallet.value.address,
created_gt: Math.floor(Date.now() / 1000) - 30 * 60 * 60 * 24
}
},
'aliases'
);
isValidAlias.value =
alias[0]?.address === web3.value.account &&
alias[0]?.alias === aliasWallet.value.address;
}
}
async function setAlias() {
const rndWallet = Wallet.createRandom();
aliases.value = {
...aliases.value,
[web3.value.account]: rndWallet.privateKey
};
lsSet('aliases', aliases.value);
if (aliasWallet.value?.address) {
await client.alias(auth.web3, web3.value.account, {
alias: aliasWallet.value.address
});
}
await checkAlias();
}
const loading = ref(false);
async function actionWithAlias(action: any) {
loading.value = true;
try {
await checkAlias();
if (aliasWallet.value && isValidAlias.value) {
return await action();
}
await setAlias();
return await action();
} catch (e) {
console.error(e);
loading.value = false;
notify(['red', t('notify.somethingWentWrong')]);
} finally {
loading.value = false;
}
}
return {
setAlias,
aliasWallet,
isValidAlias,
checkAlias,
actionWithAlias,
actionLoading: computed(() => loading.value)
};
}
================================================
FILE: src/composables/useApolloQuery.ts
================================================
import cloneDeep from 'lodash/cloneDeep';
import { apolloClient } from '@/helpers/apollo';
import { ensApolloClient } from '@/helpers/ens';
export function useApolloQuery() {
const loading = ref(false);
async function apolloQuery(options, path = '') {
try {
loading.value = true;
const response = await apolloClient.query(options);
loading.value = false;
return cloneDeep(!path ? response.data : response.data[path]);
} catch (error) {
loading.value = false;
console.log(error);
}
}
async function ensApolloQuery(options) {
try {
loading.value = true;
const response = await ensApolloClient.query(options);
loading.value = false;
return response.data;
} catch (error) {
loading.value = false;
console.log(error);
}
}
return {
apolloQuery,
ensApolloQuery,
queryLoading: computed(() => loading.value)
};
}
================================================
FILE: src/composables/useApp.ts
================================================
import { getInstance } from '@snapshot-labs/lock/plugins/vue3';
import domains from '@/../snapshot-spaces/spaces/domains.json';
import aliases from '@/../snapshot-spaces/spaces/aliases.json';
import { getInjected } from '@snapshot-labs/lock/src/utils';
// import { useStorage } from '@vueuse/core';
const domainName = window.location.hostname;
const env = import.meta.env.VITE_ENV;
let domain = domains[domainName];
if (env === 'develop') {
domain = import.meta.env.VITE_VIEW_AS_SPACE ?? domain;
}
const domainAlias = Object.keys(aliases).find(
alias => aliases[alias] === domain
);
const isReady = ref(false);
// only affects small screens
const showSidebar = ref(false);
export function useApp() {
const { loadLocale } = useI18n();
const { getSkin } = useSkin();
const { login } = useWeb3();
// const termsAccepted = useStorage('snapshot.termsAccepted', false);
function connectWallet() {
const auth = getInstance();
// if (!termsAccepted.value) return;
if (window?.parent === window)
// Auto connect if previous session was connected
auth.getConnector().then(connector => {
if (connector) return login(connector);
});
// Auto connect with gnosis-connector when gnosis safe is detected
login('gnosis');
const injected = computed(() => getInjected());
// edge case if MM and CBW are both installed
if (injected.value?.id === 'metamask') return;
// Auto connect when coinbase wallet is detected
if (injected.value?.id === 'coinbase') return login('injected');
}
async function init() {
await loadLocale();
await getSkin(domain);
isReady.value = true;
connectWallet();
}
return {
domain,
domainAlias,
env,
isReady,
init,
showSidebar
};
}
================================================
FILE: src/composables/useBalances.ts
================================================
import { formatUnits } from '@ethersproject/units';
import { getBalances, GetBalancesResponse } from '@/helpers/alchemy';
import {
ETH_CONTRACT,
COINGECKO_ASSET_PLATFORMS,
COINGECKO_BASE_ASSETS,
CHAIN_CURRENCIES,
ChainCurrency
} from '@/helpers/constants';
const COINGECKO_API_URL = 'https://api.coingecko.com/api/v3/simple';
const COINGECKO_PARAMS = '&vs_currencies=usd&include_24hr_change=true';
export function useBalances() {
const tokens = ref([]);
const loading = ref(true);
const loaded = ref(false);
async function callCoinGecko(apiUrl: string): Promise {
const response = await fetch(apiUrl);
return response.json();
}
async function getCoins(
assetPlatform: string,
baseToken: string,
contractAddresses: string[]
): Promise> {
const [baseTokenData, tokenData] = await Promise.all([
callCoinGecko(
`${COINGECKO_API_URL}/price?ids=${baseToken}${COINGECKO_PARAMS}`
),
callCoinGecko(
`${COINGECKO_API_URL}/token_price/${assetPlatform}?contract_addresses=${contractAddresses.join(
','
)}${COINGECKO_PARAMS}`
)
]);
return {
[ETH_CONTRACT]: baseTokenData[baseToken],
...tokenData
};
}
function filterValidTokens(
data: GetBalancesResponse,
baseToken: ChainCurrency
): GetBalancesResponse {
return data.filter(
asset =>
formatUnits(asset.tokenBalance, asset.decimals) !== '0.0' ||
asset.symbol === baseToken.symbol
);
}
function getCoinGeckoConfig(networkId: string): {
coingeckoAssetPlatform: string;
coingeckoBaseAsset: string;
} {
return {
coingeckoAssetPlatform: COINGECKO_ASSET_PLATFORMS[networkId],
coingeckoBaseAsset: COINGECKO_BASE_ASSETS[networkId]
};
}
async function fetchCoinPrices(
coingeckoAssetPlatform: string,
coingeckoBaseAsset: string,
tokensWithBalance: GetBalancesResponse
): Promise> {
if (!coingeckoBaseAsset || !coingeckoAssetPlatform) return {};
const contractAddresses = tokensWithBalance
.filter(asset => asset.contractAddress !== ETH_CONTRACT)
.map(token => token.contractAddress);
return getCoins(
coingeckoAssetPlatform,
coingeckoBaseAsset,
contractAddresses
);
}
function mapTokenValues(
tokensWithBalance: GetBalancesResponse,
coins: Record
): GetBalancesResponse {
return tokensWithBalance.map(asset => {
const coinData = coins[asset.contractAddress];
if (!coinData) return asset;
const price = coinData.usd || 0;
const change = coinData.usd_24h_change || 0;
const value =
parseFloat(formatUnits(asset.tokenBalance, asset.decimals)) * price;
return {
...asset,
price,
change,
value
};
});
}
async function loadBalances(
address: string,
networkId: string
): Promise {
try {
loading.value = true;
tokens.value = [];
const baseToken = CHAIN_CURRENCIES[networkId];
const data = await getBalances(address, Number(networkId), baseToken);
const tokensWithBalance = filterValidTokens(data, baseToken);
const { coingeckoAssetPlatform, coingeckoBaseAsset } =
getCoinGeckoConfig(networkId);
const coins = await fetchCoinPrices(
coingeckoAssetPlatform,
coingeckoBaseAsset,
tokensWithBalance
);
tokens.value = mapTokenValues(tokensWithBalance, coins);
loaded.value = true;
} catch (error) {
console.error('Error loading balances:', error);
} finally {
loading.value = false;
}
}
const assetsMap = computed(
() => new Map(tokens.value.map(asset => [asset.contractAddress, asset]))
);
return { loading, loaded, tokens, assetsMap, loadBalances };
}
================================================
FILE: src/composables/useBoost.ts
================================================
import { claimTokens } from '@/helpers/boost';
import { BoostSubgraph } from '@/helpers/boost/types';
import { Proposal, ExtendedSpace } from '@/helpers/interfaces';
import { TWO_WEEKS } from '@/helpers/constants';
import { getVouchers } from '@/helpers/boost/api';
import { toChecksumAddress } from '@/helpers/utils';
import { getInstance } from '@snapshot-labs/lock/plugins/vue3';
export function useBoost() {
const auth = getInstance();
const { web3Account, web3 } = useWeb3();
const { changeNetwork } = useChangeNetwork();
const loadingClaim = ref(false);
function sanitizeBoosts(
boosts: BoostSubgraph[],
proposals: Proposal[],
space: ExtendedSpace
) {
return boosts.filter(boost => {
if (
!space.boost.bribeEnabled &&
boost.strategy.eligibility.type === 'bribe'
) {
return false;
}
if (
Number(boost.start) !==
proposals.find(p => p.id === boost.strategy.proposal)?.end
) {
return false;
}
if (Number(boost.end) - Number(boost.start) !== TWO_WEEKS) {
return false;
}
return true;
});
}
async function loadVouchers(boosts: BoostSubgraph[], proposalId: string) {
try {
const vouchers = await getVouchers(proposalId, web3Account.value, boosts);
return vouchers;
} catch (e) {
console.error('Get vouchers error:', e);
}
}
async function handleClaim(boost: BoostSubgraph, proposalId: string) {
if (boost.chainId !== web3.value.network.chainId.toString()) {
await changeNetwork(boost.chainId);
handleClaim(boost, proposalId);
}
try {
loadingClaim.value = true;
const response = await loadVouchers([boost], proposalId);
if (!response) throw new Error('Failed to get vouchers');
const voucher = response[0];
const signature = voucher.signature;
const chainId = voucher.chain_id;
const tx = await claimTokens(
auth.web3,
chainId,
{
boostId: voucher.boost_id,
recipient: toChecksumAddress(web3Account.value),
amount: voucher.reward
},
signature
);
await tx.wait();
} catch (e: any) {
console.error('Claim error:', e);
} finally {
loadingClaim.value = false;
}
}
return {
sanitizeBoosts,
loadVouchers,
handleClaim,
loadingClaim
};
}
================================================
FILE: src/composables/useChangeNetwork.ts
================================================
import networks from '@snapshot-labs/snapshot.js/src/networks.json';
import { sleep } from '@snapshot-labs/snapshot.js/src/utils';
export function useChangeNetwork() {
const changingNetwork = ref(false);
async function changeNetwork(network: string) {
changingNetwork.value = true;
await window.ethereum?.request({
method: 'wallet_switchEthereumChain',
params: [
{
chainId: `0x${Number(networks[network].chainId).toString(16)}`
}
]
});
await sleep(1000);
changingNetwork.value = false;
}
return {
changingNetwork,
changeNetwork
};
}
================================================
FILE: src/composables/useClient.ts
================================================
import clientEIP712 from '@/helpers/clientEIP712';
import { getInstance } from '@snapshot-labs/lock/plugins/vue3';
export function useClient() {
const { t } = useI18n();
const { notify } = useFlashNotification();
const { notifyModal } = useModalNotification();
const { isGnosisSafe } = useGnosis();
const { web3 } = useWeb3();
const auth = getInstance();
const route = useRoute();
const DEFINED_APP = (route?.query.app as string) || 'snapshot';
const isSending = ref(false);
function errorNotification(description: string) {
notify([
'red',
description ? `Oops, ${description}` : t('notify.somethingWentWrong')
]);
notifyModal('warning', description);
}
async function send(space: { id: string }, type: string, payload: any) {
isSending.value = true;
try {
return await sendEIP712(space, type, payload);
} catch (e: any) {
errorNotification(e?.error_description || e?.message || '');
return e;
} finally {
isSending.value = false;
}
}
async function sendEIP712(space: { id: string }, type: string, payload: any) {
let plugins = {};
const client = clientEIP712;
if (
payload.metadata?.plugins &&
Object.keys(payload.metadata?.plugins).length !== 0
)
plugins = payload.metadata.plugins;
if (type === 'create-proposal') {
return client.proposal(auth.web3, web3.value.account, {
space: space.id,
type: payload.type,
title: payload.name,
body: payload.body,
discussion: payload.discussion,
choices: payload.choices,
labels: [],
start: payload.start,
end: payload.end,
snapshot: payload.snapshot,
privacy: payload.privacy,
plugins: JSON.stringify(plugins),
app: DEFINED_APP
});
} else if (type === 'update-proposal') {
return client.updateProposal(auth.web3, web3.value.account, {
proposal: payload.id,
space: space.id,
type: payload.type,
title: payload.name,
body: payload.body,
discussion: payload.discussion,
choices: payload.choices,
labels: payload.labels,
privacy: payload.privacy,
plugins: JSON.stringify(plugins)
});
} else if (type === 'vote') {
return client.vote(auth.web3, web3.value.account, {
space: space.id,
proposal: payload.proposal.id,
type: payload.proposal.type,
choice: payload.choice,
privacy: payload.privacy,
app: DEFINED_APP,
reason: payload.reason
});
} else if (type === 'delete-proposal') {
return client.cancelProposal(auth.web3, web3.value.account, {
space: space.id,
proposal: payload.proposal.id
});
} else if (type === 'settings') {
return client.space(auth.web3, web3.value.account, {
space: space.id,
settings: JSON.stringify(payload)
});
} else if (type === 'delete-space') {
return client.deleteSpace(auth.web3, web3.value.account, {
space: space.id
});
} else if (type === 'set-statement') {
return client.statement(auth.web3, web3.value.account, {
space: space.id,
about: payload.about,
statement: payload.statement,
discourse: payload.discourse,
network: payload.network,
status: payload.status
});
} else if (type === 'flag-proposal') {
return client.flagProposal(auth.web3, web3.value.account, {
space: space.id,
proposal: payload.proposal.id
});
}
}
return { send, isSending, isGnosisSafe };
}
================================================
FILE: src/composables/useCopy.ts
================================================
import { useClipboard } from '@vueuse/core';
export function useCopy() {
const { t } = useI18n();
const { copy, copied } = useClipboard();
const { notify } = useFlashNotification();
function copyToClipboard(text) {
copy(text);
if (copied) notify(t('notify.copied'));
}
return { copyToClipboard };
}
================================================
FILE: src/composables/useDelegate.ts
================================================
import { useEns } from './useEns';
import getProvider from '@snapshot-labs/snapshot.js/src/utils/provider';
import { contractAddress } from '@/helpers/delegation';
import { formatBytes32String } from '@ethersproject/strings';
import {
sendTransaction,
sleep,
SNAPSHOT_SUBGRAPH_URL
} from '@snapshot-labs/snapshot.js/src/utils';
import { getInstance } from '@snapshot-labs/lock/plugins/vue3';
export function useDelegate() {
const abi = ['function setDelegate(bytes32 id, address delegate)'];
const auth = getInstance();
const { notify } = useFlashNotification();
const {
createPendingTransaction,
updatePendingTransaction,
removePendingTransaction
} = useTxStatus();
const { validEnsTlds } = useEns();
const { t } = useI18n();
const { web3 } = useWeb3();
const loading = ref(false);
const networkKey = computed(() => web3.value.network.key);
const networkSupportsDelegate = computed(
() => SNAPSHOT_SUBGRAPH_URL[networkKey.value] !== undefined
);
async function delegateTo(address, spaceId = '') {
loading.value = true;
const txPendingId = createPendingTransaction();
try {
let ethAddress = address;
if (validEnsTlds.includes(address.split('.').pop())) {
const networkId = import.meta.env.VITE_DEFAULT_NETWORK;
const broviderUrl = import.meta.env.VITE_BROVIDER_URL;
const provider = getProvider(networkId, { broviderUrl });
ethAddress = await provider.resolveName(address);
}
const tx = await sendTransaction(
auth.web3,
contractAddress,
abi,
'setDelegate',
[formatBytes32String(spaceId), ethAddress]
);
notify(t('notify.transactionSent'));
updatePendingTransaction(txPendingId, { hash: tx.hash });
loading.value = false;
const receipt = await tx.wait();
console.log('Receipt', receipt);
await sleep(3e3);
notify(t('notify.delegationSuccess'));
} catch (e) {
notify(['red', t('notify.somethingWentWrong')]);
console.log(e);
} finally {
loading.value = false;
removePendingTransaction(txPendingId);
}
}
return {
delegateTo,
delegationLoading: computed(() => loading.value),
networkSupportsDelegate,
networkKey
};
}
================================================
FILE: src/composables/useDelegates.ts
================================================
import { LEADERBOARD_QUERY } from '@/helpers/queries';
import { getInstance } from '@snapshot-labs/lock/plugins/vue3';
import { getAddress } from '@ethersproject/address';
import { DelegateWithPercent, ExtendedSpace } from '@/helpers/interfaces';
import {
DelegationTypes,
setupDelegation as getDelegationAdapter
} from '@/helpers/delegationV2';
type DelegatesStats = Record;
const DELEGATES_LIMIT = 18;
export function useDelegates(space: ExtendedSpace) {
const auth = getInstance();
const { resolveName } = useResolveName();
const { apolloQuery } = useApolloQuery();
const { reader, writer } = getDelegationAdapter(space, auth);
const delegates = ref([]);
const delegate = ref(null);
const isLoadingDelegate = ref(false);
const isLoadingDelegates = ref(false);
const isLoadingMoreDelegates = ref(false);
const hasDelegatesLoadFailed = ref(false);
const isLoadingDelegatingTo = ref(false);
const isLoadingDelegateBalance = ref(false);
const hasMoreDelegates = ref(false);
const delegatesStats = ref({});
const hasDelegationPortal =
space.delegationPortal.delegationType === DelegationTypes.COMPOUND ||
(space.delegationPortal.delegationType ===
DelegationTypes.SPLIT_DELEGATION &&
space.strategies.some(
({ name }) => name === DelegationTypes.SPLIT_DELEGATION
));
async function fetchDelegateBatch(orderBy: string, skip = 0) {
return reader.getDelegates(DELEGATES_LIMIT, skip, orderBy);
}
async function loadDelegates(orderBy: string) {
if (isLoadingDelegates.value) return;
isLoadingDelegates.value = true;
hasDelegatesLoadFailed.value = false;
try {
const response = await fetchDelegateBatch(orderBy);
delegates.value = response;
loadStats(response.map(d => d.id));
hasMoreDelegates.value = response.length === DELEGATES_LIMIT;
} catch (e) {
console.error(e);
hasDelegatesLoadFailed.value = true;
} finally {
isLoadingDelegates.value = false;
}
}
async function fetchMoreDelegates(orderBy: string) {
if (!delegates.value.length || isLoadingMoreDelegates.value) return;
isLoadingMoreDelegates.value = true;
hasDelegatesLoadFailed.value = false;
try {
const response = await fetchDelegateBatch(
orderBy,
delegates.value.length
);
loadStats(response.map(d => d.id));
delegates.value = [...delegates.value, ...response];
hasMoreDelegates.value = response.length === DELEGATES_LIMIT;
} catch (e) {
console.error(e);
hasDelegatesLoadFailed.value = true;
} finally {
isLoadingMoreDelegates.value = false;
}
}
async function loadDelegate(addressOrEns: string) {
if (isLoadingDelegate.value) return;
hasDelegatesLoadFailed.value = false;
isLoadingDelegate.value = true;
delegate.value = null;
try {
const resolvedAddress = await resolveName(addressOrEns);
if (!resolvedAddress) return;
const response = await reader.getDelegate(getAddress(resolvedAddress));
loadStats([response.id]);
delegate.value = response;
} catch (e) {
console.error(e);
hasDelegatesLoadFailed.value = true;
} finally {
isLoadingDelegate.value = false;
}
}
async function loadDelegateBalance(id: string) {
try {
isLoadingDelegateBalance.value = true;
return await reader.getBalance(id.toLowerCase());
} catch (e) {
console.error(e);
} finally {
isLoadingDelegateBalance.value = false;
}
}
async function setDelegates(
addresses: string[],
ratio?: number[],
expirationTimestamp?: number
) {
return writer.sendSetDelegationTx(addresses, ratio, expirationTimestamp);
}
async function clearDelegations() {
if (!writer.sendClearDelegationsTx) {
throw new Error('Clear delegations not supported');
}
return writer.sendClearDelegationsTx();
}
async function fetchDelegatingTo(address: string) {
if (!address) return;
isLoadingDelegatingTo.value = true;
try {
return await reader.getDelegatingTo(address);
} catch (e) {
console.error(e);
} finally {
isLoadingDelegatingTo.value = false;
}
}
async function loadStats(addresses: string[]) {
const leaderboards = await apolloQuery(
{
query: LEADERBOARD_QUERY,
variables: {
space: space.id,
user_in: addresses
}
},
'leaderboards'
);
leaderboards.forEach(leaderboard => {
delegatesStats.value[leaderboard.user] = {
votes: leaderboard.votesCount,
proposals: leaderboard.proposalsCount
};
});
}
return {
isLoadingDelegate,
isLoadingDelegates,
isLoadingMoreDelegates,
hasDelegatesLoadFailed,
hasDelegationPortal,
isLoadingDelegateBalance,
isLoadingDelegatingTo,
hasMoreDelegates,
delegate,
delegates,
delegatesStats,
loadDelegate,
loadDelegates,
fetchMoreDelegates,
setDelegates,
clearDelegations,
loadDelegateBalance,
fetchDelegatingTo
};
}
================================================
FILE: src/composables/useEmailFetchClient.ts
================================================
import sign, { DataType } from '@/helpers/sign';
import { createFetch } from '@vueuse/core';
import { getInstance } from '@snapshot-labs/lock/plugins/vue3';
const SubscribeSchema: DataType = {
Subscribe: [
{ name: 'address', type: 'address' },
{ name: 'email', type: 'string' }
]
};
const UpdateSubscriptionsSchema: DataType = {
Subscriptions: [
{ name: 'address', type: 'address' },
{ name: 'email', type: 'string' },
{ name: 'subscriptions', type: 'string[]' }
]
};
const useEmailFetch = createFetch({
baseUrl: import.meta.env.VITE_ENVELOP_URL,
options: {
headers: {
'Content-Type': 'application/json'
}
}
});
export function useEmailFetchClient() {
const { web3Account } = useWeb3();
function plainSign(message, typesSchema) {
const { web3 } = getInstance();
return sign(web3, web3Account.value, message, typesSchema);
}
const fetchSubscriptionsDetails = body => {
return useEmailFetch('/subscriber').post(body).json();
};
const subscribeWithEmail = async unsignedParams => {
let signature;
try {
signature = await plainSign(unsignedParams, SubscribeSchema);
} catch (error: any) {
return {
error: { value: 'sign_error' },
data: { value: null }
};
}
const body = {
method: 'snapshot.subscribe',
params: {
...unsignedParams,
signature
}
};
return useEmailFetch('/').post(body).json();
};
const updateEmailSubscriptions = async unsignedParams => {
let signature;
try {
signature = await plainSign(unsignedParams, UpdateSubscriptionsSchema);
} catch (error: any) {
return {
error: { value: 'sign_error' },
data: { value: null }
};
}
const body = {
method: 'snapshot.update',
params: {
...unsignedParams,
signature
}
};
return useEmailFetch('/').post(body).json();
};
return {
fetchSubscriptionsDetails,
subscribeWithEmail,
updateEmailSubscriptions
};
}
================================================
FILE: src/composables/useEmailSubscription.ts
================================================
import { createSharedComposable } from '@vueuse/core';
const subscriptionTypes = ['summary', 'newProposal', 'closedProposal'] as const;
type SubscriptionType = (typeof subscriptionTypes)[number];
type SubscriptionStatus = 'NOT_SUBSCRIBED' | 'VERIFIED' | 'UNVERIFIED';
function useEmailSubscriptionComposable() {
const { web3Account } = useWeb3();
const {
fetchSubscriptionsDetails,
subscribeWithEmail,
updateEmailSubscriptions
} = useEmailFetchClient();
const userState = ref('NOT_SUBSCRIBED');
const error = ref('');
const initialized = ref(false);
const loading = ref(false);
const apiSubscriptions = ref([]);
const clientSubscriptions = computed({
get() {
return subscriptionTypes.reduce(
(acc, type) => {
acc[type] = apiSubscriptions.value.includes(type);
return acc;
},
{} as Record
);
},
set(value) {
apiSubscriptions.value = Object.entries(value)
.map(([key, value]) => (value ? key : undefined))
.filter(Boolean)
.map(key => key as SubscriptionType);
}
});
const loadEmailSubscriptions = async () => {
loading.value = true;
const { error: err, data } = await fetchSubscriptionsDetails({
address: web3Account.value
});
if (err.value) {
loading.value = false;
return;
}
const { status: usrState, subscriptions } = data.value;
userState.value = usrState;
apiSubscriptions.value = subscriptions || [];
loading.value = false;
initialized.value = true;
};
const subscribe = async (email: string) => {
loading.value = true;
const { data, error: err } = await subscribeWithEmail({
address: web3Account.value,
email
});
loading.value = false;
error.value = err.value;
if (!data.value || data.value?.result !== 'OK') {
error.value = 'unknown';
}
return data.value?.result === 'OK';
};
const updateSubscriptions = async () => {
loading.value = true;
const { error: err } = await updateEmailSubscriptions({
address: web3Account.value,
email: '',
subscriptions: apiSubscriptions.value
});
error.value = err.value;
loading.value = false;
loadEmailSubscriptions();
};
return {
userState,
error,
clientSubscriptions,
subscribe,
updateSubscriptions,
loadEmailSubscriptions,
loading,
initialized
};
}
export const useEmailSubscription = createSharedComposable(
useEmailSubscriptionComposable
);
================================================
FILE: src/composables/useEns.ts
================================================
import {
ENS_DOMAINS_BY_ACCOUNT_QUERY,
ENS_DOMAIN_BY_HASH_QUERY
} from '@/helpers/queries';
const VALID_ENS_TLDS = ['eth', 'xyz', 'com', 'org', 'io', 'app', 'art', 'id'];
export function useEns() {
const { ensApolloQuery } = useApolloQuery();
const ownedEnsDomains = ref[]>([]);
// Fetch owned ENS domains for a given address
const loadOwnedEnsDomains = async (address: string) => {
if (!address) {
ownedEnsDomains.value = [];
return;
}
const response = await ensApolloQuery({
query: ENS_DOMAINS_BY_ACCOUNT_QUERY,
variables: {
id: address.toLowerCase()
}
});
const domains = response.account?.domains || [];
const wrappedDomains = response.account?.wrappedDomains || [];
let allDomains = [...domains, ...wrappedDomains];
// Filter out expired domains
const now = (Date.now() / 1000).toFixed(0);
allDomains = allDomains.filter(
domain =>
!domain.expiryDate ||
domain.expiryDate === '0' ||
domain.expiryDate > now
);
ownedEnsDomains.value = await fetchAllDomainData(allDomains);
};
// Fetch data for all domains and handle hash-based TLDs
async function fetchAllDomainData(domains: any[]) {
const filteredDomains = domains.filter(
domain => !domain.name.endsWith('.addr.reverse')
);
const domainPromises = filteredDomains.map(fetchDomainData);
return (await Promise.all(domainPromises)) || [];
}
// Fetch data for a single domain and update the name if it's a hash-based TLD
async function fetchDomainData(domain) {
const hash = domain.name.match(/\[(.*?)\]/)?.[1];
if (!hash) return domain;
const response = await ensApolloQuery({
query: ENS_DOMAIN_BY_HASH_QUERY,
variables: {
id: `0x${hash}`
}
});
if (response.registration?.domain?.labelName) {
return {
...domain,
name: domain.name.replace(
`[${hash}]`,
response.registration.domain.labelName
)
};
}
return {
...domain,
isInvalid: true
};
}
// Check if a domain is valid based on the TLD
function isValidEnsDomain(domain: string): boolean {
if (!domain?.includes('.')) return false;
return VALID_ENS_TLDS.includes(domain.split('.').pop() ?? '');
}
return {
loadOwnedEnsDomains,
ownedEnsDomains,
validEnsTlds: VALID_ENS_TLDS,
isValidEnsDomain
};
}
================================================
FILE: src/composables/useExtendedSpaces.ts
================================================
import { SPACE_QUERY } from '@/helpers/queries';
import { ExtendedSpace } from '@/helpers/interfaces';
import { mapOldPluginNames } from '@/helpers/utils';
const extendedSpaces = ref([]);
export function useExtendedSpaces() {
const loading = ref(false);
const { apolloQuery } = useApolloQuery();
async function loadExtendedSpace(spaceId: string) {
if (!spaceId || extendedSpaces.value.some(s => s.id === spaceId)) return;
loading.value = true;
try {
const response = await apolloQuery(
{
query: SPACE_QUERY,
variables: {
id: spaceId
}
},
'space'
);
const mappedSpace = mapOldPluginNames(response);
extendedSpaces.value.push(mappedSpace);
// Remove any duplicates incase two requests were made at the same time
extendedSpaces.value = extendedSpaces.value.filter(
(space, index, self) => index === self.findIndex(t => t.id === space.id)
);
loading.value = false;
} catch (e) {
loading.value = false;
console.error(e);
return e;
}
}
async function reloadSpace(spaceId: string) {
try {
const response = await apolloQuery(
{
query: SPACE_QUERY,
variables: {
id: spaceId
}
},
'space'
);
const mappedSpace = mapOldPluginNames(response);
extendedSpaces.value = extendedSpaces.value.filter(
s => s.id !== mappedSpace.id
);
extendedSpaces.value.push(mappedSpace);
} catch (e) {
console.error(e);
return e;
}
}
function deleteSpace(id: string) {
extendedSpaces.value = extendedSpaces.value.filter(s => s.id !== id);
}
return {
loadExtendedSpace,
reloadSpace,
deleteSpace,
extendedSpaces: computed(() => extendedSpaces.value),
spaceLoading: computed(() => loading.value)
};
}
================================================
FILE: src/composables/useFlaggedMessageStatus.ts
================================================
import { createGlobalState } from '@vueuse/core';
const useFlaggedMessageState = createGlobalState(() => {
const flaggedMessageStateMap = ref({});
return {
setVisibility: (id, state) => {
const isNotVisible = flaggedMessageStateMap.value[id] === false;
if (isNotVisible) return;
flaggedMessageStateMap.value = {
...flaggedMessageStateMap.value,
[id]: state
};
},
isFlaggedMessageVisible: id => {
return flaggedMessageStateMap.value[id] || false;
}
};
});
export function useFlaggedMessageStatus(pageId: Ref | string) {
const { setVisibility, isFlaggedMessageVisible } = useFlaggedMessageState();
const id = typeof pageId === 'string' ? pageId : pageId.value;
return {
isMessageVisible: computed(() => isFlaggedMessageVisible(id)),
setMessageVisibility: (state: boolean) => setVisibility(id, state)
};
}
================================================
FILE: src/composables/useFlashNotification.ts
================================================
interface Notification {
id: number;
message: string;
type: string;
remove(): any;
}
const items = ref([]);
export function useFlashNotification() {
function notify(payload: any, duration = 4000) {
const item: Notification = {
id: Math.floor(Date.now() * Math.random()),
message: Array.isArray(payload) ? payload[1] : payload,
type: Array.isArray(payload) ? payload[0] : 'green',
remove() {
items.value.splice(
items.value.findIndex(i => i.id === this.id),
1
);
}
};
items.value.push(item);
setTimeout(() => item.remove(), duration);
}
return { notify, items };
}
================================================
FILE: src/composables/useFollowSpace.ts
================================================
import { getInstance } from '@snapshot-labs/lock/plugins/vue3';
import { FOLLOWS_QUERY } from '@/helpers/queries';
import client from '@/helpers/clientEIP712';
import { useSpaceSubscription } from './useSpaceSubscription';
const following = ref([]);
const loadingFollows = ref(false);
export function useFollowSpace(spaceId: any = {}) {
const { web3, web3Account } = useWeb3();
const { modalAccountOpen } = useModal();
const { apolloQuery } = useApolloQuery();
const { setAlias, aliasWallet, isValidAlias, checkAlias } = useAliasAction();
const { toggleSubscription, isSubscribed } = useSpaceSubscription(spaceId);
const { notify } = useFlashNotification();
const { t } = useI18n();
const loadingFollow = ref('');
const followingSpaces = computed(() =>
Array.isArray(following.value)
? following.value.map((f: any) => f.space.id)
: []
);
const isFollowing = computed(() =>
following.value.some(
(f: any) => f.space.id === spaceId && f.follower === web3Account.value
)
);
async function loadFollows(spaceId?: string) {
const { isAuthenticated } = getInstance();
if (!isAuthenticated.value) return;
loadingFollows.value = true;
try {
following.value = await apolloQuery(
{
query: FOLLOWS_QUERY,
variables: {
follower_in: web3Account.value,
space_in: spaceId ? [spaceId] : undefined
}
},
'follows'
);
loadingFollows.value = false;
} catch (e) {
loadingFollows.value = false;
console.error(e);
}
}
function clickFollow(space) {
!web3.value.authLoading
? web3Account.value
? follow(space)
: (modalAccountOpen.value = true)
: null;
}
async function follow(space) {
loadingFollow.value = spaceId;
try {
await checkAlias();
if (!aliasWallet.value || !isValidAlias.value) {
await setAlias();
follow(space);
} else {
const network = process.env.VITE_ENV === 'production' ? 's' : 's-tn';
if (isFollowing.value) {
// Also unsubscribe to the notifications if the user leaves the space.
if (isSubscribed.value) {
await toggleSubscription();
}
await client.unfollow(aliasWallet.value, aliasWallet.value.address, {
from: web3Account.value,
space,
network
});
} else {
await client.follow(aliasWallet.value, aliasWallet.value.address, {
from: web3Account.value,
space,
network
});
}
await loadFollows();
loadingFollow.value = '';
}
} catch (e: any) {
loadingFollow.value = '';
console.error(e);
notify([
'red',
e?.error_description
? `Oops, ${e.error_description}`
: t('notify.somethingWentWrong')
]);
}
}
return {
clickFollow,
loadFollows,
loadingFollow: computed(() => loadingFollow.value),
loadingFollows: computed(() => loadingFollows.value),
isFollowing,
followingSpaces
};
}
================================================
FILE: src/composables/useFormSpaceProposal.ts
================================================
import { useStorage } from '@vueuse/core';
import { clone } from '@snapshot-labs/snapshot.js/src/utils';
import schemas from '@snapshot-labs/snapshot.js/src/schemas';
import { validateForm } from '@/helpers/validation';
import { OsnapPluginData } from '@/plugins/oSnap/types';
interface ProposalForm {
name: string;
body: string;
discussion: string;
choices: { key: number; text: string }[];
labels: string[];
start: number;
end: number;
snapshot: number;
type: string;
metadata: {
plugins: {
safeSnap?: { valid: boolean };
oSnap?: OsnapPluginData;
};
};
}
const EMPTY_PROPOSAL: ProposalForm = {
name: '',
body: '',
discussion: '',
choices: [
{ key: 0, text: '' },
{ key: 1, text: '' }
],
labels: [],
start: parseInt((Date.now() / 1e3).toFixed()),
end: 0,
snapshot: 0,
metadata: {
plugins: {}
},
type: 'single-choice'
};
const EMPTY_PROPOSAL_DRAFT = {
name: '',
body: '',
choices: [
{ key: 0, text: '' },
{ key: 1, text: '' }
],
isBodySet: false
};
const form = ref(clone(EMPTY_PROPOSAL));
const userSelectedDateStart = ref(false);
const userSelectedDateEnd = ref(false);
const sourceProposalLoaded = ref(false);
export function useFormSpaceProposal({ spaceType = 'default' } = {}) {
const route = useRoute();
const formDraft = useStorage<{
name: string;
body: string;
choices: { key: number; text: string }[];
labels: string[];
isBodySet: boolean;
}>(`snapshot.proposal.${route.params.key}`, clone(EMPTY_PROPOSAL_DRAFT));
const sourceProposal = computed(() => route.params.sourceProposal as string);
function resetForm() {
formDraft.value = clone(EMPTY_PROPOSAL_DRAFT);
form.value = clone(EMPTY_PROPOSAL);
sourceProposalLoaded.value = false;
userSelectedDateEnd.value = false;
userSelectedDateStart.value = false;
}
const validationErrors = computed(() =>
validateForm(schemas.proposal, form.value, { spaceType })
);
const isValid = computed(() => {
return Object.values(validationErrors.value).length === 0;
});
return {
form,
formDraft,
userSelectedDateStart,
userSelectedDateEnd,
sourceProposalLoaded,
sourceProposal,
validationErrors,
isValid,
resetForm
};
}
================================================
FILE: src/composables/useFormSpaceSettings.ts
================================================
import { clone } from '@snapshot-labs/snapshot.js/src/utils';
import schemas from '@snapshot-labs/snapshot.js/src/schemas';
import { ExtendedSpace } from '@/helpers/interfaces';
import isEqual from 'lodash/isEqual';
import isEmpty from 'lodash/isEmpty';
import { validateForm } from '@/helpers/validation';
const DEFAULT_PROPOSAL_VALIDATION = { name: 'any', params: {} };
const DEFAULT_VOTE_VALIDATION = { name: 'any', params: {} };
const DEFAULT_DELEGATION = {
delegationType: 'compound-governor',
delegationContract: '',
delegationNetwork: '1',
delegationApi: ''
};
const EMPTY_SPACE_FORM = {
strategies: [],
categories: [],
labels: [],
treasuries: [],
admins: [],
moderators: [],
members: [],
plugins: {},
delegationPortal: clone(DEFAULT_DELEGATION),
filters: {
minScore: 0,
onlyMembers: false
},
voting: {
delay: 0,
hideAbstain: false,
period: 0,
quorum: 0,
type: '',
privacy: ''
},
boost: {
enabled: true,
bribeEnabled: false
},
validation: clone(DEFAULT_PROPOSAL_VALIDATION),
voteValidation: clone(DEFAULT_VOTE_VALIDATION),
name: '',
about: '',
avatar: '',
network: '1',
symbol: '',
terms: '',
website: '',
twitter: '',
github: '',
coingecko: '',
parent: null,
children: [],
private: false,
domain: '',
skin: '',
guidelines: '',
template: ''
};
const formSetup = ref(clone(EMPTY_SPACE_FORM));
const formSettings = ref(clone(EMPTY_SPACE_FORM));
const initialFormState = ref(clone(EMPTY_SPACE_FORM));
const inputRefs = ref([]);
export function useFormSpaceSettings(
context: 'setup' | 'settings',
{ spaceType = 'default' } = {}
) {
const { isSending } = useClient();
const { isUploadingImage } = useImageUpload();
const form = computed({
get: () => (context === 'setup' ? formSetup.value : formSettings.value),
set: newVal =>
context === 'setup'
? (formSetup.value = newVal)
: (formSettings.value = newVal)
});
const hasFormChanged = computed(() => {
return !isEqual(formSettings.value, initialFormState.value);
});
const prunedForm = computed(() => {
const formData = clone(form.value);
Object.entries(formData).forEach(([key, value]) => {
if (value === null || value === '') delete formData[key];
});
if (
!formData.delegationPortal.delegationContract &&
!formData.delegationPortal.delegationApi
) {
delete formData.delegationPortal;
}
if (formData.voting.quorumType === 'default') {
delete formData.voting.quorumType;
}
return formData;
});
function populateForm(extendedSpace: ExtendedSpace) {
const formData = clone(extendedSpace);
removeUnnecessaryFields(formData);
ensureDefaultValues(formData);
ensureMembersLowerCase(formData);
if (shouldUseAnyValidation(formData)) {
formData.validation.name = 'any';
}
if (shouldUseBasicValidation(formData)) {
formData.validation.name = 'basic';
}
form.value = clone(formData);
initialFormState.value = clone(formData);
}
function ensureMembersLowerCase(formData: any) {
formData.admins =
formData?.admins.map((admin: string) => admin.toLowerCase()) || [];
formData.moderators =
formData?.moderators.map((moderator: string) =>
moderator.toLowerCase()
) || [];
formData.members =
formData?.members.map((member: string) => member.toLowerCase()) || [];
}
function removeUnnecessaryFields(formData: any) {
delete formData.id;
delete formData.followersCount;
delete formData.verified;
delete formData.flagged;
delete formData.hibernated;
delete formData.turbo;
if (formData.filters.invalids) delete formData.filters.invalids;
}
function ensureDefaultValues(formData: any) {
formData.strategies = formData.strategies || [];
formData.plugins = formData.plugins || {};
formData.delegationPortal =
formData.delegationPortal || clone(DEFAULT_DELEGATION);
formData.validation =
formData.validation || clone(DEFAULT_PROPOSAL_VALIDATION);
formData.voteValidation =
formData.voteValidation || clone(DEFAULT_VOTE_VALIDATION);
formData.filters = formData.filters || {};
formData.voting = formData.voting || {};
formData.voting = {
...formData.voting,
delay: formData.voting.delay || undefined,
period: formData.voting.period || undefined,
type: formData.voting.type || undefined,
quorum: formData.voting?.quorum || undefined,
quorumType: formData.voting.quorumType || 'default',
privacy: formData.voting.privacy || undefined
};
formData.children = formData.children
? formData.children.map((child: any) => child.id)
: [];
formData.parent = formData.parent?.id || '';
formData.boost = formData.boost || { enabled: true, bribeEnabled: false };
}
function shouldUseAnyValidation(formData: any) {
return (
formData.validation.name === 'basic' &&
!formData.filters.minScore &&
!formData.validation.params.minScore &&
isEmpty(formData.validation.params)
);
}
function shouldUseBasicValidation(formData: any) {
return (
formData.validation.name === 'nouns' ||
formData.validation.name === 'aave'
);
}
function validateStrategies(errors: any) {
const isTicket = form.value.strategies.some(
(strategy: any) => strategy.name === 'ticket'
);
const isAnyOrBasic = form.value.voteValidation.name === 'any';
if (isTicket && isAnyOrBasic) {
errors.strategies = 'ticketWithAnyOrBasicError';
}
}
function validateProposalValidation(errors: any) {
const hasProposalValidation =
(form.value.validation?.name && form.value.validation.name !== 'any') ||
!!form.value.filters?.minScore ||
!!form.value.filters?.onlyMembers;
if (!hasProposalValidation) {
errors.validation = 'missingProposalValidationError';
}
}
const validationErrors = computed(() => {
const errors = validateForm(schemas.space, prunedForm.value, { spaceType });
validateStrategies(errors);
validateProposalValidation(errors);
return errors;
});
const isValid = computed(() => {
return Object.values(validationErrors.value).length === 0;
});
const isReadyToSubmit = computed(
() => !isUploadingImage.value && !isSending.value
);
function resetForm() {
form.value = clone(initialFormState.value);
}
function forceShowError() {
inputRefs?.value?.forEach((ref: any) => {
if (ref?.forceShowError) ref?.forceShowError();
});
}
function addRef(ref: any) {
if (ref) inputRefs.value.push(ref);
}
return {
form,
prunedForm,
validationErrors,
isValid,
isReadyToSubmit,
hasFormChanged,
populateForm,
resetForm,
addRef,
forceShowError,
DEFAULT_VOTE_VALIDATION
};
}
================================================
FILE: src/composables/useFormValidation.ts
================================================
import defaults from '@/locales/default.json';
import { validateSchema } from '@snapshot-labs/snapshot.js/src/utils';
import { watchDebounced } from '@vueuse/core';
export function useFormValidation(schema, form) {
const { t } = useI18n();
const validationResult = ref>(null);
const validate = () => {
validationResult.value = validateSchema(schema, form.value);
};
watchDebounced(form, validate, {
debounce: 200,
maxWait: 1000,
deep: true,
immediate: true
});
const isValid = computed(() => validationResult.value === true);
function getValidationMessage(key: string): string {
const defaultErrors = Object.keys(defaults.errors);
if (validationResult.value === true || !validationResult.value) return '';
const errorFound = validationResult.value.find(
error =>
(defaultErrors.includes(error.keyword) &&
error.params.missingProperty === key) ||
(defaultErrors.includes(error.keyword) &&
error.instancePath.includes(key))
);
// Custom error messages for address fields (needed because minLength validation
// on the strategies schema would always show field required)
if (
errorFound &&
errorFound?.instancePath.includes('address') &&
errorFound?.keyword.includes('minLength')
)
return t('errors.invalidAddress');
if (
errorFound?.instancePath.includes('strategies') &&
errorFound?.keyword.includes('minItems')
)
return t('errors.minStrategy');
if (
errorFound?.instancePath.includes('website') ||
errorFound?.instancePath.includes('terms') ||
errorFound?.instancePath.includes('discussion') ||
errorFound?.instancePath.includes('guidelines')
)
return t('errors.website');
if (
(errorFound?.instancePath.includes('admins') ||
errorFound?.instancePath.includes('moderators') ||
errorFound?.instancePath.includes('members')) &&
errorFound?.keyword.includes('maxItems')
)
return t('errors.members.maxItems', {
limit: errorFound?.params.limit,
role:
errorFound?.instancePath.replace('/', '') === 'members'
? 'authors'
: errorFound?.instancePath.replace('/', '')
});
return errorFound
? t(`errors.${errorFound.keyword}`, [errorFound?.params.limit])
: '';
}
return { getValidationMessage, validationResult, isValid };
}
================================================
FILE: src/composables/useGnosis.ts
================================================
import { getInstance } from '@snapshot-labs/lock/plugins/vue3';
import { ExtendedSpace } from '@/helpers/interfaces';
import utils from '@snapshot-labs/snapshot.js/src/utils';
import { computedAsync, useMemoize } from '@vueuse/core';
import { Contract } from '@ethersproject/contracts';
const defaultNetwork = import.meta.env.VITE_DEFAULT_NETWORK;
const getSafeVersion = useMemoize(
async (networkKey: string, account: string) => {
const broviderUrl = import.meta.env.VITE_BROVIDER_URL;
const provider = utils.getProvider(networkKey, { broviderUrl });
const code = await provider.getCode(account);
if (code === '0x') return undefined;
const abi = ['function VERSION() view returns (string)'];
const contract = new Contract(account, abi, provider);
return contract.VERSION([]);
}
);
export function useGnosis(space?: ExtendedSpace) {
const { web3 } = useWeb3();
const auth = getInstance();
const connectorName = computed(() => auth.provider.value?.connectorName);
const networkKey = computed(() => web3.value.network.key);
const spaceNetworkKey = computed(() => space?.network);
const isSafeContract = computedAsync(async () => {
if (!web3.value.account) return false;
const safeVersion = await getSafeVersion(
networkKey.value,
web3.value.account
);
return typeof safeVersion === 'string';
}, false);
const isGnosisSafe = computed(
() =>
web3.value?.walletConnectType === 'Gnosis Safe Multisig' ||
web3.value?.walletConnectType === 'WalletConnect Safe App' ||
web3.value?.walletConnectType === 'Den' ||
connectorName.value === 'gnosis' ||
isSafeContract.value
);
const isGnosisAndNotDefaultNetwork = computed(() => {
return isGnosisSafe.value && networkKey.value !== defaultNetwork;
});
const isGnosisAndNotSpaceNetwork = computed(() => {
return isGnosisSafe.value && networkKey.value !== spaceNetworkKey.value;
});
return {
isGnosisSafe,
isGnosisAndNotDefaultNetwork,
isGnosisAndNotSpaceNetwork
};
}
================================================
FILE: src/composables/useI18n.ts
================================================
import { lsGet, lsSet } from '@/helpers/utils';
import i18n, {
defaultLocale,
setI18nLanguage,
loadLocaleMessages
} from '@/helpers/i18n';
const currentLocale = ref(lsGet('locale', defaultLocale));
export function useI18n() {
const { t, d, tc } = i18n.global;
async function setLocale(locale) {
currentLocale.value = locale;
lsSet('locale', locale);
await loadLocaleMessages(i18n, locale);
setI18nLanguage(i18n, locale);
}
async function loadLocale() {
await loadLocaleMessages(i18n, currentLocale.value);
setI18nLanguage(i18n, currentLocale.value);
}
return {
t,
d,
tc,
setLocale,
loadLocale,
currentLocale
};
}
================================================
FILE: src/composables/useImageUpload.ts
================================================
import { upload as pin } from '@snapshot-labs/pineapple';
import { useI18n } from './useI18n';
const isUploadingImage = ref(false);
export function useImageUpload() {
const imageUploadError = ref('');
const imageUrl = ref('');
const imageName = ref('');
const { t } = useI18n();
const { notify } = useFlashNotification();
const reset = () => {
isUploadingImage.value = false;
imageUploadError.value = '';
imageUrl.value = '';
imageName.value = '';
};
const upload = async (
file,
onSuccess: (image: { name: string; url: string }) => void
) => {
reset();
if (!file) return;
isUploadingImage.value = true;
const formData = new FormData();
if (!['image/jpeg', 'image/jpg', 'image/png'].includes(file.type)) {
imageUploadError.value = t('errors.unsupportedImageType');
isUploadingImage.value = false;
return;
}
if (file.size > 1024 * 1024) {
imageUploadError.value = t('errors.fileTooBig');
isUploadingImage.value = false;
return;
}
formData.append('file', file);
try {
const receipt = await pin(formData, import.meta.env.VITE_PINEAPPLE_URL);
imageUrl.value = `ipfs://${receipt.cid}`;
imageName.value = file.name;
onSuccess({ name: file.name, url: imageUrl.value });
} catch (err: any) {
notify(['red', t('notify.somethingWentWrong')]);
imageUploadError.value = err.error?.message || err;
} finally {
isUploadingImage.value = false;
}
};
return {
isUploadingImage,
imageUploadError,
image: {
url: imageUrl,
name: imageName
},
upload
};
}
================================================
FILE: src/composables/useInfiniteLoader.ts
================================================
export function useInfiniteLoader(loadBy = 6) {
const loadingMore = ref(false);
const stopLoadingMore = ref(false);
async function loadMore(loadFn) {
if (loadingMore.value) return;
if (!stopLoadingMore.value) {
loadingMore.value = true;
await loadFn();
loadingMore.value = false;
}
}
return { loadBy, loadingMore, stopLoadingMore, loadMore };
}
================================================
FILE: src/composables/useIntl.ts
================================================
/**
* Wrapper functions for Intl.RelativeTimeFormat/NumberFormat
* returning computed properties based on current locale from i18n
*/
/**
* This is needed since Intl still doesn't support durations:
* https://github.com/tc39/proposal-intl-duration-format (hopefully soon!)
*
* The Intl.relativeTimeFormat API (same as basically all libraries like day.js, timeago.js)
* only supports phrases like "5 hours ago" or "in 35 minutes". But these time durations can be phrased
* differently, e.g. we also use "12 hours left" instead of "(ends) in 12 hours". For that you need
* a simple duration formatter, that turns 3600 into "1 hour" and 180000 into "2 days". More granular
* formats are possible like "1 hour, 30 minutes" (which will be covered by Intl.Duration).
* For now, this function just returns the biggest/closest unit and the resulting number from an integer
* of seconds. (3678 => { duration: 1, unit: 'hour'}) This is accompanied by manual translations in our message
* catalogues of strings like "second", "seconds", "minute", "minutes", etc.
*/
const getDurationAndUnit = (seconds: number) => {
let unit = 'second';
let duration = seconds;
const abs = Math.abs(seconds);
if (abs >= 60) {
unit = 'minute';
duration = duration / 60;
if (abs >= 60 * 60) {
unit = 'hour';
duration = duration / 60;
if (abs >= 60 * 60 * 24) {
unit = 'day';
duration = duration / 24;
if (abs >= 60 * 60 * 24 * 365) {
unit = 'year';
duration = duration / 365;
} else if (abs >= 60 * 60 * 24 * 30) {
unit = 'month';
duration = duration / 30;
} else if (abs >= 60 * 60 * 24 * 7) {
unit = 'week';
duration = duration / 7;
}
}
}
}
duration = Math.round(duration);
return { duration, unit };
};
export function useIntl() {
const { currentLocale, t } = useI18n();
/**
* functions to create computed formatters based on locale
*
* If you need a custom format in only one component, you can import these
* functions to create a custom formatter locally in that component, to use
* it as the formatting functions' 2nd argument.
* Otherwise you can add a predefined formatter and a formatting function
* below and add them to the return list.
*/
const getRelativeTimeFormatter = (options?: object) =>
computed(
() =>
new Intl.RelativeTimeFormat(
currentLocale.value,
options || { style: 'short', numeric: 'always' }
)
);
const getNumberFormatter = (options?: object) =>
computed(
() =>
new Intl.NumberFormat(
// currently we are using only english number formatting because other
// languages can result in very different string length, which we need to deal with.
// (en: 10.2k, de: 10.200)
'en', // currentLocale.value,
options || { notation: 'standard' }
)
);
/**
* predefined formatters
*/
const defaultRelativeTimeFormatter = getRelativeTimeFormatter();
const longRelativeTimeFormatter = getRelativeTimeFormatter({
style: 'long',
numeric: 'always'
});
const defaultNumberFormatter = getNumberFormatter(
// format with two decimal places
{ maximumFractionDigits: 2 }
);
const compactNumberFormatter = getNumberFormatter({
notation: 'compact',
compactDisplay: 'short'
});
const percentNumberFormatter = getNumberFormatter({
style: 'percent',
maximumFractionDigits: 2
});
/**
* formatting functions
*/
const formatRelativeTime = (
timestamp: number,
formatter?: Intl.RelativeTimeFormat
) => {
const relativeTo = new Date().getTime() / 1e3;
const { duration, unit } = getDurationAndUnit(timestamp - relativeTo);
formatter = formatter || defaultRelativeTimeFormatter.value;
return formatter.format(duration, unit);
};
// doesn't use Intl (yet), needs useI18n's t function, to translate the unit
const formatDuration = (seconds: number) => {
const { duration, unit } = getDurationAndUnit(seconds);
return t(`timeUnits.${unit}`, { n: duration });
};
const formatNumber = (number: number, formatter?: Intl.NumberFormat) => {
formatter = formatter || defaultNumberFormatter.value;
return formatter.format(number);
};
const formatCompactNumber = (number: number) =>
formatNumber(number, compactNumberFormatter.value);
const formatPercentNumber = (number: number) =>
formatNumber(number, percentNumberFormatter.value);
const getRelativeProposalPeriod = (state: any, start: any, end: any): any => {
if (state === 'closed') {
return t('endedAgo', [
formatRelativeTime(end, longRelativeTimeFormatter.value)
]);
}
if (state === 'active') {
return t('endIn', [
formatRelativeTime(end, longRelativeTimeFormatter.value)
]);
}
return t('startIn', [
formatRelativeTime(start, longRelativeTimeFormatter.value)
]);
};
const getPercentFractionDigits = value => {
const absValue = Math.abs(value);
if (absValue === 0) {
return 0;
}
let leadingZeros = 0;
let tempValue = absValue;
while (tempValue < 1) {
tempValue *= 10;
leadingZeros++;
}
return Math.max(1, Math.min(leadingZeros, 8));
};
return {
getRelativeTimeFormatter,
getNumberFormatter,
formatRelativeTime,
formatDuration,
formatNumber,
formatCompactNumber,
formatPercentNumber,
getRelativeProposalPeriod,
getPercentFractionDigits,
longRelativeTimeFormatter
};
}
================================================
FILE: src/composables/useMeta.ts
================================================
import { useHead } from '@vueuse/head';
type metaInfo = {
title: {
key: string;
params?: Record;
};
description: {
key: string;
params?: Record;
};
};
export function useMeta(metaInfo: metaInfo) {
const { t } = useI18n();
useHead({
title: t(metaInfo.title.key, metaInfo.title.params || {}),
meta: [
{
name: 'description',
content: t(metaInfo.description.key, metaInfo.description.params || {})
}
]
});
return {};
}
================================================
FILE: src/composables/useModal.ts
================================================
const modalAccountOpen = ref(false);
const isModalPostVoteOpen = ref(false);
const modalEmailOpen = ref(false);
export function useModal() {
return { modalAccountOpen, isModalPostVoteOpen, modalEmailOpen };
}
================================================
FILE: src/composables/useModalNotification.ts
================================================
interface Notification {
id: number;
description: string;
type: 'info' | 'warning' | 'warning-red';
remove(): any;
}
const items = ref([]);
export function useModalNotification() {
function notifyModal(
type: 'info' | 'warning' | 'warning-red',
description: string
) {
const item: Notification = {
id: Math.floor(Date.now() * Math.random()),
description,
type,
remove() {
items.value.splice(
items.value.findIndex(i => i.id === this.id),
1
);
}
};
items.value.push(item);
}
return { notifyModal, items };
}
================================================
FILE: src/composables/useNetworksFilter.ts
================================================
/**
* Orders networks by spaces count and returns a list of networks
* filtered by the search string (case insensitive).
*/
import { NETWORKS_COUNT_QUERY } from '@/helpers/queries';
import networks from '@snapshot-labs/snapshot.js/src/networks.json';
const networksSpacesCount: any = ref(null);
export function useNetworksFilter() {
const loading = ref(false);
const filterNetworks = (q = '') => {
const networksArray = Object.keys(networks)
.map(s => ({ ...networks[s] }))
.filter(n => !n.disabled);
const networksArrayBySearchString = networksArray.filter(n =>
JSON.stringify(n).toLowerCase().includes(q.toLowerCase())
);
const networksArrayBySpaceCount = networksArrayBySearchString.sort(
(a, b) =>
(networksSpacesCount.value?.[b.key] ?? 0) -
(networksSpacesCount.value?.[a.key] ?? 0)
);
const networksArrayByExactKeyMatch = networksArrayBySpaceCount.sort(
(a, b) => (a.key === q ? -1 : b.key === q ? 1 : 0)
);
return networksArrayByExactKeyMatch;
};
const { apolloQuery } = useApolloQuery();
async function getNetworksSpacesCount() {
if (networksSpacesCount.value) return;
loading.value = true;
const res = await apolloQuery(
{
query: NETWORKS_COUNT_QUERY
},
'networks'
);
networksSpacesCount.value = res.reduce(
(obj: any, item: any) => ({ ...obj, [item.id]: item.spacesCount }),
{}
);
loading.value = false;
}
return {
filterNetworks,
getNetworksSpacesCount,
networksSpacesCount,
loadingNetworksSpacesCount: loading
};
}
================================================
FILE: src/composables/useNotifications.ts
================================================
/**
* A componsable for handling proposal notifications and possibly more.
* Seen proposals are stored in the local storage.
*/
import { NOTIFICATION_PROPOSALS_QUERY } from '@/helpers/queries';
import { useStorage } from '@vueuse/core';
import uniqBy from 'lodash/uniqBy';
interface Notification {
id: string;
event: string;
time: number;
title: string;
space?: { id: string; name: string; avatar: string };
}
const notifications = ref([]);
const loading = ref(false);
const selectedFilter = ref('all');
const NotificationEvents = {
ProposalStart: 'proposal/start',
ProposalEnd: 'proposal/end'
};
const filters = ['all', 'unread'];
export function useNotifications() {
const router = useRouter();
const { web3, web3Account } = useWeb3();
const { followingSpaces, loadingFollows } = useFollowSpace();
const { apolloQuery } = useApolloQuery();
const notificationsLoading = computed(
() => loading.value || web3.value.authLoading || loadingFollows.value
);
async function loadProposals(state, date) {
if (followingSpaces.value.length === 0) return [];
return (
(await apolloQuery(
{
query: NOTIFICATION_PROPOSALS_QUERY,
variables: {
first: 100,
state,
space_in: followingSpaces.value,
start_gte: date
}
},
'proposals'
)) ?? []
);
}
function mapProposalToNotifications(proposals) {
if (proposals.length === 0) return;
const now = Number(new Date().getTime() / 1000).toFixed(0);
proposals.forEach(proposal => {
if (notifications.value.some(n => n.id === proposal.id)) return;
notifications.value.push({
id: proposal.id,
event:
proposal.end <= now
? NotificationEvents.ProposalEnd
: NotificationEvents.ProposalStart,
time: proposal.end <= now ? proposal.end : proposal.start,
title: proposal.title,
space: proposal.space
});
});
}
async function loadRecentProposals() {
const unixTimestampTwoWeeksAgo = Number(
(new Date().getTime() / 1000 - 604800 * 2).toFixed(0)
);
const proposalsObj = await Promise.all([
loadProposals('active', unixTimestampTwoWeeksAgo),
loadProposals('closed', unixTimestampTwoWeeksAgo)
]);
mapProposalToNotifications(proposalsObj.flat());
}
async function loadNotifications() {
if (notifications.value.length > 0) return;
if (!web3Account.value) return;
loading.value = true;
await loadRecentProposals();
loading.value = false;
}
// Reactive local storage with help from vueuse package
const readNotificationsStorage = useStorage(
`snapshot.unread.${web3Account.value.slice(0, 8).toLowerCase()}`,
['initialValue']
);
function selectNotification(id: string, spaceId: string) {
router.push({
name: 'spaceProposal',
params: { key: spaceId, id: id }
});
readNotificationsStorage.value.push(id);
}
// Mark all notifications as read and remove duplicates from local storage
function markAllAsRead() {
readNotificationsStorage.value = readNotificationsStorage.value.concat(
notifications.value.map(n => n.id)
);
readNotificationsStorage.value = uniqBy(readNotificationsStorage.value);
}
const notificationsSortedByTime = computed(() =>
[
...notifications.value.map(n => ({
text: n.title,
action: { spaceId: n.space?.id, id: n.id },
seen: readNotificationsStorage.value.includes(n.id),
...n
}))
]
.sort((a, b) => b.time - a.time)
.filter(n => (selectedFilter.value === 'unread' ? !n.seen : true))
);
function reloadNotifications() {
notifications.value = [];
loadNotifications();
}
watch(followingSpaces, () => {
reloadNotifications();
});
// Refresh notifications every 15 minutes and clear on unmount
const refreshNotificationInterval = setInterval(
reloadNotifications,
60000 * 15
);
onBeforeUnmount(() => {
clearInterval(refreshNotificationInterval);
});
return {
notifications: computed(() => notifications.value),
notificationsSortedByTime,
notificationsLoading,
NotificationEvents,
selectedFilter,
filters,
loadNotifications,
selectNotification,
markAllAsRead
};
}
================================================
FILE: src/composables/usePayment.ts
================================================
import { sendTransaction } from '@snapshot-labs/snapshot.js/src/utils';
import { getInstance } from '@snapshot-labs/lock/plugins/vue3';
import { parseUnits } from '@ethersproject/units';
import { BigNumber } from '@ethersproject/bignumber';
const BASE_PRICE = 1666.66666667;
const BASE_UNIT = 1;
const BASE_CURRENCY = {
symbol: '$'
};
const PLANS = {
y1: { label: '1 year', unit: 12, discount: 0 }
// m6: { label: '6 months', unit: 6, discount: 0 }
};
const CURRENCIES = {
ethereum: {
name: 'Ethereum',
code: 'ETH',
decimal: 18,
logo: 'ipfs://bafybeieaiuehxdqgmdgxfl7addolzht3x5qnyhk4epn5f7yko7h4egpsvi',
address: {
1: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
5: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'
}
},
'usd-coin': {
name: 'USD Coin',
code: 'USDC',
decimal: 6,
address: {
1: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
5: '0x07865c6e87b9f70255377e024ace6630c1eaa37f'
},
logo: 'ipfs://bafybeiffspz4hsyc5drzf5ioum2x535dxmvfmt3342g4iizbinie7cpehy'
},
dai: {
name: 'DAI',
code: 'DAI',
decimal: 18,
address: {
1: '0x6b175474e89094c44da98b954eedeac495271d0f',
5: '0xdc31ee1784292379fbb2964b3b9c4124d8f89c60'
},
logo: 'ipfs://bafkreib3ujd27dovs3aqezrttibnspkprplyb7nf6wzm2vgjohleajkg4q'
}
};
const DEFAULT_CURRENCY = 'ethereum';
const DEFAULT_PLAN = 'y1';
const TRANSFER_ABI = [
{
name: 'transfer',
type: 'function',
inputs: [
{
name: '_to',
type: 'address'
},
{
type: 'uint256',
name: '_tokens'
}
],
constant: false,
outputs: [],
payable: false
}
];
const SNAPSHOT_WALLET = '0x01e8CEC73B020AB9f822fD0dee3Aa4da2fe39e38';
const fxRates = reactive(
Object.fromEntries(Object.keys(CURRENCIES).map(id => [id, 0]))
);
const fxLoadStatus = ref(0);
const COINGECKO_API_URL = 'https://api.coingecko.com/api/v3/simple';
const COINGECKO_PARAMS = '&vs_currencies=usd&include_24hr_change=true';
export function usePayment(network: number) {
const auth = getInstance();
const { web3 } = useWeb3();
const { notify } = useFlashNotification();
const loading = ref(false);
const paymentTx = ref(null);
const modalUnsupportedNetworkOpen = ref(false);
const walletNetworkKey = computed(() => web3.value.network.key);
refreshFx();
async function transfer(amount: number, currencyId: string) {
loading.value = true;
const currency = CURRENCIES[currencyId];
try {
if (network.toString() !== walletNetworkKey.value) {
modalUnsupportedNetworkOpen.value = true;
return false;
}
const parsedAmount = parseUnits(
amount.toFixed(currency.decimal),
currency.decimal
);
let tx;
if (currencyId === 'ethereum') {
tx = await transferEth(parsedAmount);
} else {
tx = await transferErc20(parsedAmount, currency.address[network]);
}
paymentTx.value = { network, ...tx };
return tx.wait();
} catch (e: any) {
if (/(insufficient|exceeds).*(balance|fund)/i.test(e.message)) {
return notify(['red', 'Insufficient funds']);
} else {
console.error('Transfer error', e);
}
} finally {
loading.value = false;
}
}
async function transferEth(amount: BigNumber) {
const signer = auth.web3.getSigner();
return signer.sendTransaction({
to: SNAPSHOT_WALLET,
value: amount
});
}
async function transferErc20(amount: BigNumber, address: string) {
return sendTransaction(auth.web3, address, TRANSFER_ABI, 'transfer', [
SNAPSHOT_WALLET,
amount
]);
}
async function refreshFx(): Promise {
try {
const response = await fetch(
`${COINGECKO_API_URL}/price?ids=${Object.keys(CURRENCIES)
.map(id => id)
.join(',')}${COINGECKO_PARAMS}`
);
const data = await response.json();
fxLoadStatus.value = 1;
Object.keys(data).map(id => {
fxRates[id] = data[id].usd;
});
} catch (e: any) {
fxLoadStatus.value = 2;
}
}
return {
BASE_PRICE,
BASE_UNIT,
BASE_CURRENCY,
DEFAULT_CURRENCY,
DEFAULT_PLAN,
CURRENCIES,
PLANS,
transfer,
fxRates,
loading,
paymentTx,
refreshFx,
fxLoadStatus,
modalUnsupportedNetworkOpen
};
}
================================================
FILE: src/composables/usePlugins.ts
================================================
/**
* Creates a plugin index from each individual plugin.json and provides
* functions to register and load plugin components and filter plugins by a
* search string (case insensitive), ordered by space count fetched from
* backend.
*/
import { PLUGINS_COUNT_QUERY } from '@/helpers/queries';
import { Plugin } from '@/helpers/interfaces';
// aggregate all plugin.json files in src/plugins
const pluginIndex = Object.fromEntries(
Object.entries(
import.meta.globEager('../plugins/*/plugin.json') as {
[key: string]: Plugin;
}
).map(([path, config]) => {
const pluginKey = path
.replace('../plugins/', '')
.replace('/plugin.json', '');
return [pluginKey, { key: pluginKey, ...config }];
})
);
// import all plugin's main components (Create.vue, Proposal.vue, etc.)
// (plugin root directories should not contain any other components)
const allPluginComponents = import.meta.globEager(`../plugins/*/*.vue`);
// Based on list of active plugins in a space (pluginKeys) returns a list of
// required component objects for a specific location (componentName, e.g.
// Create) to then mount them with the built-in ... component. (e.g.
// in src/components/Plugin/Create.vue)
const getPluginComponents = (componentName: string, pluginKeys) => {
pluginKeys = pluginKeys.filter(key => !!pluginIndex[key]); // remove old/non-existent plugins
return Object.entries(allPluginComponents)
.map(([path, componentModule]) => {
if (path.endsWith(`${componentName}.vue`)) {
const pluginKey = path
.replace('../plugins/', '')
.replace(`/${componentName}.vue`, '');
if (pluginKeys.includes(pluginKey)) {
// prefix component name for better debugging, e.g. in console warnings
componentModule.default.name = `Plugins${pluginKey[0].toUpperCase()}${pluginKey.substring(
1
)}${componentName}`;
return componentModule.default;
}
}
return null;
})
.filter(c => c);
};
// space count and filter function
const pluginsSpacesCount: any = ref(null);
const loadingPluginsSpacesCount = ref(false);
const { apolloQuery } = useApolloQuery();
const getPluginsSpacesCount = async () => {
if (pluginsSpacesCount.value) return; // run only once
loadingPluginsSpacesCount.value = true;
const res = await apolloQuery({ query: PLUGINS_COUNT_QUERY }, 'plugins');
// turn [{ id: "myPlugin", spaceCount: 1 }, ...] to { myPlugin: 1, ... }
pluginsSpacesCount.value = res.reduce(
(obj: any, item: any) => ({ ...obj, [item.id]: item.spacesCount }),
{}
);
loadingPluginsSpacesCount.value = false;
};
const filterPlugins = (q = '') => {
return Object.values(pluginIndex)
.filter(plugin =>
JSON.stringify(plugin).toLowerCase().includes(q.toLowerCase())
)
.sort(
(a, b) =>
(pluginsSpacesCount.value?.[b.key] ?? 0) -
(pluginsSpacesCount.value?.[a.key] ?? 0)
);
};
/**
* Composable
*
* Does it really make sense to use the composable pattern here? Most of it can
* be normal imports.
*/
export function usePlugins() {
return {
pluginIndex,
getPluginComponents,
filterPlugins,
getPluginsSpacesCount,
pluginsSpacesCount,
loadingPluginsSpacesCount
};
}
================================================
FILE: src/composables/useProfiles.ts
================================================
import { lookupAddress } from '@/helpers/utils';
import { PROFILES_QUERY } from '@/helpers/queries';
import { Profile } from '@/helpers/interfaces';
import { getAddress } from '@ethersproject/address';
// Holds profile data (ENS name, username, about) for all addresses appearing in the frontend
const profiles = ref<{
[address: string]: Profile;
}>({});
const reloadingProfile = ref(false);
export function useProfiles() {
const loadingProfiles = ref(false);
const profilesCreated = computed(() => {
const profilesWithCreatedAndAvatar = Object.values(profiles.value).filter(
profile => profile.avatar && profile.created
);
const profilesCreatedWithinLastWeek = profilesWithCreatedAndAvatar.filter(
profile => profile.created ?? 0 > Date.now() - 1000 * 60 * 60 * 24 * 7
);
const addressCreatedObject = profilesCreatedWithinLastWeek.reduce(
(acc, profile) => ({ ...acc, [profile.id as string]: profile.created }),
{}
);
return addressCreatedObject;
});
/**
* Populates global ref with profile data for batches of addresses.
*/
const loadProfiles = async (addresses: string[]) => {
const addressesToAdd = addresses.filter(
address =>
!Object.keys(profiles.value).includes(address) && address !== ''
);
const { apolloQuery } = useApolloQuery();
let profilesRes: any = {};
if (addressesToAdd.length > 0) {
loadingProfiles.value = true;
profilesRes = await Promise.all([
await lookupAddress(addressesToAdd),
await apolloQuery(
{
query: PROFILES_QUERY,
variables: {
addresses
}
},
'users'
)
]);
// add ens from profilesRes to corresponding address in profilesObj
Object.keys(profilesRes[0] ?? {}).forEach(address => {
profilesRes[0][address] = {
...{ ens: profilesRes[0][address] },
...profilesRes[1]?.find(
p => p.id.toLowerCase() === address.toLowerCase()
)
};
});
}
profiles.value = { ...profilesRes[0], ...profiles.value };
loadingProfiles.value = false;
reloadingProfile.value = false;
};
// Reload a profile in profiles object
const reloadProfile = (address: string) => {
// find profile in profiles object and delete it (to force reload)
const profile = profiles.value[address];
if (profile) {
delete profiles.value[address];
}
reloadingProfile.value = true;
loadProfiles([address]);
};
function getProfile(address: string) {
const normalizedAddress = getAddress(address);
return profiles.value[normalizedAddress];
}
return {
profiles,
loadProfiles,
reloadProfile,
getProfile,
loadingProfiles,
reloadingProfile,
profilesCreated
};
}
================================================
FILE: src/composables/useProposalVotes.ts
================================================
import { Proposal, Vote, VoteFilters } from '@/helpers/interfaces';
import { VOTES_QUERY } from '@/helpers/queries';
type QueryParams = {
voter?: string;
} & Partial;
export function useProposalVotes(proposal: Proposal, loadBy = 6) {
const { profiles, loadProfiles } = useProfiles();
const { apolloQuery } = useApolloQuery();
const { resolveName } = useResolveName();
const loadingVotes = ref(false);
const loadingMoreVotes = ref(false);
const loadingUserVote = ref(false);
const votes = ref([]);
const userVote = ref(null);
async function _fetchVotes(queryParams: QueryParams, skip = 0) {
const response = await apolloQuery(
{
query: VOTES_QUERY,
variables: {
id: proposal.id,
first: loadBy,
skip,
orderBy: 'vp',
orderDirection: queryParams.orderDirection || 'desc',
reason_not: queryParams.onlyWithReason ? '' : undefined,
voter: queryParams.voter || undefined
}
},
'votes'
);
loadProfiles(response.map(vote => vote.voter));
return response;
}
async function _fetchVote(queryParams: QueryParams) {
const response = await apolloQuery(
{
query: VOTES_QUERY,
variables: {
id: proposal.id,
voter: queryParams.voter
}
},
'votes'
);
loadProfiles(response.map(vote => vote.voter));
return response;
}
function formatProposalVotes(votes: Vote[]) {
if (!votes?.length) return [];
return votes.map(vote => {
vote.balance = vote.vp;
vote.scores = vote.vp_by_strategy;
return vote;
});
}
async function loadVotes(filter: Partial = {}) {
if (loadingVotes.value) return;
loadingVotes.value = true;
try {
const response = await _fetchVotes(filter);
votes.value = formatProposalVotes(response);
} catch (e) {
console.log(e);
} finally {
loadingVotes.value = false;
}
}
async function loadSingleVote(search: string) {
loadingVotes.value = true;
const response = await resolveName(search);
const voter = response || search;
try {
const response = await _fetchVote({ voter });
votes.value = formatProposalVotes(response);
} catch (e) {
console.log(e);
} finally {
loadingVotes.value = false;
}
}
async function loadMoreVotes(filter: Partial = {}) {
if (
loadingMoreVotes.value ||
loadingVotes.value ||
loadBy > votes.value.length
)
return;
loadingMoreVotes.value = true;
try {
const response = await _fetchVotes(filter, votes.value.length);
votes.value = votes.value.concat(formatProposalVotes(response));
} catch (e) {
console.log(e);
} finally {
loadingMoreVotes.value = false;
}
}
async function loadUserVote(voter: string) {
if (!voter) return;
userVote.value = null;
try {
loadingUserVote.value = true;
const response = await _fetchVote({ voter });
userVote.value = formatProposalVotes(response)[0];
} catch (e) {
console.log(e);
} finally {
loadingUserVote.value = false;
}
}
return {
votes,
profiles,
loadingVotes,
loadingMoreVotes,
loadingUserVote: computed(() => loadingUserVote.value),
userVote,
formatProposalVotes,
loadVotes,
loadMoreVotes,
loadSingleVote,
loadUserVote
};
}
================================================
FILE: src/composables/useProposals.ts
================================================
import { Proposal } from '@/helpers/interfaces';
import { USER_VOTED_PROPOSAL_IDS_QUERY } from '@/helpers/queries';
interface ProposalsStore {
space: {
proposals: Proposal[];
};
timeline: {
proposals: Proposal[];
};
}
const store = reactive({
space: {
proposals: []
},
timeline: {
proposals: []
}
});
const userVotedProposalIds = ref([]);
export function useProposals() {
function setTimelineProposals(proposals) {
store.timeline.proposals = proposals;
}
function addTimelineProposals(proposals) {
store.timeline.proposals = store.timeline.proposals.concat(proposals);
}
function setSpaceProposals(proposals) {
store.space.proposals = proposals;
}
function addSpaceProposals(proposals) {
store.space.proposals = store.space.proposals.concat(proposals);
}
function resetSpaceProposals() {
store.space.proposals = [];
}
function removeSpaceProposal(id: string) {
store.space.proposals = store.space.proposals.filter(
proposal => proposal.id !== id
);
}
function addVotedProposalId(id: string) {
userVotedProposalIds.value.push(id);
}
const { apolloQuery } = useApolloQuery();
async function getUserVotedProposalIds(voter: string, proposals: string[]) {
if (!voter || !proposals?.length) return;
const votes = await apolloQuery(
{
query: USER_VOTED_PROPOSAL_IDS_QUERY,
variables: {
voter,
proposals
}
},
'votes'
);
const proposalId = votes?.map(vote => vote.proposal.id) ?? [];
userVotedProposalIds.value = [
...new Set(userVotedProposalIds.value.concat(proposalId))
];
}
const proposalIds = computed(() => {
const timelineProposals =
store.timeline.proposals?.map(proposal => proposal.id) ?? [];
const spaceProposals =
store.space.proposals?.map(proposal => proposal.id) ?? [];
return [...timelineProposals, ...spaceProposals];
});
const { web3Account } = useWeb3();
watch(
() => [store.space.proposals, store.timeline.proposals],
() => {
getUserVotedProposalIds(web3Account.value, proposalIds.value);
}
);
watch(web3Account, () => {
userVotedProposalIds.value = [];
getUserVotedProposalIds(web3Account.value, proposalIds.value);
});
return {
store,
userVotedProposalIds,
addVotedProposalId,
setSpaceProposals,
addSpaceProposals,
resetSpaceProposals,
removeSpaceProposal,
setTimelineProposals,
addTimelineProposals
};
}
================================================
FILE: src/composables/useQuorum.ts
================================================
import { ExtendedSpace, Proposal, Results } from '@/helpers/interfaces';
import getProvider from '@snapshot-labs/snapshot.js/src/utils/provider';
import { BigNumber } from '@ethersproject/bignumber';
import { call } from '@snapshot-labs/snapshot.js/src/utils';
import { getSnapshots } from '@snapshot-labs/snapshot.js/src/utils/blockfinder';
interface QuorumProps {
space: ExtendedSpace;
proposal: Proposal;
results: Results;
}
const broviderUrl = import.meta.env.VITE_BROVIDER_URL;
export function useQuorum(props: QuorumProps) {
const loading = ref(false);
const quorum = ref(0);
const quorumType = ref('default');
const totalQuorumScore = computed(() => {
if (props.proposal.quorumType === 'rejection') {
return props.results.scores
.filter((c, i) => i === 1)
.reduce((a, b) => a + b, 0);
}
const basicCount = props.space.plugins?.quorum?.basicCount;
if (basicCount && props.proposal.type === 'basic')
return props.results.scores
.filter((c, i) => basicCount.includes(i))
.reduce((a, b) => a + b, 0);
if (props.space.voting.hideAbstain && props.proposal.type === 'basic') {
return props.results.scores
.filter((c, i) => i !== 2)
.reduce((a, b) => a + b, 0);
}
if (props.results.scoresTotal) return props.results.scoresTotal;
return 0;
});
async function getQuorum(web3: any, quorumOptions: any, snapshot: string) {
if (props.proposal?.quorum) {
return props.proposal?.quorum;
}
if (!quorumOptions) return 0;
const { strategy } = quorumOptions;
const quorumModifier = quorumOptions.quorumModifier ?? 1;
switch (strategy) {
case 'static': {
return quorumOptions.total;
}
case 'balance': {
const { address, methodABI, decimals } = quorumOptions;
const blockTag = snapshot === 'latest' ? snapshot : parseInt(snapshot);
const votingPower = await call(
web3,
[methodABI],
[address, methodABI.name],
{ blockTag }
);
return (
BigNumber.from(votingPower)
.div(BigNumber.from(10).pow(decimals))
.toNumber() * quorumModifier
);
}
case 'multichainBalance': {
const { network, strategies } = quorumOptions;
const blocks = await getSnapshots(
network,
parseInt(snapshot),
web3,
strategies.map(s => s.network || network)
);
const requests: Promise[] = strategies.map(s =>
call(
getProvider(s.network, { broviderUrl }),
[s.methodABI],
[s.address, s.methodABI.name],
{ blockTag: blocks[s.network] }
)
);
const results = await Promise.all(requests);
const totalBalance = results.reduce((total, ele, index) => {
if (index === 1) {
total = total.div(BigNumber.from(10).pow(strategies[0].decimals));
}
return total.add(
ele.div(BigNumber.from(10).pow(strategies[index].decimals))
);
});
return totalBalance.toNumber() * quorumModifier;
}
default:
throw new Error(`Unsupported quorum strategy: ${strategy}`);
}
}
async function loadQuorum() {
loading.value = true;
quorum.value = await getQuorum(
getProvider(props.space.network, { broviderUrl }),
props.space.plugins.quorum,
props.proposal.snapshot
);
quorumType.value = props.proposal.quorumType;
loading.value = false;
}
onMounted(() => loadQuorum());
return {
loadingQuorum: loading,
totalQuorumScore,
quorum,
quorumType
};
}
================================================
FILE: src/composables/useReportDownload.ts
================================================
import pkg from '@/../package.json';
export function useReportDownload() {
const { env } = useApp();
const isDownloadingVotes = ref(false);
const errorCode: globalThis.Ref = ref(null);
async function downloadFile(blob: Blob, fileName: string) {
const href = URL.createObjectURL(blob);
const a = Object.assign(document.createElement('a'), {
href,
style: 'display:none',
download: fileName
});
document.body.appendChild(a);
a.click();
URL.revokeObjectURL(href);
a.remove();
}
async function downloadVotes(proposalId: string) {
if (env === 'demo') {
errorCode.value = new Error('UNSUPPORTED_ENV');
return false;
}
isDownloadingVotes.value = true;
errorCode.value = null;
try {
const response = await fetch(
`${import.meta.env.VITE_SIDEKICK_URL}/api/votes/${proposalId}`,
{
method: 'POST'
}
);
if (response.status !== 200) {
throw new Error((await response.json()).error.message);
}
downloadFile(await response.blob(), `${pkg.name}-report-${proposalId}`);
return true;
} catch (e: any) {
errorCode.value = e;
return false;
} finally {
isDownloadingVotes.value = false;
}
}
return {
downloadVotes,
isDownloadingVotes,
errorCode
};
}
================================================
FILE: src/composables/useResolveName.ts
================================================
import { isAddress } from '@ethersproject/address';
import { resolveHandle } from '@/helpers/utils';
export function useResolveName() {
async function resolveName(value: string): Promise {
let address = value;
if (isAddress(value)) {
return address.toLowerCase();
}
address = await resolveHandle(value);
return isAddress(address) ? address.toLowerCase() : null;
}
return {
resolveName
};
}
================================================
FILE: src/composables/useSafe.ts
================================================
interface ExecutionStatus {
batchError:
| undefined
| {
num: number;
message: string;
};
}
const state = reactive({
batchError: undefined
});
export function useSafe() {
function setBatchError(num: number, message: string) {
state.batchError = { num, message };
}
function clearBatchError() {
state.batchError = undefined;
}
return {
setBatchError,
clearBatchError,
safesnap: computed(() => state)
};
}
================================================
FILE: src/composables/useSharing.ts
================================================
import { useShare } from '@vueuse/core';
import { ExtendedSpace, Proposal } from '@/helpers/interfaces';
export function useSharing() {
const { t } = useI18n();
const sharingItems = [
{
text: 'Share on X',
action: 'shareProposalX',
extras: { icon: 'x' }
},
{
text: 'Share on Hey',
action: 'shareProposalHey',
extras: { icon: 'hey' }
},
{
text: t('copyLink'),
action: 'shareToClipboard',
extras: { icon: 'link' }
}
];
function proposalUrl(key, proposal) {
return `https://${window.location.hostname}/#/${key}/proposal/${proposal.id}`;
}
function encodedProposalUrl(key, proposal) {
return encodeURIComponent(proposalUrl(key, proposal));
}
const { share, isSupported } = useShare();
function shareProposal(space, proposal) {
share({
title: '',
text: `${space.name} - ${proposal.title}`,
url: proposalUrl(space.id, proposal)
});
}
function shareClaim(shareTo: 'x' | 'hey', payload: { proposal: Proposal }) {
const postText = getClaimedText(shareTo, payload);
if (window && shareTo === 'hey') return shareHey(postText);
if (isSupported.value)
return share({
title: '',
text: postText,
url: proposalUrl(payload.proposal.space.id, payload.proposal)
});
if (window && shareTo === 'x') return shareX(postText);
}
function getClaimedText(
shareTo: 'x' | 'hey',
payload: { proposal: Proposal }
): string {
const claimedText = `I just claimed my reward for voting on`;
const spaceHandle = payload.proposal.space.twitter
? `@${payload.proposal.space.twitter}`
: payload.proposal.space.name;
const hashTag = 'SnapshotClaim';
if (shareTo === 'hey')
return `${encodeURIComponent(claimedText)}%20"${encodeURIComponent(
payload.proposal.title
)}"%20${encodedProposalUrl(
payload.proposal.space.id,
payload.proposal
)}&hashtags=${hashTag}`;
if (isSupported.value)
return `${claimedText} "${payload.proposal.title}" ${spaceHandle} #${hashTag}`;
if (shareTo === 'x')
return `${encodeURIComponent(claimedText)}%20"${encodeURIComponent(
payload.proposal.title
)}"%20${encodedProposalUrl(
payload.proposal.space.id,
payload.proposal
)}%20${spaceHandle}%20%23${hashTag}`;
return `${claimedText} "${payload.proposal.title}"`;
}
function shareVote(
shareTo: 'x' | 'hey',
payload: { space: ExtendedSpace; proposal: Proposal; choices: string }
) {
const postText = getVotedText(shareTo, payload);
if (window && shareTo === 'hey') return shareHey(postText);
if (isSupported.value)
return share({
title: '',
text: postText,
url: proposalUrl(payload.space.id, payload.proposal)
});
if (window && shareTo === 'x') return shareX(postText);
}
function getVotedText(shareTo: 'x' | 'hey', payload): string {
const isSingleChoice =
payload.proposal.type === 'single-choice' ||
payload.proposal.type === 'basic';
const isPrivate = payload.proposal.privacy === 'shutter';
const votedText =
payload.choices && isSingleChoice && !isPrivate
? `I just voted "${payload.choices}" on`
: `I just voted on`;
const spaceHandle = payload.space.twitter
? `@${payload.space.twitter}`
: payload.space.name;
const hashTag = 'SnapshotVote';
if (shareTo === 'hey')
return `${encodeURIComponent(votedText)}%20"${encodeURIComponent(
payload.proposal.title
)}"%20${encodedProposalUrl(
payload.space.id,
payload.proposal
)}&hashtags=${hashTag}`;
if (isSupported.value)
return `${votedText} "${payload.proposal.title}" ${spaceHandle} #${hashTag}`;
if (shareTo === 'x')
return `${encodeURIComponent(votedText)}%20"${encodeURIComponent(
payload.proposal.title
)}"%20${encodedProposalUrl(
payload.space.id,
payload.proposal
)}%20${spaceHandle}%20%23${hashTag}`;
return `${votedText} "${payload.proposal.title}"`;
}
function shareX(text) {
const url = `https://x.com/intent/tweet?text=${text}`;
window.open(url, '_blank')?.focus();
}
function shareHey(text) {
const url = `https://hey.xyz/?text=${text}`;
window.open(url, '_blank')?.focus();
}
function shareProposalX(space, proposal) {
const handle = space.twitter ? `@${space.twitter}` : space.name;
shareX(
`${encodeURIComponent(proposal.title)}%20${encodedProposalUrl(
space.id,
proposal
)}%20${handle}%20%23Snapshot`
);
}
function shareProposalHey(space, proposal) {
shareHey(
`${encodeURIComponent(proposal.title)}%20${encodedProposalUrl(
space.id,
proposal
)}&hashtags=Snapshot`
);
}
const { copyToClipboard } = useCopy();
function shareToClipboard(space, proposal) {
copyToClipboard(proposalUrl(space.id, proposal));
}
return {
shareProposalX,
shareProposalHey,
shareToClipboard,
proposalUrl,
shareProposal,
shareVote,
shareClaim,
sharingIsSupported: isSupported,
sharingItems
};
}
================================================
FILE: src/composables/useShortUrls.ts
================================================
const shortUrls = ref([]);
export function useShortUrls() {
async function fetchShortUrlData(): Promise {
try {
const response = await fetch(
'https://raw.githubusercontent.com/PeterDaveHello/url-shorteners/master/list'
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.text();
} catch (error: any) {
console.error(`Error fetching short URLs: ${error.message}`);
return '';
}
}
function parseShortUrlData(data: string): string[] {
const lines = data.split('\n');
return lines.filter(line => !line.startsWith('#') && line.trim() !== '');
}
async function getShortUrls(): Promise {
const rawData = await fetchShortUrlData();
return parseShortUrlData(rawData);
}
function containsShortUrl(text: string): boolean {
if (shortUrls.value.length === 0) return false;
return shortUrls.value.some(
shortUrl =>
text.includes(`http://${shortUrl}/`) ||
text.includes(`https://${shortUrl}/`) ||
text.includes(`.${shortUrl}/`)
);
}
onMounted(async () => {
if (shortUrls.value.length === 0) shortUrls.value = await getShortUrls();
});
return { containsShortUrl };
}
================================================
FILE: src/composables/useSkin.ts
================================================
import { SPACE_SKIN_QUERY } from '@/helpers/queries';
import { useStorage, usePreferredColorScheme } from '@vueuse/core';
export const DARK = 'dark';
export const LIGHT = 'light';
const preferredColor = usePreferredColorScheme();
const userTheme = useStorage('snapshot.userTheme', '');
function toggleUserTheme() {
if (userTheme.value === LIGHT) {
userTheme.value = DARK;
} else {
userTheme.value = LIGHT;
}
}
const theme = computed(() =>
[DARK, LIGHT].includes(userTheme.value)
? userTheme.value
: preferredColor.value
);
const skinClass = ref('default');
export function useSkin() {
const { apolloQuery } = useApolloQuery();
async function getSkin(domain: string): Promise {
if (domain) {
const space = await apolloQuery(
{
query: SPACE_SKIN_QUERY,
variables: {
id: domain
}
},
'space'
);
if (space?.skin) {
skinClass.value = space.skin;
document.body.classList.add(skinClass.value);
return space.skin;
}
}
return null;
}
const getThemeIcon = () => (theme.value === LIGHT ? 'moon' : 'sun');
watch(
theme,
() => {
document.documentElement.setAttribute(
'data-color-scheme',
theme.value === LIGHT ? 'light' : 'dark'
);
},
{ immediate: true }
);
return {
getThemeIcon,
toggleUserTheme,
getSkin
};
}
================================================
FILE: src/composables/useSkinsFilter.ts
================================================
/**
* Orders skins by spaces count and returns a list of skins
* filtered by the search string (case insensitive).
*/
import { SKINS_COUNT_QUERY } from '@/helpers/queries';
import skins from '@/../snapshot-spaces/skins';
const skinsSpacesCount: any = ref(null);
export function useSkinsFilter() {
const loading = ref(false);
const filterSkins = (q = '') =>
Object.keys(skins)
.filter(s => s.toLowerCase().includes(q.toLowerCase()))
.sort(
(a, b) =>
(skinsSpacesCount.value[b] ?? 0) - (skinsSpacesCount.value[a] ?? 0)
);
const { apolloQuery } = useApolloQuery();
async function getSkinsSpacesCount() {
if (skinsSpacesCount.value) return;
loading.value = true;
const res = await apolloQuery(
{
query: SKINS_COUNT_QUERY
},
'skins'
);
skinsSpacesCount.value = res.reduce(
(obj: any, item: any) => ({ ...obj, [item.id]: item.spacesCount }),
{}
);
loading.value = false;
}
return {
filterSkins,
getSkinsSpacesCount,
skinsSpacesCount,
loadingSkins: loading
};
}
================================================
FILE: src/composables/useSnapshot.ts
================================================
import { getBlockNumber } from '@snapshot-labs/snapshot.js/src/utils/web3';
import getProvider from '@snapshot-labs/snapshot.js/src/utils/provider';
const isLoading = ref(false);
const error = ref(false);
export function useSnapshot() {
async function getSnapshot(network: string): Promise {
try {
isLoading.value = true;
error.value = false;
const broviderUrl = import.meta.env.VITE_BROVIDER_URL;
const currentBlock = await getBlockNumber(
getProvider(network, { broviderUrl })
);
console.log('Snapshot block number', currentBlock);
return currentBlock - 4;
} catch (e) {
error.value = true;
return 0;
} finally {
isLoading.value = false;
}
}
return {
getSnapshot,
isSnapshotLoading: isLoading,
errorFetchingSnapshot: error
};
}
================================================
FILE: src/composables/useSpaceController.ts
================================================
import { getInstance } from '@snapshot-labs/lock/plugins/vue3';
import namehash from '@ensdomains/eth-ens-namehash';
import networks from '@snapshot-labs/snapshot.js/src/networks.json';
import { getAddress } from '@ethersproject/address';
import {
sendTransaction,
getEnsOwner,
getSpaceController
} from '@snapshot-labs/snapshot.js/src/utils';
const spaceControllerInput = ref('');
const modalWrongNetworkOpen = ref(false);
const modalConfirmSetTextRecordOpen = ref(false);
const settingENSRecord = ref(false);
const pendingENSRecord = ref(false);
const ensOwner = ref(null);
const spaceController = ref(null);
const defaultNetwork = import.meta.env.VITE_DEFAULT_NETWORK;
const broviderUrl = import.meta.env.VITE_BROVIDER_URL;
export function useSpaceController() {
const { web3, web3Account } = useWeb3();
const auth = getInstance();
const { t } = useI18n();
const route = useRoute();
const { domain } = useApp();
const { notify } = useFlashNotification();
const ensAbi = ['function setText(bytes32 node, string key, string value)'];
const networkKey = computed(() => web3.value.network.key);
const ensAddress = computed(
() => domain || route.params.ens || route.params.key
);
const isEnsOwner = computed(
() => ensOwner.value?.toLowerCase() === web3Account.value?.toLowerCase()
);
const isSpaceController = computed(
() =>
spaceController.value?.toLowerCase() === web3Account.value?.toLowerCase()
);
async function setRecord() {
settingENSRecord.value = true;
try {
const ensResolvers = networks[networkKey.value].ensResolvers;
const ensPublicResolverAddress = ensResolvers[0];
if (!ensPublicResolverAddress) {
throw new Error('No ENS resolver address for this network');
}
const ensname = ensAddress.value;
const node = namehash.hash(ensname);
const tx = await sendTransaction(
auth.web3,
ensPublicResolverAddress,
ensAbi,
'setText',
[node, 'snapshot', getAddress(spaceControllerInput.value)]
);
settingENSRecord.value = false;
notify(t('notify.transactionSent'));
return tx;
} catch (e) {
notify(['red', t('notify.somethingWentWrong')]);
console.log(e);
} finally {
settingENSRecord.value = false;
}
}
function confirmSetRecord() {
if (networkKey.value !== defaultNetwork) modalWrongNetworkOpen.value = true;
else modalConfirmSetTextRecordOpen.value = true;
}
async function loadEnsOwner() {
ensOwner.value = await getEnsOwner(ensAddress.value, defaultNetwork, {
broviderUrl
});
}
async function loadSpaceController() {
spaceController.value = await getSpaceController(
ensAddress.value,
defaultNetwork,
{ broviderUrl }
);
}
return {
spaceControllerInput,
settingENSRecord,
pendingENSRecord,
modalWrongNetworkOpen,
modalConfirmSetTextRecordOpen,
setRecord,
confirmSetRecord,
ensAddress,
loadEnsOwner,
ensOwner,
isEnsOwner,
loadSpaceController,
spaceController,
isSpaceController
};
}
================================================
FILE: src/composables/useSpaceSubscription.ts
================================================
import { SUBSCRIPTIONS_QUERY } from '@/helpers/queries';
// import { beams } from '@/helpers/beams';
import { useFlashNotification } from './useFlashNotification';
import client from '@/helpers/clientEIP712';
const subscriptions = ref(undefined);
export function useSpaceSubscription(spaceId: any) {
const { web3, web3Account } = useWeb3();
const { apolloQuery } = useApolloQuery();
const { setAlias, aliasWallet, isValidAlias, checkAlias } = useAliasAction();
const { notify } = useFlashNotification();
const { t } = useI18n();
const loading = ref(false);
const isSubscribed = computed(() => {
return (
subscriptions.value?.some((subscription: any) => {
return (
subscription.space.id === spaceId &&
subscription.address === web3Account.value
);
}) ?? false
);
});
async function loadSubscriptions() {
if (!web3Account.value) return;
loading.value = true;
try {
const spaceSubscriptions = await apolloQuery(
{
query: SUBSCRIPTIONS_QUERY,
variables: {
address: web3Account.value
}
},
'subscriptions'
);
if (spaceSubscriptions) {
subscriptions.value = spaceSubscriptions;
} else {
subscriptions.value = undefined;
}
} catch (e) {
console.error(e);
subscriptions.value = undefined;
} finally {
loading.value = false;
}
}
const configurePush = async () => {
try {
// if (!beams) {
// notify(['red', t('notificationsNotSupported')]);
// return;
// }
// await beams.start();
// await beams.addDeviceInterest(web3Account.value);
await client.subscribe(aliasWallet.value, aliasWallet.value.address, {
from: web3Account.value,
space: spaceId
});
} catch (error: any) {
// thrown by beams when the user denies the notification permission or browser doesn't support it
if (error.name === 'NotAllowedError')
notify(['red', t('notificationsBlocked')]);
console.error(error);
}
};
async function toggleSubscription() {
if (web3.value.authLoading) {
return null;
}
loading.value = true;
try {
await checkAlias();
if (!aliasWallet.value || !isValidAlias.value) {
await setAlias();
}
if (isSubscribed.value) {
await client.unsubscribe(aliasWallet.value, aliasWallet.value.address, {
from: web3Account.value,
space: spaceId
});
} else {
await configurePush();
}
await loadSubscriptions();
} catch (e) {
console.error(e);
} finally {
loading.value = false;
}
}
return {
toggleSubscription,
loading,
isSubscribed,
subscriptions,
loadSubscriptions
};
}
================================================
FILE: src/composables/useSpaces.ts
================================================
import { RankedSpace, Space } from '@/helpers/interfaces';
import { SPACES_QUERY, SPACES_RANKING_QUERY } from '@/helpers/queries';
interface Metrics {
total: number;
categories: Record;
}
export function useSpaces() {
const { apolloQuery } = useApolloQuery();
const loadingSpacesHome = ref(false);
const loadingMoreSpacesHome = ref(false);
const spacesHome = ref([]);
const spacesHomeMetrics = ref({ total: 0, categories: {} });
const enableSpaceHomeScroll = ref(false);
const loadingSpacesRanking = ref(false);
const loadingMoreSpacesRanking = ref(false);
const spacesRanking = ref([]);
const spacesRankingMetrics = ref({ total: 0, categories: {} });
const isLoadingSpaces = ref(false);
const isLoadingDeletedSpaces = ref(false);
const spaces = ref([]);
function _fetchRankedSpaces(variables: any = {}, skip = 0) {
return apolloQuery(
{
query: SPACES_RANKING_QUERY,
variables: {
skip,
first: 12,
private: false,
search: variables.search || undefined,
network: variables.network || undefined,
category: variables.category || undefined
}
},
'ranking'
);
}
async function loadSpacesHome(variables?: any) {
if (loadingSpacesHome.value) return;
loadingSpacesHome.value = true;
try {
const response = await _fetchRankedSpaces(variables);
if (!response) return;
spacesHome.value = response.items || [];
spacesHomeMetrics.value = response.metrics as Metrics;
loadingSpacesHome.value = false;
} catch (e) {
console.error(e);
} finally {
loadingSpacesHome.value = false;
}
}
async function loadMoreSpacesHome(variables?: any) {
if (
loadingMoreSpacesHome.value ||
spacesHomeMetrics.value.total <= spacesHome.value.length
)
return;
loadingMoreSpacesHome.value = true;
try {
const response = await _fetchRankedSpaces(
variables,
spacesHome.value.length
);
if (!response) return;
spacesHome.value = [...spacesHome.value, ...response.items];
spacesHomeMetrics.value = response.metrics as Metrics;
loadingMoreSpacesHome.value = false;
} catch (e) {
console.error(e);
} finally {
loadingMoreSpacesHome.value = false;
}
}
async function loadSpacesRanking(variables?: any) {
if (loadingSpacesRanking.value) return;
loadingSpacesRanking.value = true;
try {
const response = await _fetchRankedSpaces(variables);
if (!response) return;
spacesRanking.value = response.items;
spacesRankingMetrics.value = response.metrics as Metrics;
loadingSpacesRanking.value = false;
} catch (e) {
console.error(e);
} finally {
loadingSpacesRanking.value = false;
}
}
async function loadMoreSpacesRanking(variables?: any) {
if (
loadingMoreSpacesRanking.value ||
spacesRankingMetrics.value.total <= spacesRanking.value.length
)
return;
loadingMoreSpacesRanking.value = true;
try {
const response = await _fetchRankedSpaces(
variables,
spacesRanking.value.length
);
if (!response) return;
spacesRanking.value = [...spacesRanking.value, ...response.items];
spacesRankingMetrics.value = response.metrics as Metrics;
loadingMoreSpacesRanking.value = false;
} catch (e) {
console.error(e);
} finally {
loadingMoreSpacesRanking.value = false;
}
}
async function loadSpaces(id_in: string[]) {
if (isLoadingSpaces.value || !id_in.length) return;
isLoadingSpaces.value = true;
try {
const response = await apolloQuery(
{
query: SPACES_QUERY,
variables: {
id_in,
skip: 0,
first: 1000
}
},
'spaces'
);
if (!response) return;
spaces.value = response;
isLoadingSpaces.value = false;
} catch (e) {
console.error(e);
} finally {
isLoadingSpaces.value = false;
}
}
async function getDeletedSpaces(ids: string[]) {
isLoadingDeletedSpaces.value = true;
const results = await Promise.allSettled(
ids.map(async id => {
try {
const response = await fetch(
`${import.meta.env.VITE_HUB_URL}/api/spaces/${id}`,
{
headers: { 'Content-Type': 'application/json' }
}
);
return (await response.json())?.deleted === true ? id : null;
} catch (e) {
console.error(e);
return null;
}
})
);
isLoadingDeletedSpaces.value = false;
return results.map(r => r.value).filter(a => a);
}
return {
loadSpaces,
loadSpacesHome,
loadMoreSpacesHome,
loadSpacesRanking,
loadMoreSpacesRanking,
getDeletedSpaces,
spaces,
spacesHome,
spacesHomeMetrics,
isLoadingSpaces,
isLoadingDeletedSpaces,
loadingSpacesHome,
loadingMoreSpacesHome,
enableSpaceHomeScroll,
spacesRanking,
spacesRankingMetrics,
loadingSpacesRanking,
loadingMoreSpacesRanking
};
}
================================================
FILE: src/composables/useStatement.ts
================================================
import { STATEMENTS_QUERY } from '@/helpers/queries';
import { Statement } from '@/helpers/interfaces';
import { clone } from '@snapshot-labs/snapshot.js/src/utils';
const SET_STATEMENT_ACTION = 'set-statement';
const statements = ref>({});
const savedSpaceId = ref('');
export function useStatement() {
const { send, isSending } = useClient();
const { apolloQuery } = useApolloQuery();
const { notify } = useFlashNotification();
const { formatNumber, getNumberFormatter, getPercentFractionDigits } =
useIntl();
const loadingStatements = ref(false);
async function saveStatement(
spaceId: string,
statement: {
about: string;
statement: string;
discourse: string;
network: string;
status: string;
}
) {
const result = await send({ id: spaceId }, SET_STATEMENT_ACTION, statement);
if (!result.id) throw new Error('Error saving statement');
notify(['green', 'Statement saved successfully']);
return result;
}
async function loadStatements(spaceId: string, delegateIds: string[]) {
loadingStatements.value = true;
if (savedSpaceId.value !== spaceId) {
statements.value = {};
savedSpaceId.value = spaceId;
}
try {
const filteredDelegateIds = delegateIds.filter(
id => !statements.value[id]
);
if (!filteredDelegateIds.length) return;
const response: Statement[] = await apolloQuery(
{
query: STATEMENTS_QUERY,
variables: {
space: spaceId,
delegate_in: filteredDelegateIds
}
},
'statements'
);
if (!response) throw new Error('No statements found');
const newStatements = response.reduce(
(acc, statement) => {
acc[statement.delegate.toLowerCase()] = statement;
return acc;
},
{} as Record
);
statements.value = { ...statements.value, ...newStatements };
} catch (e) {
console.error(e);
} finally {
loadingStatements.value = false;
}
}
async function reloadStatement(spaceId: string, id: string) {
id = id.toLowerCase();
delete statements.value[id];
await loadStatements(spaceId, [id]);
}
function getStatement(id: string): {
about: string;
statement: string;
network: string;
status: string;
discourse: string;
} {
const defaultStatement = {
about: '',
statement: '',
network: 's',
status: 'INACTIVE',
discourse: ''
};
return clone(statements.value?.[id?.toLowerCase()] || defaultStatement);
}
function formatPercentageNumber(value: string | number) {
const fractionDigits = getPercentFractionDigits(Number(value));
return formatNumber(
Number(value),
getNumberFormatter({
style: 'percent',
minimumFractionDigits: fractionDigits,
maximumFractionDigits: fractionDigits
}).value
);
}
return {
statements: computed(() => statements.value),
loadingStatements: computed(() => loadingStatements.value),
savingStatement: computed(() => isSending.value),
saveStatement,
loadStatements,
reloadStatement,
getStatement,
formatPercentageNumber
};
}
================================================
FILE: src/composables/useStrategies.ts
================================================
/**
* Orders strategies by spaces count and returns a list of strategies
* filtered by the search string (case insensitive).
*/
import { STRATEGIES_QUERY, EXTENDED_STRATEGY_QUERY } from '@/helpers/queries';
import { Strategy } from '@/helpers/interfaces';
const strategies = ref([]);
const extendedStrategies = ref([]);
export function useStrategies() {
const isLoadingStrategies = ref(false);
const extendedStrategy = ref(null);
const loadingExtendedStrategy = ref(false);
const strategyDefinition = computed(() => {
if (extendedStrategy.value?.schema?.$ref) {
return extendedStrategy.value.schema.definitions.Strategy;
}
return false;
});
const filterStrategies = (q = '') =>
strategies.value.filter(s => s.id.toLowerCase().includes(q.toLowerCase()));
const { apolloQuery } = useApolloQuery();
// Get full list of strategies without about, schema and examples
async function getStrategies() {
if (strategies.value.length > 0) return;
isLoadingStrategies.value = true;
strategies.value = await apolloQuery(
{
query: STRATEGIES_QUERY
},
'strategies'
);
strategies.value = strategies.value.filter(
strategy => strategy.id !== 'pagination'
);
isLoadingStrategies.value = false;
}
// Get extended strategy by Id and save it in extendedStrategies
// don't load strategy if it's already loaded
async function getExtendedStrategy(id: string) {
if (extendedStrategies.value.some(st => st?.id === id)) {
extendedStrategy.value =
extendedStrategies.value.find(st => st?.id === id) ?? null;
return;
}
loadingExtendedStrategy.value = true;
const strategyObj = await apolloQuery(
{
query: EXTENDED_STRATEGY_QUERY,
variables: { id }
},
'strategy'
);
if (strategyObj) {
extendedStrategies.value.push(strategyObj);
extendedStrategy.value = strategyObj;
}
loadingExtendedStrategy.value = false;
}
return {
filterStrategies,
getStrategies,
getExtendedStrategy,
strategies,
isLoadingStrategies,
extendedStrategy,
strategyDefinition,
loadingExtendedStrategy
};
}
================================================
FILE: src/composables/useTerms.ts
================================================
import { lsSet, lsGet } from '@/helpers/utils';
export function useTerms(spaceKey) {
const modalTermsOpen = ref(false);
const acceptedSpaces = ref(JSON.parse(lsGet('acceptedTerms', '[]')));
const termsAccepted = ref(acceptedSpaces.value.includes(spaceKey));
function acceptTerms() {
acceptedSpaces.value.push(spaceKey);
lsSet('acceptedTerms', JSON.stringify(acceptedSpaces.value));
termsAccepted.value = true;
}
return { modalTermsOpen, termsAccepted, acceptTerms };
}
================================================
FILE: src/composables/useTreasury.ts
================================================
import snapshot from '@snapshot-labs/snapshot.js';
import { getTokenBalances, ETHER_CONTRACT } from '@/helpers/covalent';
import { TreasuryAsset } from '@/helpers/interfaces';
const TOKEN_LIST_URL = 'https://ipfs.io/ipns/tokens.uniswap.org';
const tokenListContractAddresses = ref(null);
const treasuryAssets = ref<{ [key: string]: TreasuryAsset[] }>({});
export function useTreasury() {
async function loadTokenList() {
if (tokenListContractAddresses.value) return;
const response = await snapshot.utils.getJSON(TOKEN_LIST_URL);
tokenListContractAddresses.value = [
ETHER_CONTRACT,
...response.tokens.map(token => token.address.toLowerCase())
];
}
const loading = ref(false);
async function loadFilteredTokenBalances(address: string, chainId: string) {
loading.value = true;
try {
await loadTokenList();
if (!tokenListContractAddresses.value)
throw new Error('Token list not found');
if (treasuryAssets.value[address]) return;
const balances = await getTokenBalances(address, chainId)
.then(
balances =>
balances?.filter(
balance =>
tokenListContractAddresses.value?.includes(
balance.contract_address
)
)
)
.catch(() => []);
if (balances) treasuryAssets.value[address] = balances;
} catch (error) {
console.error(error);
} finally {
loading.value = false;
}
}
function resetTreasuryAssets() {
treasuryAssets.value = {};
}
return {
loadFilteredTokenBalances,
resetTreasuryAssets,
treasuryAssets: computed(() => treasuryAssets.value),
loadingBalances: computed(() => loading.value)
};
}
================================================
FILE: src/composables/useTxStatus.ts
================================================
import { useStorage } from '@vueuse/core';
import getProvider from '@snapshot-labs/snapshot.js/src/utils/provider';
import { PendingTransaction } from '@/helpers/interfaces';
const PENDING_TRANSACTIONS_STORAGE_KEY = 'snapshot.pendingTransactions';
const pendingTransactions = useStorage(
PENDING_TRANSACTIONS_STORAGE_KEY,
[] as PendingTransaction[]
);
export function useTxStatus() {
const { web3 } = useWeb3();
const pendingTransactionsWithHash = computed(() => {
return pendingTransactions.value.filter(tx => tx.hash);
});
const createPendingTransaction = (hash?: string) => {
const createdAt = Date.now();
const id = createdAt.toString();
const tx = {
id,
network: web3.value.network.key,
createdAt,
hash: hash ?? null
};
pendingTransactions.value.push(tx);
return id;
};
const updatePendingTransaction = (
id: string,
payload: Partial
) => {
const tx = pendingTransactions.value.find(tx => tx.id === id);
if (tx) {
Object.assign(tx, payload);
}
};
const removePendingTransaction = (id: string) => {
const tx = pendingTransactions.value.find(tx => tx.id === id);
if (tx) {
pendingTransactions.value = pendingTransactions.value.filter(
tx => tx.id !== id
);
}
};
const restorePendingTransactions = () => {
pendingTransactions.value.forEach(async tx => {
if (tx.hash) {
if (Date.now() > tx.createdAt + 1000 * 60)
return removePendingTransaction(tx.id);
try {
const broviderUrl = import.meta.env.VITE_BROVIDER_URL;
const provider = getProvider(tx.network, { broviderUrl });
await provider.waitForTransaction(tx.hash, 1, 1000 * 60 * 4);
} finally {
removePendingTransaction(tx.id);
}
} else {
if (Date.now() > tx.createdAt + 1000 * 10)
return removePendingTransaction(tx.id);
}
});
};
return {
pendingTransactions,
pendingTransactionsWithHash,
createPendingTransaction,
updatePendingTransaction,
removePendingTransaction,
restorePendingTransactions
};
}
================================================
FILE: src/composables/useUnseenProposals.ts
================================================
import { subgraphRequest } from '@snapshot-labs/snapshot.js/src/utils';
import { useStorage } from '@vueuse/core';
type proposal = { id: string; created: number; space: { id: string } };
const proposals = ref([]);
const lastSeenProposals = useStorage('lastSeenProposals', {});
export function useUnseenProposals() {
const { followingSpaces } = useFollowSpace();
async function getProposals() {
if (followingSpaces.value.length === 0) return;
try {
const activeProposals = await subgraphRequest(
`${import.meta.env.VITE_HUB_URL}/graphql`,
{
proposals: {
__args: {
first: 200,
where: {
space_in: followingSpaces.value
}
},
id: true,
created: true,
space: {
id: true
}
}
}
);
proposals.value = activeProposals.proposals;
} catch (e) {
console.log(e);
}
}
watch(followingSpaces, getProposals);
const { web3Account } = useWeb3();
function emitUpdateLastSeenProposal(spaceId: string) {
if (!web3Account.value || !spaceId) return;
lastSeenProposals.value[web3Account.value] =
lastSeenProposals.value[web3Account.value] || {};
lastSeenProposals.value[web3Account.value][spaceId] = new Date().getTime();
}
function spaceHasUnseenProposals(spaceId: string) {
return proposals.value.some(p => {
return (
p.space.id === spaceId &&
p.created >
(lastSeenProposals.value?.[web3Account.value]?.[spaceId] || 0)
);
});
}
return {
proposals,
spaceHasUnseenProposals,
emitUpdateLastSeenProposal
};
}
================================================
FILE: src/composables/useUsername.ts
================================================
import { shorten } from '@/helpers/utils';
import { Profile } from '@/helpers/interfaces';
export function useUsername() {
const { web3Account } = useWeb3();
function getUsername(address: string, profile?: Profile) {
if (
web3Account?.value &&
address.toLowerCase() === web3Account.value.toLowerCase()
) {
return 'You';
}
if (profile?.name) return profile.name;
if (profile?.ens) return profile.ens;
return shorten(address);
}
return { getUsername };
}
================================================
FILE: src/composables/useWeb3.ts
================================================
import { Web3Provider } from '@ethersproject/providers';
import { getInstance } from '@snapshot-labs/lock/plugins/vue3';
import networks from '@snapshot-labs/snapshot.js/src/networks.json';
import { formatUnits } from '@ethersproject/units';
let auth;
const defaultNetwork: any =
import.meta.env.VITE_DEFAULT_NETWORK || Object.keys(networks)[0];
const state = reactive<{
account: string;
network: Record;
authLoading: boolean;
walletConnectType: string | null;
}>({
account: '',
network: networks[defaultNetwork],
authLoading: false,
walletConnectType: null
});
export function useWeb3() {
async function login(connector = 'injected') {
auth = getInstance();
state.authLoading = true;
await auth.login(connector);
if (auth.provider.value) {
auth.web3 = new Web3Provider(auth.provider.value, 'any');
await loadProvider();
}
state.authLoading = false;
}
function logout() {
auth = getInstance();
auth.logout();
state.account = '';
}
async function loadProvider() {
try {
if (
auth.provider.value.removeAllListeners &&
!auth.provider.value.isTorus
)
auth.provider.value.removeAllListeners();
if (auth.provider.value.on) {
try {
auth.provider.value.on('chainChanged', async chainId => {
handleChainChanged(parseInt(formatUnits(chainId, 0)));
});
auth.provider.value.on('accountsChanged', async accounts => {
if (accounts.length !== 0) {
await login();
}
});
} catch (e) {
console.log(`failed to subscribe to events for provider: ${e}`);
}
}
console.log('Provider', auth.provider.value);
let network, accounts;
try {
const connector = auth.provider.value?.connectorName;
if (connector === 'gnosis') {
const { chainId: safeChainId, safeAddress } = auth.web3.provider.safe;
network = { chainId: safeChainId };
accounts = [safeAddress];
} else {
[network, accounts] = await Promise.all([
auth.web3.getNetwork(),
auth.web3.listAccounts()
]);
}
} catch (e) {
console.log(e);
}
console.log('Network', network);
console.log('Accounts', accounts);
handleChainChanged(network.chainId);
const acc = accounts.length > 0 ? accounts[0] : null;
state.account = acc;
state.walletConnectType = auth.provider.value?.wc?.peerMeta?.name || null;
} catch (e) {
state.account = '';
return Promise.reject(e);
}
}
function handleChainChanged(chainId) {
if (!networks[chainId]) {
networks[chainId] = {
...networks[defaultNetwork],
chainId,
name: 'Unknown',
network: 'unknown',
unknown: true
};
}
state.network = networks[chainId];
}
return {
login,
logout,
loadProvider,
handleChainChanged,
web3: computed(() => state),
web3Account: computed(() => state.account)
};
}
================================================
FILE: src/env.d.ts
================================================
interface ImportMetaEnv {
readonly VITE_APP_TITLE: string;
readonly VITE_HUB_URL: string;
readonly VITE_RELAYER_URL: string;
readonly VITE_SCORES_URL: string;
readonly VITE_IPFS_GATEWAY: string;
readonly VITE_DEFAULT_NETWORK: string;
readonly VITE_PUSHER_BEAMS_INSTANCE_ID: string;
readonly VITE_BROVIDER_URL: string;
}
================================================
FILE: src/helpers/alchemy/index.ts
================================================
import { ETH_CONTRACT } from '@/helpers/constants';
import {
GetTokenBalancesResponse,
GetTokensMetadataResponse,
GetBalancesResponse
} from './types';
export * from './types';
const apiKey = import.meta.env.VITE_ALCHEMY_API_KEY;
const NETWORKS = {
1: 'eth-mainnet',
11155111: 'eth-sepolia',
137: 'polygon-mainnet',
42161: 'arb-mainnet',
8453: 'base-mainnet'
};
function getApiUrl(networkId: number) {
const network = NETWORKS[networkId] ?? 'mainnet';
return `https://${network}.g.alchemy.com/v2/${apiKey}`;
}
export async function request(
method: string,
params: any[],
networkId: number
) {
const res = await fetch(getApiUrl(networkId), {
method: 'POST',
body: JSON.stringify({
id: 1,
jsonrpc: '2.0',
method,
params
})
});
const { result } = await res.json();
return result;
}
export async function batchRequest(
requests: { method: string; params: any[] }[],
networkId: number
) {
const res = await fetch(getApiUrl(networkId), {
method: 'POST',
body: JSON.stringify(
requests.map((request, i) => ({
id: i,
jsonrpc: '2.0',
method: request.method,
params: request.params
}))
)
});
const response = await res.json();
return response.map(entry => entry.result);
}
/**
* Gets Ethereum balance as hex encoded string.
* @param address Ethereum address to fetch ETH balance for
* @param networkId Network ID
* @returns Hex encoded ETH balance
*/
export async function getBalance(
address: string,
networkId: number
): Promise {
return request('eth_getBalance', [address], networkId);
}
/**
* Gets ERC20 balances of tokens that provided address interacted with.
* Response might include 0 balances.
* @param address Ethereum address to fetch token balances for
* @param networkId Network ID
* @returns Token balances
*/
export async function getTokenBalances(
address: string,
networkId: number
): Promise {
return request('alchemy_getTokenBalances', [address], networkId);
}
/**
* Gets ERC20 tokens metadata (name, symbol, decimals, logo).
* @param addresses Array of ERC20 tokens addresses
* @param networkId Network ID
* @returns Array of token metadata
*/
export async function getTokensMetadata(
addresses: string[],
networkId: number
): Promise {
return batchRequest(
addresses.map(address => ({
method: 'alchemy_getTokenMetadata',
params: [address]
})),
networkId
);
}
/**
* Gets Ethereum and ERC20 balances including metadata for tokens.
* @param address Ethereum address to fetch balances for
* @param networkId Network ID
* @returns Array of balances
*/
export async function getBalances(
address: string,
networkId: number,
baseToken: { name: string; symbol: string; logo?: string }
): Promise {
if (!address) {
return [
{
name: baseToken.name,
symbol: baseToken.symbol,
decimals: 18,
contractAddress: ETH_CONTRACT,
tokenBalance: '0x0',
price: 0,
value: 0,
change: 0
}
];
}
const [ethBalance, { tokenBalances }] = await Promise.all([
getBalance(address, networkId),
getTokenBalances(address, networkId)
]);
const contractAddresses = tokenBalances.map(
balance => balance.contractAddress
);
const metadata = await getTokensMetadata(contractAddresses, networkId);
return [
{
name: baseToken.name,
symbol: baseToken.symbol,
decimals: 18,
contractAddress: ETH_CONTRACT,
tokenBalance: ethBalance,
price: 0,
value: 0,
change: 0
},
...tokenBalances
.map((balance, i) => ({
...balance,
...metadata[i],
price: 0,
value: 0,
change: 0
}))
.filter(token => !token.symbol?.includes('.'))
];
}
================================================
FILE: src/helpers/alchemy/types.ts
================================================
export type BalanceData = { contractAddress: string; tokenBalance: string };
export type Metadata = {
decimals: number;
name: string;
symbol: string;
};
export type Token = BalanceData &
Metadata & {
price: number;
value: number;
change: number;
};
export type GetTokenBalancesResponse = {
address: string;
tokenBalances: BalanceData[];
};
export type GetTokensMetadataResponse = Metadata[];
export type GetBalancesResponse = Token[];
================================================
FILE: src/helpers/apollo.ts
================================================
import gql from 'graphql-tag';
import {
ApolloClient,
createHttpLink,
InMemoryCache
} from '@apollo/client/core';
// HTTP connection to the API
const httpLink = createHttpLink({
// You should use an absolute URL here
uri: `${import.meta.env.VITE_HUB_URL}/graphql`
});
// Create the apollo client
export const apolloClient = new ApolloClient({
link: httpLink,
cache: new InMemoryCache({
addTypename: false
}),
defaultOptions: {
query: {
fetchPolicy: 'no-cache'
}
},
typeDefs: gql`
enum OrderDirection {
asc
desc
}
`
});
================================================
FILE: src/helpers/auth.ts
================================================
import injected from '@snapshot-labs/lock/connectors/injected';
import walletconnect from '@snapshot-labs/lock/connectors/walletconnect';
import portis from '@snapshot-labs/lock/connectors/portis';
import connectors from '@/helpers/connectors';
import walletlink from '@snapshot-labs/lock/connectors/walletlink';
import gnosis from '@snapshot-labs/lock/connectors/gnosis';
import stargazer from '@snapshot-labs/lock/connectors/stargazer';
import kaikas from '@snapshot-labs/lock/connectors/kaikas';
const options: any = { connectors: [] };
const lockConnectors = {
injected,
walletconnect,
walletlink,
portis,
stargazer,
gnosis,
kaikas
};
Object.entries(connectors).forEach(([connectorName, params]: [string, any]) => {
options.connectors.push({
key: connectorName,
connector: lockConnectors[connectorName],
options: params.options
});
});
export default options;
================================================
FILE: src/helpers/b64.ts
================================================
/// URL-safe Base64 encoding and decoding.
const B64U_LOOKUP = {
'/': '_',
_: '/',
'+': '-',
'-': '+',
'=': '.',
'.': '='
};
export const encode = str =>
btoa(str).replace(/(\+|\/|=)/g, m => B64U_LOOKUP[m]);
export const decode = str =>
atob(str.replace(/(-|_|\.)/g, m => B64U_LOOKUP[m]));
export const encodeJson = json => encode(JSON.stringify(json));
export const decodeJson = str => JSON.parse(decode(str));
================================================
FILE: src/helpers/beams.ts
================================================
import * as PusherPushNotifications from '@pusher/push-notifications-web';
let beams: any;
try {
beams = new PusherPushNotifications.Client({
instanceId: (import.meta.env.VITE_PUSHER_BEAMS_INSTANCE_ID as string) ?? ''
});
} catch (e) {
console.log(e);
}
export { beams };
================================================
FILE: src/helpers/boost/abi.json
================================================
[
{
"inputs": [
{
"internalType": "address",
"name": "_protocolOwner",
"type": "address"
},
{ "internalType": "string", "name": "name", "type": "string" },
{ "internalType": "string", "name": "symbol", "type": "string" },
{ "internalType": "string", "name": "version", "type": "string" },
{ "internalType": "uint256", "name": "_ethFee", "type": "uint256" },
{ "internalType": "uint256", "name": "_tokenFee", "type": "uint256" }
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{ "inputs": [], "name": "BoostDepositRequired", "type": "error" },
{ "inputs": [], "name": "BoostDoesNotExist", "type": "error" },
{ "inputs": [], "name": "BoostEndDateBeforeStart", "type": "error" },
{ "inputs": [], "name": "BoostEndDateInPast", "type": "error" },
{ "inputs": [], "name": "BoostEnded", "type": "error" },
{
"inputs": [{ "internalType": "uint256", "name": "end", "type": "uint256" }],
"name": "BoostNotEnded",
"type": "error"
},
{
"inputs": [
{ "internalType": "uint256", "name": "start", "type": "uint256" }
],
"name": "BoostNotStarted",
"type": "error"
},
{ "inputs": [], "name": "ClaimingPeriodStarted", "type": "error" },
{ "inputs": [], "name": "InsufficientBoostBalance", "type": "error" },
{ "inputs": [], "name": "InsufficientEthFee", "type": "error" },
{ "inputs": [], "name": "InvalidGuard", "type": "error" },
{ "inputs": [], "name": "InvalidRecipient", "type": "error" },
{ "inputs": [], "name": "InvalidSignature", "type": "error" },
{ "inputs": [], "name": "InvalidTokenFee", "type": "error" },
{ "inputs": [], "name": "OnlyBoostOwner", "type": "error" },
{ "inputs": [], "name": "RecipientAlreadyClaimed", "type": "error" },
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "approved",
"type": "address"
},
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"indexed": false,
"internalType": "bool",
"name": "approved",
"type": "bool"
}
],
"name": "ApprovalForAll",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "boostId",
"type": "uint256"
}
],
"name": "Burn",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"components": [
{ "internalType": "uint256", "name": "boostId", "type": "uint256" },
{ "internalType": "address", "name": "recipient", "type": "address" },
{ "internalType": "uint256", "name": "amount", "type": "uint256" }
],
"indexed": false,
"internalType": "struct IBoost.ClaimConfig",
"name": "claim",
"type": "tuple"
}
],
"name": "Claim",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "boostId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "Deposit",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "ethFee",
"type": "uint256"
}
],
"name": "EthFeeSet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "recipient",
"type": "address"
}
],
"name": "EthFeesCollected",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "boostId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"components": [
{
"internalType": "contract IERC20",
"name": "token",
"type": "address"
},
{ "internalType": "uint256", "name": "balance", "type": "uint256" },
{ "internalType": "address", "name": "guard", "type": "address" },
{ "internalType": "uint48", "name": "start", "type": "uint48" },
{ "internalType": "uint48", "name": "end", "type": "uint48" }
],
"indexed": false,
"internalType": "struct IBoost.BoostConfig",
"name": "boost",
"type": "tuple"
},
{
"indexed": false,
"internalType": "string",
"name": "strategyURI",
"type": "string"
}
],
"name": "Mint",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "tokenFee",
"type": "uint256"
}
],
"name": "TokenFeeSet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "contract IERC20",
"name": "token",
"type": "address"
},
{
"indexed": false,
"internalType": "address",
"name": "recipient",
"type": "address"
}
],
"name": "TokenFeesCollected",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
},
{
"inputs": [
{ "internalType": "address", "name": "to", "type": "address" },
{ "internalType": "uint256", "name": "tokenId", "type": "uint256" }
],
"name": "approve",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{ "internalType": "address", "name": "owner", "type": "address" }
],
"name": "balanceOf",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"name": "boosts",
"outputs": [
{ "internalType": "contract IERC20", "name": "token", "type": "address" },
{ "internalType": "uint256", "name": "balance", "type": "uint256" },
{ "internalType": "address", "name": "guard", "type": "address" },
{ "internalType": "uint48", "name": "start", "type": "uint48" },
{ "internalType": "uint48", "name": "end", "type": "uint48" }
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"components": [
{ "internalType": "uint256", "name": "boostId", "type": "uint256" },
{ "internalType": "address", "name": "recipient", "type": "address" },
{ "internalType": "uint256", "name": "amount", "type": "uint256" }
],
"internalType": "struct IBoost.ClaimConfig",
"name": "_claimConfig",
"type": "tuple"
},
{ "internalType": "bytes", "name": "_signature", "type": "bytes" }
],
"name": "claim",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"components": [
{ "internalType": "uint256", "name": "boostId", "type": "uint256" },
{ "internalType": "address", "name": "recipient", "type": "address" },
{ "internalType": "uint256", "name": "amount", "type": "uint256" }
],
"internalType": "struct IBoost.ClaimConfig[]",
"name": "_claimConfigs",
"type": "tuple[]"
},
{ "internalType": "bytes[]", "name": "_signatures", "type": "bytes[]" }
],
"name": "claimMultiple",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{ "internalType": "uint256", "name": "", "type": "uint256" },
{ "internalType": "address", "name": "", "type": "address" }
],
"name": "claimed",
"outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{ "internalType": "address", "name": "_recipient", "type": "address" }
],
"name": "collectEthFees",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "contract IERC20",
"name": "_token",
"type": "address"
},
{ "internalType": "address", "name": "_recipient", "type": "address" }
],
"name": "collectTokenFees",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{ "internalType": "uint256", "name": "_boostId", "type": "uint256" },
{ "internalType": "uint256", "name": "_amount", "type": "uint256" }
],
"name": "deposit",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "ethFee",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{ "internalType": "uint256", "name": "tokenId", "type": "uint256" }
],
"name": "getApproved",
"outputs": [{ "internalType": "address", "name": "", "type": "address" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{ "internalType": "address", "name": "owner", "type": "address" },
{ "internalType": "address", "name": "operator", "type": "address" }
],
"name": "isApprovedForAll",
"outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{ "internalType": "string", "name": "_strategyURI", "type": "string" },
{
"internalType": "contract IERC20",
"name": "_token",
"type": "address"
},
{ "internalType": "uint256", "name": "_amount", "type": "uint256" },
{ "internalType": "address", "name": "_owner", "type": "address" },
{ "internalType": "address", "name": "_guard", "type": "address" },
{ "internalType": "uint48", "name": "_start", "type": "uint48" },
{ "internalType": "uint48", "name": "_end", "type": "uint48" }
],
"name": "mint",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "name",
"outputs": [{ "internalType": "string", "name": "", "type": "string" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "nextBoostId",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [{ "internalType": "address", "name": "", "type": "address" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{ "internalType": "uint256", "name": "tokenId", "type": "uint256" }
],
"name": "ownerOf",
"outputs": [{ "internalType": "address", "name": "", "type": "address" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{ "internalType": "address", "name": "from", "type": "address" },
{ "internalType": "address", "name": "to", "type": "address" },
{ "internalType": "uint256", "name": "tokenId", "type": "uint256" }
],
"name": "safeTransferFrom",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{ "internalType": "address", "name": "from", "type": "address" },
{ "internalType": "address", "name": "to", "type": "address" },
{ "internalType": "uint256", "name": "tokenId", "type": "uint256" },
{ "internalType": "bytes", "name": "data", "type": "bytes" }
],
"name": "safeTransferFrom",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{ "internalType": "address", "name": "operator", "type": "address" },
{ "internalType": "bool", "name": "approved", "type": "bool" }
],
"name": "setApprovalForAll",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{ "internalType": "uint256", "name": "_ethFee", "type": "uint256" }
],
"name": "setEthFee",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{ "internalType": "uint256", "name": "_tokenFee", "type": "uint256" }
],
"name": "setTokenFee",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{ "internalType": "bytes4", "name": "interfaceId", "type": "bytes4" }
],
"name": "supportsInterface",
"outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "symbol",
"outputs": [{ "internalType": "string", "name": "", "type": "string" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "tokenFee",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [{ "internalType": "address", "name": "", "type": "address" }],
"name": "tokenFeeBalances",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{ "internalType": "uint256", "name": "tokenId", "type": "uint256" }
],
"name": "tokenURI",
"outputs": [{ "internalType": "string", "name": "", "type": "string" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{ "internalType": "address", "name": "from", "type": "address" },
{ "internalType": "address", "name": "to", "type": "address" },
{ "internalType": "uint256", "name": "tokenId", "type": "uint256" }
],
"name": "transferFrom",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{ "internalType": "address", "name": "newOwner", "type": "address" }
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{ "internalType": "uint256", "name": "_boostId", "type": "uint256" },
{ "internalType": "address", "name": "_to", "type": "address" }
],
"name": "withdrawAndBurn",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]
================================================
FILE: src/helpers/boost/api.ts
================================================
import {
BoostRewardGuard,
BoostVoucherGuard,
BoostWinnersGuard,
BoostSubgraph
} from '@/helpers/boost/types';
const GUARD_URL = 'https://boost.snapshot.org';
export async function getRewards(
proposal_id: string,
voter_address: string,
boosts: BoostSubgraph[]
): Promise {
const results = await fetch(`${GUARD_URL}/get-rewards`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
proposal_id,
voter_address,
boosts: boosts.map(boost => [boost.id, boost.chainId])
})
});
if (results.status !== 200) throw new Error('Error fetching rewards');
return results.json();
}
export async function getVouchers(
proposal_id: string,
voter_address: string,
boosts: BoostSubgraph[]
): Promise {
const results = await fetch(`${GUARD_URL}/create-vouchers`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
proposal_id,
voter_address,
boosts: boosts.map(boost => [boost.id, boost.chainId])
})
});
if (results.status !== 200) throw new Error('Error fetching vouchers');
return results.json();
}
export async function getWinners(
proposal_id: string,
boost_id: string,
chain_id: string
): Promise {
const results = await fetch(`${GUARD_URL}/get-lottery-winners`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
proposal_id,
boost_id,
chain_id
})
});
if (results.status !== 200) {
const text = await results.text();
throw new Error(text ? text : 'Error fetching winners');
}
return results.json();
}
================================================
FILE: src/helpers/boost/index.ts
================================================
import { Web3Provider } from '@ethersproject/providers';
import { Contract } from '@ethersproject/contracts';
import { parseEther } from '@ethersproject/units';
import { pin } from '@snapshot-labs/pineapple';
import { BoostStrategy } from '@/helpers/boost/types';
import ABI from './abi.json';
export const BOOST_VERSION = '0.0.1';
export const BOOST_CONTRACTS = {
'1': '0x8E8913197114c911F13cfBfCBBD138C1DC74B964',
'11155111': '0x8E8913197114c911F13cfBfCBBD138C1DC74B964',
'137': '0x8E8913197114c911F13cfBfCBBD138C1DC74B964',
'8453': '0x8E8913197114c911F13cfBfCBBD138C1DC74B964'
};
export const SUPPORTED_NETWORKS = Object.keys(BOOST_CONTRACTS);
export const SNAPSHOT_GUARD_ADDRESS =
'0x064417ab192edC00E791d5911ecDbb7c9a718383';
export async function createBoost(
web3: Web3Provider,
networkId: string,
ethFee: string,
params: {
strategyURI: string;
token: string;
amount: string;
owner: string;
guard: string;
start: number;
end: number;
}
): Promise {
const { strategyURI, token, amount, guard, start, end, owner } = params;
const signer = web3.getSigner();
const contract = new Contract(BOOST_CONTRACTS[networkId], ABI, signer);
const options = { value: parseEther(ethFee) };
return await contract.mint(
strategyURI,
token,
amount,
owner,
guard,
start,
end,
options
);
}
export async function claimTokens(
web3: Web3Provider,
networkId: string,
boost: {
boostId: string;
recipient: string;
amount: string;
},
signature: string
): Promise {
const { boostId, recipient, amount } = boost;
const signer = web3.getSigner();
const contract = new Contract(BOOST_CONTRACTS[networkId], ABI, signer);
return await contract.claim([boostId, recipient, amount], signature);
}
export async function claimAllTokens(
web3: Web3Provider,
networkId: string,
boosts: {
boostId: string;
recipient: string;
amount: string;
}[],
signatures: string[]
): Promise {
const boostsArray = boosts.map(boost => [
boost.boostId,
boost.recipient,
boost.amount
]);
const signer = web3.getSigner();
const contract = new Contract(BOOST_CONTRACTS[networkId], ABI, signer);
return await contract.claimMultiple(boostsArray, signatures);
}
export async function getStrategyURI(strategy: BoostStrategy) {
const { cid } = await pin(strategy);
return `ipfs://${cid}`;
}
export async function withdrawAndBurn(
web3: Web3Provider,
networkId: string,
boostId: string,
to: string
): Promise {
const signer = web3.getSigner();
const contract = new Contract(BOOST_CONTRACTS[networkId], ABI, signer);
return await contract.withdrawAndBurn(boostId, to);
}
export async function getFees(web3: Web3Provider, networkId: string) {
const contract = new Contract(BOOST_CONTRACTS[networkId], ABI, web3);
const ethFee = await contract.ethFee();
const tokenFeePercent = await contract.tokenFee();
return { ethFee, tokenFeePercent };
}
================================================
FILE: src/helpers/boost/subgraph.ts
================================================
import { subgraphRequest } from '@snapshot-labs/snapshot.js/src/utils';
import { SUPPORTED_NETWORKS } from '@/helpers/boost';
import { BoostSubgraph } from '@/helpers/boost/types';
const SUBGRAPH_URLS = {
'1': 'https://subgrapher.snapshot.org/subgraph/arbitrum/A6EEuSAB7mFrWvLBnL1HZXwfiGfqFYnFJjc14REtMNkd',
'11155111':
'https://subgrapher.snapshot.org/subgraph/arbitrum/6T64qrPe7S46zhArSoBF8CAmc5cG3PyKa92Nt4Jhymcy',
'137':
'https://subgrapher.snapshot.org/subgraph/arbitrum/CkNpf5gY7XPCinJWP1nh8K7u6faXwDjchGGV4P9rgJ7',
'8453':
'https://subgrapher.snapshot.org/subgraph/arbitrum/52uVpyUHkkMFieRk1khbdshUw26CNHWAEuqLojZzcyjd'
};
export async function getClaims(recipient: string) {
async function query(chainId: string) {
const data = await subgraphRequest(SUBGRAPH_URLS[chainId], {
claims: {
__args: {
where: {
recipient
}
},
id: true,
amount: true,
transactionHash: true,
boost: {
id: true
}
}
});
if (data?.claims) {
data.claims = data.claims.map(claim => ({
...claim,
chainId
}));
}
return data;
}
const requests = SUPPORTED_NETWORKS.map(chainId => query(chainId));
const responses = await Promise.all(requests);
return responses.map(response => response.claims).flat();
}
export async function getBoosts(proposalIds: string[]) {
async function query(chainId: string) {
const data = await subgraphRequest(SUBGRAPH_URLS[chainId], {
boosts: {
__args: {
where: { strategy_: { proposal_in: proposalIds } }
},
id: true,
strategyURI: true,
poolSize: true,
guard: true,
start: true,
end: true,
owner: true,
currentBalance: true,
transaction: true,
token: {
id: true,
name: true,
symbol: true,
decimals: true
},
strategy: {
id: true,
name: true,
version: true,
proposal: true,
eligibility: {
type: true,
choice: true
},
distribution: {
type: true,
limit: true,
numWinners: true
}
}
}
});
if (data?.boosts) {
data.boosts = data.boosts.map(boost => ({
...boost,
chainId
}));
}
return data;
}
const requests = SUPPORTED_NETWORKS.map(chainId => query(chainId));
const responses: { boosts: BoostSubgraph }[] = await Promise.all(requests);
return responses
.map(response => response.boosts)
.filter(boost => boost)
.flat();
}
================================================
FILE: src/helpers/boost/tokens.ts
================================================
export const EXCLUDED_TOKENS = [
{
symbol: 'default',
'1': '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
'11155111': '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
'137': '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
'8453': '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'
},
{
symbol: 'USDT',
'1': '0xdac17f958d2ee523a2206206994597c13d831ec7',
'137': '0xc2132d05d31c914a87c6611c10748aeb04b58e8f'
},
{
symbol: 'USDC',
'1': '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
'137': '0x3c499c542cef5e3811e1192ce70d8cc03d5c3359'
},
{
symbol: 'DAI',
'1': '0x6b175474e89094c44da98b954eedeac495271d0f',
'137': '0x8f3cf7ad23cd3cadbd9735aff958023239c6a063'
},
{
symbol: 'FDUSD',
'1': '0xc5f0f7b66764f6ec8c8dff7ba683102295e16409',
'137': ''
},
{
symbol: 'TUSD',
'1': '0x0000000000085d4780b73119b644ae5ecd22b376',
'137': ''
},
{
symbol: 'FRAX',
'1': '0x853d955acef822db058eb8505911ed77f175b99e',
'137': '0x45c32fa6df82ead1e2ef74d17b76547eddfaff89'
},
{
symbol: 'GUSD',
'1': '0x056fd409e1d7a124bd7017459dfea2f387b6d5cd',
'137': ''
},
{
symbol: 'PYUSD',
'1': '0x6c3ea9036406852006290770bedfcaba0e23a0e8',
'137': ''
},
{
symbol: 'sUSD',
'1': '0x57ab1ec28d129707052df4df418d58a2d46d5f51',
'137': ''
},
{
symbol: 'USDP',
'1': '0x8e870d67f660d95d5be530380d0ec0bd388289e1',
'137': ''
},
{
symbol: 'LUSD',
'1': '0x5f98805a4e8be255a32880fdec7f6728c6568ba0',
'137': '0x23001f892c0c82b79303edc9b9033cd190bb21c7'
},
{
symbol: 'WBTC',
'1': '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599',
'137': '0x1bfd67037b42cf73acF2047067bd4F2C47D9BfD6'
},
{
symbol: 'WETH',
'1': '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
'11155111': '0x7b79995e5f793a07bc00c21412e50ecae098e7f9',
'137': '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619'
},
{
symbol: 'STETH',
'1': '0xae7ab96520de3a18e5e111b5eaab095312d7fe84',
'137': ''
},
{
symbol: 'WSTETH',
'1': '0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0',
'137': '0x03b54a6e9a984069379fae1a4fc4dbae93b3bccd'
},
{
symbol: 'CBETH',
'1': '0xbe9895146f7af43049ca1c1ae358b0541ea49704',
'137': '0x4b4327db1600b8b1440163f667e199cef35385f5'
},
{
symbol: 'ANKRETH',
'1': '0xe95a203b1a91a908f9b9ce46459d101078c2c3cb',
'137': ''
},
{
symbol: 'OSETH',
'1': '0xf1c9acdc66974dfb6decb12aa385b9cd01190e38',
'137': ''
}
];
export function isExcludedToken(chainId: string, contractAddress: string) {
return (
EXCLUDED_TOKENS.find(token => token?.[chainId] === contractAddress) !==
undefined
);
}
================================================
FILE: src/helpers/boost/types.ts
================================================
export type BoostClaimSubgraph = {
id: string;
amount: string;
transactionHash: string;
chainId: string;
boost: {
id: string;
};
};
export type BoostRewardGuard = {
boost_id: string;
chain_id: string;
reward: string;
};
export type BoostVoucherGuard = {
boost_id: string;
chain_id: string;
signature: string;
reward: string;
};
export type BoostWinnersGuard = {
winners: string[];
prize: string;
};
export interface BoostStrategy {
name: string;
description: string;
image: string;
external_url: string;
params: {
version: string;
env: string;
proposal: string;
eligibility: {
type: 'incentive' | 'bribe';
choice?: string;
};
distribution: {
type: 'weighted' | 'lottery';
limit?: string;
numWinners?: string;
};
};
}
export type BoostSubgraph = {
id: string;
strategyURI: string;
poolSize: string;
guard: string;
start: string;
end: string;
owner: string;
chainId: string;
currentBalance: string;
transaction: string;
token: {
id: string;
name: string;
symbol: string;
decimals: string;
};
strategy: {
id: string;
version: string;
name: string;
proposal: string;
eligibility: {
type: 'incentive' | 'prediction' | 'bribe';
choice: string | null;
};
distribution: {
type: 'weighted' | 'lottery';
limit: string | null;
numWinners: string | null;
};
};
};
================================================
FILE: src/helpers/clientEIP712.ts
================================================
import Client from '@snapshot-labs/snapshot.js/src/sign';
const hubUrl =
import.meta.env.VITE_HUB_URL || 'https://testnet.hub.snapshot.org';
const relayerURL = import.meta.env.VITE_RELAYER_URL;
const client = new Client(hubUrl, { relayerURL });
export default client;
================================================
FILE: src/helpers/connectors.ts
================================================
const connectors = {
injected: {
id: 'injected',
name: 'MetaMask'
},
walletconnect: {
id: 'walletconnect',
name: 'WalletConnect',
network: '1',
options: {
projectId: 'e6454bd61aba40b786e866a69bd4c5c6',
chains: [],
optionalChains: [
1, 4, 5, 10, 42, 56, 100, 137, 146, 246, 250, 4002, 1088, 42161, 73799,
33139, 11155111
],
optionalMethods: [
'eth_sendTransaction',
'personal_sign',
'eth_accounts',
'eth_signTypedData_v4'
],
rpcMap: {
'1': `${import.meta.env.VITE_BROVIDER_URL}/1`,
'4': `${import.meta.env.VITE_BROVIDER_URL}/4`,
'5': `${import.meta.env.VITE_BROVIDER_URL}/5`,
'10': `${import.meta.env.VITE_BROVIDER_URL}/10`,
'42': `${import.meta.env.VITE_BROVIDER_URL}/42`,
'56': `${import.meta.env.VITE_BROVIDER_URL}/56`,
'100': `${import.meta.env.VITE_BROVIDER_URL}/100`,
'137': `${import.meta.env.VITE_BROVIDER_URL}/137`,
'246': `${import.meta.env.VITE_BROVIDER_URL}/246`,
'42161': `${import.meta.env.VITE_BROVIDER_URL}/42161`,
'73799': `${import.meta.env.VITE_BROVIDER_URL}/73799`,
'11155111': `${import.meta.env.VITE_BROVIDER_URL}/11155111`
},
showQrModal: true
},
icon: 'ipfs://QmZRVqHpgRemw13aoovP2EaQdVtjzXRaQGQZsCLXWaNn9x'
},
walletlink: {
id: 'walletlink',
name: 'Coinbase Wallet',
network: '1',
options: {
appName: 'Snapshot',
appChainIds: [1],
appLogoUrl: 'https://snapshot.box/favicon.svg'
},
icon: 'ipfs://QmbJKEaeMz6qR3DmJSTxtYtrZeQPptVfnnYK72QBsvAw5q',
hidden: false
},
portis: {
id: 'portis',
name: 'Portis',
network: '1',
options: {
dappId: '3eb93706-c71d-456b-b4eb-322ea27f7d48',
network: 'mainnet'
},
icon: 'ipfs://QmNuLXa47xSrDNKRfpPNhoFTuoztvtWCcwGnPpT5MXJWMb'
},
stargazer: {
id: 'stargazer',
name: 'Stargazer',
icon: 'ipfs://bafkreiapdizo36f3yeg7g6l46f7ahbbkyo4otufnfyqri6louysr3grpzy'
},
gnosis: {
id: 'gnosis',
name: 'Gnosis Safe',
icon: 'ipfs://QmfJUHZLtRvadM7fvEJUWWxhS869KXXCMxPCr7TUqkwvUc',
hidden: true
},
kaikas: {
id: 'kaikas',
name: 'Kaikas',
icon: 'ipfs://QmXD4kkxKzXKbbBu3zAzZ279Sm91JhxCDAwSypyzxwe2Hj'
}
};
export default connectors;
================================================
FILE: src/helpers/constants.ts
================================================
export const ETH_CONTRACT = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee';
export const DEFAULT_ETH_ADDRESS = '0x0000000000000000000000000000000000000000';
export const SNAPSHOT_BREAKPOINTS = {
xs: '420px',
sm: '544px',
md: '768px',
lg: '1012px',
xl: '1280px',
'2xl': '1536px'
};
export const KNOWN_HOSTS = [
'app.safe.global',
'horizen-eon.safe.onchainden.com',
'pilot.gnosisguild.org',
'metissafe.tech',
'multisig.mantle.xyz',
'wallet.ambire.com',
'multisig.moonbeam.network',
'worldassociation.org',
'safe.mainnet.frax.com',
'safe.fantom.network',
'safe.apechain.com'
];
// All subdomains of these domains are allowed
export const KNOWN_DOMAINS = ['blockscout.com'];
export const SPACE_CATEGORIES = [
'protocol',
'social',
'investment',
'grant',
'service',
'media',
'creator',
'collector',
'ai-agent',
'gaming',
'wallet',
'music',
'layer-2',
'defai',
'defi',
'rwa',
'depin',
'meme'
];
export const ERC20ABI = [
'constructor(string name, string symbol)',
'event Approval(address indexed owner, address indexed spender, uint256 value)',
'event Transfer(address indexed from, address indexed to, uint256 value)',
'function allowance(address owner, address spender) view returns (uint256)',
'function approve(address spender, uint256 amount) returns (bool)',
'function balanceOf(address account) view returns (uint256)',
'function decimals() view returns (uint8)',
'function decreaseAllowance(address spender, uint256 subtractedValue) returns (bool)',
'function increaseAllowance(address spender, uint256 addedValue) returns (bool)',
'function name() view returns (string)',
'function symbol() view returns (string)',
'function totalSupply() view returns (uint256)',
'function transfer(address recipient, uint256 amount) returns (bool)',
'function transferFrom(address sender, address recipient, uint256 amount) returns (bool)'
];
export const COINGECKO_ASSET_PLATFORMS = {
'137': 'polygon-pos',
'42161': 'arbitrum-one'
};
export const COINGECKO_BASE_ASSETS = {
'137': 'matic-network',
'42161': 'ethereum'
};
export type ChainCurrency = {
name: string;
symbol: string;
decimals: number;
contractAddress: string;
};
export const CHAIN_CURRENCIES: Record = {
'1': {
name: 'Ether',
symbol: 'ETH',
decimals: 18,
contractAddress: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'
},
'8453': {
name: 'Ether',
symbol: 'ETH',
decimals: 18,
contractAddress: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'
},
'11155111': {
name: 'Sepolia Ether',
symbol: 'SepoliaETH',
decimals: 18,
contractAddress: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'
},
'137': {
name: 'MATIC',
symbol: 'MATIC',
decimals: 18,
contractAddress: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'
},
'42161': {
name: 'Arbitrum',
symbol: 'ARB',
decimals: 18,
contractAddress: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'
}
};
export const TWO_WEEKS = 1209600;
export const ONE_DAY = 86400;
export const SNAPSHOT_HELP_LINK = 'https://help.snapshot.org/en';
export const BOOST_ENABLED_VOTING_TYPES = [
'basic',
'single-choice',
'ranked-choice'
];
export const STRATEGIES_LIMITS = {
default: 8,
verified: 8,
turbo: 10
};
export const PROPOSAL_BODY_LIMITS = {
default: 10000,
turbo: 40000
};
================================================
FILE: src/helpers/covalent.ts
================================================
import snapshot from '@snapshot-labs/snapshot.js';
const API_URL = 'https://api.covalenthq.com/v1';
const API_KEY = 'ckey_2d082caf47f04a46947f4f212a8';
export const ETHER_CONTRACT = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee';
export async function getTokenBalances(
address: string,
chainId: string
): Promise {
const tokenBalanceUrl = `${API_URL}/${chainId}/address/${address}/balances_v2/?quote-currency=USD&format=JSON&nft=false&no-nft-fetch=true&key=${API_KEY}`;
const tokenBalances = await snapshot.utils.getJSON(tokenBalanceUrl);
const validTokenBalances = tokenBalances.data.items.filter(
item =>
item.contract_name &&
item.contract_ticker_symbol &&
item.logo_url &&
item.quote
);
// If there is an ether item, move it to the top of the list
const etherItem = validTokenBalances.find(
item => item.contract_address === ETHER_CONTRACT
);
if (etherItem) {
const index = validTokenBalances.findIndex(
item => item.contract_address === ETHER_CONTRACT
);
validTokenBalances.splice(index, 1);
validTokenBalances.unshift(etherItem);
}
return validTokenBalances;
}
export async function getTokenPrices(
contract: string,
chainId: string
): Promise {
const tokenPricesUrl = `${API_URL}/pricing/historical_by_addresses_v2/${chainId}/USD/${contract}/?quote-currency=USD&format=JSON&key=${API_KEY}`;
return await snapshot.utils.getJSON(tokenPricesUrl);
}
================================================
FILE: src/helpers/delegation.ts
================================================
import {
SNAPSHOT_SUBGRAPH_URL,
subgraphRequest
} from '@snapshot-labs/snapshot.js/src/utils';
export const contractAddress = '0x469788fE6E9E9681C6ebF3bF78e7Fd26Fc015446';
export async function getDelegates(network: string, address: string) {
const params = {
delegations: {
__args: {
where: {
delegator: address.toLowerCase()
},
first: 1000
},
space: true,
delegate: true
}
};
return await subgraphRequest(SNAPSHOT_SUBGRAPH_URL[network], params);
}
export async function getDelegators(network: string, address: string) {
const params = {
delegations: {
__args: {
where: {
delegate: address.toLowerCase()
},
first: 1000
},
delegator: true,
space: true
}
};
return await subgraphRequest(SNAPSHOT_SUBGRAPH_URL[network], params);
}
================================================
FILE: src/helpers/delegationV2/compound/index.ts
================================================
export { getDelegationReader } from './read';
export { getDelegationWriter } from './write';
================================================
FILE: src/helpers/delegationV2/compound/queries.ts
================================================
export const getDelegateQuery = (id: string): Record => ({
delegate: {
__args: {
id
},
id: true,
delegatedVotes: true,
tokenHoldersRepresentedAmount: true
},
governance: {
__args: {
id: 'GOVERNANCE'
},
delegatedVotes: true,
totalTokenHolders: true,
totalDelegates: true
}
});
export const getDelegatesQuery = (
first: number,
skip: number,
orderBy: string
): Record => ({
delegates: {
__args: {
first,
skip,
orderBy: orderBy || 'delegatedVotes',
orderDirection: 'desc'
},
id: true,
delegatedVotes: true,
tokenHoldersRepresentedAmount: true
},
governance: {
__args: {
id: 'GOVERNANCE'
},
delegatedVotes: true,
totalTokenHolders: true,
totalDelegates: true
}
});
export const getBalanceQuery = (id: string): Record => ({
tokenHolder: {
__args: {
id
},
id: true,
tokenBalance: true
}
});
================================================
FILE: src/helpers/delegationV2/compound/read.ts
================================================
import getProvider from '@snapshot-labs/snapshot.js/src/utils/provider';
import { subgraphRequest, call } from '@snapshot-labs/snapshot.js/src/utils';
import { DelegateWithPercent, ExtendedSpace } from '@/helpers/interfaces';
import { DelegatingTo, DelegationReader } from '@/helpers/delegationV2/types';
import {
getBalanceQuery,
getDelegateQuery,
getDelegatesQuery
} from './queries';
type Governance = {
delegatedVotes: string;
totalTokenHolders: string;
totalDelegates: string;
};
type Delegate = {
id: string;
delegatedVotes: string;
tokenHoldersRepresentedAmount: number;
};
function adjustUrl(apiUrl: string) {
const hostedPattern =
/https:\/\/thegraph\.com\/hosted-service\/subgraph\/([\w-]+)\/([\w-]+)/;
const hostedMatch = apiUrl.match(hostedPattern);
return hostedMatch
? `https://api.thegraph.com/subgraphs/name/${hostedMatch[1]}/${hostedMatch[2]}`
: apiUrl;
}
const emptyDelegate = (address: string): DelegateWithPercent => ({
id: address,
delegatedVotes: '0',
tokenHoldersRepresentedAmount: 0,
delegatorsPercentage: 0,
votesPercentage: 0
});
const formatDelegatesResponse = (response: any): DelegateWithPercent[] => {
const governanceData = response.governance as Governance;
const delegatesData = response.delegates as Delegate[];
return delegatesData.map(delegate => {
const delegatorsPercentage =
Number(delegate.tokenHoldersRepresentedAmount) /
Number(governanceData.totalTokenHolders);
const votesPercentage =
Number(delegate.delegatedVotes) / Number(governanceData.delegatedVotes);
delegate.id = delegate.id.toLowerCase();
return {
...delegate,
delegatorsPercentage,
votesPercentage
};
});
};
const formatDelegateResponse = (response: any): DelegateWithPercent => {
const delegate = response.delegate as Delegate;
const governanceData = response.governance as Governance;
const delegatorsPercentage =
Number(delegate.tokenHoldersRepresentedAmount) /
Number(governanceData.totalTokenHolders);
const votesPercentage =
Number(delegate.delegatedVotes) / Number(governanceData.delegatedVotes);
return {
...{
id: delegate.id.toLowerCase(),
delegatedVotes: delegate?.delegatedVotes || '0',
tokenHoldersRepresentedAmount:
delegate?.tokenHoldersRepresentedAmount || 0
},
delegatorsPercentage: delegatorsPercentage || 0,
votesPercentage: votesPercentage || 0
};
};
const formatBalanceResponse = (response: any): string =>
response.tokenHolder?.tokenBalance || '0';
const getDelegations =
(space: ExtendedSpace): DelegationReader['getDelegates'] =>
async (first: number, skip: number, orderBy: string) => {
const query: any = getDelegatesQuery(first, skip, orderBy);
const response = await subgraphRequest(
adjustUrl(space.delegationPortal.delegationApi),
query
);
return formatDelegatesResponse(response);
};
const getDelegate =
(space: ExtendedSpace): DelegationReader['getDelegate'] =>
async (address: string) => {
const query: any = getDelegateQuery(address);
const response = await subgraphRequest(
adjustUrl(space.delegationPortal.delegationApi),
query
);
if (!response.delegate) return emptyDelegate(address);
return formatDelegateResponse(response);
};
const getBalance =
(space: ExtendedSpace): DelegationReader['getBalance'] =>
async (id: string) => {
const query: any = getBalanceQuery(id.toLowerCase());
const response = await subgraphRequest(
adjustUrl(space.delegationPortal.delegationApi),
query
);
return formatBalanceResponse(response);
};
const getDelegatingTo =
(space: ExtendedSpace): DelegationReader['getDelegatingTo'] =>
async (address: string): Promise => {
const broviderUrl = import.meta.env.VITE_BROVIDER_URL;
const provider = getProvider(space.network, { broviderUrl });
const delegates = await call(
provider,
['function delegates(address) view returns (address)'],
[space.delegationPortal.delegationContract, 'delegates', [address]]
);
return { delegates };
};
export const getDelegationReader = (
space: ExtendedSpace
): DelegationReader => ({
getDelegates: getDelegations(space),
getDelegate: getDelegate(space),
getBalance: getBalance(space),
getDelegatingTo: getDelegatingTo(space)
});
================================================
FILE: src/helpers/delegationV2/compound/write.ts
================================================
import { sendTransaction } from '@snapshot-labs/snapshot.js/src/utils';
import { DelegationWriter } from '@/helpers/delegationV2/types';
import { ExtendedSpace } from '@/helpers/interfaces';
const sendSetDelegationTx =
(space: ExtendedSpace, auth: any): DelegationWriter['sendSetDelegationTx'] =>
async (addresses: string[]) => {
if (addresses.length !== 1) {
throw new Error('Compound delegation only supports one delegate');
}
const tx = await sendTransaction(
auth.web3,
space.delegationPortal.delegationContract,
['function delegate(address delegatee)'],
'delegate',
[addresses[0]]
);
return tx;
};
export const getDelegationWriter = (
space: ExtendedSpace,
auth: any
): DelegationWriter => ({
sendSetDelegationTx: sendSetDelegationTx(space, auth)
});
================================================
FILE: src/helpers/delegationV2/index.ts
================================================
import {
DelegationReader,
DelegationWriter
} from '@/helpers/delegationV2/types';
import * as compound from '@/helpers/delegationV2/compound';
import * as splitDelegation from '@/helpers/delegationV2/splitDelegation';
import { ExtendedSpace } from '@/helpers/interfaces';
export enum DelegationTypes {
COMPOUND = 'compound-governor',
SPLIT_DELEGATION = 'split-delegation'
}
export function setupDelegation(
space: ExtendedSpace,
auth?: any
): {
reader: DelegationReader;
writer: DelegationWriter;
} {
if (
space.delegationPortal?.delegationType ===
DelegationTypes.SPLIT_DELEGATION &&
space.strategies.some(
({ name }) => name === DelegationTypes.SPLIT_DELEGATION
)
) {
return {
reader: splitDelegation.getDelegationReader(space),
writer: splitDelegation.getDelegationWriter(space, auth)
};
}
return {
reader: compound.getDelegationReader(space),
writer: compound.getDelegationWriter(space, auth)
};
}
================================================
FILE: src/helpers/delegationV2/splitDelegation/abi.ts
================================================
export const abi = [
{
inputs: [{ internalType: 'string', name: 'context', type: 'string' }],
name: 'clearDelegation',
outputs: [],
stateMutability: 'nonpayable',
type: 'function'
},
{
inputs: [
{ internalType: 'string', name: 'context', type: 'string' },
{ internalType: 'bool', name: '_optout', type: 'bool' }
],
name: 'optout',
outputs: [],
stateMutability: 'nonpayable',
type: 'function'
},
{
inputs: [
{ internalType: 'string', name: 'context', type: 'string' },
{
components: [
{ internalType: 'bytes32', name: 'delegate', type: 'bytes32' },
{ internalType: 'uint256', name: 'ratio', type: 'uint256' }
],
internalType: 'struct Delegation[]',
name: 'delegation',
type: 'tuple[]'
},
{
internalType: 'uint256',
name: 'expirationTimestamp',
type: 'uint256'
}
],
name: 'setDelegation',
outputs: [],
stateMutability: 'nonpayable',
type: 'function'
},
{
inputs: [
{ internalType: 'string', name: 'context', type: 'string' },
{
internalType: 'uint256',
name: 'expirationTimestamp',
type: 'uint256'
}
],
name: 'setExpiration',
outputs: [],
stateMutability: 'nonpayable',
type: 'function'
}
];
================================================
FILE: src/helpers/delegationV2/splitDelegation/index.ts
================================================
export { getDelegationReader } from './read';
export { getDelegationWriter } from './write';
================================================
FILE: src/helpers/delegationV2/splitDelegation/read.ts
================================================
import { DelegateWithPercent, ExtendedSpace } from '@/helpers/interfaces';
import {
DelegateTreeItem,
DelegationReader,
DelegatorTreeItem
} from '@/helpers/delegationV2/types';
const SPLIT_DELEGATE_BACKEND_URL = 'https://delegate-api.gnosisguild.org';
type DelegateFromSD = {
address: string;
delegatorCount: number;
percentOfDelegators: number;
votingPower: number;
percentOfVotingPower: number;
};
type AddressResponse = {
chainId: number;
blockNumber: number;
address: string;
votingPower: Record;
percentOfVotingPower: number;
percentOfDelegators: number;
delegates: string[];
delegateTree: DelegateTreeItem[];
delegators: string[];
delegatorTree: DelegatorTreeItem[];
};
// const emptyDelegate = (address: string): DelegateWithPercent => ({
// id: address,
// delegatedVotes: '0',
// tokenHoldersRepresentedAmount: 0,
// delegatorsPercentage: 0,
// votesPercentage: 0
// });
const bpsToPercent = (bps: number): number => bps / 10000;
const getDelegations =
(space: ExtendedSpace): DelegationReader['getDelegates'] =>
async (first: number, skip: number, matchFilter: string) => {
let orderBy = 'power';
if (matchFilter === 'tokenHoldersRepresentedAmount') {
orderBy = 'count';
}
const splitDelStrategy = space.strategies.find(
strat => strat.name === 'split-delegation'
);
const response = (await fetch(
`${
space.delegationPortal.delegationApi || SPLIT_DELEGATE_BACKEND_URL
}/api/v1/${
space.id
}/pin/top-delegates?by=${orderBy}&limit=${first}&offset=${skip}`,
{
method: 'POST',
body: JSON.stringify({
strategy: splitDelStrategy
})
}
).then(res => res.json())) as { delegates: DelegateFromSD[] };
const formatted: DelegateWithPercent[] = response.delegates.map(d => ({
id: d.address,
delegatedVotes: d.votingPower.toString(),
tokenHoldersRepresentedAmount: d.delegatorCount,
delegatorsPercentage: bpsToPercent(d.percentOfDelegators),
votesPercentage: bpsToPercent(d.percentOfVotingPower)
}));
return formatted;
};
const getDelegate =
(space: ExtendedSpace): DelegationReader['getDelegate'] =>
async (address: string) => {
const splitDelStrategy = space.strategies.find(
strat => strat.name === 'split-delegation'
);
const response = (await fetch(
`${
space.delegationPortal.delegationApi || SPLIT_DELEGATE_BACKEND_URL
}/api/v1/${space.id}/pin/${address}`,
{
method: 'POST',
body: JSON.stringify({
strategy: splitDelStrategy
})
}
).then(res => res.json())) as AddressResponse;
const formatted: DelegateWithPercent = {
id: address,
delegatedVotes: response.votingPower.toString(),
tokenHoldersRepresentedAmount: response.delegators.length,
delegatorsPercentage: bpsToPercent(response.percentOfDelegators),
votesPercentage: bpsToPercent(response.percentOfVotingPower)
};
return formatted;
};
const getBalance =
(space: ExtendedSpace): DelegationReader['getBalance'] =>
async (address: string) => {
const splitDelStrategy = space.strategies.find(
strat => strat.name === 'split-delegation'
);
const response = (await fetch(
`${
space.delegationPortal.delegationApi || SPLIT_DELEGATE_BACKEND_URL
}/api/v1/${space.id}/pin/${address}`,
{
method: 'POST',
body: JSON.stringify({
strategy: splitDelStrategy
})
}
).then(res => res.json())) as DelegateFromSD;
return response.votingPower.toString();
};
const getDelegatingTo =
(space: ExtendedSpace): DelegationReader['getDelegatingTo'] =>
async (address: string) => {
const splitDelStrategy = space.strategies.find(
strat => strat.name === 'split-delegation'
);
const response = (await fetch(
`${
space.delegationPortal.delegationApi || SPLIT_DELEGATE_BACKEND_URL
}/api/v1/${space.id}/pin/${address}`,
{
method: 'POST',
body: JSON.stringify({
strategy: splitDelStrategy
})
}
).then(res => res.json())) as AddressResponse;
return {
delegates: response.delegates,
delegateTree: response.delegateTree
};
};
export const getDelegationReader = (
space: ExtendedSpace
): DelegationReader => ({
getDelegates: getDelegations(space),
getDelegate: getDelegate(space),
getBalance: getBalance(space),
getDelegatingTo: getDelegatingTo(space)
});
================================================
FILE: src/helpers/delegationV2/splitDelegation/write.ts
================================================
import { sendTransaction } from '@snapshot-labs/snapshot.js/src/utils';
import { hexZeroPad } from '@ethersproject/bytes';
import { DelegationWriter } from '@/helpers/delegationV2/types';
import { ExtendedSpace } from '@/helpers/interfaces';
import { abi } from './abi';
const DELEGATION_CONTRACT = '0xDE1e8A7E184Babd9F0E3af18f40634e9Ed6F0905'; //All chains
const sendSetDelegationTx =
(space: ExtendedSpace, auth: any): DelegationWriter['sendSetDelegationTx'] =>
async (addresses, ratio, expirationTimestamp) => {
const delegationContract =
space.delegationPortal.delegationContract || DELEGATION_CONTRACT;
console.log('sendSetDelegationTx', addresses, ratio, expirationTimestamp);
if (addresses.length <= 0) {
throw new Error('Delegation must have at least one delegate');
}
if (addresses.length !== ratio?.length) {
throw new Error(
'Delegation must have the same number of delegates and ratios'
);
}
if (expirationTimestamp == null) {
throw new Error('Delegation must have an expiration timestamp');
}
if (
expirationTimestamp &&
expirationTimestamp < Math.floor(Date.now() / 1000)
) {
throw new Error('Delegation expiration must be in the future');
}
const delegations = addresses
.map((address, index) => ({
delegate: hexZeroPad(address, 32),
ratio: ratio[index]
}))
.sort((a, b) => {
return BigInt(a.delegate) < BigInt(b.delegate) ? -1 : 1;
});
console.log('delegations', delegations);
const tx = await sendTransaction(
auth.web3,
delegationContract,
abi,
'setDelegation',
[space.id, delegations, expirationTimestamp] //space.id should be the ENS name
);
return tx;
};
const sendClearDelegationsTx =
(
space: ExtendedSpace,
auth: any
): DelegationWriter['sendClearDelegationsTx'] =>
async () => {
const delegationContract =
space.delegationPortal.delegationContract || DELEGATION_CONTRACT;
const tx = await sendTransaction(
auth.web3,
delegationContract,
abi,
'clearDelegation',
[space.id] //space.id should be the ENS name
);
return tx;
};
export const getDelegationWriter = (
space: ExtendedSpace,
auth: any
): DelegationWriter => ({
sendSetDelegationTx: sendSetDelegationTx(space, auth),
sendClearDelegationsTx: sendClearDelegationsTx(space, auth)
});
================================================
FILE: src/helpers/delegationV2/types.ts
================================================
import { DelegateWithPercent } from '@/helpers/interfaces';
export type DelegationReader = {
getDelegates(
first: number,
skip: number,
orderBy: string
): Promise;
getDelegate(id: string): Promise;
getBalance(id: string): Promise;
getDelegatingTo(address: string): Promise;
};
export type DelegationWriter = {
sendSetDelegationTx: (
addresses: string[],
ratio?: number[],
expirationTimestamp?: number
) => Promise;
sendClearDelegationsTx?: () => Promise;
};
export type DelegatingTo = {
delegates: string[];
delegateTree?: DelegateTreeItem[];
};
export type DelegateTreeItem = {
delegate: string;
weight: number;
delegatedPower: number;
children: DelegateTreeItem[];
};
export type DelegatorTreeItem = {
delegator: string;
weight: number;
delegatedPower: number;
parents: [];
};
================================================
FILE: src/helpers/ens.ts
================================================
import networks from '@snapshot-labs/snapshot.js/src/networks.json';
import {
ApolloClient,
createHttpLink,
InMemoryCache
} from '@apollo/client/core';
const env = import.meta.env.VITE_DEFAULT_NETWORK;
const uri = networks[env].ensSubgraph;
const httpLink = createHttpLink({ uri });
export const ensApolloClient = new ApolloClient({
link: httpLink,
cache: new InMemoryCache({
addTypename: false
}),
defaultOptions: {
query: {
fetchPolicy: 'no-cache'
}
}
});
================================================
FILE: src/helpers/i18n.ts
================================================
import { createI18n } from 'vue-i18n';
import en from '@/locales/default.json';
import languages from '@/locales/languages.json';
import { lsRemove } from '@/helpers/utils';
export let defaultLocale = 'en-US';
export function getBrowserLocale() {
if (typeof navigator !== 'undefined') {
return (
navigator['userLanguage'] ||
navigator['language'] ||
(navigator.languages?.[0] ? navigator.languages[0] : undefined)
);
}
return undefined;
}
const browserLocale = getBrowserLocale();
Object.keys(languages).forEach(locale => {
if (locale.slice(0, 2) === browserLocale.slice(0, 2)) defaultLocale = locale;
});
export function setI18nLanguage(i18n, locale) {
if (i18n.mode === 'legacy') {
i18n.global.locale = locale;
} else {
i18n.global.locale.value = locale;
}
document.querySelector('html')?.setAttribute('lang', locale);
}
export async function loadLocaleMessages(i18n, locale) {
if (!Object.keys(languages).includes(locale)) {
lsRemove('locale');
locale = 'default';
}
if (locale === 'en-US') locale = 'default';
try {
// load locale messages with dynamic import
const messages = await import(
/* webpackChunkName: "locale-[request]" */ `../locales/${locale}.json`
);
// set locale and locale message
i18n.global.setLocaleMessage(locale, messages.default);
} catch (e) {
console.log(e);
}
return nextTick();
}
const i18n = createI18n({
locale: defaultLocale,
datetimeFormats: {
'en-US': {
short: {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: 'numeric'
}
}
},
messages: { 'en-US': en },
fallbackLocale: 'en-US'
});
setI18nLanguage(i18n, defaultLocale);
export default i18n;
================================================
FILE: src/helpers/interfaces.ts
================================================
import { BigNumber } from '@ethersproject/bignumber';
import { Fragment, JsonFragment } from '@ethersproject/abi';
import { DelegationTypes } from '@/helpers/delegationV2';
export interface Strategy {
id: string;
spacesCount: number;
author: string;
version: string;
about?: string;
schema?: StrategySchema | null;
examples?: StrategyExample[];
}
interface StrategyExample {
name: string;
strategy: Record;
network: string;
addresses: string[];
snapshot: number;
space?: string;
}
interface StrategySchema {
$schema: string;
$ref: string;
definitions: {
Strategy: Record;
};
}
export interface StrategyDefinitionProperties {
type: string;
title: string;
default?: any;
examples?: string[];
description?: string;
minLength?: number;
maxLength?: number;
pattern?: string;
}
export interface StrategyDefinition {
title: string;
type: string;
default?: any;
description?: string;
required?: string[];
additionalProperties?: boolean;
properties?: StrategyDefinitionProperties;
}
export interface Profile {
id: string;
name: string;
ens: string;
about?: string;
avatar?: string;
created?: number;
}
export interface ProfileActivity {
id: string;
created: number;
type: string;
title: string;
space: { id: string; avatar: string };
vote?: {
proposalId: string;
choice: string;
type: string;
};
}
export interface TreasuryAsset {
contract_name: string;
contract_ticker_symbol: string;
contract_address: string;
contract_decimals: number;
logo_url: string;
balance: string;
balance_24h: string;
quote: number;
quote_24h: number;
}
export interface TreasuryWallet {
name: string;
address: string;
network: string;
}
export interface ExploreSpace {
id: string;
name: string;
private?: boolean;
terms?: string;
network?: string;
networks?: string[];
categories?: string[];
proposals?: number;
proposals_active?: number;
proposals_7d?: number;
votes?: number;
votes_7d?: number;
followers?: number;
followers_7d?: number;
}
export interface Space {
id: string;
name: string;
avatar: string;
verified: boolean;
turbo: boolean;
activeProposals: number;
followersCount: number;
flagged: boolean;
hibernated: boolean;
terms: string;
}
export interface RankedSpace {
id: string;
name: string;
avatar: string;
verified: boolean;
turbo: boolean;
rank: number;
categories: string[];
activeProposals: number;
proposalsCount: number;
proposalsCount7d: number;
followersCount: number;
followersCount7d: number;
votesCount: number;
votesCount7d: number;
terms: string;
}
export interface ExtendedSpace {
id: string;
name: string;
symbol: string;
network: string;
strategies: SpaceStrategy[];
delegationPortal: DelegatesConfig;
about: string;
avatar: string;
skin: string;
domain: string | null;
website: string | null;
terms: string | null;
coingecko: string | null;
github: string | null;
twitter: string | null;
followersCount: number;
private: boolean;
admins: string[];
moderators: string[];
members: string[];
categories: string[];
parent: ExtendedSpace | null;
children: ExtendedSpace[];
filters: { minScore: number; onlyMembers: boolean };
plugins: Record;
validation: SpaceValidation;
voteValidation: VoteValidation;
treasuries: TreasuryWallet[];
template: string;
guidelines: string;
verified: boolean;
flagged: boolean;
hibernated: boolean;
turbo: boolean;
boost: {
enabled: boolean;
bribeEnabled: boolean;
};
voting: {
delay: number | null;
hideAbstain: boolean;
period: number | null;
quorum: number | null;
quorumType: 'default' | 'rejection';
type: string | null;
privacy: string | null;
};
}
export interface DelegatesConfig {
delegationType: DelegationTypes;
delegationContract: string;
delegationNetwork: string;
delegationApi: string;
}
export interface SpaceValidation {
name: string;
params: Record;
}
export interface SpaceStrategy {
name: string;
network: string;
params: Record;
}
export interface ProposalSpace {
id: string;
name: string;
members: string[];
avatar: string;
symbol: string;
verified: boolean;
turbo: boolean;
}
export interface Proposal {
id: string;
title: string;
ipfs: string;
network: string;
choices: string[];
type: string;
snapshot: string;
author: string;
body: string;
created: number;
start: number;
end: number;
state: string;
symbol: string;
privacy: string;
validation: VoteValidation;
discussion: string;
quorum: number;
quorumType: 'default' | 'rejection';
scores: number[];
scores_state: string;
scores_total: number;
scores_by_strategy: number[][];
votes: number;
plugins: Record;
space: ExtendedSpace;
strategies: SpaceStrategy[];
flagged: boolean;
}
export interface VoteValidation {
name: string;
params: Record;
}
export interface Results {
scoresByStrategy: number[][];
scores: number[];
scoresTotal: number;
}
export type Choice = number | number[] | Record;
export interface Vote {
ipfs: string;
voter: string;
choice: Choice;
balance: number;
scores: number[];
vp: number;
vp_by_strategy: number[];
reason: string;
created: number;
}
export interface VoteFilters {
orderDirection: string;
onlyWithReason: boolean;
}
// Execution
export type ABI = string | Array;
export interface PendingTransaction {
id: string;
network: string;
createdAt: number;
hash: string | null;
}
export interface SafeTransaction {
to: string;
value: string;
data: string;
operation: string;
nonce: string;
}
export interface RealityOracleProposal {
dao: string;
oracle: string;
cooldown: number;
expiration: number;
proposalId: string;
questionId: string | undefined;
executionApproved: boolean;
finalizedAt: number | undefined;
nextTxIndex: number | undefined;
transactions: SafeTransaction[];
txHashes: string[];
currentBond: BigNumber | undefined;
isApproved: boolean;
endTime: number | undefined;
}
export interface UmaOracleProposal {
dao: string;
oracle: string;
rules: string;
expiration: number;
proposalId: string;
transactions: SafeTransaction[];
minimumBond: BigNumber | number | undefined;
explanation: string;
allowance: BigNumber | number | undefined;
collateral: string;
decimals: number;
symbol: string;
userBalance: BigNumber | number | undefined;
}
export interface SafeAsset {
address: string;
name: string;
logoUri?: string;
}
export interface CollectableAsset extends SafeAsset {
id: string;
tokenName?: string;
}
export interface TokenAsset extends SafeAsset {
symbol: string;
decimals: number;
balance: string;
verified?: any;
chainId?: number;
}
export interface CollectableAssetTransaction extends SafeTransaction {
type: 'transferNFT';
recipient: string;
collectable: CollectableAsset;
}
export interface TokenAssetTransaction extends SafeTransaction {
type: 'transferFunds';
amount: string;
recipient: any;
token: TokenAsset;
}
export interface CustomContractTransaction extends SafeTransaction {
type: 'contractInteraction';
abi: string[];
}
export interface SafeModuleTransactionBatch {
hash: string;
transactions: SafeTransaction[];
}
export interface SafeExecutionData {
hash: string | null;
txs: SafeModuleTransactionBatch[];
network: string;
realityModule: string;
}
export interface Plugin {
name: string;
author: string;
version: string;
defaults?: any;
icon?: string;
description?: string;
website?: string;
}
export interface PluginIndex extends Plugin {
key: string;
}
export interface FormError {
message: string;
push?: boolean;
}
export interface Delegate {
id: string;
delegatedVotes: string;
tokenHoldersRepresentedAmount: number;
}
export interface DelegateWithPercent extends Delegate {
delegatorsPercentage: number;
votesPercentage: number;
}
export interface Statement {
delegate: string;
space: string;
statement: string;
about: string;
ipfs: string;
id: string;
}
export type DelegatesVote = {
created: number;
voter: string;
choice: any;
vp: number;
};
export type DelegatesProposal = {
created: number;
author: string;
title: string;
};
================================================
FILE: src/helpers/pin.ts
================================================
import { create } from 'kubo-rpc-client';
import { pin } from '@snapshot-labs/pineapple';
const client = create({ url: 'https://api.thegraph.com/ipfs/api/v0' });
export async function pinGraph(payload: any) {
const res = await client.add(JSON.stringify(payload), { pin: true });
return {
provider: 'graph',
cid: res.cid.toV0().toString()
};
}
export async function pinPineapple(payload: any) {
const pinned = await pin(payload);
if (!pinned) throw new Error('Failed to pin');
return {
provider: pinned.provider,
cid: pinned.cid
};
}
================================================
FILE: src/helpers/queries.ts
================================================
import gql from 'graphql-tag';
export const VOTES_QUERY = gql`
query Votes(
$id: String!
$first: Int
$skip: Int
$orderBy: String
$orderDirection: OrderDirection
$reason_not: String
$voter: String
$space: String
$created_gte: Int
) {
votes(
first: $first
skip: $skip
where: {
proposal: $id
vp_gt: 0
voter: $voter
space: $space
reason_not: $reason_not
created_gte: $created_gte
}
orderBy: $orderBy
orderDirection: $orderDirection
) {
ipfs
voter
choice
vp
vp_by_strategy
reason
created
}
}
`;
export const PROPOSAL_QUERY = gql`
query Proposal($id: String!) {
proposal(id: $id) {
id
ipfs
title
body
discussion
choices
labels
start
end
snapshot
state
author
created
plugins
network
type
quorum
quorumType
symbol
privacy
validation {
name
params
}
strategies {
name
network
params
}
space {
id
name
}
scores_state
scores
scores_by_strategy
scores_total
votes
flagged
}
}
`;
export const PROPOSALS_QUERY = gql`
query Proposals(
$first: Int!
$skip: Int!
$state: String!
$space: String
$space_in: [String]
$author_in: [String]
$title_contains: String
$space_verified: Boolean
$flagged: Boolean
) {
proposals(
first: $first
skip: $skip
where: {
space: $space
state: $state
space_in: $space_in
author_in: $author_in
title_contains: $title_contains
space_verified: $space_verified
flagged: $flagged
}
) {
id
ipfs
title
body
start
end
state
author
created
choices
space {
id
name
members
avatar
symbol
verified
turbo
plugins
}
scores_state
scores_total
scores
votes
quorum
quorumType
symbol
flagged
}
}
`;
export const NOTIFICATION_PROPOSALS_QUERY = gql`
query Proposals(
$first: Int!
$state: String!
$space_in: [String]
$start_gte: Int
) {
proposals(
first: $first
where: { state: $state, space_in: $space_in, start_gte: $start_gte }
) {
id
title
start
end
state
space {
id
name
avatar
}
}
}
`;
export const FOLLOWS_QUERY = gql`
query Follows($space_in: [String], $follower_in: [String]) {
follows(
where: { space_in: $space_in, follower_in: $follower_in }
first: 500
) {
id
follower
space {
id
}
}
}
`;
export const SUBSCRIPTIONS_QUERY = gql`
query Subscriptions($space: String, $address: String) {
subscriptions(where: { space: $space, address: $address }) {
id
address
space {
id
}
}
}
`;
export const ALIASES_QUERY = gql`
query Aliases($address: String!, $alias: String!, $created_gt: Int) {
aliases(
where: { address: $address, alias: $alias, created_gt: $created_gt }
) {
address
alias
}
}
`;
export const ENS_DOMAINS_BY_ACCOUNT_QUERY = gql`
query Domain($id: String!) {
account(id: $id) {
domains {
name
expiryDate
}
wrappedDomains {
name
expiryDate
}
}
}
`;
export const ENS_DOMAIN_BY_HASH_QUERY = gql`
query Registration($id: String!) {
registration(id: $id) {
domain {
name
labelName
}
}
}
`;
export const SPACE_SKIN_QUERY = gql`
query Space($id: String!) {
space(id: $id) {
skin
}
}
`;
export const SPACE_DELEGATE_QUERY = gql`
query Space($id: String!) {
space(id: $id) {
id
symbol
network
strategies {
name
network
params
}
}
}
`;
export const SKINS_COUNT_QUERY = gql`
query Skins {
skins {
id
spacesCount
}
}
`;
export const NETWORKS_COUNT_QUERY = gql`
query Networks {
networks {
id
spacesCount
}
}
`;
export const PLUGINS_COUNT_QUERY = gql`
query Plugins {
plugins {
id
spacesCount
}
}
`;
export const VALIDATIONS_COUNT_QUERY = gql`
query Validations {
validations {
id
spacesCount
}
}
`;
export const STRATEGIES_QUERY = gql`
query Strategies {
strategies {
id
author
version
spacesCount
}
}
`;
export const EXTENDED_STRATEGY_QUERY = gql`
query Strategy($id: String!) {
strategy(id: $id) {
id
author
version
spacesCount
about
schema
examples
}
}
`;
export const ACTIVITY_VOTES_QUERY = gql`
query Votes(
$voter: String!
$first: Int
$skip: Int
$orderBy: String
$orderDirection: OrderDirection
) {
votes(
first: $first
skip: $skip
where: { voter: $voter }
orderBy: $orderBy
orderDirection: $orderDirection
) {
id
created
choice
proposal {
id
title
choices
type
}
space {
id
avatar
}
}
}
`;
export const PROFILES_QUERY = gql`
query Users($addresses: [String]!, $first: Int, $skip: Int) {
users(first: $first, skip: $skip, where: { id_in: $addresses }) {
id
name
about
avatar
created
}
}
`;
export const USER_VOTED_PROPOSAL_IDS_QUERY = gql`
query Votes($voter: String!, $proposals: [String]!) {
votes(first: 1000, where: { voter: $voter, proposal_in: $proposals }) {
proposal {
id
}
}
}
`;
export const SPACES_RANKING_QUERY = gql`
query Ranking(
$first: Int
$skip: Int
$search: String
$network: String
$category: String
) {
ranking(
first: $first
skip: $skip
where: { search: $search, network: $network, category: $category }
) {
metrics {
total
categories
}
items {
id
name
avatar
private
verified
turbo
categories
rank
activeProposals
proposalsCount
proposalsCount7d
followersCount
followersCount7d
votesCount
votesCount7d
terms
}
}
}
`;
export const SPACES_QUERY = gql`
query Spaces($id_in: [String], $first: Int, $skip: Int) {
spaces(first: $first, skip: $skip, where: { id_in: $id_in }) {
id
name
avatar
verified
turbo
activeProposals
followersCount
terms
flagged
hibernated
}
}
`;
export const STATEMENTS_QUERY = gql`
query Statements($space: String!, $delegate_in: [String]!) {
statements(where: { space: $space, delegate_in: $delegate_in }) {
delegate
space
statement
about
ipfs
id
discourse
network
status
}
}
`;
export const SPACE_QUERY = gql`
query Space($id: String!) {
space(id: $id) {
id
name
about
network
symbol
network
terms
skin
avatar
twitter
website
github
coingecko
private
domain
admins
moderators
members
categories
labels {
id
name
description
color
}
plugins
followersCount
template
guidelines
verified
turbo
flagged
hibernated
parent {
id
name
avatar
followersCount
children {
id
}
}
children {
id
name
avatar
followersCount
parent {
id
}
}
voting {
delay
period
type
quorum
quorumType
privacy
hideAbstain
}
strategies {
name
network
params
}
validation {
name
params
}
voteValidation {
name
params
}
filters {
minScore
onlyMembers
}
delegationPortal {
delegationType
delegationContract
delegationNetwork
delegationApi
}
treasuries {
name
address
network
}
boost {
enabled
bribeEnabled
}
}
}
`;
export const LEADERBOARD_QUERY = gql`
query Leaderboard($space: String!, $user_in: [String]) {
leaderboards(where: { space: $space, user_in: $user_in }) {
space
user
proposalsCount
votesCount
lastVote
}
}
`;
================================================
FILE: src/helpers/shutter.ts
================================================
import { randomBytes } from '@ethersproject/random';
import { BigNumber } from '@ethersproject/bignumber';
import { arrayify, hexlify } from '@ethersproject/bytes';
import { toUtf8Bytes, formatBytes32String } from '@ethersproject/strings';
import shutterWasm from '@shutter-network/shutter-crypto/dist/shutter-crypto.wasm?url';
import { init, encrypt } from '@shutter-network/shutter-crypto';
export default async function encryptChoice(
choice: string,
id: string
): Promise {
await init(shutterWasm);
const bytesChoice = toUtf8Bytes(choice);
const message = arrayify(bytesChoice);
const eonPublicKey = arrayify(import.meta.env.VITE_SHUTTER_EON_PUBKEY);
const is32ByteString = id.substring(0, 2) === '0x';
const proposalId = arrayify(is32ByteString ? id : formatBytes32String(id));
const sigma = arrayify(BigNumber.from(randomBytes(32)));
const encryptedMessage = await encrypt(
message,
eonPublicKey,
proposalId,
sigma
);
return hexlify(encryptedMessage) ?? null;
}
================================================
FILE: src/helpers/sign.ts
================================================
import { getAddress } from '@ethersproject/address';
import type { Web3Provider } from '@ethersproject/providers';
import type { Wallet } from '@ethersproject/wallet';
const domain = {
name: 'snapshot',
version: '0.1.4'
};
export type DataType = Record;
export type ISubscribe = {
address: string;
} & Record;
export default async function sign(
web3: Web3Provider | Wallet,
address: string,
message: ISubscribe,
types: DataType
) {
const signer = 'getSigner' in web3 ? web3.getSigner() : web3;
message.address = getAddress(address);
return await signer._signTypedData(domain, types, message);
}
================================================
FILE: src/helpers/snapshot.ts
================================================
import { getVp, validate } from '@snapshot-labs/snapshot.js/src/utils';
import { apolloClient } from '@/helpers/apollo';
import { PROPOSAL_QUERY, VOTES_QUERY } from '@/helpers/queries';
import { ExtendedSpace, Proposal, Vote } from '@/helpers/interfaces';
import { isAddress } from '@ethersproject/address';
import cloneDeep from 'lodash/cloneDeep';
export async function getProposalVotes(
proposalId: string,
{
first = 1000,
voter = '',
skip = 0,
space = '',
orderBy = 'vp',
orderDirection = 'desc',
created_gte = 0
} = {}
): Promise {
try {
console.time('getProposalVotes');
const response = await apolloClient.query({
query: VOTES_QUERY,
variables: {
id: proposalId,
orderBy,
orderDirection,
first,
voter: isAddress(voter) ? voter : undefined,
skip,
space: space || undefined,
created_gte
}
});
console.timeEnd('getProposalVotes');
const votesResClone = cloneDeep(response);
return votesResClone.data.votes || [];
} catch (e) {
console.log(e);
return [];
}
}
export async function getProposal(id) {
try {
console.time('getProposal');
const response = await apolloClient.query({
query: PROPOSAL_QUERY,
variables: {
id
}
});
console.timeEnd('getProposal');
const proposalResClone = cloneDeep(response);
const proposal = proposalResClone.data.proposal;
if (proposal?.plugins?.daoModule) {
// The Dao Module has been renamed to SafeSnap
// Previous proposals have to be renamed
proposal.plugins.safeSnap = proposal.plugins.daoModule;
delete proposal.plugins.daoModule;
}
return proposal;
} catch (e) {
console.log(e);
return e;
}
}
export async function getPower(space, address, proposal) {
console.log('[score] getPower');
const options: any = {};
if (import.meta.env.VITE_SCORES_URL)
options.url = import.meta.env.VITE_SCORES_URL;
return getVp(
address,
proposal.network,
proposal.strategies,
parseInt(proposal.snapshot),
space.id,
proposal.delegation === 1,
options
);
}
export async function voteValidation(
space: ExtendedSpace,
address: string,
proposal: Proposal
): Promise {
console.log('[score] getValidation');
const options: any = {};
if (import.meta.env.VITE_SCORES_URL)
options.url = import.meta.env.VITE_SCORES_URL;
const params = cloneDeep(proposal.validation?.params) || {};
if (proposal.validation.name === 'basic') {
params.strategies = params.strategies ?? proposal.strategies;
}
const validateRes = await validate(
proposal.validation.name,
address,
space.id,
proposal.network,
parseInt(proposal.snapshot),
params,
options
);
if (typeof validateRes !== 'boolean') {
console.error('Vote validation failed', validateRes);
return false;
}
return validateRes;
}
export async function proposalValidation(
space: ExtendedSpace,
address: string
): Promise {
console.log('[score] getProposalValidation');
const options: any = {};
if (import.meta.env.VITE_SCORES_URL)
options.url = import.meta.env.VITE_SCORES_URL;
const params = space.validation?.params || {};
if (space.validation.name === 'basic') {
params.minScore =
space.validation?.params?.minScore || space.filters.minScore;
params.strategies =
space.validation?.params?.strategies || space.strategies;
}
const validateRes = await validate(
space.validation.name,
address,
space.id,
space.network,
'latest',
params,
options
);
if (typeof validateRes !== 'boolean') {
console.error('Proposal validation failed', validateRes);
return false;
}
return validateRes;
}
================================================
FILE: src/helpers/transaction.ts
================================================
import { ERC20ABI } from '@/helpers/constants';
import { sendTransaction } from '@snapshot-labs/snapshot.js/src/utils';
export function sendApprovalTransaction(
provider: any,
token: string,
contract: string,
amount: string
) {
return sendTransaction(
provider,
token,
ERC20ABI,
'approve',
[contract, amount],
{}
);
}
================================================
FILE: src/helpers/utils.test.js
================================================
import { describe, expect, it } from 'vitest';
import { calcFromSeconds, calcToSeconds } from './utils';
describe('calcFromSeconds', () => {
it('should return 3 for seconds from 10800 to 14399 and unit h', () => {
expect(calcFromSeconds(60 * 60 * 3, 'h')).toBe(3);
expect(calcFromSeconds(60 * 60 * 4 - 1, 'h')).toBe(3);
});
it('should return 3 for seconds from 259200 to 345599 and unit d', () => {
expect(calcFromSeconds(60 * 60 * 24 * 3, 'd')).toBe(3);
expect(calcFromSeconds(60 * 60 * 24 * 4 - 1, 'd')).toBe(3);
});
it('should return 3600 for 1 hour and 5400 for 1.5 hours', () => {
expect(calcToSeconds(1, 'h')).toBe(3600);
expect(calcToSeconds(1.5, 'h')).toBe(5400);
});
it('should return 86400 for 1 day and 129600 for 1.5 days', () => {
expect(calcToSeconds(1, 'd')).toBe(86400);
expect(calcToSeconds(1.5, 'd')).toBe(129600);
});
});
================================================
FILE: src/helpers/utils.ts
================================================
import pkg from '@/../package.json';
import { formatEther } from '@ethersproject/units';
import { BigNumber } from '@ethersproject/bignumber';
import networks from '@snapshot-labs/snapshot.js/src/networks.json';
import voting from '@snapshot-labs/snapshot.js/src/voting';
import { getUrl } from '@snapshot-labs/snapshot.js/src/utils';
import { getAddress } from '@ethersproject/address';
export function shortenAddress(str = '') {
return `${str.slice(0, 6)}...${str.slice(str.length - 4)}`;
}
export function shorten(str: string, key?: any): string {
if (!str) return str;
let limit;
if (typeof key === 'number') limit = key;
if (key === 'symbol') limit = 6;
if (key === 'name') limit = 64;
if (key === 'choice') limit = 12;
if (limit)
return str.length > limit ? `${str.slice(0, limit).trim()}...` : str;
return shortenAddress(str);
}
export function getChoiceString(proposal, selected) {
const votingClass = new voting[proposal.type](proposal, '', '', selected);
return votingClass.getChoiceString();
}
export function jsonParse(input, fallback?) {
if (typeof input !== 'string') {
return fallback || {};
}
try {
return JSON.parse(input);
} catch (err) {
return fallback || {};
}
}
export function lsSet(key: string, value: any) {
return localStorage.setItem(`${pkg.name}.${key}`, JSON.stringify(value));
}
export function lsGet(key: string, fallback?: any) {
const item = localStorage.getItem(`${pkg.name}.${key}`);
return jsonParse(item, fallback);
}
export function lsRemove(key: string) {
return localStorage.removeItem(`${pkg.name}.${key}`);
}
export function mapOldPluginNames(space) {
// The Dao Module has been renamed to SafeSnap
// Previous spaces plugins have to be renamed
if (space.plugins?.daoModule) {
space.plugins.safeSnap = space.plugins.daoModule;
delete space.plugins.daoModule;
}
return space;
}
export function formatAmount(amount, maxDecimals) {
let out = formatEther(amount);
if (maxDecimals && out.includes('.')) {
const parts = out.split('.');
if (parts[1].length > maxDecimals) {
out = `~${parts[0]}.${parts[1].slice(0, maxDecimals)}`;
}
}
return `${out} ETH`;
}
export function parseAmount(input) {
return BigNumber.from(input).toString();
}
export function parseValueInput(input) {
try {
return parseAmount(input);
} catch (e) {
return input;
}
}
export function getNumberWithOrdinal(n) {
const s = ['th', 'st', 'nd', 'rd'],
v = n % 100;
return n + (s[(v - 20) % 10] || s[v] || s[0]);
}
export function explorerUrl(network, str: string, type = 'address'): string {
return `${networks[network].explorer.url}/${type}/${str}`;
}
export function openProfile(address: string, domain: string, router: any) {
return domain
? window.open(`https://snapshot.org/#/profile/${address}`, '_blank')
: router.push({
name: 'profileActivity',
params: { address: address }
});
}
export function calcFromSeconds(value, unit) {
if (unit === 'm') return Math.floor(value / 60);
if (unit === 'h') return Math.floor(value / (60 * 60));
if (unit === 'd') return Math.floor(value / (60 * 60 * 24));
}
export function calcToSeconds(value, unit) {
if (unit === 'm') return value * 60;
if (unit === 'h') return value * 60 * 60;
if (unit === 'd') return value * 60 * 60 * 24;
}
export function getIpfsUrl(url: string) {
const gateway: any =
import.meta.env.VITE_IPFS_GATEWAY || 'cloudflare-ipfs.com';
return getUrl(url, gateway);
}
export async function clearStampCache(id: string, type = 'space') {
if (type === 'space')
return await fetch(`https://cdn.stamp.fyi/clear/space/${id}`);
if (type === 'avatar')
return await fetch(`https://cdn.stamp.fyi/clear/avatar/eth:${id}`);
}
export async function resolveHandle(handle: string) {
try {
const results = await fetch(import.meta.env.VITE_STAMP_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ method: 'resolve_names', params: [handle] })
});
return (await results.json()).result?.[handle];
} catch (e) {
console.error('Error resolving handle:', handle, e);
return null;
}
}
export async function lookupAddress(
addresses: string[]
): Promise> {
if (addresses.length === 0) {
return {};
}
try {
const response = await fetch(import.meta.env.VITE_STAMP_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
method: 'lookup_addresses',
params: addresses.slice(0, 50)
})
});
const results = (await response.json()).result;
return Object.fromEntries(
addresses.map(address => [address, results[address] || ''])
);
} catch (e) {
console.error('Error resolving addresses:', e);
return {};
}
}
export function isSnapshotUrl(url: string) {
let parsedUrl;
try {
parsedUrl = new URL(url);
} catch (err) {
console.error('Invalid URL', err);
return;
}
if (parsedUrl.hostname === 'snapshot.org') {
return true;
}
return false;
}
export function toChecksumAddress(address: string) {
try {
return getAddress(address.toLowerCase());
} catch (e) {
return address;
}
}
export function addressEqual(address1: string, address2: string) {
return address1.toLowerCase() === address2.toLowerCase();
}
================================================
FILE: src/helpers/validation.ts
================================================
import snapshot from '@snapshot-labs/snapshot.js';
const { env } = useApp();
function getErrorMessage(errorObject): string {
if (!errorObject.message) return 'Invalid field.';
if (errorObject.keyword === 'format') {
switch (errorObject.params.format) {
case 'address':
return 'Must be a valid address.';
case 'ethValue':
return 'Must be a number.';
case 'customUrl':
return 'Must be a valid URL.';
case 'uri':
return 'Must be a valid URL.';
case 'percentage':
return 'Percentage must be between 0 and 100.';
default:
return 'Invalid format.';
}
}
return `${errorObject.message
.charAt(0)
.toLocaleUpperCase()}${errorObject.message.slice(1)}.`;
}
export function validateForm(
schema: Record,
form: Record,
options = {
spaceType: 'default'
}
): Record {
const valid = snapshot.utils.validateSchema(schema, form, {
spaceType: options.spaceType || 'default',
snapshotEnv: env === 'production' ? 'mainnet' : 'default'
});
if (!Array.isArray(valid)) return {};
return transformAjvErrors(valid);
}
interface ValidationErrorOutput {
[key: string]: ValidationErrorOutput | string;
}
function transformAjvErrors(errors): ValidationErrorOutput {
errors = errors.map(error => {
if (error.instancePath) return error;
const propertyName = error.params.missingProperty;
if (!propertyName) return error;
const path = `/${propertyName}`;
return {
...error,
instancePath: path
};
});
return errors.reduce((output: ValidationErrorOutput, error) => {
const path: string[] = extractPathFromError(error);
// Skip the current error if the path is empty
if (path.length === 0) {
return output;
}
const targetObject: ValidationErrorOutput = findOrCreateNestedObject(
output,
path
);
targetObject[path[path.length - 1]] = getErrorMessage(error);
return output;
}, {});
}
function extractPathFromError(error): string[] {
if (!error.instancePath) {
return [];
}
return error.instancePath.split('/').slice(1);
}
function findOrCreateNestedObject(
output: ValidationErrorOutput,
path: string[]
): ValidationErrorOutput {
const parentPath: string[] = path.slice(0, path.length - 1);
const parentObject: ValidationErrorOutput = parentPath.reduce(
(current: ValidationErrorOutput, subpath: string) => {
if (!current[subpath]) {
current[subpath] = {} as ValidationErrorOutput;
}
return current[subpath] as ValidationErrorOutput;
},
output
);
return parentObject;
}
================================================
FILE: src/helpers/vitePlugins.ts
================================================
import vue from '@vitejs/plugin-vue';
import ViteComponents from 'unplugin-vue-components/vite';
import visualizer from 'rollup-plugin-visualizer';
import Icons from 'unplugin-icons/vite';
import IconsResolver from 'unplugin-icons/resolver';
import { FileSystemIconLoader } from 'unplugin-icons/loaders';
import { sentryVitePlugin } from '@sentry/vite-plugin';
import AutoImport from 'unplugin-auto-import/vite';
export const getPlugins = () => [
vue({ reactivityTransform: true }),
AutoImport({
dts: true,
imports: ['vue', 'vue-router'],
dirs: ['./src/composables'],
eslintrc: {
enabled: true
}
}),
ViteComponents({
globs: ['src/components/**/*.vue', '!src/components/Tune/_Form/*.vue'],
resolvers: [
IconsResolver({
customCollections: ['s'],
alias: {
ho: 'heroicons-outline'
}
})
]
}),
visualizer({
filename: './dist/stats.html',
template: 'sunburst',
gzipSize: true
}),
Icons({
compiler: 'vue3',
customCollections: {
s: FileSystemIconLoader('./src/assets/icons', svg =>
svg.replace(/^ Be careful, this link could be malicious. Are you sure you want to continue? ",
"version": "Version",
"timeline": "Timeline",
"ended": "ended",
"started": "started",
"filters": "Filters",
"allSpaces": "All spaces",
"submitOnchain": "Submit on-chain",
"inSpaces": "In {0} space(s)",
"votes": "Votes",
"seeMore": "See more",
"seeAll": "See all",
"network": "Network",
"networks": "Networks",
"skins": "Skins",
"spaceMembers": "Members",
"members": "No members | {count} member | {count} members",
"editStrategy": "Edit strategy",
"invalidProposals": "Invalid proposals",
"account": "Account",
"create3box": "Create profile on 3Box",
"view3box": "View profile on 3Box",
"edit3box": "Edit profile on 3Box",
"connectWallet": "Connect wallet",
"toggleSkin": "Toggle skin",
"about": "About",
"license": "License",
"showMore": "Show more",
"voted": "Voted",
"reload": "Reload",
"ipfsServer": "IPFS server",
"hub": "Hub",
"cancel": "Cancel",
"delete": "Delete",
"demoSite": "This is the demo site, give it a try!",
"removeDelegation": "Remove delegation",
"confirmRemove": "Are you sure you want to remove your delegation to",
"removeSpace": "for the space {0}",
"noVotingPower": "Oops, it seems you don't have any voting power at block {blockNumber}.",
"quorumReached": "quorum reached",
"options": "Option(s)",
"votingPower": "Your voting power",
"comment": {
"placeholder": "Share your reason (optional)"
},
"receipt": "Receipt",
"relayer": "Relayer",
"verifyOnMycrypto": "Verify receipt on MyCrypto",
"verifyOnSignatorio": "Verify on Signator.io",
"isCore": "Core",
"notificationsBlocked": "Your browser is blocking notifications",
"notificationsNotSupported": "Your browser does not support notifications",
"walletNotSupported": "Wallet is not supported",
"seeInExplorer": "See explorer",
"learnMore": "Learn more",
"logout": "Log out",
"continue": "Continue",
"add": "Add",
"edit": "Edit",
"strategyParameters": "Strategy parameters",
"addAction": "Add action",
"removeAction": "Remove action",
"yourChoice": "Choice {0}",
"targetAddress": "Target address",
"value": "Value",
"date": "Data",
"marketDetails": "Market details",
"addMarket": "Add market",
"selectNetwork": "Select network",
"conditionId": "Condition ID",
"basetokenAddress": "Base token address",
"quoteAddress": "Quote currency address",
"removeMarket": "Remove market",
"back": "Back",
"set": "Set",
"loading": "Loading...",
"predictedImpact": "Predicted impact",
"marketSymbol": "{0} market",
"twoChoicesRequired": "Two choices are required for this plugin.",
"noResultsFound": "Oops, we can't find any results",
"createFirstProposal": "Let's create your first proposal",
"noSpacesJoined": "Oops, you haven't joined any spaces yet",
"addFavorites": "Add favorites",
"createdBy": "By {0}",
"startIn": "start {0}",
"endIn": "ends {0}",
"proposalTimeLeft": "{0} left",
"endedAgo": "ended {0}",
"proposalBy": "by {0}",
"endDate": "end {0}",
"defaultSkin": "Default skin",
"select": "Select",
"language": "Language",
"agree": "I agree",
"moderators": "Moderators",
"playground": "Playground",
"strategyDetails": "Strategy details",
"strategyParams": "Strategy params",
"useCustomStrategies": "Use custom strategies",
"addresses": "Addresses",
"networkErrorPlayground": "Network error - please open your browser console for more information",
"upload": "Upload",
"join": "Join",
"joined": "Joined",
"leave": "Leave",
"subspaces": "Sub-spaces",
"mainspace": "Main space",
"copyLink": "Copy link",
"duplicate": "Duplicate",
"report": "Report",
"flag": "Flag",
"joinedSpaces": "Joined spaces",
"joinSpaces": "Join spaces",
"setDelegationToSpace": "Limit delegation to a specific space",
"theCurrentNetwork": "the current network",
"optional": "(optional)",
"homeLoadmore": "Load more",
"confirmAction": "Confirm action",
"or": "or",
"share": "Share",
"shareOnTwitter": "Share on Twitter",
"shareOnHey": "Share on Hey",
"createButton": "Create",
"discussion": "Discussion",
"changeWallet": "Change wallet",
"createASpace": "Create a space",
"getStarted": "Get started",
"skip": "Skip",
"reactivateSpace": "Reactivate space",
"newSpaceNotice": {
"header": "Your space is live!",
"mainText": "You can change how voting power is calculated via strategies in your {settings}. Changes to your settings will only affect new proposals, existing proposals can not be changed.",
"learnMore": "Learn more in the {documentation} or contact support on {help}.",
"gotIt": "Got it!"
},
"errors": {
"required": "Field is required",
"minLength": "Field is required",
"maxLength": "Maximum length is {0}",
"pattern": "Invalid character",
"minItems": "Minimum {0} item(s) required",
"maxItems": "Maximum {0} item(s) allowed",
"members": {
"maxItems": "Maximum {limit} {role} allowed"
},
"minStrategy": "At least one strategy is required.",
"website": "URL should be in the format https://www.example.com",
"format": "Invalid format",
"type": "Invalid type",
"unsupportedImageType": "File type not supported, Supported formats are jpeg, jpg and png",
"fileTooBig": "File size should be smaller than 1MB",
"invalidAddress": "Invalid address"
},
"create": {
"proposalTitle": "Title",
"discussion": "Discussion (optional)",
"categorie(s)": "Select up to 2 categorie(s)",
"proposalDescription": "Description (optional)",
"preview": "Preview",
"choices": "Choices",
"addChoice": "Add choice",
"startDate": "Select start date",
"endDate": "Select end date",
"startTime": "Select start time",
"endTime": "Select end time",
"publish": "Publish",
"untitled": "Untitled",
"snapshotBlock": "Snapshot block number",
"voting": "Voting",
"votingSystem": "Voting system",
"choice": "Choice {0}",
"period": "Voting period",
"start": "Start",
"end": "End",
"days": "Days",
"hours": "Hours",
"minutes": "Minutes",
"schedule": "Schedule proposal",
"delayEnforced": "The space enforces a delay until voting can start",
"periodEnforced": "The space enforces the duration of the voting period",
"typeEnforced": "{type} is enforced by the space",
"privacyEnforced": "{type} is enforced by the space",
"edit": "Edit",
"continue": "Continue",
"now": "Now",
"votingPeriodExplainer": "This is the time period in which users will be able to vote. The proposal will be visible and pending before the start of the voting period.",
"uploadImageExplainer": "Attach images by dragging & dropping, selecting or pasting them.",
"uploading": "Uploading image",
"markdown": "Styling with Markdown is supported",
"errorGettingSnapshot": "We encountered an error while fetching the snapshot block number which is needed to calculate your voting power. Please try again later.",
"errorTimeInPast": "Entered time is in the past. Please select a future time.",
"errorSpaceHibernatedAdmin": "Admins your attention is required. This space has been hibernated due to a lack of activity, and as a result, proposal creation is currently disabled. To re-enable your space, please go to the settings page and click \"Reactivate\".",
"errorSpaceHibernatedUsers": "This space has been hibernated due to a lack of activity. As a result, the creation of proposals has been temporarily disabled. To resume your ability to create proposals, the space needs to be reactivated by an admin. Please check back later or contact your space admin for further information."
},
"delegates": {
"header": "Delegates",
"filters": {
"mostVotingPower": "Most voting power",
"mostDelegators": "Most delegators",
"mostProposals": "Most proposals",
"mostVotes": "Most votes"
},
"delegateModal": {
"title": "Delegate",
"sub": "You are about to delegate all of your voting power."
},
"profileModal": {
"title": "Profile"
}
},
"delegate": {
"header": "Delegate",
"selectDelegate": "Select delegate",
"to": "To",
"addressPlaceholder": "Address or ENS name",
"delegations": "Your delegation(s)",
"allSpaces": "For all spaces",
"delegated": "Delegated to you",
"pendingTransaction": "no pending transaction | 1 pending transaction | {count} pending transactions",
"topDelegates": "Top delegates",
"noDelegatesFoundFor": "No delegates found for {0}",
"noValidEns": "Not a valid ENS address.",
"noValidAddress": "Not a valid address",
"delegateToSelf": "You cannot delegate to yourself",
"delegateToSelfAddress": "You cannot delegate to your own ENS address",
"noValidSpaceId": "Not a valid space ID",
"noDelegationsAndDelegates": "Can't find your delegations and delegates? Make sure you are connected to the correct network.",
"delegateNotSupported": "Delegation is currently not supported for {network}."
},
"proposal": {
"castVote": "Cast your vote",
"vote": "Vote",
"startDate": "Start date",
"endDate": "End date",
"votingSystem": "Voting system",
"privacy": "Privacy",
"invalidChoice": "Invalid choice",
"postVoteModal": {
"defaultTitle": "Your vote is in!",
"gnosisSafeTitle": "Your vote is pending...",
"gnosisSafeDescription": " Votes with a Safe require additional signers and will be visible once the transaction is confirmed",
"seeQueue": "See queued transactions",
"tips": {
"1": "Votes can be changed while the proposal is active"
},
"subscribe": "Subscribe with email"
},
"downloadCsvVotes": {
"title": "Download as CSV",
"postDownloadModal": {
"title": "Generating votes report",
"message": {
"pendingGeneration": {
"title": "Your report is currently being generated",
"description": "It may take a few minutes. Please check back shortly."
},
"unsupportedEnv": {
"title": "Unsupported environment",
"description": "Votes report are not available on demo."
},
"unknownError": {
"title": "We're having trouble connecting to the server responsible for downloads",
"description": "Please try again in a few moments. If the problem persists, consider contacting our support team."
}
}
}
},
"votesModal": {
"title": "Votes",
"filtersPopover": {
"title": "Filters",
"votingPower": "Voting power",
"onlyVotesWithReason": "Only show votes with a reason",
"more": "More"
}
}
},
"proposals": {
"header": "Proposals",
"new": "New proposal",
"noProposals": "There aren't any proposals here yet!",
"createProposal": "Create proposal",
"showMore": "Show more",
"showLess": "Show less",
"states": {
"all": "All",
"core": "Core",
"community": "Community",
"active": "Active",
"pending": "Pending",
"closed": "Closed"
}
},
"notifications": {
"header": "Notifications",
"noNotifications": "You have no notifications",
"proposalStarted": "proposal has started:",
"proposalEnded": "proposal has ended:",
"markAllAsRead": "Mark all as read",
"all": "All",
"unread": "Unread"
},
"modalNotifications": {
"wrong timestamp": {
"title": "Wrong timestamp",
"message": "The message timestamp is not valid. Please check the clock on your device, make sure it is set to the correct date and time and try again.\n\n If you need further assistance, feel free to reach out to our support team on {help}."
},
"space with ticket requires voting validation": {
"title": "Missing voting validation",
"message": "Your proposal cannot be submitted due to a missing voting validation rule required with the 'ticket' strategy. Please adjust your settings by either using another strategy or adding the required validation.\n\n If further assistance is needed, contact our support team on {help}."
},
"space missing proposal validation": {
"title": "Missing proposal validation",
"message": "Your proposal cannot be submitted due to a missing proposal validation rule. To prevent unauthorized proposals and spam, add a validation rule in your space settings.\n\n For assistance, reach out to our support team on {help}."
}
},
"modalTerms": {
"mustAgreeTo": "To {action} this space, you must agree to the {spaceName} terms of service.",
"actionJoin": "join",
"actionCreate": "create a proposal in",
"actionVote": "vote in"
},
"settings": {
"header": "Settings",
"confirmLeaveMessage": " Do you really want to leave? You have unsaved changes that will be lost.",
"confirmDeleteSpace": " Do you really want to delete this space? This action cannot be undone and you will not be able to use {name} again to create another space.",
"validationErrorsMessage": "Please fix the following fields before saving:",
"confirmInputDeleteSpace": "Enter {space} to continue:",
"noticeSavedTitle": "Settings saved",
"noticeSavedText": "New settings only affect future proposals, not existing ones.",
"noticeSavedInputCheckboxLabel": "Don't show again",
"navigation": {
"general": "General",
"strategies": "Strategies",
"proposal": "Proposal",
"voting": "Voting",
"delegation": "Delegation",
"members": "Members",
"advanced": "Advanced"
},
"editController": "Edit controller",
"connectWithSpaceOwner": "You are in view only mode, to modify space settings connect with a controller or admin wallet.",
"gnosisWrongNetwork": {
"base": "Your Gnosis Safe is on the wrong network. Please connect to {network} to {action}.",
"settings": "edit the space settings",
"create": "create a proposal",
"vote": "vote on this proposal"
},
"currentSpaceControllerIs": "The current space controller is {address}",
"newController": "New controller",
"noRecord": "No text-record found. Make sure you have registered {id} domain on {network}, then edit the controller text-record to regain access to the space settings.",
"set": "Set",
"profile": "Profile",
"avatar": "Avatar",
"name": {
"label": "Name",
"placeholder": "e.g. Yam Network"
},
"about": {
"label": "About",
"placeholder": "What is your organisation about?"
},
"categories": {
"label": "Categorie(s)",
"select": "Select categorie(s)"
},
"terms": {
"label": "Terms of service",
"information": "Users will be required to accept these terms once before they can create a proposal or cast a vote"
},
"hideSpace": "Hide space from homepage",
"links": "Social accounts",
"subspaces": {
"label": "Sub-spaces",
"information": "Add a sub-space to display its proposals within your space. If you want the current space to be displayed on the sub-space's page, the space needs to be added as main space in the sub-space settings to make relation mutual. {docs}",
"parent": {
"label": "Main space",
"placeholder": "pistachiodao.eth",
"information": "The space that this space is a sub-space of will be displayed on the space page"
},
"children": {
"label": "Sub-spaces",
"placeholder": "pistachiodao.eth",
"information": "Related Sub-spaces listed here will be displayed on the space page"
}
},
"website": "Website",
"strategies": {
"label": "Strategie(s)",
"information": "Strategies are used to determine voting power or whether a user is eligible to create a proposal"
},
"network": {
"label": "Network",
"information": "The default network used for this space. Networks can also be specified in individual strategies"
},
"symbol": {
"label": "Symbol",
"information": "The default symbol used for this space, usually the token symbol i.e. BAL for Balancer"
},
"strategiesList": "Select up to {0} strategies",
"votingPowerIsCumulative": "Voting power is cumulative",
"addStrategy": "Add strategy",
"testInPlayground": "Test in playground",
"members": {
"title": "Members",
"information": "Members have different roles and permissions within the space.",
"addMembers": "Add members",
"addMembersInformation": "Paste an address to add a member. You can add multiple members at once by separating them with a comma.",
"alreadyExists": "Member already exists",
"invalidAddress": "Invalid address",
"membersAdded": "Members added",
"admin": {
"description": "Able to modify the space settings, manage the space's proposals and create proposals"
},
"moderator": {
"description": "Able to manage the space's proposals and create proposals"
},
"author": {
"description": "Able to create proposals without having to go through proposal validation"
}
},
"proposalValidation": "Proposal validation",
"validation": "Type",
"proposalThreshold": {
"label": "Threshold",
"information": "The minimum amount of voting power required to create a proposal"
},
"allowOnlyAuthors": "Allow only authors to submit a proposal",
"editValidation": "Edit validation",
"selectValidation": "Select validation",
"validationParameters": "Validation parameters",
"voting": "Voting",
"votingDelay": "Voting delay",
"votingPeriod": "Voting period",
"hours": "Hours",
"days": "Days",
"quorum": {
"label": "Quorum",
"information": "The minimum amount of voting power required for the proposal to pass"
},
"type": {
"label": "Type",
"information": "The type of voting system used for this space. (Enforced on all future proposals)"
},
"anyType": "Any",
"hideAbstain": "Ignore abstain votes in basic voting results",
"customDomain": "Custom domain",
"domain": {
"label": "Domain name",
"placeholder": "e.g. vote.balancer.fi",
"info": "To setup a custom domain you additionally need to open a pull request on github after you have created the space. {docs}"
},
"skin": "Skin",
"treasuries": {
"label": "Treasury",
"add": "Add treasury",
"edit": "Edit treasury",
"information": "Add treasuries of your organization to show them in your space"
},
"addPlugin": "Add plugin",
"editPlugin": "Edit plugin",
"pluginParameters": "Plugin parameters",
"proposal": {
"title": "Proposal",
"guidelines": {
"title": "Guidelines",
"information": "Display a link to your guidelines on proposal creation to help users understand what constitutes a good/valid proposal"
},
"template": {
"title": "Template",
"information": "Start every proposal with a template to help users understand what information is required"
}
},
"dangerZone": {
"title": "Danger zone",
"changeController": {
"title": "Change space controller",
"information": "Change the controller of this space. This can only be done by the ENS controller.",
"button": "Change controller",
"disabledInformation": "You need to be the ENS owner ({owner}) to change the space controller."
},
"deleteSpace": {
"title": "Delete space",
"information": "Delete this space and all its content. This cannot be undone and you will not be able to create a new space with the same ENS domain name.",
"button": "Delete space",
"disabledInformation": "You need to be the space controller to delete the space"
}
},
"delegationPortal": {
"title": "Delegation portal",
"information": "Please ensure your token adheres to a compatible delegation standard to enable delegate discovery and activity within your Snapshot space."
},
"reactivatingHibernatedSpace": {
"information": "Your space is currently in hibernation due to six months of inactivity. To re-enable proposal creation, please click the button below to reactivate your space. There are no penalties for hibernation, but the space will remain inactive for proposal creation until reactivation by an admin.",
"disabledInformation": "Your space contains invalid settings since your last update. Fix the errors below and your space will be reactivated automatically upon save."
}
},
"setup": {
"example": "e.g. yam.eth",
"chooseExistingEns": "Choose one of your existing ENS domains to create a space with:",
"useSingleExistingEns": "Use your existing ENS domain:",
"orRegisterNewEns": "Or register a new domain:",
"demoTestnetEnsMessage": "To create a test space you need an ENS domain on {network}.",
"toCreateASpace": "To create a space, you first need an ENS domain. Enter one below and follow the ENS registration instructions.",
"createASpace": "Create a space",
"registerEnsButton": "Register",
"supportedEnsTLDs": "Supported domain endings",
"helpDocsAndDiscordLinks": "Not sure how to setup your space? Learn more in the {docs} or contact support on {help}.",
"setSpaceController": "Space controller",
"setSpaceControllerExists": "The snapshot text-record for this domain has already been set. Choose edit to change it, otherwise you can skip to the next step.",
"setSpaceControllerInfo": " The space controller is the account that will be able to manage the space settings. Additional space controllers (admins) can be added later.",
"setSpaceControllerInfoGnosisSafe": "When creating a space with a Gnosis Safe it's recommended to set the safe address as the space controller. If you don't, you need to follow some additional steps. {link}",
"editSpaceController": "Edit controller on ENS",
"setController": "Set controller",
"explainControllerAndEns": "Setting the controller requires a transaction on the {network} which will add a \"snapshot\" TEXT record to your ENS domain.",
"confirmToSetAddress": "Are you sure you want to set {address} as the controller of the space?",
"controllerHasAuthority": "The controller has full authority over the space settings",
"controller": "Controller",
"selectEnsForSpace": "Choose ENS address",
"spaceOwnerAddressPlaceHolder": "e.g. {address}",
"controllerAddress": "Controller address",
"updateController": "Update controller",
"seeOnEns": "See on ENS",
"goToSettings": "Go to settings",
"setSpaceProfile": "Customize your space",
"waitForTransaction": "The transaction need to confirm before you can create your space. {txUrl}",
"pendingTransactions": "Pending transactions",
"pleaseWaitMessage": "This can take a few minutes, please wait while the transaction is being confirmed",
"notControllerAddress": "Please connect with the controller address {wallet} to create the space.",
"fillCurrentAccount": "Use currently logged in account",
"domain": {
"title": "Setup your space domain",
"ensMessage": " One thing you need before you can create your own space, is an ENS domain on Ethereum mainnet.",
"ensMessageTestnet": "You can also {link} - a Sepolia testnet playground dedicated to testing before creating your space or proposals on Snapshot",
"tryDemo": "try the testnet.v1.snapshot.org",
"yourExistingSpaces": "Your existing spaces",
"invalidEns": "This ENS name is invalid. Usually this is due the use of invalid characters during registration."
},
"strategy": {
"title": "How would you like to setup voting?",
"subtitle": "You can change these settings any time.",
"blockTitle": "Setup voting strategy",
"onePersonOneVote": {
"title": "One person, one vote",
"description": "Manage a whitelist of people who can vote or simply allow any address to vote. Every vote is equal and no token is required",
"whitelistInformation": "Specify a number of accounts that can vote",
"ticketInformation": "Any account can vote",
"votesEqualInfo": "Each vote is equal and no token is required"
},
"tokenVoting": {
"title": "Token weighted voting",
"description": "Votes are weighted by a token. The token can be an ERC-20, ERC-721 or ERC-1155 token standard",
"tokenNotFound": "Token not found",
"seeOnEtherscan": "See on Etherscan"
},
"advanced": {
"title": "Custom setup",
"description": "Select up to 8 strategies with a wide range of options. If you can't find the right strategy for your use case, you can create your own"
}
},
"validationTitle": "Who can manage this space and create proposals?"
},
"profile": {
"buttonEdit": "Edit profile",
"viewProfile": "View profile",
"about": {
"header": "About",
"joinedSpaces": "Joined spaces",
"createdSpaces": "Created spaces",
"biography": "Bio",
"notJoinSpacesYet": "Hasn't joined any spaces yet",
"notCreatedSpacesYet": "Hasn't created any spaces yet",
"delegatorNetworkInfo": "Change by switching network in your wallet",
"delegate": "Delegate",
"delegated": "Delegated",
"delegateTo": "Delegate to",
"delegateFor": "Delegator for",
"noDelegatorsMessage": "No delegators on {network}",
"notSupportedNetwork": "Delegation currently isn't supported on {network} "
},
"activity": {
"header": "Activity",
"votedFor": "Voted {choice}",
"today": "Today",
"thisWeek": "This week",
"olderThanWeek": "Older than a week",
"noActivity": "No activity yet"
},
"settings": {
"header": "Edit profile",
"name": "Name",
"biography": "Bio",
"namePlaceholder": "Enter name",
"bioPlaceholder": "Tell your story",
"change": "Change",
"remove": "Remove"
}
},
"notify": {
"youDidIt": "You did it!",
"copied": "Copied!",
"proposalDeleted": "Proposal deleted",
"somethingWentWrong": "Oops, something went wrong!",
"saved": "Saved!",
"delegationSuccess": "Delegation successful",
"delegationRemoved": "Delegation removed",
"proposalCreated": "Proposal created",
"proposalUpdated": "Proposal updated",
"waitingForOtherSigners": "Waiting for other signers",
"voteSuccessful": "Your vote is in!",
"ensSet": "ENS text record was successfully set",
"transactionSent": "Transaction sent",
"emailPreferencesUpdated": "Email preferences updated",
"delegationAdded": "Well done. Delegate was successfully added",
"spaceReactivated": "The space has been reactivated"
},
"explore": {
"createStrategy": "Create strategy",
"createSkin": "Create skin",
"addNetwork": "Add network",
"createPlugin": "Create plugin",
"strategies": "strategie(s)",
"skins": "skin(s)",
"networks": "network(s)",
"plugins": "plugin(s)",
"results": "result(s)",
"category": "Category",
"categories": {
"all": "All",
"protocol": "Protocol",
"social": "Social",
"investment": "Investment",
"grant": "Grant",
"service": "Service",
"media": "Media",
"creator": "Creator",
"collector": "Collector",
"ai-agent": "AI agent",
"gaming": "Gaming",
"wallet": "Wallet",
"music": "Music",
"layer-2": "Layer 2",
"defai": "DeFi AI",
"defi": "DeFi",
"rwa": "RWA",
"depin": "DePin",
"meme": "Meme"
}
},
"voting": {
"selectVoting": "Select voting system",
"single-choice": {
"label": "Single choice voting",
"description": "Each voter may select only one choice."
},
"approval": {
"label": "Approval voting",
"description": "Each voter may select any number of choices."
},
"quadratic": {
"label": "Quadratic voting",
"description": "Each voter may spread voting power across any number of choices. Results are calculated quadratically."
},
"ranked-choice": {
"label": "Ranked choice voting",
"description": "Each voter may select and rank any number of choices. Results are calculated by instant-runoff counting method."
},
"weighted": {
"label": "Weighted voting",
"description": "Each voter may spread voting power across any number of choices."
},
"basic": {
"label": "Basic voting",
"description": "Single choice voting with three choices: For, Against or Abstain"
}
},
"privacy": {
"label": "Privacy",
"title": "Select voting privacy",
"information": "The type of privacy used on proposals. (Enforced on all future proposals)",
"any": "No privacy",
"none": "None",
"shutter": {
"label": "Shutter",
"description": "Choices are encrypted and only visible once the voting period is over",
"tooltip": "This proposal has Shutter privacy enabled. All votes will be encrypted until the voting period has ended and the final score is calculated",
"url": "https://blog.shutter.network/shielded-voting/"
}
},
"proposalValidation": {
"label": "Validation",
"title": "Select validation",
"settingsTitle": "Configure validation",
"paramPlaceholder": "Validation parameters",
"information": "The type of validation used to determine if a user can create a proposal. (Enforced on all future proposals)",
"executionError": "There was an error on our side and we could not verify if you are eligible to create a proposal. This is often due to a misconfigured voting validation or an unresponsive API involved in the validation.",
"notValidMessage": "Oops, you don't seem to be eligible to submit a proposal.",
"onlyMemberMessage": "You need to be a core member of the space in order to submit a proposal.",
"notConnectedMessage": "You need to connect your wallet in order to submit a proposal.",
"any": {
"label": "Anyone can create",
"description": "Anyone can create a proposal."
},
"basic": {
"label": "Basic",
"description": "Use a minimum score along with any strategy to determine if a user can create a proposal.",
"invalidMessage": "You do not meet the minimum balance requirement of {amount} {symbol} to create a proposal.",
"minScoreHint": "The score is calculated with Voting Strategies used by the space.",
"customStrategiesHint": "Calculate the score with a different configuration of Voting Strategies"
},
"passport-gated": {
"label": "Gitcoin Passport gated",
"description": "Protect your space from spam by requiring users to have a Gitcoin Passport to create a proposal.",
"invalidMessage": "You need a Gitcoin Passport with score above {scoreThreshold}",
"invalidMessageStamps": " and {operator} of the following stamps to create a proposal: {stamps} "
},
"arbitrum": {
"label": "Arbitrum DAO votable supply",
"description": "Use with erc20-votes to validate by percentage of votable supply."
},
"karma-eas-attestation": {
"label": "Karma EAS Attestation",
"description": "Use EAS attest.sh to determine if user can create a proposal."
}
},
"votingValidation": {
"label": "Validation",
"title": "Select validation",
"settingsTitle": "Configure validation",
"paramPlaceholder": "Validation parameters",
"information": "The type of validation used to determine if a user can vote. (Enforced on all future proposals)",
"executionError": "There was an error on our side and we could not verify if you are eligible to vote. This is often due to a misconfigured voting validation or an unresponsive API involved in the validation.",
"notValidMessage": "Oops, you don't seem to be eligible to vote on this proposal.",
"any": {
"label": "Anyone can vote",
"description": "Anyone with voting power can cast a vote."
},
"basic": {
"label": "Basic",
"description": "Use a minimum score along with any strategy to determine if a user can vote.",
"invalidMessage": "You do not meet the minimum balance requirement of {amount} {symbol} to vote on this proposal.",
"minScoreHint": "The score is calculated with Voting Strategies used by the space.",
"customStrategiesHint": "Calculate the score with a different configuration of Voting Strategies"
},
"passport-gated": {
"label": "Gitcoin Passport gated",
"description": "Protect your proposals from spam and vote manipulation by requiring users to have a Gitcoin Passport.",
"invalidMessage": "You need a Gitcoin Passport with score above {scoreThreshold}",
"invalidMessageStamps": " and {operator} of the following stamps to vote on this proposal: {stamps} "
}
},
"safeSnap": {
"currentOutcome": "Current outcome",
"currentBond": "Current bond",
"finalizedIn": "Finalized {0}",
"executableIn": "Executable {0}",
"finalOutcome": "Outcome",
"nextBond": "Bond to set outcome",
"setOutcomeTo": "Set outcome to",
"claimBond": "Claim bond",
"addBatch": "Add transaction batch",
"batch": "Transaction batch",
"transactions": "Transactions",
"to": "To (address)",
"invalidAddress": "Invalid address",
"invalidAmount": "Invalid amount",
"invalidValue": "Invalid value",
"invalidAbi": "Invalid ABI",
"invalidData": "Invalid data",
"value": "Value (wei)",
"data": "Data",
"noCollectibles": "No collectibles",
"asset": "Asset",
"amount": "Amount",
"type": "Type",
"transferFunds": "Transfer funds",
"transferNFT": "Transfer NFT",
"contractInteraction": "Contract interaction",
"rawTransaction": "Raw transaction",
"addTransaction": "Add transaction",
"transactionLabels": {
"contractInteraction": "{functionName}() - {amount} wei to {address}",
"transferFunds": "Transfer {amount} {tokenSymbol} to {address}",
"transferNFT": "Send {name} #{id} to {address}",
"raw": "Send {amount} wei to {address}"
},
"labels": {
"request": "Request execution",
"setOutcome": "Set outcome",
"changeOutcome": "Change outcome",
"executeTxs": "Execute transaction batch {0} of {1}",
"executed": "All transactions have been executed",
"noTransactions": "There are no transactions to execute",
"rejected": "Proposal rejected",
"error": "Something went wrong",
"connectWallet": "Connect wallet to see execution details",
"switchChain": "Switch your wallet to {0} to request execution",
"question": "Did this proposal pass and does it meet the",
"criteria": "acceptance criteria?",
"proposalPassed": "Did the proposal pass?",
"expired": "The proposal has expired",
"approveBond": "Approve bond",
"confirmVoteResults": "Click to request proposal execution",
"executeTxsUma": "Execute transaction batch",
"deleteDisputedProposal": "Delete disputed proposal",
"confirmVoteResultsToolTip": "Make sure this proposal was approved by this Snapshot vote before proposing on-chain. If the Snapshot vote rejected the proposal, the on-chain proposal will be rejected as well and you will lose your bond.",
"approveBondToolTip": "On-chain proposals require a bond from the proposer. This will approve tokens from your wallet to be posted as a bond. If you make an invalid proposal, it will be disputed and you will lose your bond. If the proposal is valid, your bond will be returned when the transactions are executed.",
"executeToolTip": "This will execute the transactions from this proposal and return the proposer's bond.",
"bondWarning": "Bond is greater than users balance",
"quorumWarning": "Quorum did not reach space requirement",
"requiredBond": "Required Bond:",
"challengePeriod": "Challenge Period:",
"disputeProposal": "UMA Oracle URL to dispute"
}
},
"poap": {
"no_poap_header": "A POAP hasn't been setup for this proposal yet :'(",
"no_voted_header": "Vote to get this POAP",
"unclaimed_header": "Mint your I voted POAP",
"claimed_header": "Congratulations! The POAP has been minted to your collection",
"loading_header": "The POAP is being minted to your collection",
"button_claim": "Mint",
"button_show": "Browse collection",
"success_claim": "The POAP has been minted to your collection",
"error_claim": "There was a problem minting the token"
},
"progress": {
"progress": "Progress",
"inProgress": "In Progress",
"completed": "Completed",
"complete": "Complete",
"newStep": "New Step",
"description": "Description",
"add": "Add",
"deleteStep": "Delete Step",
"deleteConfirm": "Are you sure you want to delete?",
"delete": "Delete",
"cancel": "Cancel",
"comeBack": "Come back after voting is complete to see how this proposal is progressing!",
"confirmSignature": "Signing this message will allow us to authorize your request to update the progress of your proposal.",
"wentWrong": "Oops something went wrong.",
"voting": "Voting",
"soon": "Soon"
},
"charts": {
"charts": "Charts",
"noVotesYet": "There are no votes to visualize yet.",
"totalVotesPerDay": "Total votes per day",
"shareOfVotingPower": "Share of voting power",
"votingPowerPerDay": "Voting power per day"
},
"metaInfo": {
"home": {
"title": "Snapshot - Where decisions get made",
"description": "Snapshot is an off-chain voting platform that allows DAOs, DeFi protocols, or NFT communities to participate in the decentralized governance easily and without gas fees."
},
"setup": {
"title": "Create a space",
"description": "Snapshot is a free, open-source platform for community governance. Create your own space now and start making decisions!"
},
"timeline": {
"title": "Timeline",
"description": "See what's happening in the Snapshot community."
},
"playground": {
"title": "Playground",
"description": "{strategy}"
},
"strategy": {
"title": "{strategy} strategy",
"description": ""
},
"space": {
"create": {
"title": "{space} Create proposal",
"description": "Create a proposal and start a vote."
},
"about": {
"title": "{space} About ",
"description": "{about}"
},
"proposals": {
"title": "{space} Proposals",
"description": "{about}"
},
"proposal": {
"title": "{space} Proposal: {proposal}",
"description": "{body}"
},
"settings": {
"title": "{space} Settings",
"description": "Change how proposals and voting works in this space."
}
}
},
"domino": {
"title": "Automations",
"information": "Create automated workflows with just a single click using Domino.",
"viewMore": "View More",
"createCustomWorkflow": "Create custom workflow"
},
"timeUnits": {
"second": "1 second | {n} seconds",
"minute": "1 minute | {n} minutes",
"hour": "1 hour | {n} hours",
"day": "1 day | {n} days",
"week": "1 week | {n} weeks",
"month": "1 month | {n} months",
"year": "1 year | {n} years"
},
"unsupportedNetwork": {
"unsupportedNetwork": "Unsupported network",
"switchNetworkToNetwork": "To continue, you need to change the network in your wallet to {network}.",
"switchToNetwork": "Switch to {network}",
"goToDemoSite": "Go to demo website"
},
"treasury": {
"title": "Treasury",
"wallets": {
"title": "Wallets",
"empty": "This space doesn't have a treasury yet",
"addTreasury": "Add a treasury"
},
"assets": {
"title": "Assets",
"empty": "There are no assets in this contract"
},
"24hChange": "24h change"
},
"newsletter": {
"yourEmail": "Your email",
"title": "Get the latest Snapshot updates",
"join": "Join Snapshot newsletter"
},
"emailSubscription": {
"title": "Email subscriptions",
"subscribe": "Subscribe",
"manage": "Manage subscriptions",
"description": "Subscribe to receive email notifications about your joined spaces and proposals activities.",
"inputCaption": "You may be asked to sign a message to verify wallet ownership",
"inputPlaceholder": "Your email",
"postSubscribeMessage": {
"successThanks": "Thanks for subscribing!",
"successConfirmation": "Please click the confirmation link that has been sent to your email."
}
},
"emailManagement": {
"title": "Subscription management",
"subtitle": "Choose the types of email updates that matter to you:",
"optionNewProposal": "Proposal creation",
"optionNewProposalDescription": "Get informed when a new proposal is submitted in your followed spaces.",
"optionClosedProposal": "Proposal closure",
"optionClosedProposalDescription": "Get informed when a proposal is closed in your followed spaces.",
"optionSummary": "Weekly summary",
"optionSummaryDescription": "Get a weekly report detailing the activity in your followed spaces.",
"updatePreferences": "Update preferences"
},
"emailResend": {
"title": "Verify email",
"description": "Email validation letter has been sent to your email address. Please click the confirmation link to complete the subscription."
},
"joinCommunity": "Join Snapshot community",
"header": {
"title": "Where decisions get made",
"description": "Snapshot is a free, open-source platform for community governance. Create your own space now and start making decisions!"
},
"aboutPage": {
"description": "Snapshot is a decentralized governance platform that makes it easy to create and vote on proposals - all without spending a fortune on gas fees! Plus, our flexible system supports various voting types and strategies, so you can tailor the voting process to your needs.",
"subHeader": "Governance should be a snap",
"subDescription": "Web3 governance doesn't have to be complicated. Snapshot is the perfect solution for organizations looking for an easy and efficient way to govern their community or organization."
},
"footerView": {
"resources": "Resources",
"about": "About",
"blog": "Blog",
"network-support": "Network support",
"terms": "Terms and conditions",
"jobs": "Jobs",
"faqs": "FAQs",
"github": "GitHub",
"featureRequests": "Request a feature",
"docs": "Docs",
"support": "Support",
"hiring": "Join us!"
}
}
================================================
FILE: src/locales/es-ES.json
================================================
{
"searchPlaceholder": "Buscar",
"spaceCount": "{0} espacio(s)",
"createSpace": "Crear espacio",
"backToHome": "Inicio",
"actions": "Acciones",
"poweredBy": "Powered by",
"results": "Resultados",
"resultsError": "Results could not be calculated. This is often due to a misconfigured strategy or an unresponsive RPC node involved in the strategy.",
"resultsCalculating": "Final results are being calculated. If you still see this message after a few minutes contact the space admin.",
"votingPowerFailedMessage": "Your voting power could not be calculated. This is often due to a misconfigured strategy or an unresponsive RPC node involved in the strategy.",
"votingValidationFailedMessage": "There was an error on our side and we could not verify if you are eligible to vote. This is often due to a misconfigured voting validation or an unresponsive API involved in the validation.",
"notValidVoterMessage": "Oops, you don't seem to be eligible to vote on this proposal.",
"getHelp": "Get help",
"retry": "Retry",
"currentResults": "Resultados actuales",
"reset": "Restablecer",
"close": "Close",
"save": "Guardar",
"author": "Autor",
"next": "Siguente",
"choice": "Choice",
"submit": "Enviar",
"plugins": "Plugins",
"information": "Información",
"confirm": "Confirmar",
"snapshot": "Snapshot",
"strategies": "Estrategia(s)",
"strategiesPage": "Estrategias",
"space": "Espacio",
"spaces": "Espacios",
"verifiedSpace": "Verified space",
"warningSpace": "This space has been flagged as potentially malicious. Proceed with caution.",
"version": "Versión",
"timeline": "Línea de tiempo",
"ended": "ended",
"started": "started",
"filters": "Filtros",
"allSpaces": "Todos los espacios",
"submitOnchain": "Enviar on-chain",
"inSpaces": "En {0} espacio(s)",
"votes": "Votos",
"seeMore": "Ver más",
"seeAll": "See all",
"network": "Red",
"networks": "Redes",
"skins": "Skins",
"spaceMembers": "Members",
"members": "Sin miembros | {count} member | {count} members",
"editStrategy": "Editar estrategia",
"invalidProposals": "Propuestas invalidas",
"account": "Cuenta",
"create3box": "Crear perfil en 3Box",
"view3box": "Ver perfil en 3Box",
"edit3box": "Editar perfil en 3Box",
"connectWallet": "Conectar cartera",
"toggleSkin": "Toggle skin",
"about": "Acerca de",
"license": "Licencia",
"showMore": "Show more",
"voted": "Voted",
"reload": "Reload",
"ipfsServer": "Servidor IPFS",
"hub": "Hub",
"cancel": "Cancelar",
"delete": "Delete",
"demoSite": "Este es el sitio de demostración, ¡pruébalo!",
"removeDelegation": "Eliminar delegación",
"confirmRemove": "¿Está seguro de que desea eliminar su delegación a",
"removeSpace": "para el espacio {0}",
"noVotingPower": "Oops, it seems you don't have any voting power at block {blockNumber}.",
"quorumReached": "quorum reached",
"options": "Opción(es)",
"votingPower": "Tu poder de voto",
"comment": {
"placeholder": "Share your reason (optional)"
},
"receipt": "Recibo",
"relayer": "Retransmisor",
"verifyOnMycrypto": "Verificar recibo en MyCrypto",
"verifyOnSignatorio": "Verifica en Signator.io",
"isCore": "Core",
"notificationsBlocked": "Tu navegador está bloqueando las notificaciones",
"notificationsNotSupported": "Your browser does not support notifications",
"walletNotSupported": "Wallet is not supported",
"seeInExplorer": "See explorer",
"learnMore": "Conoce más",
"logout": "Cerrar sesión",
"continue": "Continuar",
"add": "Agregar",
"edit": "Editar",
"strategyParameters": "Parámetros de estrategia",
"addAction": "Añadir acción",
"removeAction": "Eliminar acción",
"yourChoice": "Elegir {0}",
"targetAddress": "Dirección de destino",
"value": "Valor",
"date": "Datos",
"marketDetails": "Detalles del mercado",
"addMarket": "Añadir mercado",
"selectNetwork": "Seleccionar red",
"conditionId": "ID de condición",
"basetokenAddress": "Dirección de token base",
"quoteAddress": "Dirección de la moneda",
"removeMarket": "Eliminar mercado",
"back": "Atrás",
"loading": "Cargando...",
"predictedImpact": "Impacto previsto",
"marketSymbol": "{0} mercado",
"twoChoicesRequired": "Se requieren dos opciones para este plugin.",
"noResultsFound": "Oops, no podemos encontrar ningún resultado",
"createFirstProposal": "Creemos tu primer propuesta",
"noSpacesJoined": "Oops, no has unido a ningún espacio todavía",
"addFavorites": "Añadir favoritos",
"createdBy": "Por {0}",
"startIn": "iniciar {0}",
"endIn": "finalizar {0}",
"proposalTimeLeft": "{0} left",
"endedAgo": "ended {0}",
"proposalBy": "por {0}",
"endDate": "acaba {0}",
"defaultSkin": "Aspecto predeterminado",
"select": "Selecciona",
"language": "Idioma",
"agree": "Acepto",
"moderators": "Moderators",
"playground": "Centro de Pruebas",
"strategyParams": "Parámetros de estrategia",
"addresses": "Direcciones",
"networkErrorPlayground": "Error de red - por favor, abra la consola de su navegador para obtener más información",
"upload": "Subir",
"join": "Unirse",
"joined": "Registrado",
"leave": "Salir",
"subspaces": "Sub-spaces",
"mainspace": "Main space",
"copyLink": "Copiar enlace",
"duplicate": "Duplicate",
"joinedSpaces": "Espacios a los que te has unido",
"joinSpaces": "Únete a los espacios",
"setDelegationToSpace": "Limit delegation to a specific space",
"theCurrentNetwork": "the current network",
"optional": "(optional)",
"homeLoadmore": "Load more",
"confirmAction": "Confirm action",
"or": "or",
"share": "Share",
"shareOnTwitter": "Share on Twitter",
"shareOnLenster": "Share on Lenster",
"createButton": "Create",
"discussion": "Discussion",
"changeWallet": "Change wallet",
"createASpace": "Create a space",
"getStarted": "Get started",
"skip": "Skip",
"newSpaceNotice": {
"header": "Your space is live!",
"mainText": "You can change how voting power is calculated via strategies in your {settings}. Changes to your settings will only affect new proposals, existing proposals can not be changed.",
"learnMore": "Learn more in the {documentation} or join Snapshot {discord} for help.",
"gotIt": "Got it!"
},
"errors": {
"required": "Este campo es obligatorio",
"minLength": "Field is required",
"maxLength": "La cantidad máxima es {0}",
"pattern": "Carácter inválido",
"minItems": "Mínimo de {0} artículo(s) requeridos",
"maxItems": "Maximum {0} item(s) allowed",
"minStrategy": "Se requiere de al menos una estrategia.",
"website": "URL should be in the format https://www.example.com",
"format": "Formato inválido",
"type": "Type invalido",
"unsupportedImageType": "File type not supported, Supported formats are jpeg, jpg and png",
"invalidAddress": "Invalid address"
},
"create": {
"proposalTitle": "Title",
"discussion": "Discussion (optional)",
"categorie(s)": "Select up to 2 categorie(s)",
"proposalDescription": "Description (optional)",
"preview": "Vista previa",
"choices": "Opciones",
"addChoice": "Añadir opción",
"startDate": "Selecciona una fecha de inicio",
"endDate": "Seleccionar fecha de finalización",
"startTime": "Selecciona una hora de inicio",
"endTime": "Seleccionar hora de finalización",
"publish": "Publicar",
"untitled": "Untitled",
"snapshotBlock": "Número de bloque Snapshot",
"voting": "Voting",
"votingSystem": "Voting system",
"choice": "Choice {0}",
"period": "Voting period",
"start": "Start",
"end": "End",
"days": "Days",
"hours": "Hours",
"minutes": "Minutes",
"schedule": "Schedule proposal",
"delayEnforced": "The space enforces a delay until voting can start",
"periodEnforced": "The space enforces the duration of the voting period",
"typeEnforced": "{type} is enforced by the space",
"privacyEnforced": "{type} is enforced by the space",
"edit": "Edit",
"continue": "Continue",
"now": "Now",
"votingPeriodExplainer": "This is the time period in which users will be able to vote. The proposal will be visible and pending before the start of the voting period.",
"uploadImageExplainer": "Attach images by dragging & dropping, selecting or pasting them.",
"uploading": "Uploading image",
"markdown": "Styling with Markdown is supported",
"validationWarning": {
"basic": {
"member": "Necesitas ser un autor del espacio para poder presentar una propuesta.",
"minScore": "Necesitas tener un mínimo de {0} {1} para poder presentar una propuesta."
},
"customValidation": "Es necesario aprobar la validación de la propuesta para presentarla.",
"executionError": "Verifying your eligibility to create proposals in this space has failed. This is likely due to a misconfigured strategy."
},
"errorGettingSnapshot": "We encountered an error while fetching the snapshot block number which is needed to calculate your voting power. Please try again later."
},
"delegate": {
"header": "Delegar",
"selectDelegate": "Seleccionar delegado",
"to": "Para",
"addressPlaceholder": "Dirección o nombre de ENS",
"delegations": "Tu(s) delegación(es)",
"allSpaces": "Para todos los espacios",
"delegated": "Delegado a ti",
"pendingTransaction": "no hay transacciones pendientes | 1 transacción pendiente | {count} transacciones pendientes",
"topDelegates": "Los mejores delegados",
"noDelegatesFoundFor": "No delegates found for {0}",
"noValidEns": "Not a valid ENS address.",
"noValidAddress": "Not a valid address",
"delegateToSelf": "You cannot delegate to yourself",
"delegateToSelfAddress": "You cannot delegate to your own ENS address",
"noValidSpaceId": "Not a valid space ID",
"noDelegationsAndDelegates": "Can't find your delegations and delegates? Make sure you are connected to the correct network.",
"delegateNotSupported": "Delegation is currently not supported for {network}."
},
"proposal": {
"castVote": "Emite tu voto",
"vote": "Votar",
"startDate": "Fecha de inicio",
"endDate": "Fecha final",
"votingSystem": "Sistema de votación",
"privacy": "Privacy",
"invalidChoice": "Invalid choice",
"postVoteModal": {
"defaultTitle": "Your vote is in!",
"gnosisSafeTitle": "Your vote is pending...",
"gnosisSafeDescription": " Votes with a Safe require additional signers and will be visible once the transaction is confirmed",
"seeQueue": "See queued transactions",
"tips": {
"1": "Votes can be changed while the proposal is active"
}
}
},
"proposals": {
"header": "Propuestas",
"new": "Nueva propuesta",
"noProposals": "¡No hay ninguna propuesta aquí todavía!",
"createProposal": "Crea una propuesta",
"showMore": "Show more",
"showLess": "Show less",
"states": {
"all": "Todo",
"core": "Core",
"community": "Comunidad",
"active": "Activo",
"pending": "Pendiente",
"closed": "Cerrado"
}
},
"notifications": {
"header": "Notifications",
"noNotifications": "You have no notifications",
"proposalStarted": "proposal has started:",
"proposalEnded": "proposal has ended:",
"markAllAsRead": "Mark all as read",
"all": "All",
"unread": "Unread"
},
"modalTerms": {
"mustAgreeTo": "To {action} this space, you must agree to the {spaceName} terms of service.",
"actionJoin": "join",
"actionCreate": "create a proposal in",
"actionVote": "vote in"
},
"settings": {
"header": "Ajustes",
"editController": "Edit controller",
"connectWithSpaceOwner": "You are in view only mode, to modify space settings connect with a controller or admin wallet.",
"gnosisWrongNetwork": {
"base": "Your Gnosis Safe is on the wrong network. Please connect to {network} to {action}.",
"settings": "edit the space settings",
"create": "create a proposal",
"vote": "vote on this proposal"
},
"currentSpaceControllerIs": "The current space controller is {address}",
"newController": "New controller",
"noRecord": "No text-record found. Make sure you have registered {id} domain on {network}, then edit the controller text-record to regain access to the space settings.",
"set": "Set",
"profile": "Perfil",
"avatar": "Avatar",
"name": {
"label": "Name",
"placeholder": "e.g. Yam Network"
},
"about": {
"label": "About",
"placeholder": "What is your organisation about?"
},
"categories": {
"label": "Categorie(s)",
"select": "Select categorie(s)"
},
"terms": {
"label": "Terms of service",
"information": "Users will be required to accept these terms once before they can create a proposal or cast a vote"
},
"hideSpace": "Ocultar espacio de la página de inicio",
"links": "Social accounts",
"subspaces": {
"label": "Sub-spaces",
"information": "Sub-spaces will only be shown once configured on both the main space and the sub-space(s). {docs}",
"parent": {
"label": "Main space",
"placeholder": "pistachiodao.eth",
"information": "The space that this space is a sub-space of will be displayed on the space page"
},
"children": {
"label": "Sub-spaces",
"placeholder": "pistachiodao.eth",
"information": "Related Sub-spaces listed here will be displayed on the space page"
}
},
"website": "Website",
"strategies": {
"label": "Strategie(s)",
"information": "Strategies are used to determine voting power or whether a user is eligible to create a proposal"
},
"network": {
"label": "Network",
"information": "The defaul network used for this space. Networks can also be specified in individual strategies"
},
"symbol": {
"label": "Symbol",
"information": "The default symbol used for this space, usually the token symbol i.e. BAL for Balancer"
},
"strategiesList": "Select up to 8 strategies",
"votingPowerIsCumulative": "Voting power is cumulative",
"addStrategy": "Añadir estrategia",
"testInPlayground": "Test in playground",
"admins": {
"label": "Admins",
"information": "Admins are able to modify the space settings and manage the space's proposals"
},
"authors": {
"label": "Authors",
"information": "Authors are always able to create proposals"
},
"proposalValidation": "Validación de oferta",
"validation": "Type",
"proposalThreshold": {
"label": "Threshold",
"information": "The minimum amount of voting power required to create a proposal"
},
"allowOnlyAuthors": "Habilita que sólo autores puedan enviar una propuesta",
"editValidation": "Editar validación",
"selectValidation": "Seleccionar validación",
"validationParameters": "Parámetros de validación",
"voting": "Votación",
"votingDelay": "Retraso en la votación",
"votingPeriod": "Periodo de votación",
"hours": "Hours",
"days": "Days",
"quorum": {
"label": "Quorum",
"information": "The minimum amount of voting power required for the proposal to pass"
},
"type": {
"label": "Type",
"information": "The type of voting system used for this space. (Enforced on all future proposals)"
},
"anyType": "Cualquiera",
"hideAbstain": "Ignore abstain votes in basic voting results",
"customDomain": "Dominio personalizado",
"domain": {
"label": "Domain name",
"placeholder": "e.g. vote.balancer.fi",
"info": "To setup a custom domain you additionally need to open a pull request on github after you have created the space. {docs}"
},
"skin": "Tema",
"treasuries": {
"label": "Treasury",
"add": "Add treasury",
"edit": "Edit treasury",
"information": "Add treasuries of your organization to show them in your space"
},
"addPlugin": "Agregar extensión",
"editPlugin": "Editar extensión",
"pluginParameters": "Parámetros de la extensión",
"proposal": {
"title": "Proposal",
"guidelines": {
"title": "Guidelines",
"information": "Display a link to your guidelines on proposal creation to help users understand what constitutes a good/valid proposal"
},
"template": {
"title": "Template",
"information": "Start every proposal with a template to help users understand what information is required"
}
}
},
"setup": {
"example": "ej. yam.eth",
"chooseExistingEns": "Choose one of your existing ENS domains to create a space with:",
"useSingleExistingEns": "Use your existing ENS domain:",
"orRegisterNewEns": "Or register a new domain:",
"demoTestnetEnsMessage": "To create a test space you need an ENS domain on {network}.",
"toCreateASpace": "To create a space, you first need an ENS domain. Enter one below and follow the ENS registration instructions.",
"createASpace": "Crear espacio",
"registerEnsButton": "Register",
"supportedEnsTLDs": "Supported domain endings",
"helpDocsAndDiscordLinks": "Not sure how to setup your space? Learn more in the {docs} or join Snapshot {discord}.",
"setSpaceController": "Space controller",
"setSpaceControllerExists": "The snapshot text-record for this domain has already been set. Choose edit to change it, otherwise you can skip to the next step.",
"setSpaceControllerInfo": " The space controller is the account that will be able to manage the space settings. Additional space controllers (admins) can be added later.",
"setSpaceControllerInfoGnosisSafe": "When creating a space with a Gnosis Safe it's recommended to set the safe address as the space controller. If you don't, you need to follow some additional steps. {link}",
"editSpaceController": "Edit controller on ENS",
"setController": "Set controller",
"explainControllerAndEns": "Setting the controller requires a transaction on the {network} which will add a \"snapshot\" TEXT record to your ENS domain.",
"confirmToSetAddress": "Are you sure you want to set {address} as the controller of the space?",
"controllerHasAuthority": "The controller has full authority over the space settings",
"controller": "Controller",
"selectEnsForSpace": "Choose ENS address",
"spaceOwnerAddressPlaceHolder": "e.g. {address}",
"controllerAddress": "Controller address",
"updateController": "Update controller",
"seeOnEns": "See on ENS",
"goToSettings": "Go to settings",
"setSpaceProfile": "Customize your space",
"waitForTransaction": "The transaction need to confirm before you can create your space. {txUrl}",
"pleaseWaitMessage": "This can take a few minutes, please wait while the transaction is being confirmed",
"notControllerAddress": "Please connect with the controller address {wallet} to create the space.",
"fillCurrentAccount": "Use currently logged in account",
"domain": {
"title": "Setup your space domain",
"ensMessage": " One thing you need before you can create your own space, is an ENS domain on Ethereum mainnet.",
"ensMessageTestnet": "You can also {link} on the Goerli testnet and mess with things there first.",
"tryDemo": "try the demo",
"yourExistingSpaces": "Your existing spaces",
"invalidEns": "This ENS name is invalid. Usually this is due the use of invalid characters during registration."
},
"strategy": {
"title": "How would you like to setup your voting strategy?",
"subtitle": "You can change your strategy settings any time.",
"blockTitle": "Setup voting strategy",
"onePersonOneVote": {
"title": "One person, one vote",
"description": "Manage a whitelist of people who can vote or simply allow any address to vote. Every vote is equal and no token is required",
"whitelistInformation": "Specify a number of accounts that can vote",
"ticketInformation": "Any account can vote",
"votesEqualInfo": "Each vote is equal and no token is required"
},
"tokenVoting": {
"title": "Token weighted voting",
"description": "Votes are weighted by a token. The token can be an ERC-20, ERC-721 or ERC-1155 token standard",
"tokenNotFound": "Token not found",
"seeOnEtherscan": "See on Etherscan"
},
"advanced": {
"title": "Custom setup",
"description": "Select up to 8 strategies with a wide range of options. If you can't find the right strategy for your use case, you can create your own"
}
},
"validationTitle": "Who can manage this space and create proposals?"
},
"profile": {
"buttonEdit": "Edit profile",
"viewProfile": "View profile",
"about": {
"header": "About",
"joinedSpaces": "Joined spaces",
"createdSpaces": "Created spaces",
"biography": "Bio",
"notJoinSpacesYet": "Hasn't joined any spaces yet",
"notCreatedSpacesYet": "Hasn't created any spaces yet",
"delegatorNetworkInfo": "Change by switching network in your wallet",
"delegate": "Delegate",
"delegated": "Delegated",
"delegateTo": "Delegate to",
"delegateFor": "Delegator for",
"noDelegatorsMessage": "No delegators on {network}",
"notSupportedNetwork": "Delegation currently isn't supported on {network} "
},
"activity": {
"header": "Activity",
"votedFor": "Voted {choice}",
"today": "Today",
"thisWeek": "This week",
"olderThanWeek": "Older than a week",
"noActivity": "No activity yet"
},
"settings": {
"header": "Edit profile",
"name": "Name",
"biography": "Bio",
"namePlaceholder": "Enter name",
"bioPlaceholder": "Tell your story",
"change": "Change",
"remove": "Remove"
}
},
"notify": {
"youDidIt": "¡Lo hiciste!",
"copied": "¡Copiado!",
"proposalDeleted": "Propuesta eliminada",
"somethingWentWrong": "¡Huy! ¡Algo salió mal!",
"saved": "¡Guardado!",
"delegationSuccess": "Delegación exitosa",
"delegationRemoved": "Delegación eliminada",
"proposalCreated": "Propuesta creada",
"voteSuccessful": "¡Tu voto está dentro!",
"ensSet": "ENS text record was successfully set",
"transactionSent": "Transaction sent"
},
"explore": {
"createStrategy": "Crear estrategia",
"createSkin": "Crear apecto",
"addNetwork": "Añadir red",
"createPlugin": "Crear extensión",
"strategies": "estrategia(s)",
"skins": "aspecto(s)",
"networks": "red(es)",
"plugins": "extensión(es)",
"results": "resultado(s)",
"category": "Category",
"categories": {
"all": "All",
"protocol": "Protocol",
"social": "Social",
"investment": "Investment",
"grant": "Grant",
"service": "Service",
"media": "Media",
"creator": "Creator",
"collector": "Collector"
}
},
"voting": {
"selectVoting": "Selecciona sistema de votación",
"single-choice": "Votación de elección única",
"approval": "Votación por aprobación",
"quadratic": "Votación cuadrática",
"ranked-choice": "Voto por ranking",
"weighted": "Votación ponderada",
"basic": "Votación básica",
"description": {
"single-choice": "Cada votante puede elegir sólo una opción.",
"approval": "Los votantes puede seleccionar cualquier cantidad de opciones.",
"quadratic": "Cada votante puede repartir sus votos entre cualquier cantidad de opciones. Los resultados son calculados de forma cuadrática.",
"ranked-choice": "Cada votante puede seleccionar y clasificar cualquier cantidad de opciones. Los resultados serán calculados con un método de segunda vuelta instantánea.",
"weighted": "Cada votante puede repartir sus votos entre cualquier cantidad de opciones.",
"basic": "Votación individual con tres opciones: A favor, En Contra o Abstenerse"
}
},
"privacy": {
"label": "Privacy",
"title": "Select voting privacy",
"information": "The type of privacy used on proposals. (Enforced on all future proposals)",
"any": "Any",
"none": "None",
"shutter": {
"label": "Shutter",
"description": "Choices are encrypted and only visible once the voting period is over",
"tooltip": "This proposal has Shutter privacy enabled. All votes will be encrypted until the voting period has ended and the final score is calculated",
"url": "https://blog.shutter.network/shielded-voting/"
}
},
"validation": {
"label": "Validation",
"title": "Select voting validation",
"information": "The type of validation used to determine if a user can vote. (Enforced on all future proposals)",
"any": {
"label": "Anyone can vote",
"description": "Anyone with voting power can cast a vote."
},
"basic": {
"label": "Basic",
"description": "Use any strategy to determine if a user can vote.",
"invalidMessage": "You do not meet the minimum balance requirement to vote on this proposal. "
},
"passport-gated": {
"label": "Gitcoin Passport gated",
"description": "Protect your proposals from spam and vote manipulation by requiring users to have a Gitcoin Passport.",
"invalidMessage": "You need a Gitcoin Passport with score above {scoreThreshold} and {operator} of the following stamps to vote on this proposal: {stamps}. "
}
},
"safeSnap": {
"currentOutcome": "Resultado actual",
"currentBond": "Bono actual",
"finalizedIn": "Finalizado {0}",
"executableIn": "Ejecutable {0}",
"finalOutcome": "Resultado",
"nextBond": "Vínculo para establecer resultado",
"setOutcomeTo": "Establecer el resultado en",
"claimBond": "Reclamar vínculo",
"addBatch": "Agrega conjunto de transacciones",
"batch": "Conjunto de transacciones",
"transactions": "Transacciones",
"to": "To (address)",
"invalidAddress": "Dirección inválida",
"invalidAmount": "Monto inválido",
"invalidValue": "Valor inválido",
"invalidAbi": "ABI inválido",
"invalidData": "Data invalida",
"value": "Valor (wei)",
"data": "Data",
"noCollectibles": "No coleccionables",
"asset": "Activo",
"amount": "Monto",
"type": "Tipo",
"transferFunds": "Transferir fondos",
"transferNFT": "Transferir NFT",
"contractInteraction": "Contrato de interacción",
"rawTransaction": "Transacción cruda",
"addTransaction": "Añadir transacción",
"transactionLabels": {
"contractInteraction": "{functionName}() - {amount} comisión para {address}",
"transferFunds": "Transfer {amount} {tokenSymbol} para {address}",
"transferNFT": "Enviar {name} #{id} a {address}",
"raw": "Enviar {amount} comisión a {address}"
},
"labels": {
"request": "Solicitar ejecución",
"setOutcome": "Establecer resultado",
"changeOutcome": "Cambiar resultado",
"executeTxs": "Ejecutar transacción de lote {0} a {1}",
"executed": "Todas las transacciones han sido ejecutadas",
"noTransactions": "There are no transactions to execute",
"rejected": "Proposal rejected",
"error": "Algo salió mal",
"connectWallet": "Conecta tu cartera para ver los detalles de la ejecución",
"switchChain": "Cambia tu cartera a {0} para solicitar la ejecución",
"question": "Acaso pasó esta propuesta y cumple los",
"criteria": "criterios de aceptación?",
"proposalPassed": "Did the proposal pass?",
"expired": "The proposal has expired",
"approveBond": "Approve bond",
"confirmVoteResults": "Click to confirm the proposal passed",
"executeTxsUma": "Execute transaction batch",
"deleteDisputedProposal": "Delete disputed proposal",
"confirmVoteResultsToolTip": "Make sure this proposal was approved by this Snapshot vote before proposing on-chain. If the Snapshot vote rejected the proposal, the on-chain proposal will be rejected as well and you will lose your bond.",
"approveBondToolTip": "On-chain proposals require a bond from the proposer. This will approve tokens from your wallet to be posted as a bond. If you make an invalid proposal, it will be disputed and you will lose your bond. If the proposal is valid, your bond will be returned when the transactions are executed.",
"requestToolTip": "This will propose the transactions from this Snapshot vote on-chain. After a challenge window, if the proposal is valid, the transactions can be executed and your bond will be returned.",
"executeToolTip": "This will execute the transactions from this proposal and return the proposer's bond."
}
},
"poap": {
"no_poap_header": "Aun no se ha creado un POAP para esta oferta :'(",
"no_voted_header": "Vote para obtener este POAP",
"unclaimed_header": "Mint your I voted POAP",
"claimed_header": "Congratulations! The POAP has been minted to your collection",
"loading_header": "The POAP is being minted to your collection",
"button_claim": "Mint",
"button_show": "Navegar colección",
"success_claim": "The POAP has been minted to your collection",
"error_claim": "Hubo un problema en la acuñación de esta token"
},
"progress": {
"progress": "Progress",
"inProgress": "In Progress",
"completed": "Completed",
"complete": "Complete",
"newStep": "New Step",
"description": "Description",
"add": "Add",
"deleteStep": "Delete Step",
"deleteConfirm": "Are you sure you want to delete?",
"delete": "Delete",
"cancel": "Cancel",
"comeBack": "Come back after voting is complete to see how this proposal is progressing!",
"confirmSignature": "Signing this message will allow us to authorize your request to update the progress of your proposal.",
"wentWrong": "Oops something went wrong.",
"voting": "Voting",
"soon": "Soon"
},
"charts": {
"charts": "Gráficos",
"noVotesYet": "No hay votos para visualizar todavía.",
"totalVotesPerDay": "Total de votos por día",
"shareOfVotingPower": "Participación en el poder de voto",
"votingPowerPerDay": "Poder de votación por día"
},
"comment_box": {
"title": "Caja de comentarios",
"add": "Agrega aquí tu comentario",
"submit": "Enviar",
"preview": "Vista previa",
"continue_editing": "Continuar editando",
"edit": "Edita tu respuesta aquí",
"edit_button": "Editar",
"dismiss": "Descartar",
"delete": "Eliminar",
"add_reply": "Agrega tu respuesta aquí",
"edit_comment": "Editar comentario",
"edit_modal": "¿Estás seguro que quieres editar?",
"yes": "Sí",
"no": "No",
"delete_comment": "Eliminar comentario",
"delete_modal": "¿Estás seguro que quieres eliminar?",
"error": "Oops, algo salió mal",
"replies": "respuestas",
"hide": "Esconder",
"show": "Mostrar",
"reply": "Responder",
"load_more": "Cargar más"
},
"page": {
"title": {
"home": "Snapshot",
"setup": "Create a space",
"timeline": "Timeline",
"notifications": "Notifications",
"explore": "Explore",
"playground": "Playground",
"space": {
"create": "Create {space} proposal",
"about": "About {space}",
"proposals": "{space} Proposals",
"proposal": "{space} proposal: {proposal}",
"settings": "{space} Settings"
},
"strategy": "{key} strategy",
"delegate": "Delegate",
"ranking": "Ranking"
}
},
"hal": {
"title": "Track proposals for {spaceName}",
"text": "Receive notifications every time a new proposal is created or ends"
},
"timeUnits": {
"second": "1 second | {n} seconds",
"minute": "1 minute | {n} minutes",
"hour": "1 hour | {n} hours",
"day": "1 day | {n} days",
"week": "1 week | {n} weeks",
"month": "1 month | {n} months",
"year": "1 year | {n} years"
},
"unsupportedNetwork": {
"unsupportedNetwork": "Unsupported network",
"switchNetworkToNetwork": "To continue, you need to change the network in your wallet to {network}.",
"switchToNetwork": "Switch to {network}",
"goToDemoSite": "Go to demo website"
},
"treasury": {
"title": "Treasury",
"wallets": {
"title": "Wallets",
"empty": "This space doesn't have a treasury yet",
"addTreasury": "Add a treasury"
},
"assets": {
"title": "Assets",
"empty": "There are no assets in this contract"
},
"24hChange": "24h change"
},
"newsletter": {
"yourEmail": "Your email",
"title": "Get the latest Snapshot updates"
},
"joinCommunity": "Join Snapshot community",
"header": {
"title": "Where decisions get made",
"description": "Snapshot is a free, open-source platform for community governance. Create your own space now and start making decisions!"
},
"aboutPage": {
"description": "Snapshot is a decentralized governance platform that makes it easy to create and vote on proposals - all without spending a fortune on gas fees! Plus, our flexible system supports various voting types and strategies, so you can tailor the voting process to your needs.",
"subHeader": "Governance should be a snap",
"subDescription": "Web3 governance doesn't have to be complicated. Snapshot is the perfect solution for organizations looking for an easy and efficient way to govern their community or organization."
},
"footerView": {
"resources": "Resources",
"about": "About",
"blog": "Blog",
"jobs": "Jobs",
"discussions": "Discussions",
"github": "GitHub",
"docs": "Docs",
"support": "Support",
"hiring": "Join us!"
}
}
================================================
FILE: src/locales/fil-PH.json
================================================
{
"searchPlaceholder": "Magsaliksik",
"spaceCount": "{0} puwang",
"createSpace": "Lumikha ng puwang",
"backToHome": "Bahay",
"actions": "Mga aksyon",
"poweredBy": "Powered by",
"results": "Mga resulta",
"resultsError": "Results could not be calculated. This is often due to a misconfigured strategy or an unresponsive RPC node involved in the strategy.",
"resultsCalculating": "Final results are being calculated. If you still see this message after a few minutes contact the space admin.",
"votingPowerFailedMessage": "Your voting power could not be calculated. This is often due to a misconfigured strategy or an unresponsive RPC node involved in the strategy.",
"votingValidationFailedMessage": "There was an error on our side and we could not verify if you are eligible to vote. This is often due to a misconfigured voting validation or an unresponsive API involved in the validation.",
"notValidVoterMessage": "Oops, you don't seem to be eligible to vote on this proposal.",
"getHelp": "Get help",
"retry": "Retry",
"currentResults": "Mga kasalukuyang resulta",
"reset": "I-reset",
"close": "Close",
"save": "I-save",
"author": "May-akda",
"next": "Susunod",
"choice": "Choice",
"submit": "Ipadala",
"plugins": "Mga plugin",
"information": "Impormasyon",
"confirm": "Kumpirmahin",
"snapshot": "Snapshot",
"strategies": "\t\nMga estratehiya",
"strategiesPage": "\t\nMga estratehiya",
"space": "Silid",
"spaces": "Mga silid",
"verifiedSpace": "Verified space",
"warningSpace": "This space has been flagged as potentially malicious. Proceed with caution.",
"version": "Bersyon",
"timeline": "Timeline",
"ended": "ended",
"started": "started",
"filters": "Mga filter",
"allSpaces": "Lahat ng mga silid",
"submitOnchain": "Isumite ang on-chain",
"inSpaces": "Sa {0} (mga) espasyo",
"votes": "Mga boto",
"seeMore": "Tumingin pa",
"seeAll": "See all",
"network": "Network",
"networks": "Mga network",
"skins": "Mga Skin",
"spaceMembers": "Members",
"members": "Walang miyembro | {count} miyembro | {count} mga miyembro",
"editStrategy": "Edit strategy",
"invalidProposals": "Mga di-wastong panukala",
"account": "Account",
"create3box": "Gumawa ng profile sa 3Box",
"view3box": "Ipakita ang profile sa 3Box",
"edit3box": "Ibahin ang profile sa 3Box",
"connectWallet": "Ikonekta ang wallet",
"toggleSkin": "Toggle skin",
"about": "Tungkol",
"license": "Lisensya",
"showMore": "Show more",
"voted": "Voted",
"reload": "Reload",
"ipfsServer": "IPFS server",
"hub": "Hub",
"cancel": "Kanselahin",
"delete": "Delete",
"demoSite": "Ito ay isang demo site, subukan!",
"removeDelegation": "Tanggalin ang delegasyon",
"confirmRemove": "Sigurado kaba na na gusto mo tanggalin ang iyong delegasyon sa",
"removeSpace": "para sa puwang {0}",
"noVotingPower": "Oops, it seems you don't have any voting power at block {blockNumber}.",
"quorumReached": "quorum reached",
"options": "Opsyon",
"votingPower": "Ang lakas mo sa pagboto",
"comment": {
"placeholder": "Share your reason (optional)"
},
"receipt": "Resibo",
"relayer": "Taga relay",
"verifyOnMycrypto": "I-verify ang receipt sa MyCrypto",
"verifyOnSignatorio": "I-verify sa Signator.io",
"isCore": "Core",
"notificationsBlocked": "Bina-block ng iyong browser ang mga notification",
"notificationsNotSupported": "Your browser does not support notifications",
"walletNotSupported": "Wallet is not supported",
"seeInExplorer": "See explorer",
"learnMore": "Karagdagang impormasyon",
"logout": "I-logout",
"continue": "Ipagpatuloy",
"add": "Idagdag",
"edit": "I-Edit",
"strategyParameters": "Mga parameter ng estratehiya",
"addAction": "Magdagdag ng aksyon",
"removeAction": "Alisin ang pagkilos",
"yourChoice": "Pagpipilian {0}",
"targetAddress": "Target na address",
"value": "Halaga",
"date": "Datos",
"marketDetails": "Market details",
"addMarket": "Magdagdag ng merkado",
"selectNetwork": "Pumili ng network",
"conditionId": "Condition ID",
"basetokenAddress": "Base token address",
"quoteAddress": "I-quote ang address ng pera",
"removeMarket": "Alisin ang merkado",
"back": "Bumalik",
"loading": "Naglo-load...",
"predictedImpact": "Hinulaang epekto",
"marketSymbol": "{0} market",
"twoChoicesRequired": "Dalawang choice ang kailangan para sa plugin na ito.",
"noResultsFound": "Oops, wala kaming makitang mga resulta",
"createFirstProposal": "Gawin natin ang iyong unang panukala",
"noSpacesJoined": "Oops, hindi ka pa nakakasali sa anumang space",
"addFavorites": "Magdagdag ng mga paborito",
"createdBy": "By {0}",
"startIn": "simula {0}",
"endIn": "pagtatapos {0}",
"proposalTimeLeft": "{0} left",
"endedAgo": "ended {0}",
"proposalBy": "by {0}",
"endDate": "pagtatapos {0}",
"defaultSkin": "Default skin",
"select": "Piliin",
"language": "Lingwahe",
"agree": "Sang-ayon ako",
"moderators": "Moderators",
"playground": "Palaruan",
"strategyParams": "Param ng estratehiya",
"addresses": "Mga address",
"networkErrorPlayground": "Error sa network - mangyaring buksan ang iyong browser console para sa higit pang impormasyon",
"upload": "I-upload",
"join": "Sumali",
"joined": "Sumali",
"leave": "Umalis",
"subspaces": "Sub-spaces",
"mainspace": "Main space",
"copyLink": "Kopyahin ang link",
"duplicate": "Duplicate",
"joinedSpaces": "Pinagsamang mga puwang",
"joinSpaces": "Sumali sa mga puwang",
"setDelegationToSpace": "Limit delegation to a specific space",
"theCurrentNetwork": "the current network",
"optional": "(optional)",
"homeLoadmore": "Load more",
"confirmAction": "Confirm action",
"or": "or",
"share": "Share",
"shareOnTwitter": "Share on Twitter",
"shareOnLenster": "Share on Lenster",
"createButton": "Create",
"discussion": "Discussion",
"changeWallet": "Change wallet",
"createASpace": "Create a space",
"getStarted": "Get started",
"skip": "Skip",
"newSpaceNotice": {
"header": "Your space is live!",
"mainText": "You can change how voting power is calculated via strategies in your {settings}. Changes to your settings will only affect new proposals, existing proposals can not be changed.",
"learnMore": "Learn more in the {documentation} or join Snapshot {discord} for help.",
"gotIt": "Got it!"
},
"errors": {
"required": "Kailangang punan ang patlang",
"minLength": "Field is required",
"maxLength": "Ang maximum na haba ay {0}",
"pattern": "Di-wastong character",
"minItems": "Kinakailangan ang minimum na {0} (mga) item",
"maxItems": "Maximum {0} item(s) allowed",
"minStrategy": "Hindi bababa sa isang diskarte ang kinakailangan.",
"website": "URL should be in the format https://www.example.com",
"format": "Di-wastong format",
"type": "Di-wastong uri",
"unsupportedImageType": "File type not supported, Supported formats are jpeg, jpg and png",
"invalidAddress": "Invalid address"
},
"create": {
"proposalTitle": "Title",
"discussion": "Discussion (optional)",
"categorie(s)": "Select up to 2 categorie(s)",
"proposalDescription": "Description (optional)",
"preview": "Ipakita",
"choices": "Mga pagpipilian",
"addChoice": "Magdagdag ng pagpipilian",
"startDate": "Pumili ng petsa ng pagsisimula",
"endDate": "Pumili ng petsa ng pagtatapos",
"startTime": "Pumili ng oras ng pagsisimula",
"endTime": "Pumili ng oras ng pagtatapos",
"publish": "Ilathala",
"untitled": "Untitled",
"snapshotBlock": "Snapshot block number",
"voting": "Voting",
"votingSystem": "Voting system",
"choice": "Choice {0}",
"period": "Voting period",
"start": "Start",
"end": "End",
"days": "Days",
"hours": "Hours",
"minutes": "Minutes",
"schedule": "Schedule proposal",
"delayEnforced": "The space enforces a delay until voting can start",
"periodEnforced": "The space enforces the duration of the voting period",
"typeEnforced": "{type} is enforced by the space",
"privacyEnforced": "{type} is enforced by the space",
"edit": "Edit",
"continue": "Continue",
"now": "Now",
"votingPeriodExplainer": "This is the time period in which users will be able to vote. The proposal will be visible and pending before the start of the voting period.",
"uploadImageExplainer": "Attach images by dragging & dropping, selecting or pasting them.",
"uploading": "Uploading image",
"markdown": "Styling with Markdown is supported",
"validationWarning": {
"basic": {
"member": "Kailangan mo maging miyembro ng ispasyo para makapag sumite ng proposal.",
"minScore": "Kailangan mong magkaroon ng minimum na {0} {1} upang makapagsumite ng panukala."
},
"customValidation": "Kailangan mong ipasa ang pagpapatunay ng panukala upang makapagsumite ng isang panukala.",
"executionError": "Verifying your eligibility to create proposals in this space has failed. This is likely due to a misconfigured strategy."
},
"errorGettingSnapshot": "We encountered an error while fetching the snapshot block number which is needed to calculate your voting power. Please try again later."
},
"delegate": {
"header": "Delegasyon",
"selectDelegate": "Pumili ng delegado",
"to": "Sa",
"addressPlaceholder": "Address o pangalan ng ENS",
"delegations": "Ang iyong delegasyon",
"allSpaces": "Para sa lahat ng mga puwang",
"delegated": "Pinagkatiwala kay",
"pendingTransaction": "walang nakabinbing transaksyon | 1 nakabinbing transaksyon | {count} mga nakabinbing transaksyon",
"topDelegates": "Nangungunang mga delegado",
"noDelegatesFoundFor": "No delegates found for {0}",
"noValidEns": "Not a valid ENS address.",
"noValidAddress": "Not a valid address",
"delegateToSelf": "You cannot delegate to yourself",
"delegateToSelfAddress": "You cannot delegate to your own ENS address",
"noValidSpaceId": "Not a valid space ID",
"noDelegationsAndDelegates": "Can't find your delegations and delegates? Make sure you are connected to the correct network.",
"delegateNotSupported": "Delegation is currently not supported for {network}."
},
"proposal": {
"castVote": "Ilagay ang iyong boto",
"vote": "Iboto",
"startDate": "Simula ng petsa",
"endDate": "Pagtatapos ng petsa",
"votingSystem": "Sistema ng pagboto",
"privacy": "Privacy",
"invalidChoice": "Invalid choice",
"postVoteModal": {
"defaultTitle": "Your vote is in!",
"gnosisSafeTitle": "Your vote is pending...",
"gnosisSafeDescription": " Votes with a Safe require additional signers and will be visible once the transaction is confirmed",
"seeQueue": "See queued transactions",
"tips": {
"1": "Votes can be changed while the proposal is active"
}
}
},
"proposals": {
"header": "Mga Panukala",
"new": "Bagong panukala",
"noProposals": "Wala pang masyadong kadaming mga panukala ang naririto!",
"createProposal": "Gumawa ng panukala",
"showMore": "Show more",
"showLess": "Show less",
"states": {
"all": "Lahat",
"core": "Core",
"community": "Komunidad",
"active": "Aktibo",
"pending": "Pending",
"closed": "Nakasara"
}
},
"notifications": {
"header": "Notifications",
"noNotifications": "You have no notifications",
"proposalStarted": "proposal has started:",
"proposalEnded": "proposal has ended:",
"markAllAsRead": "Mark all as read",
"all": "All",
"unread": "Unread"
},
"modalTerms": {
"mustAgreeTo": "To {action} this space, you must agree to the {spaceName} terms of service.",
"actionJoin": "join",
"actionCreate": "create a proposal in",
"actionVote": "vote in"
},
"settings": {
"header": "Mga Setting",
"editController": "Edit controller",
"connectWithSpaceOwner": "You are in view only mode, to modify space settings connect with a controller or admin wallet.",
"gnosisWrongNetwork": {
"base": "Your Gnosis Safe is on the wrong network. Please connect to {network} to {action}.",
"settings": "edit the space settings",
"create": "create a proposal",
"vote": "vote on this proposal"
},
"currentSpaceControllerIs": "The current space controller is {address}",
"newController": "New controller",
"noRecord": "No text-record found. Make sure you have registered {id} domain on {network}, then edit the controller text-record to regain access to the space settings.",
"set": "Set",
"profile": "Profile",
"avatar": "Avatar",
"name": {
"label": "Name",
"placeholder": "e.g. Yam Network"
},
"about": {
"label": "About",
"placeholder": "What is your organisation about?"
},
"categories": {
"label": "Categorie(s)",
"select": "Select categorie(s)"
},
"terms": {
"label": "Terms of service",
"information": "Users will be required to accept these terms once before they can create a proposal or cast a vote"
},
"hideSpace": "Itago ang espasyo sa homepage",
"links": "Social accounts",
"subspaces": {
"label": "Sub-spaces",
"information": "Sub-spaces will only be shown once configured on both the main space and the sub-space(s). {docs}",
"parent": {
"label": "Main space",
"placeholder": "pistachiodao.eth",
"information": "The space that this space is a sub-space of will be displayed on the space page"
},
"children": {
"label": "Sub-spaces",
"placeholder": "pistachiodao.eth",
"information": "Related Sub-spaces listed here will be displayed on the space page"
}
},
"website": "Website",
"strategies": {
"label": "Strategie(s)",
"information": "Strategies are used to determine voting power or whether a user is eligible to create a proposal"
},
"network": {
"label": "Network",
"information": "The defaul network used for this space. Networks can also be specified in individual strategies"
},
"symbol": {
"label": "Symbol",
"information": "The default symbol used for this space, usually the token symbol i.e. BAL for Balancer"
},
"strategiesList": "Select up to 8 strategies",
"votingPowerIsCumulative": "Voting power is cumulative",
"addStrategy": "Add strategy",
"testInPlayground": "Test in playground",
"admins": {
"label": "Admins",
"information": "Admins are able to modify the space settings and manage the space's proposals"
},
"authors": {
"label": "Authors",
"information": "Authors are always able to create proposals"
},
"proposalValidation": "Pagpapatunay ng panukala",
"validation": "Type",
"proposalThreshold": {
"label": "Threshold",
"information": "The minimum amount of voting power required to create a proposal"
},
"allowOnlyAuthors": "Pahintulutan lamang ang mga may-akda na magsumite ng isang panukala",
"editValidation": "I-edit ang pagpapatunay",
"selectValidation": "Piliin ang pagpapatunay",
"validationParameters": "Mga parameter ng pagpapatunay",
"voting": "Pagboto",
"votingDelay": "Pagkaantala ng pagboto",
"votingPeriod": "Panahon ng pagboto",
"hours": "Hours",
"days": "Days",
"quorum": {
"label": "Quorum",
"information": "The minimum amount of voting power required for the proposal to pass"
},
"type": {
"label": "Type",
"information": "The type of voting system used for this space. (Enforced on all future proposals)"
},
"anyType": "Kahit Ano",
"hideAbstain": "Ignore abstain votes in basic voting results",
"customDomain": "Custom na domain",
"domain": {
"label": "Domain name",
"placeholder": "e.g. vote.balancer.fi",
"info": "To setup a custom domain you additionally need to open a pull request on github after you have created the space. {docs}"
},
"skin": "Skin",
"treasuries": {
"label": "Treasury",
"add": "Add treasury",
"edit": "Edit treasury",
"information": "Add treasuries of your organization to show them in your space"
},
"addPlugin": "Magdagdag ng Plugin",
"editPlugin": "I-edit ang plugin",
"pluginParameters": "Plugin parameters",
"proposal": {
"title": "Proposal",
"guidelines": {
"title": "Guidelines",
"information": "Display a link to your guidelines on proposal creation to help users understand what constitutes a good/valid proposal"
},
"template": {
"title": "Template",
"information": "Start every proposal with a template to help users understand what information is required"
}
}
},
"setup": {
"example": "e.g. yam.eth",
"chooseExistingEns": "Choose one of your existing ENS domains to create a space with:",
"useSingleExistingEns": "Use your existing ENS domain:",
"orRegisterNewEns": "Or register a new domain:",
"demoTestnetEnsMessage": "To create a test space you need an ENS domain on {network}.",
"toCreateASpace": "To create a space, you first need an ENS domain. Enter one below and follow the ENS registration instructions.",
"createASpace": "Create a space",
"registerEnsButton": "Register",
"supportedEnsTLDs": "Supported domain endings",
"helpDocsAndDiscordLinks": "Not sure how to setup your space? Learn more in the {docs} or join Snapshot {discord}.",
"setSpaceController": "Space controller",
"setSpaceControllerExists": "The snapshot text-record for this domain has already been set. Choose edit to change it, otherwise you can skip to the next step.",
"setSpaceControllerInfo": " The space controller is the account that will be able to manage the space settings. Additional space controllers (admins) can be added later.",
"setSpaceControllerInfoGnosisSafe": "When creating a space with a Gnosis Safe it's recommended to set the safe address as the space controller. If you don't, you need to follow some additional steps. {link}",
"editSpaceController": "Edit controller on ENS",
"setController": "Set controller",
"explainControllerAndEns": "Setting the controller requires a transaction on the {network} which will add a \"snapshot\" TEXT record to your ENS domain.",
"confirmToSetAddress": "Are you sure you want to set {address} as the controller of the space?",
"controllerHasAuthority": "The controller has full authority over the space settings",
"controller": "Controller",
"selectEnsForSpace": "Choose ENS address",
"spaceOwnerAddressPlaceHolder": "e.g. {address}",
"controllerAddress": "Controller address",
"updateController": "Update controller",
"seeOnEns": "See on ENS",
"goToSettings": "Go to settings",
"setSpaceProfile": "Customize your space",
"waitForTransaction": "The transaction need to confirm before you can create your space. {txUrl}",
"pleaseWaitMessage": "This can take a few minutes, please wait while the transaction is being confirmed",
"notControllerAddress": "Please connect with the controller address {wallet} to create the space.",
"fillCurrentAccount": "Use currently logged in account",
"domain": {
"title": "Setup your space domain",
"ensMessage": " One thing you need before you can create your own space, is an ENS domain on Ethereum mainnet.",
"ensMessageTestnet": "You can also {link} on the Goerli testnet and mess with things there first.",
"tryDemo": "try the demo",
"yourExistingSpaces": "Your existing spaces",
"invalidEns": "This ENS name is invalid. Usually this is due the use of invalid characters during registration."
},
"strategy": {
"title": "How would you like to setup your voting strategy?",
"subtitle": "You can change your strategy settings any time.",
"blockTitle": "Setup voting strategy",
"onePersonOneVote": {
"title": "One person, one vote",
"description": "Manage a whitelist of people who can vote or simply allow any address to vote. Every vote is equal and no token is required",
"whitelistInformation": "Specify a number of accounts that can vote",
"ticketInformation": "Any account can vote",
"votesEqualInfo": "Each vote is equal and no token is required"
},
"tokenVoting": {
"title": "Token weighted voting",
"description": "Votes are weighted by a token. The token can be an ERC-20, ERC-721 or ERC-1155 token standard",
"tokenNotFound": "Token not found",
"seeOnEtherscan": "See on Etherscan"
},
"advanced": {
"title": "Custom setup",
"description": "Select up to 8 strategies with a wide range of options. If you can't find the right strategy for your use case, you can create your own"
}
},
"validationTitle": "Who can manage this space and create proposals?"
},
"profile": {
"buttonEdit": "Edit profile",
"viewProfile": "View profile",
"about": {
"header": "About",
"joinedSpaces": "Joined spaces",
"createdSpaces": "Created spaces",
"biography": "Bio",
"notJoinSpacesYet": "Hasn't joined any spaces yet",
"notCreatedSpacesYet": "Hasn't created any spaces yet",
"delegatorNetworkInfo": "Change by switching network in your wallet",
"delegate": "Delegate",
"delegated": "Delegated",
"delegateTo": "Delegate to",
"delegateFor": "Delegator for",
"noDelegatorsMessage": "No delegators on {network}",
"notSupportedNetwork": "Delegation currently isn't supported on {network} "
},
"activity": {
"header": "Activity",
"votedFor": "Voted {choice}",
"today": "Today",
"thisWeek": "This week",
"olderThanWeek": "Older than a week",
"noActivity": "No activity yet"
},
"settings": {
"header": "Edit profile",
"name": "Name",
"biography": "Bio",
"namePlaceholder": "Enter name",
"bioPlaceholder": "Tell your story",
"change": "Change",
"remove": "Remove"
}
},
"notify": {
"youDidIt": "You did it!",
"copied": "Copied!",
"proposalDeleted": "Proposal deleted",
"somethingWentWrong": "Oops, may maling nangyari!",
"saved": "Na-save!",
"delegationSuccess": "Tagumpay ng Delegasyon",
"delegationRemoved": "Inalis ang delegasyon",
"proposalCreated": "Nagawa ang panukala",
"voteSuccessful": "Ang iyong boto ay nasa!",
"ensSet": "ENS text record was successfully set",
"transactionSent": "Transaction sent"
},
"explore": {
"createStrategy": "Lumikha ng diskarte",
"createSkin": "Lumikha ng skin",
"addNetwork": "Magdagdag ng network",
"createPlugin": "Lumikha ng plugin",
"strategies": "\t\nmga estratehiya",
"skins": "mga Skin",
"networks": "mga network",
"plugins": "mga plugin",
"results": "(mga) resulta",
"category": "Category",
"categories": {
"all": "All",
"protocol": "Protocol",
"social": "Social",
"investment": "Investment",
"grant": "Grant",
"service": "Service",
"media": "Media",
"creator": "Creator",
"collector": "Collector"
}
},
"voting": {
"selectVoting": "Piliin ang sistema ng pagboto",
"single-choice": "Single choice na pagboto",
"approval": "Pagboto sa pag-apruba",
"quadratic": "Quadratic na pagboto",
"ranked-choice": "Ranggo na pagpipiliang pagboto",
"weighted": "Timbang na pagboto",
"basic": "Basic na pagboto",
"description": {
"single-choice": "Ang bawat botante ay maaaring pumili lamang ng isang pagpipilian.",
"approval": "Ang bawat botante ay maaaring pumili ng anumang bilang ng mga pagpipilian.",
"quadratic": "Ang bawat botante ay maaaring magpakalat ng kapangyarihan sa pagboto sa anumang bilang ng mga pagpipilian. Ang mga resulta ay kinakalkula nang quadratically.",
"ranked-choice": "Ang bawat botante ay maaaring pumili at magranggo ng anumang bilang ng mga pagpipilian. Kinakalkula ang mga resulta sa pamamagitan ng instant-runoff counting method.",
"weighted": "Ang bawat botante ay maaaring magpakalat ng kapangyarihan sa pagboto sa anumang bilang ng mga pagpipilian.",
"basic": "Single choice na pagboto na may tatlong pagpipilian: Para sa, Laban o Abstain"
}
},
"privacy": {
"label": "Privacy",
"title": "Select voting privacy",
"information": "The type of privacy used on proposals. (Enforced on all future proposals)",
"any": "Any",
"none": "None",
"shutter": {
"label": "Shutter",
"description": "Choices are encrypted and only visible once the voting period is over",
"tooltip": "This proposal has Shutter privacy enabled. All votes will be encrypted until the voting period has ended and the final score is calculated",
"url": "https://blog.shutter.network/shielded-voting/"
}
},
"validation": {
"label": "Validation",
"title": "Select voting validation",
"information": "The type of validation used to determine if a user can vote. (Enforced on all future proposals)",
"any": {
"label": "Anyone can vote",
"description": "Anyone with voting power can cast a vote."
},
"basic": {
"label": "Basic",
"description": "Use any strategy to determine if a user can vote.",
"invalidMessage": "You do not meet the minimum balance requirement to vote on this proposal. "
},
"passport-gated": {
"label": "Gitcoin Passport gated",
"description": "Protect your proposals from spam and vote manipulation by requiring users to have a Gitcoin Passport.",
"invalidMessage": "You need a Gitcoin Passport with score above {scoreThreshold} and {operator} of the following stamps to vote on this proposal: {stamps}. "
}
},
"safeSnap": {
"currentOutcome": "Kasalukuyang kinalabasan",
"currentBond": "Kasalukuyang bono",
"finalizedIn": "Finalized {0}",
"executableIn": "Naipapatupad {0}",
"finalOutcome": "Kinalabasan",
"nextBond": "Bond para itakda ang kinalabasan",
"setOutcomeTo": "Itakda ang kinalabasan sa",
"claimBond": "Mag-claim ng bono",
"addBatch": "Magdagdag ng batch ng transaksyon",
"batch": "Batch ng transaksyon",
"transactions": "Transaksyon",
"to": "To (address)",
"invalidAddress": "Di-wastong address",
"invalidAmount": "Di-wastong halaga",
"invalidValue": "Hindi tama ang halaga",
"invalidAbi": "Di-wastong ABI",
"invalidData": "Di-wastong data",
"value": "Halaga (wei)",
"data": "Datos",
"noCollectibles": "Walang mga collectible",
"asset": "Ang Asset",
"amount": "Halaga",
"type": "Uri",
"transferFunds": "Maglipat ng pondo",
"transferNFT": "Paglipat ng NFT",
"contractInteraction": "Pakikipag-ugnayan sa kontrata",
"rawTransaction": "Raw transaksyon",
"addTransaction": "Magdagdag ng transaksyon",
"transactionLabels": {
"contractInteraction": "{functionName}() - {amount} wei sa {address}",
"transferFunds": "Transfer {amount}{tokenSymbol} sa {address}",
"transferNFT": "Ipadala {name} #{id} sa {address}",
"raw": "Ipadala {amount} wei sa {address}"
},
"labels": {
"request": "Humiling ng pagpapatupad",
"setOutcome": "Itakda ang kinalabasan",
"changeOutcome": "Baguhin ang kinalabasan",
"executeTxs": "Isagawa ang batch ng transaksyon {0} ng {1}",
"executed": "Ang lahat ng mga transaksyon ay naisakatuparan",
"noTransactions": "There are no transactions to execute",
"rejected": "Proposal rejected",
"error": "May nangyaring mali",
"connectWallet": "Ikonekta ang wallet upang makita ang mga detalye ng pagpapatupad",
"switchChain": "Ilipat ang iyong wallet sa {0} upang humiling ng pagpapatupad",
"question": "Pumasa ba ang panukala ito at natutugunan ba nito ang",
"criteria": "pamantayan sa pagtanggap?",
"proposalPassed": "Did the proposal pass?",
"expired": "The proposal has expired",
"approveBond": "Approve bond",
"confirmVoteResults": "Click to confirm the proposal passed",
"executeTxsUma": "Execute transaction batch",
"deleteDisputedProposal": "Delete disputed proposal",
"confirmVoteResultsToolTip": "Make sure this proposal was approved by this Snapshot vote before proposing on-chain. If the Snapshot vote rejected the proposal, the on-chain proposal will be rejected as well and you will lose your bond.",
"approveBondToolTip": "On-chain proposals require a bond from the proposer. This will approve tokens from your wallet to be posted as a bond. If you make an invalid proposal, it will be disputed and you will lose your bond. If the proposal is valid, your bond will be returned when the transactions are executed.",
"requestToolTip": "This will propose the transactions from this Snapshot vote on-chain. After a challenge window, if the proposal is valid, the transactions can be executed and your bond will be returned.",
"executeToolTip": "This will execute the transactions from this proposal and return the proposer's bond."
}
},
"poap": {
"no_poap_header": "Hindi pa nase-setup ang POAP para sa panukalang ito :'(",
"no_voted_header": "Bumoto para makuha ang POAP na ito",
"unclaimed_header": "Mint your I voted POAP",
"claimed_header": "Congratulations! The POAP has been minted to your collection",
"loading_header": "The POAP is being minted to your collection",
"button_claim": "Mint",
"button_show": "I-browse ang koleksyon",
"success_claim": "The POAP has been minted to your collection",
"error_claim": "Nagkaroon ng problema sa pag-minting ng token"
},
"progress": {
"progress": "Progress",
"inProgress": "In Progress",
"completed": "Completed",
"complete": "Complete",
"newStep": "New Step",
"description": "Description",
"add": "Add",
"deleteStep": "Delete Step",
"deleteConfirm": "Are you sure you want to delete?",
"delete": "Delete",
"cancel": "Cancel",
"comeBack": "Come back after voting is complete to see how this proposal is progressing!",
"confirmSignature": "Signing this message will allow us to authorize your request to update the progress of your proposal.",
"wentWrong": "Oops something went wrong.",
"voting": "Voting",
"soon": "Soon"
},
"charts": {
"charts": "Charts",
"noVotesYet": "There are no votes to visualize yet.",
"totalVotesPerDay": "Total votes per day",
"shareOfVotingPower": "Share of voting power",
"votingPowerPerDay": "Voting power per day"
},
"comment_box": {
"title": "Kahon ng komento",
"add": "Magdagdag ng iyong mga komento dito",
"submit": "Ipadala",
"preview": "I-preview",
"continue_editing": "Ipagpatuloy an pag-edit",
"edit": "I-edit ang iyong tugon dito",
"edit_button": "I-Edit",
"dismiss": "I-dismiss",
"delete": "Burahin",
"add_reply": "Idagdag ang iyong tugon dito",
"edit_comment": "I-edit ang komento",
"edit_modal": "Sigurado ka bang gusto mong i-edit?",
"yes": "Oo",
"no": "Hindi",
"delete_comment": "I-delete ang komento",
"delete_modal": "Sigurado ka bang gusto mong tanggalin?",
"error": "Oops, may maling nangyari",
"replies": "mga sagot",
"hide": "Tago",
"show": "Ipakita",
"reply": "Tumugon",
"load_more": "Mag load pa"
},
"page": {
"title": {
"home": "Snapshot",
"setup": "Create a space",
"timeline": "Timeline",
"notifications": "Notifications",
"explore": "Explore",
"playground": "Playground",
"space": {
"create": "Create {space} proposal",
"about": "About {space}",
"proposals": "{space} Proposals",
"proposal": "{space} proposal: {proposal}",
"settings": "{space} Settings"
},
"strategy": "{key} strategy",
"delegate": "Delegate",
"ranking": "Ranking"
}
},
"hal": {
"title": "Track proposals for {spaceName}",
"text": "Receive notifications every time a new proposal is created or ends"
},
"timeUnits": {
"second": "1 second | {n} seconds",
"minute": "1 minute | {n} minutes",
"hour": "1 hour | {n} hours",
"day": "1 day | {n} days",
"week": "1 week | {n} weeks",
"month": "1 month | {n} months",
"year": "1 year | {n} years"
},
"unsupportedNetwork": {
"unsupportedNetwork": "Unsupported network",
"switchNetworkToNetwork": "To continue, you need to change the network in your wallet to {network}.",
"switchToNetwork": "Switch to {network}",
"goToDemoSite": "Go to demo website"
},
"treasury": {
"title": "Treasury",
"wallets": {
"title": "Wallets",
"empty": "This space doesn't have a treasury yet",
"addTreasury": "Add a treasury"
},
"assets": {
"title": "Assets",
"empty": "There are no assets in this contract"
},
"24hChange": "24h change"
},
"newsletter": {
"yourEmail": "Your email",
"title": "Get the latest Snapshot updates"
},
"joinCommunity": "Join Snapshot community",
"header": {
"title": "Where decisions get made",
"description": "Snapshot is a free, open-source platform for community governance. Create your own space now and start making decisions!"
},
"aboutPage": {
"description": "Snapshot is a decentralized governance platform that makes it easy to create and vote on proposals - all without spending a fortune on gas fees! Plus, our flexible system supports various voting types and strategies, so you can tailor the voting process to your needs.",
"subHeader": "Governance should be a snap",
"subDescription": "Web3 governance doesn't have to be complicated. Snapshot is the perfect solution for organizations looking for an easy and efficient way to govern their community or organization."
},
"footerView": {
"resources": "Resources",
"about": "About",
"blog": "Blog",
"jobs": "Jobs",
"discussions": "Discussions",
"github": "GitHub",
"docs": "Docs",
"support": "Support",
"hiring": "Join us!"
}
}
================================================
FILE: src/locales/fr-FR.json
================================================
{
"searchPlaceholder": "Rechercher",
"spaceCount": "{0} espace(s)",
"createSpace": "Créer un espace",
"backToHome": "Accueil",
"actions": "Actions",
"poweredBy": "Powered by",
"results": "Résultats",
"resultsError": "Les résultats n’ont pas pu être calculés. C’est habituellement dû à une stratégie mal configurée ou à un RPC désynchronisé impliqué dans la stratégie.",
"resultsCalculating": "Les résultats finaux sont en cours de calcul. Si vous voyez toujours ce message après quelques minutes, contactez l'administrateur de l'espace.",
"votingPowerFailedMessage": "Votre voting power n’a pas pu être calculé. C’est habituellement dû à une stratégie mal configurée ou à un RPC désynchronisé impliqué dans la stratégie.",
"votingValidationFailedMessage": "Il y a eu une erreur de notre côté et nous n'avons pas pu vérifier si vous êtes éligible au vote. Cela est souvent dû à une validation de vote mal configurée ou à une API non réactive impliquée dans la validation.",
"notValidVoterMessage": "Oups, vous ne semblez pas être éligible au vote sur cette proposition.",
"getHelp": "Obtenir de l’aide",
"retry": "Réessayer",
"currentResults": "Résultats actuels",
"reset": "Réinitialiser",
"close": "Fermer",
"save": "Enregistrer",
"author": "Auteur",
"next": "Suivant",
"choice": "Choix",
"submit": "Valider",
"plugins": "Plugins",
"information": "Informations",
"confirm": "Confirmer",
"snapshot": "Snapshot",
"strategies": "Stratégie(s)",
"strategiesPage": "Stratégies",
"space": "Espace",
"spaces": "Espaces",
"verifiedSpace": "Espace vérifié",
"warningSpace": "Cet espace a été signalé comme potentiellement malveillant. Faites preuve de prudence.",
"version": "Version",
"timeline": "Timeline",
"ended": "terminé",
"started": "commencé",
"filters": "Filtres",
"allSpaces": "Tous les espaces",
"submitOnchain": "Envoyer on-chain",
"inSpaces": "Dans {0} espace(s)",
"votes": "Votes",
"seeMore": "Voir plus",
"seeAll": "Voir tout",
"network": "Réseau",
"networks": "Réseaux",
"skins": "Thèmes",
"spaceMembers": "Membres",
"members": "Aucun membre | {count} membre | {count} membres",
"editStrategy": "Modifier la stratégie",
"invalidProposals": "Propositions incorrectes",
"account": "Compte",
"create3box": "Créer un profil sur 3Box",
"view3box": "Voir le profil sur 3Box",
"edit3box": "Éditer le profil sur 3Box",
"connectWallet": "Connexion",
"toggleSkin": "Activer le thème",
"about": "À propos",
"license": "Licence",
"showMore": "Voir plus",
"voted": "Voté",
"reload": "Rafraîchir",
"ipfsServer": "Serveur IPFS",
"hub": "Hub",
"cancel": "Annuler",
"delete": "Supprimer",
"demoSite": "Ceci est le site de démonstration, testez-le !",
"removeDelegation": "Révoquer la délégation",
"confirmRemove": "Êtes-vous sûr de vouloir révoquer votre délégation à",
"removeSpace": "pour l'espace {0}",
"noVotingPower": "Oops, it seems you don't have any voting power at block {blockNumber}.",
"quorumReached": "quorum atteint",
"options": "Option(s)",
"votingPower": "Votre voting power",
"comment": {
"placeholder": "Partagez votre raison (facultatif)"
},
"receipt": "Reçu",
"relayer": "Relayer",
"verifyOnMycrypto": "Vérifier le reçu sur MyCrypto",
"verifyOnSignatorio": "Vérifier sur Signator.io",
"isCore": "Core",
"notificationsBlocked": "Votre navigateur bloque les notifications",
"notificationsNotSupported": "Votre navigateur ne prend pas en charge les notifications",
"walletNotSupported": "Le wallet n’est pas compatible",
"seeInExplorer": "Explorer",
"learnMore": "En savoir plus",
"logout": "Se déconnecter",
"continue": "Continuer",
"add": "Ajouter",
"edit": "Éditer",
"strategyParameters": "Paramètres de la stratégie",
"addAction": "Ajouter une action",
"removeAction": "Supprimer l'action",
"yourChoice": "Choix {0}",
"targetAddress": "Adresse de destination",
"value": "Valeur",
"date": "Données",
"marketDetails": "Détails du marché",
"addMarket": "Ajouter un marché",
"selectNetwork": "Sélectionnez un réseau",
"conditionId": "ID de condition",
"basetokenAddress": "Adresse du token de base",
"quoteAddress": "Adresse de la monnaie de cotation",
"removeMarket": "Supprimer le marché",
"back": "Retour",
"loading": "Chargement...",
"predictedImpact": "Impact prévu",
"marketSymbol": "{0} marché",
"twoChoicesRequired": "Deux choix sont requis pour ce plugin.",
"noResultsFound": "Oups, nous n'avons trouvé aucun résultat",
"createFirstProposal": "Créer votre première proposition",
"noSpacesJoined": "Oups, vous n'avez pas encore rejoint d'espaces",
"addFavorites": "Ajouter aux favoris",
"createdBy": "Par {0}",
"startIn": "début {0}",
"endIn": "fin {0}",
"proposalTimeLeft": "{0} restant(s)",
"endedAgo": "terminé {0}",
"proposalBy": "par {0}",
"endDate": "fin {0}",
"defaultSkin": "Thème par défaut",
"select": "Sélectionnez",
"language": "Langage",
"agree": "J'accepte",
"moderators": "Modérateurs",
"playground": "Playground",
"strategyParams": "Paramètres de la stratégie",
"addresses": "Adresses",
"networkErrorPlayground": "Erreur réseau - veuillez ouvrir la console de votre navigateur pour plus d'informations",
"upload": "Upload",
"join": "Rejoindre",
"joined": "Abonné",
"leave": "Se désabonner",
"subspaces": "Sous-espaces",
"mainspace": "Espace principal",
"copyLink": "Copier le lien",
"duplicate": "Dupliquer",
"joinedSpaces": "Espaces rejoints",
"joinSpaces": "Rejoindre un espace",
"setDelegationToSpace": "Limiter la délégation à un espace précis",
"theCurrentNetwork": "le réseau actuel",
"optional": "(facultatif)",
"homeLoadmore": "Afficher plus",
"confirmAction": "Confirmer l’action",
"or": "ou",
"share": "Partager",
"shareOnTwitter": "Partager sur Twitter",
"shareOnLenster": "Partager sur Lenster",
"createButton": "Créer",
"discussion": "Discussion",
"changeWallet": "Changer de wallet",
"createASpace": "Créer un espace",
"getStarted": "Commencer",
"skip": "Ignorer",
"newSpaceNotice": {
"header": "Votre espace est live !",
"mainText": "You can change how voting power is calculated via strategies in your {settings}. Changes to your settings will only affect new proposals, existing proposals can not be changed.",
"learnMore": "Apprenez-en plus dans la {documentation} ou joignez-vous au {discord} Snapshot pour obtenir de l’aide.",
"gotIt": "Compris !"
},
"errors": {
"required": "Le champ est requis",
"minLength": "Le champ est requis",
"maxLength": "Longueur maximale de {0}",
"pattern": "Caractère invalide",
"minItems": "Minimum {0} élément(s) requis",
"maxItems": "Maximum {0} item(s) allowed",
"minStrategy": "Au moins une stratégie est requise.",
"website": "L’URL doit être dans le format https://www.example.com",
"format": "Format invalide",
"type": "Type invalide",
"unsupportedImageType": "Type de fichier incompatible, Formats compatibles sont jpeg, jpg et png",
"invalidAddress": "Adresse invalide"
},
"create": {
"proposalTitle": "Titre",
"discussion": "Discussion (facultatif)",
"categorie(s)": "Sélectionnez jusqu’à 2 catégorie(s)",
"proposalDescription": "Description (facultatif)",
"preview": "Aperçu",
"choices": "Choix",
"addChoice": "Ajouter un choix",
"startDate": "Sélectionnez la date de début",
"endDate": "Sélectionnez la date de fin",
"startTime": "Sélectionnez l’heure de début",
"endTime": "Sélectionnez l'heure de fin",
"publish": "Publier",
"untitled": "Sans titre",
"snapshotBlock": "Numéro du bloc Snapshot",
"voting": "Vote",
"votingSystem": "Système de vote",
"choice": "Choix {0}",
"period": "Période de vote",
"start": "Début",
"end": "Fin",
"days": "Jours",
"hours": "Heures",
"minutes": "Minutes",
"schedule": "Planifier une proposition",
"delayEnforced": "L’espace impose un délai jusqu’à ce que le vote puisse commencer",
"periodEnforced": "L’espace impose la durée de la période de vote",
"typeEnforced": "{type} is enforced by the space",
"privacyEnforced": "{type} is enforced by the space",
"edit": "Modifier",
"continue": "Continuer",
"now": "Maintenant",
"votingPeriodExplainer": "Ceci représente la période pendant laquelle les utilisateurs pourront voter. La proposition sera visible et en attente avant le début de la période de vote.",
"uploadImageExplainer": "Ajouter des images en les glissant-déposant, en les sélectionnant, ou en les collant.",
"uploading": "Image en cours d’envoi",
"markdown": "Le style de Markdown est compatible",
"validationWarning": {
"basic": {
"member": "Vous devez être auteur dans cet espace pour soumettre une proposition.",
"minScore": "Vous devez posséder un minimum de {0} {1} pour soumettre une proposition."
},
"customValidation": "Vous devez approuver la validation de la proposition pour soumettre une proposition.",
"executionError": "La vérification de votre admissibilité à créer des propositions dans cet espace a échoué. C’est probablement dû à une stratégie mal configurée."
},
"errorGettingSnapshot": "Nous avons rencontré une erreur lors de la récupération du numéro de bloc nécessaire pour calculer votre voting power. Veuillez réessayer plus tard."
},
"delegate": {
"header": "Déléguer",
"selectDelegate": "Sélectionner une délégation",
"to": "Vers",
"addressPlaceholder": "Adresse ou ENS",
"delegations": "Vos délégation(s)",
"allSpaces": "Pour tous les espaces",
"delegated": "Vous délègue",
"pendingTransaction": "aucune transaction en attente | 1 transaction en attente | {count} transactions en attente",
"topDelegates": "Meilleurs délégués",
"noDelegatesFoundFor": "Pas de délégués trouvés pour {0}",
"noValidEns": "Adresse ENS invalide.",
"noValidAddress": "Adresse invalide",
"delegateToSelf": "Vous ne pouvez pas déléguer à vous-même",
"delegateToSelfAddress": "Vous ne pouvez pas déléguer à votre propre adresse ENS",
"noValidSpaceId": "ID d’espace invalide",
"noDelegationsAndDelegates": "Vous ne trouvez pas vos délégations ni vos délégués ? Assurez-vous d’être connecté au bon réseau.",
"delegateNotSupported": "Actuellement, la délégation n’est pas compatible pour {network}."
},
"proposal": {
"castVote": "Votez",
"vote": "Vote",
"startDate": "Date de début ",
"endDate": "Date de fin",
"votingSystem": "Système de vote",
"privacy": "Confidentialité",
"invalidChoice": "Choix invalide",
"postVoteModal": {
"defaultTitle": "Votre vote est enregistré !",
"gnosisSafeTitle": "Votre vote est en attente...",
"gnosisSafeDescription": "Les votes avec un Safe nécessitent des signataires supplémentaires et seront visibles une fois la transaction confirmée.",
"seeQueue": "Voir les transactions en attente",
"tips": {
"1": "Les votes peuvent être modifiés tant que la proposition est active"
}
},
"downloadCsvVotes": "Télécharger en tant que CSV",
"preparingCsvVotes": "Préparation du fichier"
},
"proposals": {
"header": "Propositions",
"new": "Nouvelle proposition",
"noProposals": "Il n'y a pas encore de propositions ici !",
"createProposal": "Créer une proposition",
"showMore": "Afficher plus",
"showLess": "Afficher moins",
"states": {
"all": "Tous",
"core": "Officiel",
"community": "Communauté",
"active": "Actif",
"pending": "En attente",
"closed": "Clos"
}
},
"notifications": {
"header": "Notifications",
"noNotifications": "Vous n’avez pas de notifications",
"proposalStarted": "La proposition a commencé :",
"proposalEnded": "La proposition est close :",
"markAllAsRead": "Marquer tous comme lu",
"all": "Tout",
"unread": "Non lus"
},
"modalTerms": {
"mustAgreeTo": "To {action} this space, you must agree to the {spaceName} terms of service.",
"actionJoin": "rejoindre",
"actionCreate": "créer une proposition dans",
"actionVote": "vote in"
},
"settings": {
"header": "Réglages",
"editController": "Modifier le controller",
"connectWithSpaceOwner": "Vous êtes en mode lecture seule, pour modifier les paramètres de l'espace, connectez-vous avec un contrôleur ou un wallet administrateur.",
"gnosisWrongNetwork": {
"base": "Your Gnosis Safe is on the wrong network. Please connect to {network} to {action}.",
"settings": "éditer les paramètres de l'espace",
"create": "créer une proposition",
"vote": "voter sur cette proposition"
},
"currentSpaceControllerIs": "Le controller de l’espace actuel est {address}",
"newController": "Nouveau controller",
"noRecord": "No text-record found. Make sure you have registered {id} domain on {network}, then edit the controller text-record to regain access to the space settings.",
"set": "Appliquer",
"profile": "Profil",
"avatar": "Avatar",
"name": {
"label": "Nom",
"placeholder": "p. ex. Yam Network"
},
"about": {
"label": "À propos",
"placeholder": "Quel est le but de votre organisation ?"
},
"categories": {
"label": "Catégorie(s)",
"select": "Sélectionner la/les catégorie(s)"
},
"terms": {
"label": "Conditions d'utilisation",
"information": "Les utilisateurs devront accepter ces conditions une fois avant de pouvoir créer une proposition ou voter"
},
"hideSpace": "Cacher l'espace de la page d'accueil",
"links": "Réseaux Sociaux",
"subspaces": {
"label": "Sous-espaces",
"information": "Sub-spaces will only be shown once configured on both the main space and the sub-space(s). {docs}",
"parent": {
"label": "Espace principal",
"placeholder": "pistachiodao.eth",
"information": "L'espace dont cet espace est un sous-espace sera affiché sur la page de l'espace"
},
"children": {
"label": "Sous-espaces",
"placeholder": "pistachiodao.eth",
"information": "Les sous-espaces apparentés listés ici seront affichés sur la page d'espace"
}
},
"website": "Site web",
"strategies": {
"label": "Stratégie(s)",
"information": "Les stratégies sont utilisées pour déterminer le voting power ou si un utilisateur est autorisé à créer une proposition"
},
"network": {
"label": "Réseau",
"information": "Le réseau par défaut utilisé pour cet espace. Les réseaux peuvent également être spécifiés dans les stratégies individuelles"
},
"symbol": {
"label": "Symbole",
"information": "Le symbole par défaut utilisé sur cet espace, généralement le symbole du jeton, p. ex. BAL pour Balancer"
},
"strategiesList": "Sélectionnez jusqu'à 8 stratégies",
"votingPowerIsCumulative": "Le voting power est cumulatif",
"addStrategy": "Ajouter une stratégie",
"testInPlayground": "Tester sur playground",
"admins": {
"label": "Administrateurs",
"information": "Les administrateurs peuvent modifier les paramètres et gérer les propositions de l'espace"
},
"authors": {
"label": "Auteurs",
"information": "Les auteurs sont toujours en mesure de créer des propositions"
},
"proposalValidation": "Validation de la proposition",
"validation": "Type",
"proposalThreshold": {
"label": "Seuil",
"information": "Le voting power minimum requis pour créer une proposition"
},
"allowOnlyAuthors": "Permettre uniquement aux auteurs de soumettre une proposition",
"editValidation": "Modifier la validation",
"selectValidation": "Sélectionner la validation",
"validationParameters": "Paramètres de validation",
"voting": "Vote",
"votingDelay": "Délai avant vote",
"votingPeriod": "Période de vote",
"hours": "Heures",
"days": "Jours",
"quorum": {
"label": "Quorum",
"information": "Le voting power minimum requis pour que la proposition passe"
},
"type": {
"label": "Type",
"information": "Le type de système de vote utilisé pour cet espace. (Appliqué à toutes les propositions futures)"
},
"anyType": "Tous",
"hideAbstain": "Ignorer les votes par abstention dans les résultats de vote basiques",
"customDomain": "Nom de domaine personnalisé",
"domain": {
"label": "Nom de domaine",
"placeholder": "p. ex. vote.balancer.fi",
"info": "To setup a custom domain you additionally need to open a pull request on github after you have created the space. {docs}"
},
"skin": "Thème",
"treasuries": {
"label": "Trésorerie",
"add": "Ajouter une trésorerie",
"edit": "Modifier la trésorerie",
"information": "Ajoutez des trésoreries à votre organisation pour les afficher dans votre espace"
},
"addPlugin": "Ajouter un plugin",
"editPlugin": "Modifier le plugin",
"pluginParameters": "Paramètres du plugin",
"proposal": {
"title": "Proposition",
"guidelines": {
"title": "Instructions",
"information": "Afficher un lien vers vos directives sur la création de propositions pour aider les utilisateurs à comprendre ce qui constitue une proposition valide"
},
"template": {
"title": "Modèle",
"information": "Créer chaque proposition avec un modèle pour aider les utilisateurs à comprendre quelles informations sont nécessaires"
}
}
},
"setup": {
"example": "ex : yam.eth",
"chooseExistingEns": "Choisissez un de vos domaines ENS existants pour créer un espace avec :",
"useSingleExistingEns": "Utiliser votre domaine ENS existant :",
"orRegisterNewEns": "Ou enregistrer un nouveau domaine :",
"demoTestnetEnsMessage": "To create a test space you need an ENS domain on {network}.",
"toCreateASpace": "Pour créer un espace, premièrement vous avez besoin d’un domaine ENS. En saisir un ci-dessous et suivre les instructions d’enregistrement ENS.",
"createASpace": "Créer un espace",
"registerEnsButton": "Enregistrer",
"supportedEnsTLDs": "Extensions de domaine prises en charge",
"helpDocsAndDiscordLinks": "Not sure how to setup your space? Learn more in the {docs} or join Snapshot {discord}.",
"setSpaceController": "Controller de l’espace",
"setSpaceControllerExists": "The snapshot text-record for this domain has already been set. Choose edit to change it, otherwise you can skip to the next step.",
"setSpaceControllerInfo": "Le contrôleur d'espace est le compte qui sera en mesure de gérer les paramètres de l'espace. Des contrôleurs d'espace supplémentaires (admins) peuvent être ajoutés ultérieurement.",
"setSpaceControllerInfoGnosisSafe": "Lors de la création d'un espace avec un Gnosis Safe, il est recommandé de définir l'adresse du Safe comme controller de l'espace. Si non, vous devez suivre quelques étapes supplémentaires. {link}",
"editSpaceController": "Modifier le controller sur ENS",
"setController": "Définir le controller",
"explainControllerAndEns": "Cette action nécessite une transaction sur {network} qui ajoutera un \"snapshot\" TEXT record à votre domaine ENS.",
"confirmToSetAddress": "Êtes-vous sûr de vouloir choisir {address} comme le controller de l’espace?",
"controllerHasAuthority": "Le controller a toute autorité sur les paramètres de l’espace",
"controller": "Controller",
"selectEnsForSpace": "Choisir l’adresse ENS",
"spaceOwnerAddressPlaceHolder": "ex : {address}",
"controllerAddress": "L’adresse du controller",
"updateController": "Mettre à jour le controller",
"seeOnEns": "Voir sur ENS",
"goToSettings": "Accéder les paramètres",
"setSpaceProfile": "Personnaliser votre espace",
"waitForTransaction": "La transaction doit être confirmée avant de pouvoir créer votre espace. {txUrl}",
"pleaseWaitMessage": "Cela peut prendre quelques minutes, veuillez attendre pendant que la transaction est confirmée",
"notControllerAddress": "Veuillez vous connecter avec l'adresse du controller {wallet} pour créer l'espace.",
"fillCurrentAccount": "Utiliser le compte connecté",
"domain": {
"title": "Configurez votre domaine d'espace",
"ensMessage": "Avant de pouvoir créer votre propre espace, vous avez besoin d'un domaine ENS sur le réseau principal Ethereum.",
"ensMessageTestnet": "You can also {link} on the Goerli testnet and mess with things there first.",
"tryDemo": "Essayer la démo",
"yourExistingSpaces": "Vos espaces existants",
"invalidEns": "This ENS name is invalid. Usually this is due the use of invalid characters during registration."
},
"strategy": {
"title": "Comment souhaitez-vous configurer votre stratégie de vote ?",
"subtitle": "Vous pouvez changer vos paramètres de stratégie à tout moment.",
"blockTitle": "Configurer la stratégie de vote",
"onePersonOneVote": {
"title": "Une personne, un vote",
"description": "Gérer une liste blanche de personnes qui peuvent voter ou simplement permettre à n'importe quelle adresse de voter. Chaque vote est égal et aucun jeton n'est requis",
"whitelistInformation": "Spécifiez les comptes pouvant voter",
"ticketInformation": "N'importe quel compte peut voter",
"votesEqualInfo": "Chaque vote est égal et aucun jeton n'est requis"
},
"tokenVoting": {
"title": "Vote pondéré par jeton",
"description": "Les votes sont pondérés par un jeton. Le jeton peut être un jeton ERC-20, ERC-721 ou ERC-1155",
"tokenNotFound": "Jeton introuvable",
"seeOnEtherscan": "Voir sur Etherscan"
},
"advanced": {
"title": "Paramètres personnalisés",
"description": "Sélectionnez jusqu'à 8 stratégies avec un vaste choix d'options. Si vous ne trouvez pas de stratégie adaptée à votre cas d'utilisation, vous pouvez créer la vôtre"
}
},
"validationTitle": "Qui peut gérer cet espace et créer des propositions ?"
},
"profile": {
"buttonEdit": "Modifier le profil",
"viewProfile": "Afficher le profil",
"about": {
"header": "À propos",
"joinedSpaces": "Espaces rejoints",
"createdSpaces": "Espaces créés",
"biography": "Biographie",
"notJoinSpacesYet": "N'a pas encore rejoint d'espaces",
"notCreatedSpacesYet": "N'a pas encore créé d'espaces",
"delegatorNetworkInfo": "Changer de réseau dans votre wallet",
"delegate": "Déléguer",
"delegated": "Délégué",
"delegateTo": "Déléguer à",
"delegateFor": "Délégue à",
"noDelegatorsMessage": "Aucune délégation sur {network}",
"notSupportedNetwork": "Actuellement, la délégation n’est pas compatible sur {network} "
},
"activity": {
"header": "Activité",
"votedFor": "Voté {choice}",
"today": "Aujourd'hui",
"thisWeek": "Cette semaine",
"olderThanWeek": "Older than a week",
"noActivity": "Pas encore d'activité"
},
"settings": {
"header": "Modifier le profil",
"name": "Nom",
"biography": "Biographie",
"namePlaceholder": "Saisir le nom",
"bioPlaceholder": "Raconter votre histoire",
"change": "Changer",
"remove": "Supprimer"
}
},
"notify": {
"youDidIt": "Tout est bon !",
"copied": "Copié !",
"proposalDeleted": "Proposition supprimée",
"somethingWentWrong": "Oups, une erreur s'est produite !",
"saved": "Enregistré !",
"delegationSuccess": "Délégation réussie",
"delegationRemoved": "Délégation retirée",
"proposalCreated": "Proposition créée",
"voteSuccessful": "Votre vote est enregistré !",
"ensSet": "ENS text record défini avec succès",
"transactionSent": "Transaction envoyée"
},
"explore": {
"createStrategy": "Créer une stratégie",
"createSkin": "Créer un thème",
"addNetwork": "Ajouter un réseau",
"createPlugin": "Créer un plugin",
"strategies": "stratégie(s)",
"skins": "thème(s)",
"networks": "réseau(x)",
"plugins": "plugin(s)",
"results": "résultat(s)",
"category": "Catégorie",
"categories": {
"all": "Tout",
"protocol": "Protocole",
"social": "Réseaux sociaux",
"investment": "Investissement",
"grant": "Bourse",
"service": "Service",
"media": "Média",
"creator": "Créateur/Créatrice",
"collector": "Collectionneur/Collectionneuse"
}
},
"voting": {
"selectVoting": "Sélectionner le système de vote",
"single-choice": "Vote à choix simple",
"approval": "Vote d'approbation",
"quadratic": "Vote quadratique",
"ranked-choice": "Vote préférentiel",
"weighted": "Vote pondéré",
"basic": "Vote basique",
"description": {
"single-choice": "Chaque votant ne peut effectuer qu'un seul choix.",
"approval": "Chaque votant peut effectuer un nombre indéfini de choix.",
"quadratic": "Chaque votant peut répartir ses droits de vote entre les différents choix. Les résultats sont calculés de manière quadratique.",
"ranked-choice": "Chaque votant peut effectuer plusieurs choix et les classer par préférence. Le système du second tour instantané est utilisé pour calculer les résultats.",
"weighted": "Chaque votant peut répartir ses droits de vote entre les différents choix.",
"basic": "Choix de vote unique avec trois choix: Pour, Contre ou S’abstenir"
}
},
"privacy": {
"label": "Confidentialité",
"title": "Sélectionner la confidentialité des votes",
"information": "Le type de confidentialité utilisé pour les propositions. (Appliqué à toutes les propositions futures)",
"any": "Any",
"none": "Aucun",
"shutter": {
"label": "Shutter",
"description": "Les choix sont chiffrés et visibles seulement après la fin de la période de vote",
"tooltip": "La confidentialité est activée pour cette proposition. Tous les votes seront cryptés jusqu'à ce que la période de vote soit terminée et le score final soit calculé",
"url": "https://blog.shutter.network/shielded-voting/"
}
},
"validation": {
"label": "Validation",
"title": "Select voting validation",
"information": "Le type de validation utilisé pour déterminer si un utilisateur peut voter. (Appliqué à toutes les propositions futures)",
"any": {
"label": "Ouvert à tous",
"description": "Toute personne ayant du voting power peut voter."
},
"basic": {
"label": "Basic",
"description": "Use any strategy to determine if a user can vote.",
"invalidMessage": "You do not meet the minimum balance requirement to vote on this proposal. "
},
"passport-gated": {
"label": "Accès réservé au Gitcoin Passport",
"description": "Protégez vos propositions contre le spam et la manipulation des votes en exigeant que les utilisateurs aient un Gitcoin Passport.",
"invalidMessage": "You need a Gitcoin Passport with score above {scoreThreshold} and {operator} of the following stamps to vote on this proposal: {stamps}. "
}
},
"safeSnap": {
"currentOutcome": "Résultats actuels",
"currentBond": "Caution actuelle",
"finalizedIn": "Terminé {0}",
"executableIn": "Exécutable {0}",
"finalOutcome": "Résultat",
"nextBond": "Ajouter une caution au résultat défini",
"setOutcomeTo": "Définir le résultat",
"claimBond": "Récupérer la caution",
"addBatch": "Ajouter un lot de transactions",
"batch": "Lot de transactions",
"transactions": "Transactions",
"to": "Vers (adresse)",
"invalidAddress": "Adresse invalide",
"invalidAmount": "Montant invalide",
"invalidValue": "Valeur incorrecte",
"invalidAbi": "ABI invalide",
"invalidData": "Données invalides",
"value": "Valeur (wei)",
"data": "Données",
"noCollectibles": "Aucun objet de collection",
"asset": "Actif",
"amount": "Montant ",
"type": "Type",
"transferFunds": "Virer des fonds",
"transferNFT": "Transférer un NFT",
"contractInteraction": "Interaction avec le contrat",
"rawTransaction": "Transaction brute",
"addTransaction": "Ajouter une transaction",
"transactionLabels": {
"contractInteraction": "{functionName}() - {amount} wei vers {address}",
"transferFunds": "Transférer {amount} {tokenSymbol} vers {address}",
"transferNFT": "Envoyer {name} #{id} vers {address}",
"raw": "Envoyer {amount} wei vers {address}"
},
"labels": {
"request": "Exécuter la requête",
"setOutcome": "Définir un résultat",
"changeOutcome": "Modifier le résultat",
"executeTxs": "Exécuter le lot de transactions {0} sur {1}",
"executed": "Toutes les transactions ont été exécutées",
"noTransactions": "There are no transactions to execute",
"rejected": "Proposal rejected",
"error": "Une erreur s'est produite",
"connectWallet": "Connectez le wallet pour voir les détails de l'exécution",
"switchChain": "Passez au wallet {0} pour demander l'exécution",
"question": "Cette proposition a-t-elle été adoptée et satisfait-elle aux",
"criteria": "critères d'acceptation ?",
"proposalPassed": "Did the proposal pass?",
"expired": "La proposition a expiré",
"approveBond": "Approve bond",
"confirmVoteResults": "Click to confirm the proposal passed",
"executeTxsUma": "Execute transaction batch",
"deleteDisputedProposal": "Delete disputed proposal",
"confirmVoteResultsToolTip": "Make sure this proposal was approved by this Snapshot vote before proposing on-chain. If the Snapshot vote rejected the proposal, the on-chain proposal will be rejected as well and you will lose your bond.",
"approveBondToolTip": "On-chain proposals require a bond from the proposer. This will approve tokens from your wallet to be posted as a bond. If you make an invalid proposal, it will be disputed and you will lose your bond. If the proposal is valid, your bond will be returned when the transactions are executed.",
"requestToolTip": "This will propose the transactions from this Snapshot vote on-chain. After a challenge window, if the proposal is valid, the transactions can be executed and your bond will be returned.",
"executeToolTip": "This will execute the transactions from this proposal and return the proposer's bond."
}
},
"poap": {
"no_poap_header": "Il n'y a pas encore de POAP pour cette proposition :'(",
"no_voted_header": "Votez pour obtenir ce POAP",
"unclaimed_header": "Mint votre POAP «J’ai voté»",
"claimed_header": "Félicitations! Le POAP a été réclamé à votre collection",
"loading_header": "Le POAP est en cours d’ajout à votre collection",
"button_claim": "Mint",
"button_show": "Parcourir la collection",
"success_claim": "Le POAP a été réclamé à votre collection",
"error_claim": "Un problème est survenu lors de la création du token"
},
"progress": {
"progress": "Progression",
"inProgress": "En cours",
"completed": "Completed",
"complete": "Complete",
"newStep": "Nouvelle étape",
"description": "Description",
"add": "Ajouter",
"deleteStep": "Supprimer l’étape",
"deleteConfirm": "Confirmez-vous la suppression ?",
"delete": "Supprimer",
"cancel": "Annuler",
"comeBack": "Revenez une fois le vote terminé pour voir comment cette proposition progresse!",
"confirmSignature": "Signer ce message nous permettra d'autoriser votre demande de mise à jour de la progression de votre proposition.",
"wentWrong": "Une erreur s'est produite.",
"voting": "Voting",
"soon": "Soon"
},
"charts": {
"charts": "Tableaux",
"noVotesYet": "Il n’y a pas encore de votes à visualiser.",
"totalVotesPerDay": "Nombre total de votes par jour",
"shareOfVotingPower": "Partage du voting power",
"votingPowerPerDay": "Voting power par jour"
},
"comment_box": {
"title": "Boîte de commentaires",
"add": "Ajoutez vos commentaires ici",
"submit": "Valider",
"preview": "Aperçu",
"continue_editing": "Poursuivre la modification",
"edit": "Modifiez votre réponse ici",
"edit_button": "Modifier",
"dismiss": "Ignorer",
"delete": "Supprimer",
"add_reply": "Ajoutez votre réponse ici",
"edit_comment": "Modifier le commentaire",
"edit_modal": "Confirmez-vous la modification ?",
"yes": "Oui",
"no": "Non",
"delete_comment": "Supprimer le commentaire",
"delete_modal": "Confirmez-vous la suppression ?",
"error": "Oups, une erreur s'est produite",
"replies": "réponses",
"hide": "Masquer",
"show": "Voir",
"reply": "Répondre",
"load_more": "Afficher plus"
},
"page": {
"title": {
"home": "Snapshot",
"setup": "Créer un espace",
"timeline": "Chronologie",
"notifications": "Notifications",
"explore": "Explorer",
"playground": "Playground",
"space": {
"create": "Créer une proposition {space}",
"about": "À propos de {space}",
"proposals": "Propositions {space}",
"proposal": "Proposition {space}: {proposal}",
"settings": "Paramètres {space}"
},
"strategy": "Stratégie {key}",
"delegate": "Déléguer",
"ranking": "Classement"
}
},
"hal": {
"title": "Suivre les propositions pour {spaceName}",
"text": "Recevoir des notifications chaque fois qu’une nouvelle proposition est créée ou se termine"
},
"timeUnits": {
"second": "1 seconde | {n} secondes",
"minute": "1 minute | {n} minutes",
"hour": "1 heure | {n} heures",
"day": "1 jour | {n} jours",
"week": "1 semaine | {n} semaines",
"month": "1 mois | {n} mois",
"year": "1 an | {n} ans"
},
"unsupportedNetwork": {
"unsupportedNetwork": "Réseau incompatible",
"switchNetworkToNetwork": "Pour continuer, vous devez changer le réseau de votre wallet à {network}.",
"switchToNetwork": "Passer au réseau {network}",
"goToDemoSite": "Accéder le site démo"
},
"treasury": {
"title": "Trésorerie",
"wallets": {
"title": "Portefeuilles",
"empty": "Cet espace n'a pas encore de trésorerie",
"addTreasury": "Ajouter une trésorerie"
},
"assets": {
"title": "Actifs",
"empty": "Ce contrat ne contient aucun actif"
},
"24hChange": "Changement sur 24h"
},
"newsletter": {
"yourEmail": "Votre e-mail",
"title": "Recevoir les dernières mises à jour de Snapshot"
},
"joinCommunity": "Rejoindre la communauté Snapshot",
"header": {
"title": "Là où les décisions sont prises",
"description": "Snapshot est une plateforme de gouvernance communautaire gratuite et open-source. Créez votre propre espace dès maintenant et commencez à prendre des décisions !"
},
"aboutPage": {
"description": "Snapshot est une plateforme de gouvernance décentralisée qui facilite la création et le vote sur les propositions - le tout sans dépenser une fortune en frais de gaz ! De plus, notre système flexible prend en charge différents types et stratégies de vote, afin que vous puissiez adapter le processus de vote à vos besoins.",
"subHeader": "La gouvernance devrait être simple",
"subDescription": "La gouvernance Web3 ne doit pas être compliquée. Snapshot est la solution idéale pour les organisations qui cherchent un moyen simple et efficace pour organiser leur communauté ou leur organisation."
},
"footerView": {
"resources": "Ressources",
"about": "À propos",
"blog": "Blog",
"jobs": "Jobs",
"discussions": "Discussions",
"github": "GitHub",
"docs": "Documentation",
"support": "Support",
"hiring": "Rejoignez-nous !"
}
}
================================================
FILE: src/locales/hi-IN.json
================================================
{
"searchPlaceholder": "खोजें",
"spaceCount": "{0} स्पेसेस",
"createSpace": "स्थान बनाएं",
"backToHome": "होम",
"actions": "एक्शन",
"results": "परिणाम",
"resultsError": "Results could not be calculated. This is often due to a misconfigured strategy or an unresponsive RPC node involved in the strategy.",
"vpError": "Your voting power could not be calculated. This is often due to a misconfigured strategy or an unresponsive RPC node involved in the strategy.",
"getHelp": "Get help",
"retry": "Retry",
"currentResults": "वर्तमान परिणाम (रिज़ल्ट्स)",
"reset": "रीसेट करें",
"save": "सेव करें",
"author": "लेखक\nरिलेयर",
"next": "आगे",
"submit": "जमा करें",
"plugins": "प्लगइन्स",
"information": "सुचना",
"confirm": "पुष्टि करें",
"snapshot": "स्नैपशॉट",
"strategies": "स्ट्रेटेजीज",
"strategiesPage": "स्ट्रेटेजीज",
"space": "जगह",
"spaces": "जगह",
"verifiedSpace": "Verified space",
"version": "संस्करण",
"timeline": "टाइमलाइन",
"filters": "छांटे",
"allSpaces": "सभी जगहें",
"submitOnchain": "चेन पर जमा करें",
"inSpaces": "रिक्त स्थान में",
"votes": "वोट",
"seeMore": "और देखें",
"network": "नेटवर्क",
"networks": "नेटवर्क",
"skins": "स्किन",
"members": "कोई सदस्य नहीं | {count} सदस्य | {count} सदस्य",
"editStrategy": "रणनीति संपादित करें",
"invalidProposals": "अमान्य प्रस्ताव",
"account": "खाता",
"create3box": "3बॉक्स पर प्रोफाइल बनाए",
"view3box": "3 बॉक्स का प्रोफ़ाइल देखें",
"edit3box": "3 बॉक्स का प्रोफाइल संपादित करें",
"connectWallet": "खता जोड़े",
"toggleSkin": "Toggle skin",
"about": "विवरण",
"license": "लाइसेंस",
"ipfsServer": "आईपीएफएस सर्वर",
"hub": "केंद्र",
"cancel": "रद्द करें",
"deleteProposal": "प्रस्ताव मिटाएँ",
"demoSite": "यहाँ एक डेमो साइट है, संदर्भ के लिए",
"removeDelegation": "डेलिगेशन हटाएं",
"confirmRemove": "क्या आप वाकई अपना डेलिगेशन हटाना चाहते हैं?",
"removeSpace": "अंतर के लिए {0}",
"confirmVote": "वोट की पुष्टी करें",
"sureToVote": "Are you sure you want to cast this vote?",
"cannotBeUndone": "इस क्रिया को पूर्ववत नहीं किया जा सकता है।",
"options": "विकल्प",
"votingPower": "आपकी वोटिंग पावर:",
"receipt": "रसीद",
"relayer": "रिलेयर",
"verifyOnMycrypto": "माइक्रिप्टो पर रसीद सत्यापित करें",
"verifyOnSignatorio": "Verify on Signator.io",
"isCore": "कोर",
"notificationsBlocked": "Your browser is blocking notifications",
"notificationsNotSupported": "Your browser does not support notifications",
"walletNotSupported": "Wallet is not supported",
"seeInExplorer": "एक्सप्लोरर पर देखें",
"learnMore": "और पढ़ें",
"logout": "लॉग आउट",
"continue": "जारी रखें",
"add": "जोड़ें",
"edit": "संपादित करें",
"strategyParameters": "स्ट्रेटेजी पैरामीटर",
"addAction": "एक्शन जोड़ें",
"removeAction": "एक्शन हटाएं",
"yourChoice": "पसंद{0}",
"targetAddress": "टारगेट एड्रेस",
"value": "मूल्य",
"date": "डेटा",
"marketDetails": "मारकेट का विवरण",
"addMarket": "मारकेट जोड़ें",
"selectNetwork": "नेटवर्क चुनें",
"conditionId": "कंडीशन आय डी",
"basetokenAddress": "बेस टोकन आय डी",
"quoteAddress": "करेंसी का एड्रेस बताएं",
"removeMarket": "मार्किट हटाए",
"back": "वापस जाएँ",
"loading": "लोड हो रहा है...",
"predictedImpact": "होने वाला प्रभाव",
"marketSymbol": "{0} मारकेट",
"twoChoicesRequired": "इस प्लगइन के लिए २ विकल्प जरुरी है",
"noResultsFound": "कोई परिणाम नहीं मिल पा रहा है",
"createFirstProposal": "Let's create your first proposal",
"noSpacesJoined": "ओह, आप अभी तक किसी जगह से नही जुडे।",
"addFavorites": "पसंदीदा में जोड़ें",
"createdBy": "तक {0}",
"startIn": "शुरू {0}",
"endIn": "ख़त्म {0}",
"proposalTimeLeft": "{0} left",
"endedAgo": "ended {0}",
"proposalBy": "द्वारा{0}",
"endDate": "ख़त्म {0}",
"defaultSkin": "डिफ़ॉल्ट स्किन",
"select": "चुनें",
"language": "भाषा",
"agree": "मैं सहमत हूँ",
"mustAgreeToTerms": "इससे पहले कि आप इस कार्रवाई को पूरा कर सकें, आपको यहां {0} सेवा की शर्तों से सहमत होना होगा",
"playground": "प्लेग्राउंड",
"strategyParams": "स्ट्रेटेजी पैरामीटर",
"addresses": "पते",
"networkErrorPlayground": "नेटवर्क त्रुटि - अधिक जानकारी के लिए कृपया अपना ब्राउज़र कंसोल खोलें",
"upload": "अपलोड करें",
"join": "शामिल हों",
"joined": "शामिल हो गए",
"leave": "छोड़ें",
"copyLink": "लिंक कॉपी करें",
"duplicateProposal": "डुप्लीकेट प्रस्ताव",
"joinedSpaces": "स्पेस में शामिल हों",
"joinSpaces": "स्पेस में शामिल हों",
"setDelegationToSpace": "Limit delegation to a specific space",
"theCurrentNetwork": "the current network",
"optional": "(optional)",
"homeLoadmore": "Load more",
"confirmAction": "Confirm action",
"or": "or",
"share": "Share",
"errors": {
"required": "यह जानकारी जरुरी है",
"minLength": "न्यूनतम लंबाई {0}",
"maxLength": "अधिकतम लंबाई {0}",
"pattern": "अमान्य वर्ण",
"minItems": "न्यूनतम {0} आइटम्स जरुरी है",
"minStrategy": "कम से कम एक स्ट्रेटेजी की आवश्यकता है",
"format": "अवैध फॉर्मेट",
"type": "Invalid type"
},
"create": {
"question": "Ask a question...",
"categorie(s)": "Select up to 2 categorie(s)",
"content": "Tell us more about your proposal (optional)",
"preview": "पूर्वावलोकन/पूर्वदर्शन",
"choices": "विकल्प",
"addChoice": "विकल्प जोड़े",
"startDate": "प्रारंभ तिथि चुनें",
"endDate": "अंतिम तिथि चुनें",
"startTime": "समाप्ति का समय",
"endTime": "समाप्ति का समय",
"publish": "प्रकाशित करें",
"untitled": "Untitled",
"snapshotBlock": "स्नैपशॉट ब्लॉक नंबर",
"voting": "Voting",
"votingSystem": "Voting system",
"choice": "Choice {0}",
"period": "Voting period",
"start": "Start",
"end": "End",
"days": "Days",
"hours": "Hours",
"minutes": "Minutes",
"schedule": "Schedule proposal",
"delayEnforced": "The space enforces a delay until voting can start",
"periodEnforced": "The space enforces the duration of the voting period",
"edit": "Edit",
"continue": "Continue",
"now": "Now",
"votingPeriodExplainer": "This is the time period in which users will be able to vote. The proposal will be visible and pending before the start of the voting period.",
"validationWarning": {
"basic": {
"member": "प्रस्ताव सबमिट करने के लिए आपको स्पेस का लेखक होना आवश्यक है।",
"minScore": "प्रपोजल सबमिट करने के लिए काम से काम {0} {1} होना जरुरी है"
},
"customValidation": "प्रस्ताव जमा करने के लिए आपको प्रस्ताव सत्यापन पास करना होगा",
"executionError": "Verifying your eligibility to create proposals in this space has failed. This is likely due to a misconfigured strategy."
}
},
"delegate": {
"header": "डेलीगेट",
"selectDelegate": "प्रतिनिधि चुनें",
"to": "को",
"addressPlaceholder": "पता या ईएनएस नाम",
"delegations": "आपके डेलीगेशन",
"allSpaces": "सब जगहों के लिए",
"delegated": "आपको दिए गए डेलीगेशन",
"pendingTransaction": "कोई लंबित लेनदेन नहीं | 1 लंबित लेनदेन | {count} लंबित लेनदेन",
"topDelegates": "Top delegates",
"noDelegatesFoundFor": "No delegates found for {0}",
"noValidEns": "Not a valid ENS address.",
"noValidAddress": "Not a valid address",
"delegateToSelf": "You cannot delegate to yourself",
"delegateToSelfAddress": "You cannot delegate to your own ENS address",
"noValidSpaceId": "Not a valid space ID",
"noDelegationsAndDelegates": "Can't find your delegations and delegates? Make sure you are connected to the correct network.",
"delegateNotSupported": "Delegation is currently not supported for {network}."
},
"proposal": {
"castVote": "अपना वोट दे",
"vote": "वोट",
"startDate": "आरंभ तिथि",
"endDate": "समाप्ति दिनांक",
"votingSystem": "मतदान प्रणाली"
},
"proposals": {
"header": "प्रपोजल्स",
"new": "नए प्रपोजल्स",
"noProposals": "यहाँ अभी प्रोपोज़ल्स नहीं है",
"createProposal": "Create proposal",
"showMore": "Show more",
"showLess": "Show less",
"states": {
"all": "सभी",
"core": "कोर",
"community": "कम्यूनिटी",
"active": "सक्रिय",
"pending": "बाकी",
"closed": "बंद"
}
},
"settings": {
"header": "सेटिंग्स",
"setEnsTextRecord": "Set ENS text record",
"editController": "Edit controller",
"needToSetEnsText": "Your space settings will be stored in a file on IPFS. Before you can edit them, you need to set an ENS text record pointing to that file.",
"profile": "प्रोफ़ाइल",
"avatar": "अवतार",
"name": "नाम",
"about": "जानकारी",
"terms": "शर्तें",
"network": "नेटवर्क",
"symbol": "चिन्ह",
"skin": "स्किन",
"customDomain": "कस्टम डोमेन",
"domain": "डोमेन नाम",
"strategies": "स्ट्रेटेजीज",
"categories": "Categorie(s)",
"selectCategories": "Select categories",
"hideSpace": "होमपेज से स्पेस छुपाये",
"addStrategy": "स्ट्रेटेजी जोड़े",
"authors": "लेखक",
"admins": "एडमिन",
"defaultTab": "डिफ़ॉल्ट टैब",
"proposalThreshold": "प्रपोजल थ्रेसहोल्ड",
"allowOnlyAuthors": "केवल लेखकों को प्रस्ताव प्रस्तुत करने की अनुमति है|",
"editPlugin": "प्लगइन संपादित करें",
"addPlugin": "प्लगइन जोड़ें",
"pluginParameters": "प्लगइन पैरामीटर्स",
"proposalValidation": "प्रस्ताव सत्यापन",
"validation": "मान्यकरण",
"editValidation": "प्रमाणीकरण जोड़ें/संपादित करें",
"selectValidation": "मान्यता का चयन करें",
"validationParameters": "इन मापदंडों को मान्य करें",
"warningTextRecord": "आपको ENS डोमेन पर एक टेक्स्ट रिकॉर्ड सेट करना होगा।",
"votingDelay": "Voting delay",
"votingPeriod": "Voting period",
"voting": "Voting",
"quorum": "Quorum",
"type": "Type",
"anyType": "Any",
"hideAbstain": "Ignore abstain votes in basic voting results",
"connectWithSpaceOwner": "To modify space settings, connect with a controller or admin wallet."
},
"setup": {
"example": "उदाहरण. yam.eth",
"chooseExistingEns": "Choose one of your existing ENS domains to create a space with:",
"useSingleExistingEns": "Use your existing ENS domain:",
"orRegisterNewEns": "Or register a new domain:",
"toCreateASpace": "To create a space, you first need an ENS domain. Enter one below and follow the ENS registration instructions.",
"createASpace": "स्पेस तैयार करे",
"registerEnsButton": "Register",
"supportedEnsTLDs": "Supported domain endings",
"helpDocsAndDiscordLinks": "Not sure how to create a space? Learn more in the documentation or join Snapshot Discord .",
"setSpaceController": "Set space controller address on ENS",
"editSpaceController": "Edit controller on ENS",
"setController": "Set controller",
"explainControllerAndEns": "This action requires a transaction on the Ethereum Mainnet which will add a \"snapshot\" TEXT record to your ENS domain.",
"confirmToSetAddress": "Are you sure you want to set {address} as the controller of the space?",
"controllerHasAuthority": "The controller has full authority over the space settings",
"controller": "Controller",
"selectEnsForSpace": "Choose ENS address",
"textRecordExists": "Currently the space controller is {address}. You can update the space controller by using the form below.",
"spaceOwnerAddressPlaceHolder": "e.g. 0xF78108c9BBaF466dd96BE41be728Fe3220b37119",
"controllerAddress": "Controller address",
"updateController": "Update controller",
"connectWithEnsController": "To modify the space controller you need to connect with the wallet that owns {ens}",
"seeOnEns": "See on ENS",
"goToSettings": "Go to settings"
},
"notify": {
"youDidIt": "संपूर्ण हुआ",
"copied": "कॉपी किया गया!",
"proposalDeleted": "प्रपोजल मिटा दिया गया",
"somethingWentWrong": "ओह! कुछ गलत हो गया है",
"saved": "Saved!",
"delegationSuccess": "Delegation successful",
"delegationRemoved": "Delegation removed",
"proposalCreated": "Proposal created",
"voteSuccessful": "Your vote is in!",
"ensSet": "ENS text record was successfully set"
},
"explore": {
"createStrategy": "स्ट्रेटेजी बनाये",
"createSkin": "स्किन बनाये",
"addNetwork": "नेटवर्क बनाये",
"createPlugin": "प्लगइन बनाये",
"strategies": "स्ट्रेटेजीज",
"skins": "स्किन",
"networks": "नेटवर्क",
"plugins": "प्लगइन्स",
"results": "परिणाम",
"categories": {
"all": "All",
"protocol": "Protocol",
"social": "Social",
"investment": "Investment",
"grant": "Grant",
"service": "Service",
"media": "Media",
"creator": "Creator",
"collector": "Collector"
}
},
"voting": {
"selectVoting": "मतदान प्रणाली का चयन करें",
"single-choice": "एकल विकल्प मतदान",
"approval": "स्वीकृति मतदान",
"quadratic": "त्रैमासिक मतदान",
"ranked-choice": "रैंकिंग विकल्प मतदान",
"weighted": "भारित मतदान",
"basic": "Basic voting",
"description": {
"single-choice": "प्रत्येक मतदाता केवल एक विकल्प का चयन कर सकता है",
"approval": "प्रत्येक मतदाता किसी भी संख्या में विकल्पों का चयन कर सकता है",
"quadratic": "प्रत्येक मतदाता किसी भी संख्या में विकल्पों में मतदान शक्ति का प्रसार कर सकता है। परिणामों की गणना त्रैमासिक रूप से की जाती है",
"ranked-choice": "प्रत्येक मतदाता किसी भी विकल्प का चयन और रैंक कर सकता है। परिणामों की गणना तत्काल गणना पद्धति द्वारा की जाती है",
"weighted": "प्रत्येक मतदाता किसी भी संख्या में विकल्पों में मतदान शक्ति का प्रसार कर सकता है",
"basic": "Single choice voting with three choices: For, Against or Abstain"
},
"choices": {
"for": "For",
"against": "Against",
"abstain": "Abstain"
}
},
"safeSnap": {
"currentOutcome": "वर्तमान परिणाम",
"currentBond": "वर्तमान बॉन्ड",
"finalizedIn": "फायनलाइस{0}",
"executableIn": "निष्पादन{0}",
"finalOutcome": "परिणाम",
"nextBond": "परिणाम निर्धारित करने के लिए बॉन्ड",
"setOutcomeTo": "परिणाम सेट करें",
"claimBond": "बॉन्ड का दावा करे",
"addBatch": "लेनदेन बैच जोड़ें",
"batch": "लेनदेन बैच",
"transactions": "लेनदेन",
"to": "भेजने का एड्रेस",
"invalidAddress": "अमान्य एड्रेस",
"invalidAmount": "अमान्य रकम",
"invalidValue": "अमान्य मूल्य",
"invalidAbi": "अमान्य एबीआई",
"invalidData": "अमान्य डेटा",
"value": "मूल्य (wei)",
"data": "डेटा",
"noCollectibles": "कोई संग्रहणीय नहीं",
"asset": "परिसंपत्ति",
"amount": "रकम",
"type": "प्रकार",
"transferFunds": "निधि अंतरण",
"transferNFT": "NFT अंतरण",
"contractInteraction": "अनुबंध बातचीत",
"rawTransaction": "कच्चा लेनदेन",
"addTransaction": "लेनदेन जोड़ें",
"transactionLabels": {
"contractInteraction": "{functionName}() - {amount} wei से {address}",
"transferFunds": "ट्रांसफ़र {amount} {tokenSymbol} में {address}",
"transferNFT": "भेजें {name} #{id} पर {address}",
"raw": "भेजें {amount} wei पर {address}"
},
"labels": {
"request": "निष्पादन का अनुरोध करें",
"setOutcome": "परिणाम सेट करें:",
"changeOutcome": "परिणाम बदले",
"executeTxs": "निष्पादित करें लेनदेन बैच {0} का {1}",
"executed": "सभी लेनदेन निष्पादित किए गए हैं",
"rejected": "Proposal rejected",
"error": "कुछ गलत हो गया",
"connectWallet": "निष्पादन विवरण देखने के लिए वॉलेट कनेक्ट करें",
"switchChain": "Switch your wallet to {0} to request execution",
"question": "क्या यह प्रस्ताव पारित हुआ और क्या यह पूरा करता है",
"criteria": "स्वीकृति क्रिटेरिऑन?",
"proposalPassed": "Did the proposal pass?"
}
},
"poap": {
"no_poap_header": "इस प्रस्ताव के लिए अभी तक POAP सेटअप नहीं किया गया है :'(",
"no_voted_header": "इस POAP को पाने के लिए वोट करें",
"unclaimed_header": "Mint your I voted POAP",
"claimed_header": "Congratulations! The POAP has been minted to your collection",
"loading_header": "The POAP is being minted to your collection",
"button_claim": "Mint",
"button_show": "Browse collection",
"success_claim": "The POAP has been minted to your collection",
"error_claim": "टोकन बनाने में एक समस्या हुई"
},
"charts": {
"charts": "Charts",
"noVotesYet": "There are no votes to visualize yet.",
"totalVotesPerDay": "Total votes per day",
"shareOfVotingPower": "Share of voting power",
"votingPowerPerDay": "Voting power per day"
},
"comment_box": {
"title": "टिप्पणी बॉक्स",
"add": "अपनी टिप्पणी यहाँ लिखें",
"submit": "जमा करें",
"preview": "पूर्वावलोकन",
"continue_editing": "एडिटिंग जारी रखें",
"edit": "अपना उत्तर यहां एडिट करें",
"edit_button": "बदले",
"dismiss": "ख़ारिज करें",
"delete": "हटाएं",
"add_reply": "अपना उत्तर यहाँ लिखें",
"edit_comment": "टिप्पणी बदले",
"edit_modal": "क्या आप वाकई एडिट करना चाहते हैं?",
"yes": "हाँ",
"no": "नहीं",
"delete_comment": "टिप्पणी हटाएँ",
"delete_modal": "क्या आप वाकई हटाने के इच्छुक हैं?",
"error": "ओह! कुछ गलत हो गया है",
"replies": "जवाब",
"hide": "छुपाएँ",
"show": "दिखाएँ",
"reply": "जवाब दें",
"load_more": "अधिक लोड करें"
},
"page": {
"title": {
"home": "Snapshot",
"setup": "Create a space",
"timeline": "Timeline",
"explore": "Explore",
"playground": "Playground",
"space": {
"create": "Create {space} proposal",
"about": "About {space}",
"proposals": "{space} Proposals",
"proposal": "{space} proposal: {proposal}",
"settings": "{space} Settings"
},
"strategy": "{key} strategy",
"delegate": "Delegate"
}
},
"hal": {
"title": "Track proposals for {spaceName}",
"text": "Receive notifications every time a new proposal is created or ends"
},
"timeUnits": {
"second": "1 second | {n} seconds",
"minute": "1 minute | {n} minutes",
"hour": "1 hour | {n} hours",
"day": "1 day | {n} days",
"week": "1 week | {n} weeks",
"month": "1 month | {n} months",
"year": "1 year | {n} years"
},
"unsupportedNetwork": {
"unsupportedNetwork": "Unsupported network",
"ensOnlyMainnet": "Snapshot only supports ENS on the ethereum mainnet. Once the space is created, you can use it across multiple chains.",
"switchNetworkToMainnet": "To continue, you need to change the network in your wallet to Ethereum Mainnet.",
"switchToMainnet": "Switch to Mainnet"
}
}
================================================
FILE: src/locales/id-ID.json
================================================
{
"searchPlaceholder": "Cari",
"spaceCount": "{0} ruang(an)",
"createSpace": "Buat ruang",
"backToHome": "Beranda",
"actions": "Aksi",
"poweredBy": "Powered by",
"results": "Hasil",
"resultsError": "Hasil tidak dapat dihitung. Hal ini sering disebabkan oleh strategi yang salah dikonfigurasi atau node RPC yang tidak responsif dalam strategi.",
"resultsCalculating": "Final results are being calculated. If you still see this message after a few minutes contact the space admin.",
"votingPowerFailedMessage": "Your voting power could not be calculated. This is often due to a misconfigured strategy or an unresponsive RPC node involved in the strategy.",
"votingValidationFailedMessage": "There was an error on our side and we could not verify if you are eligible to vote. This is often due to a misconfigured voting validation or an unresponsive API involved in the validation.",
"notValidVoterMessage": "Oops, you don't seem to be eligible to vote on this proposal.",
"getHelp": "Dapatkan bantuan",
"retry": "Coba lagi",
"currentResults": "Hasil saat ini",
"reset": "Atur ulang",
"close": "Close",
"save": "Simpan",
"author": "Penulis",
"next": "Selanjutnya",
"choice": "Choice",
"submit": "Kirim",
"plugins": "Plugins",
"information": "Informasi",
"confirm": "Konfirmasi",
"snapshot": "Snapshot",
"strategies": "Strategi",
"strategiesPage": "Strategi",
"space": "Ruang",
"spaces": "Ruang",
"verifiedSpace": "Ruang terverifikasi",
"warningSpace": "This space has been flagged as potentially malicious. Proceed with caution.",
"version": "Versi",
"timeline": "Lini Masa",
"ended": "berakhir",
"started": "dimulai",
"filters": "Filter",
"allSpaces": "Semua ruang",
"submitOnchain": "Simpan on-chain",
"inSpaces": "Di {0} ruang",
"votes": "Suara",
"seeMore": "Lihat lebih banyak",
"seeAll": "Lihat semua",
"network": "Jaringan",
"networks": "Jaringan",
"skins": "Tampilan",
"spaceMembers": "Members",
"members": "Tidak ada anggota | {count} anggota | {count} anggota",
"editStrategy": "Edit strategi",
"invalidProposals": "Proposal tidak valid",
"account": "Akun",
"create3box": "Buat profil di 3box",
"view3box": "Lihat profil di 3Box",
"edit3box": "Edit profil di 3Box",
"connectWallet": "Hubungkan dompet",
"toggleSkin": "Atur skin",
"about": "Tentang",
"license": "Lisensi",
"showMore": "Show more",
"voted": "Voted",
"reload": "Reload",
"ipfsServer": "Server IPFS",
"hub": "Hub",
"cancel": "Batal",
"delete": "Delete",
"demoSite": "Ini adalah situs demo, cobalah!",
"removeDelegation": "Hapus delegasi",
"confirmRemove": "Apakah anda yakin untuk menghapus delegasi",
"removeSpace": "untuk ruang {0}",
"noVotingPower": "Oops, it seems you don't have any voting power at block {blockNumber}.",
"quorumReached": "quorum reached",
"options": "Pilihan",
"votingPower": "Kekuatan voting anda",
"comment": {
"placeholder": "Share your reason (optional)"
},
"receipt": "Bukti penerimaan",
"relayer": "Penyampai",
"verifyOnMycrypto": "Verifikasi tanda terima di MyCrypto",
"verifyOnSignatorio": "Verifikasi di Signator.io",
"isCore": "Inti",
"notificationsBlocked": "Browser Anda memblokir notifikasi",
"notificationsNotSupported": "Browser anda tidak mendukung pemberitahuan",
"walletNotSupported": "Wallet tidak didukung",
"seeInExplorer": "Lihat di explorer",
"learnMore": "Pelajari lebih lanjut",
"logout": "Keluar",
"continue": "Lanjut",
"add": "Tambah",
"edit": "Edit",
"strategyParameters": "Parameter strategi",
"addAction": "Tambahkan tindakan",
"removeAction": "Hapus tindakan",
"yourChoice": "Pilihan {0}",
"targetAddress": "Alamat tujuan",
"value": "Nilai",
"date": "Data",
"marketDetails": "Rincian pasar",
"addMarket": "Tambahkan pasar",
"selectNetwork": "Pilih jaringan",
"conditionId": "ID kondisi",
"basetokenAddress": "Alamat basis token",
"quoteAddress": "Kutip alamat mata uang",
"removeMarket": "Hapus pasar",
"back": "Kembali",
"loading": "Memuat...",
"predictedImpact": "Dampak yang diprediksi",
"marketSymbol": "{0} pasar",
"twoChoicesRequired": "Dua pilihan diperlukan untuk plugin ini.",
"noResultsFound": "Maaf, kami tidak dapat menemukan hasil apapun",
"createFirstProposal": "Mari buat proposal pertama anda",
"noSpacesJoined": "Ups, Anda belum bergabung dengan ruang apa pun",
"addFavorites": "Tambahkan favorit",
"createdBy": "Oleh {0}",
"startIn": "mulai {0}",
"endIn": "akhir {0}",
"proposalTimeLeft": "{0} tersisa",
"endedAgo": "berakhir {0}",
"proposalBy": "oleh {0}",
"endDate": "akhir {0}",
"defaultSkin": "Skin bawaan",
"select": "Pilih",
"language": "Bahasa",
"agree": "Saya setuju",
"moderators": "Moderators",
"playground": "Playground",
"strategyParams": "Parameter strategi",
"addresses": "Alamat",
"networkErrorPlayground": "Kesalahan jaringan - buka konsol browser Anda untuk informasi lebih lanjut",
"upload": "Unggah",
"join": "Bergabung",
"joined": "Tergabung",
"leave": "Keluar",
"subspaces": "Sub-spaces",
"mainspace": "Main space",
"copyLink": "Salin tautan",
"duplicate": "Duplicate",
"joinedSpaces": "Ruangan yang diikuti",
"joinSpaces": "Gabung ke ruang",
"setDelegationToSpace": "Batasi delegasi untuk ruang tertentu",
"theCurrentNetwork": "jaringan saat ini",
"optional": "(opsional)",
"homeLoadmore": "Muat lebih banyak",
"confirmAction": "Konfirmasi tindakan",
"or": "atau",
"share": "Bagikan",
"shareOnTwitter": "Share on Twitter",
"shareOnLenster": "Share on Lenster",
"createButton": "Create",
"discussion": "Discussion",
"changeWallet": "Change wallet",
"createASpace": "Create a space",
"getStarted": "Get started",
"skip": "Skip",
"newSpaceNotice": {
"header": "Your space is live!",
"mainText": "You can change how voting power is calculated via strategies in your {settings}. Changes to your settings will only affect new proposals, existing proposals can not be changed.",
"learnMore": "Learn more in the {documentation} or join Snapshot {discord} for help.",
"gotIt": "Got it!"
},
"errors": {
"required": "Baris diperlukan",
"minLength": "Field is required",
"maxLength": "Panjang maksimum adalah {0}",
"pattern": "Karakter tidak valid",
"minItems": "Minimal {0} item diperlukan",
"maxItems": "Maximum {0} item(s) allowed",
"minStrategy": "Setidaknya satu strategi diperlukan.",
"website": "URL should be in the format https://www.example.com",
"format": "Format tidak valid",
"type": "Tipe tidak valid",
"unsupportedImageType": "File type not supported, Supported formats are jpeg, jpg and png",
"invalidAddress": "Invalid address"
},
"create": {
"proposalTitle": "Title",
"discussion": "Discussion (optional)",
"categorie(s)": "Pilih lebih dari 2 kategori",
"proposalDescription": "Description (optional)",
"preview": "Pratinjau",
"choices": "Pilihan",
"addChoice": "Tambahkan pilihan",
"startDate": "Pilih tanggal mulai",
"endDate": "Pilih tanggal selesai",
"startTime": "Pilih waktu mulai",
"endTime": "Pilih waktu selesai",
"publish": "Publikasikan",
"untitled": "Tanpa judul",
"snapshotBlock": "Jumlah blok Snapshot",
"voting": "Voting",
"votingSystem": "Sistem voting",
"choice": "Pilihan {0}",
"period": "Periode voting",
"start": "Mulai",
"end": "Selesai",
"days": "Hari",
"hours": "Jam",
"minutes": "Menit",
"schedule": "Jadwal proposal",
"delayEnforced": "The space enforces a delay until voting can start",
"periodEnforced": "The space enforces the duration of the voting period",
"typeEnforced": "{type} is enforced by the space",
"privacyEnforced": "{type} is enforced by the space",
"edit": "Sunting",
"continue": "Lanjut",
"now": "Sekarang",
"votingPeriodExplainer": "Ini adalah periode waktu di mana pengguna dapat memilih. Proposal akan terlihat dan tertunda sebelum dimulainya periode pemungutan suara.",
"uploadImageExplainer": "Attach images by dragging & dropping, selecting or pasting them.",
"uploading": "Uploading image",
"markdown": "Styling with Markdown is supported",
"validationWarning": {
"basic": {
"member": "Anda harus menjadi penulis di ruangan ini untuk bisa menulis proposal.",
"minScore": "Anda harus memiliki setidaknya minimal {0} {1} untuk mengajukan sebuah proposal."
},
"customValidation": "Anda harus lulus validasi proposal untuk mengajukan proposal.",
"executionError": "Verifikasi hak anda untuk membuat proposal di ruang ini telah gagal. Ini mungkin disebabkan karna kesalahan konfigurasi strategi."
},
"errorGettingSnapshot": "We encountered an error while fetching the snapshot block number which is needed to calculate your voting power. Please try again later."
},
"delegate": {
"header": "Perwakilan",
"selectDelegate": "Pilih delegasi",
"to": "Kepada",
"addressPlaceholder": "Alamat atau nama ENS",
"delegations": "Perwakilan Anda(s)",
"allSpaces": "Untuk semua ruang",
"delegated": "Diwakilkan kepada Anda",
"pendingTransaction": "tidak ada transaksi yang tertunda | 1 transaksi yang tertunda | {count} transaksi yang tertunda",
"topDelegates": "Delegasi teratas",
"noDelegatesFoundFor": "Tidak ada delegasi yang ditemukan untuk {0}",
"noValidEns": "Bukan alamat ENS yang valid.",
"noValidAddress": "Bukan alamat yang valid",
"delegateToSelf": "Anda tidak bisa mendelegasikan diri sendiri",
"delegateToSelfAddress": "Anda tidak bisa mendelegasikan alamat ENS anda sendiri",
"noValidSpaceId": "Bukan ID ruang yang valid",
"noDelegationsAndDelegates": "Can't find your delegations and delegates? Make sure you are connected to the correct network.",
"delegateNotSupported": "Delegation is currently not supported for {network}."
},
"proposal": {
"castVote": "Berikan voting Anda",
"vote": "Voting",
"startDate": "Tanggal mulai",
"endDate": "Tanggal selesai",
"votingSystem": "Sistem ambil suara",
"privacy": "Privacy",
"invalidChoice": "Invalid choice",
"postVoteModal": {
"defaultTitle": "Your vote is in!",
"gnosisSafeTitle": "Your vote is pending...",
"gnosisSafeDescription": " Votes with a Safe require additional signers and will be visible once the transaction is confirmed",
"seeQueue": "See queued transactions",
"tips": {
"1": "Votes can be changed while the proposal is active"
}
}
},
"proposals": {
"header": "Proposal",
"new": "Proposal baru",
"noProposals": "Belum ada proposal di sini!",
"createProposal": "Buat proposal",
"showMore": "Tampilkan lebih banyak",
"showLess": "Tampilkan lebih ringkas",
"states": {
"all": "Semua",
"core": "Inti",
"community": "Komunitas",
"active": "Aktif",
"pending": "Tertunda",
"closed": "Ditutup"
}
},
"notifications": {
"header": "Pemberitahuan",
"noNotifications": "Anda tidak memiliki pemberitahuan baru",
"proposalStarted": "proposal sudah dimulai:",
"proposalEnded": "proposal telah selesai:",
"markAllAsRead": "Tandai semua sudah dibaca",
"all": "Semua",
"unread": "Belum dibaca"
},
"modalTerms": {
"mustAgreeTo": "To {action} this space, you must agree to the {spaceName} terms of service.",
"actionJoin": "join",
"actionCreate": "create a proposal in",
"actionVote": "vote in"
},
"settings": {
"header": "Pengaturan",
"editController": "Edit controller",
"connectWithSpaceOwner": "You are in view only mode, to modify space settings connect with a controller or admin wallet.",
"gnosisWrongNetwork": {
"base": "Your Gnosis Safe is on the wrong network. Please connect to {network} to {action}.",
"settings": "edit the space settings",
"create": "create a proposal",
"vote": "vote on this proposal"
},
"currentSpaceControllerIs": "The current space controller is {address}",
"newController": "New controller",
"noRecord": "No text-record found. Make sure you have registered {id} domain on {network}, then edit the controller text-record to regain access to the space settings.",
"set": "Set",
"profile": "Profil",
"avatar": "Avatar",
"name": {
"label": "Name",
"placeholder": "e.g. Yam Network"
},
"about": {
"label": "About",
"placeholder": "What is your organisation about?"
},
"categories": {
"label": "Categorie(s)",
"select": "Select categorie(s)"
},
"terms": {
"label": "Terms of service",
"information": "Users will be required to accept these terms once before they can create a proposal or cast a vote"
},
"hideSpace": "Sembunyikan ruang dari halaman beranda",
"links": "Social accounts",
"subspaces": {
"label": "Sub-spaces",
"information": "Sub-spaces will only be shown once configured on both the main space and the sub-space(s). {docs}",
"parent": {
"label": "Main space",
"placeholder": "pistachiodao.eth",
"information": "The space that this space is a sub-space of will be displayed on the space page"
},
"children": {
"label": "Sub-spaces",
"placeholder": "pistachiodao.eth",
"information": "Related Sub-spaces listed here will be displayed on the space page"
}
},
"website": "Website",
"strategies": {
"label": "Strategie(s)",
"information": "Strategies are used to determine voting power or whether a user is eligible to create a proposal"
},
"network": {
"label": "Network",
"information": "The defaul network used for this space. Networks can also be specified in individual strategies"
},
"symbol": {
"label": "Symbol",
"information": "The default symbol used for this space, usually the token symbol i.e. BAL for Balancer"
},
"strategiesList": "Select up to 8 strategies",
"votingPowerIsCumulative": "Voting power is cumulative",
"addStrategy": "Tambahkan strategi",
"testInPlayground": "Test in playground",
"admins": {
"label": "Admins",
"information": "Admins are able to modify the space settings and manage the space's proposals"
},
"authors": {
"label": "Authors",
"information": "Authors are always able to create proposals"
},
"proposalValidation": "Validasi proposal",
"validation": "Type",
"proposalThreshold": {
"label": "Threshold",
"information": "The minimum amount of voting power required to create a proposal"
},
"allowOnlyAuthors": "Izinkan hanya penulis yang bisa membuat proposal",
"editValidation": "Sunting validitas",
"selectValidation": "Pilih validitas",
"validationParameters": "Parameter validitas",
"voting": "Pemungutan suara",
"votingDelay": "Penundaan pemungutan suara",
"votingPeriod": "Periode pemungutan suara",
"hours": "Hours",
"days": "Days",
"quorum": {
"label": "Quorum",
"information": "The minimum amount of voting power required for the proposal to pass"
},
"type": {
"label": "Type",
"information": "The type of voting system used for this space. (Enforced on all future proposals)"
},
"anyType": "Apa saja",
"hideAbstain": "Abaikan suara abstain pada hasil pemungutan suara",
"customDomain": "Domain khusus",
"domain": {
"label": "Domain name",
"placeholder": "e.g. vote.balancer.fi",
"info": "To setup a custom domain you additionally need to open a pull request on github after you have created the space. {docs}"
},
"skin": "Skin",
"treasuries": {
"label": "Treasury",
"add": "Add treasury",
"edit": "Edit treasury",
"information": "Add treasuries of your organization to show them in your space"
},
"addPlugin": "Tambahkan plugin",
"editPlugin": "Edit plugin",
"pluginParameters": "Parameter plugin",
"proposal": {
"title": "Proposal",
"guidelines": {
"title": "Guidelines",
"information": "Display a link to your guidelines on proposal creation to help users understand what constitutes a good/valid proposal"
},
"template": {
"title": "Template",
"information": "Start every proposal with a template to help users understand what information is required"
}
}
},
"setup": {
"example": "misalnya yam.eth",
"chooseExistingEns": "Pilih satu domain ENS anda untuk membuat ruang:",
"useSingleExistingEns": "Pakai domain ENS anda:",
"orRegisterNewEns": "Atau daftarkan domain yang baru:",
"demoTestnetEnsMessage": "To create a test space you need an ENS domain on {network}.",
"toCreateASpace": "Untuk membuat ruang, anda harus punya ENS domain. Masukan satu dibawah dan ikuti instruksi pendaftaran ENS.",
"createASpace": "Buat sebuah ruang",
"registerEnsButton": "Daftar",
"supportedEnsTLDs": "Akhiran domain yang didukung",
"helpDocsAndDiscordLinks": "Not sure how to setup your space? Learn more in the {docs} or join Snapshot {discord}.",
"setSpaceController": "Space controller",
"setSpaceControllerExists": "The snapshot text-record for this domain has already been set. Choose edit to change it, otherwise you can skip to the next step.",
"setSpaceControllerInfo": " The space controller is the account that will be able to manage the space settings. Additional space controllers (admins) can be added later.",
"setSpaceControllerInfoGnosisSafe": "When creating a space with a Gnosis Safe it's recommended to set the safe address as the space controller. If you don't, you need to follow some additional steps. {link}",
"editSpaceController": "Edit controller on ENS",
"setController": "Set controller",
"explainControllerAndEns": "Setting the controller requires a transaction on the {network} which will add a \"snapshot\" TEXT record to your ENS domain.",
"confirmToSetAddress": "Are you sure you want to set {address} as the controller of the space?",
"controllerHasAuthority": "The controller has full authority over the space settings",
"controller": "Controller",
"selectEnsForSpace": "Choose ENS address",
"spaceOwnerAddressPlaceHolder": "e.g. {address}",
"controllerAddress": "Controller address",
"updateController": "Update controller",
"seeOnEns": "See on ENS",
"goToSettings": "Go to settings",
"setSpaceProfile": "Customize your space",
"waitForTransaction": "The transaction need to confirm before you can create your space. {txUrl}",
"pleaseWaitMessage": "This can take a few minutes, please wait while the transaction is being confirmed",
"notControllerAddress": "Please connect with the controller address {wallet} to create the space.",
"fillCurrentAccount": "Use currently logged in account",
"domain": {
"title": "Setup your space domain",
"ensMessage": " One thing you need before you can create your own space, is an ENS domain on Ethereum mainnet.",
"ensMessageTestnet": "You can also {link} on the Goerli testnet and mess with things there first.",
"tryDemo": "try the demo",
"yourExistingSpaces": "Your existing spaces",
"invalidEns": "This ENS name is invalid. Usually this is due the use of invalid characters during registration."
},
"strategy": {
"title": "How would you like to setup your voting strategy?",
"subtitle": "You can change your strategy settings any time.",
"blockTitle": "Setup voting strategy",
"onePersonOneVote": {
"title": "One person, one vote",
"description": "Manage a whitelist of people who can vote or simply allow any address to vote. Every vote is equal and no token is required",
"whitelistInformation": "Specify a number of accounts that can vote",
"ticketInformation": "Any account can vote",
"votesEqualInfo": "Each vote is equal and no token is required"
},
"tokenVoting": {
"title": "Token weighted voting",
"description": "Votes are weighted by a token. The token can be an ERC-20, ERC-721 or ERC-1155 token standard",
"tokenNotFound": "Token not found",
"seeOnEtherscan": "See on Etherscan"
},
"advanced": {
"title": "Custom setup",
"description": "Select up to 8 strategies with a wide range of options. If you can't find the right strategy for your use case, you can create your own"
}
},
"validationTitle": "Who can manage this space and create proposals?"
},
"profile": {
"buttonEdit": "Edit profile",
"viewProfile": "View profile",
"about": {
"header": "About",
"joinedSpaces": "Joined spaces",
"createdSpaces": "Created spaces",
"biography": "Bio",
"notJoinSpacesYet": "Hasn't joined any spaces yet",
"notCreatedSpacesYet": "Hasn't created any spaces yet",
"delegatorNetworkInfo": "Change by switching network in your wallet",
"delegate": "Delegate",
"delegated": "Delegated",
"delegateTo": "Delegate to",
"delegateFor": "Delegator for",
"noDelegatorsMessage": "No delegators on {network}",
"notSupportedNetwork": "Delegation currently isn't supported on {network} "
},
"activity": {
"header": "Activity",
"votedFor": "Voted {choice}",
"today": "Today",
"thisWeek": "This week",
"olderThanWeek": "Older than a week",
"noActivity": "No activity yet"
},
"settings": {
"header": "Edit profile",
"name": "Name",
"biography": "Bio",
"namePlaceholder": "Enter name",
"bioPlaceholder": "Tell your story",
"change": "Change",
"remove": "Remove"
}
},
"notify": {
"youDidIt": "Anda berhasil!",
"copied": "Tersalin!",
"proposalDeleted": "Proposal dihapus",
"somethingWentWrong": "Amboy, sesuatu kesalahan telah terjadi!",
"saved": "Disimpan!",
"delegationSuccess": "Delegasi berhasil",
"delegationRemoved": "Delegasi dihapus",
"proposalCreated": "Proposal dibuat",
"voteSuccessful": "Suara Anda telah masuk!",
"ensSet": "ENS text record was successfully set",
"transactionSent": "Transaction sent"
},
"explore": {
"createStrategy": "Buat strategi",
"createSkin": "Buat skin",
"addNetwork": "Tambahkan jaringan",
"createPlugin": "Buat plugin",
"strategies": "strategi",
"skins": "skin",
"networks": "jaringan",
"plugins": "plugin",
"results": "hasil",
"category": "Category",
"categories": {
"all": "Semua",
"protocol": "Protokol",
"social": "Sosial",
"investment": "Investasi",
"grant": "Izinkan",
"service": "Layanan",
"media": "Media",
"creator": "Pembuat",
"collector": "Kolektor"
}
},
"voting": {
"selectVoting": "Pilih sistem ambil suara",
"single-choice": "Ambil suara pilihan tunggal",
"approval": "Ambil suara persetujuan",
"quadratic": "Pemungutan suara kuadrat",
"ranked-choice": "Pilihan peringkat pemungutan suara",
"weighted": "Suara yang berbobot",
"basic": "Pemungutan suara dasar",
"description": {
"single-choice": "Setiap pemilih hanya boleh memilih satu pilihan.",
"approval": "Setiap pemilih dapat memilih sejumlah pilihan.",
"quadratic": "Setiap pemilih dapat menyebarkan kekuatan suara di sejumlah pilihan. Hasil dihitung secara kuadrat.",
"ranked-choice": "Setiap pemilih dapat memilih dan memberi peringkat ke sejumlah pilihan. Hasil dihitung dengan metode instant-runoff.",
"weighted": "Setiap pemilih dapat menyebarkan hak suara di sejumlah pilihan.",
"basic": "Voting pilihan tunggal dengan tiga pilihan: Untuk, Menentang atau Abstain"
}
},
"privacy": {
"label": "Privacy",
"title": "Select voting privacy",
"information": "The type of privacy used on proposals. (Enforced on all future proposals)",
"any": "Any",
"none": "None",
"shutter": {
"label": "Shutter",
"description": "Choices are encrypted and only visible once the voting period is over",
"tooltip": "This proposal has Shutter privacy enabled. All votes will be encrypted until the voting period has ended and the final score is calculated",
"url": "https://blog.shutter.network/shielded-voting/"
}
},
"validation": {
"label": "Validation",
"title": "Select voting validation",
"information": "The type of validation used to determine if a user can vote. (Enforced on all future proposals)",
"any": {
"label": "Anyone can vote",
"description": "Anyone with voting power can cast a vote."
},
"basic": {
"label": "Basic",
"description": "Use any strategy to determine if a user can vote.",
"invalidMessage": "You do not meet the minimum balance requirement to vote on this proposal. "
},
"passport-gated": {
"label": "Gitcoin Passport gated",
"description": "Protect your proposals from spam and vote manipulation by requiring users to have a Gitcoin Passport.",
"invalidMessage": "You need a Gitcoin Passport with score above {scoreThreshold} and {operator} of the following stamps to vote on this proposal: {stamps}. "
}
},
"safeSnap": {
"currentOutcome": "Hasil saat ini",
"currentBond": "Obligasi saat ini",
"finalizedIn": "Selesai{0}",
"executableIn": "Bisa dilaksanakan/dijalankan {0}",
"finalOutcome": "Hasil",
"nextBond": "Obligasikan untuk mendapatkan hasil",
"setOutcomeTo": "Tetapkan hasil ke",
"claimBond": "Klaim obligasi",
"addBatch": "Tambahkan transaksi batch",
"batch": "Transaksi batch",
"transactions": "Transaksi",
"to": "To (address)",
"invalidAddress": "Alamat tidak valid",
"invalidAmount": "Jumlah tidak valid",
"invalidValue": "Nilai tidak valid",
"invalidAbi": "ABI tidak valid",
"invalidData": "Data tidak valid",
"value": "Nilai (wei)",
"data": "Data",
"noCollectibles": "Tidak ada koleksi",
"asset": "Aset",
"amount": "Jumlah",
"type": "Tipe",
"transferFunds": "Transfer dana",
"transferNFT": "Transfer NFT",
"contractInteraction": "Interaksi kontrak",
"rawTransaction": "Transaksi mentah",
"addTransaction": "Tambahkan transaksi",
"transactionLabels": {
"contractInteraction": "{functionName}() - {amount} wei ke {address}",
"transferFunds": "Transfer {amount} {tokenSymbol} ke {address}",
"transferNFT": "Kirim {name} #{id} ke {address}",
"raw": "Kirim {amount} wei ke {address}"
},
"labels": {
"request": "Minta eksekusi",
"setOutcome": "Tetapkan hasil",
"changeOutcome": "Ubah hasil",
"executeTxs": "Jalankan transaksi batch {0} dari {1}",
"executed": "Semua transaksi telah dilaksanakan",
"noTransactions": "There are no transactions to execute",
"rejected": "Proposal rejected",
"error": "Terjadi suatu kesalahan",
"connectWallet": "Hubungkan dompet untuk melihat detail pelaksanaan",
"switchChain": "Alihkan dompet Anda ke {0} untuk mengajukan pelaksanaan",
"question": "Apaka proposal ini lolos dan sesuai dengan",
"criteria": "kriteria penerimaan?",
"proposalPassed": "Did the proposal pass?",
"expired": "The proposal has expired",
"approveBond": "Approve bond",
"confirmVoteResults": "Click to confirm the proposal passed",
"executeTxsUma": "Execute transaction batch",
"deleteDisputedProposal": "Delete disputed proposal",
"confirmVoteResultsToolTip": "Make sure this proposal was approved by this Snapshot vote before proposing on-chain. If the Snapshot vote rejected the proposal, the on-chain proposal will be rejected as well and you will lose your bond.",
"approveBondToolTip": "On-chain proposals require a bond from the proposer. This will approve tokens from your wallet to be posted as a bond. If you make an invalid proposal, it will be disputed and you will lose your bond. If the proposal is valid, your bond will be returned when the transactions are executed.",
"requestToolTip": "This will propose the transactions from this Snapshot vote on-chain. After a challenge window, if the proposal is valid, the transactions can be executed and your bond will be returned.",
"executeToolTip": "This will execute the transactions from this proposal and return the proposer's bond."
}
},
"poap": {
"no_poap_header": "POAP belum dipersiapkan untuk proposal ini :'(",
"no_voted_header": "Pilih untuk menerima POAP",
"unclaimed_header": "Klaim POAP voting anda",
"claimed_header": "Selamat! POAP ini telah ditambahkan ke koleksi anda",
"loading_header": "POAP ini sedang ditambahkan ke koleksi anda",
"button_claim": "Mint",
"button_show": "Jelajahi koleksi",
"success_claim": "POAP ini telah ditambahkan ke koleksi anda",
"error_claim": "Ada masalah dalam pencetakaan token"
},
"progress": {
"progress": "Progress",
"inProgress": "In Progress",
"completed": "Completed",
"complete": "Complete",
"newStep": "New Step",
"description": "Description",
"add": "Add",
"deleteStep": "Delete Step",
"deleteConfirm": "Are you sure you want to delete?",
"delete": "Delete",
"cancel": "Cancel",
"comeBack": "Come back after voting is complete to see how this proposal is progressing!",
"confirmSignature": "Signing this message will allow us to authorize your request to update the progress of your proposal.",
"wentWrong": "Oops something went wrong.",
"voting": "Voting",
"soon": "Soon"
},
"charts": {
"charts": "Grafik",
"noVotesYet": "Belum ada pemilih untuk ditampilkan.",
"totalVotesPerDay": "Total pemilih per hari",
"shareOfVotingPower": "Kekuatan voting",
"votingPowerPerDay": "Kekuatan voting per hari"
},
"comment_box": {
"title": "Kotak komentar",
"add": "Tambahkan komentar Anda di sini",
"submit": "Kirimkan",
"preview": "Pratinjau",
"continue_editing": "Lanjutkan mengedit",
"edit": "Edit balasan Anda di sini",
"edit_button": "Edit",
"dismiss": "Abaikan",
"delete": "Hapus",
"add_reply": "Tambahkan balasan Anda di sini",
"edit_comment": "Edit komentar",
"edit_modal": "Apakah anda yakin ingin mengedit?",
"yes": "Ya",
"no": "Tidak",
"delete_comment": "Hapus komentar",
"delete_modal": "Apakah anda yakin ingin menghapus?",
"error": "Ups! Ada yang tidak beres",
"replies": "balasan",
"hide": "Sembunyikan",
"show": "Tampilkan",
"reply": "Balas",
"load_more": "Muat lebih banyak"
},
"page": {
"title": {
"home": "Snapshot",
"setup": "Buat sebuah ruang",
"timeline": "Lini masa",
"notifications": "Notifications",
"explore": "Telusuri",
"playground": "Playground",
"space": {
"create": "Buat proposal {space}",
"about": "Tentang {space}",
"proposals": "Proposal {space}",
"proposal": "Proposal {space}: {proposal}",
"settings": "{space} Pengaturan"
},
"strategy": "Strategi {key}",
"delegate": "Delegasikan",
"ranking": "Ranking"
}
},
"hal": {
"title": "Lacak proposal untuk {spaceName}",
"text": "Dapatkan pemeberitahuan setiap propoaal baru dibuat atau berakhir"
},
"timeUnits": {
"second": "1 second | {n} seconds",
"minute": "1 minute | {n} minutes",
"hour": "1 hour | {n} hours",
"day": "1 day | {n} days",
"week": "1 week | {n} weeks",
"month": "1 month | {n} months",
"year": "1 year | {n} years"
},
"unsupportedNetwork": {
"unsupportedNetwork": "Unsupported network",
"switchNetworkToNetwork": "To continue, you need to change the network in your wallet to {network}.",
"switchToNetwork": "Switch to {network}",
"goToDemoSite": "Go to demo website"
},
"treasury": {
"title": "Treasury",
"wallets": {
"title": "Wallets",
"empty": "This space doesn't have a treasury yet",
"addTreasury": "Add a treasury"
},
"assets": {
"title": "Assets",
"empty": "There are no assets in this contract"
},
"24hChange": "24h change"
},
"newsletter": {
"yourEmail": "Your email",
"title": "Get the latest Snapshot updates"
},
"joinCommunity": "Join Snapshot community",
"header": {
"title": "Where decisions get made",
"description": "Snapshot is a free, open-source platform for community governance. Create your own space now and start making decisions!"
},
"aboutPage": {
"description": "Snapshot is a decentralized governance platform that makes it easy to create and vote on proposals - all without spending a fortune on gas fees! Plus, our flexible system supports various voting types and strategies, so you can tailor the voting process to your needs.",
"subHeader": "Governance should be a snap",
"subDescription": "Web3 governance doesn't have to be complicated. Snapshot is the perfect solution for organizations looking for an easy and efficient way to govern their community or organization."
},
"footerView": {
"resources": "Resources",
"about": "About",
"blog": "Blog",
"jobs": "Jobs",
"discussions": "Discussions",
"github": "GitHub",
"docs": "Docs",
"support": "Support",
"hiring": "Join us!"
}
}
================================================
FILE: src/locales/it-IT.json
================================================
{
"searchPlaceholder": "Cerca",
"spaceCount": "{0} spazio(i)",
"createSpace": "Crea spazi",
"backToHome": "Inizio",
"actions": "Azioni",
"poweredBy": "Powered by",
"results": "Risultati",
"resultsError": "Results could not be calculated. This is often due to a misconfigured strategy or an unresponsive RPC node involved in the strategy.",
"resultsCalculating": "Final results are being calculated. If you still see this message after a few minutes contact the space admin.",
"votingPowerFailedMessage": "Your voting power could not be calculated. This is often due to a misconfigured strategy or an unresponsive RPC node involved in the strategy.",
"votingValidationFailedMessage": "There was an error on our side and we could not verify if you are eligible to vote. This is often due to a misconfigured voting validation or an unresponsive API involved in the validation.",
"notValidVoterMessage": "Oops, you don't seem to be eligible to vote on this proposal.",
"getHelp": "Get help",
"retry": "Retry",
"currentResults": "Risultati Correnti",
"reset": "Ripristina predefiniti",
"close": "Close",
"save": "Salva",
"author": "Autore",
"next": "Avanti >",
"choice": "Choice",
"submit": "Invia",
"plugins": "Plugin",
"information": "Informazioni",
"confirm": "Conferma",
"snapshot": "Snapshot",
"strategies": "Strategie",
"strategiesPage": "Strategie",
"space": "Spazio",
"spaces": "Spazi",
"verifiedSpace": "Verified space",
"warningSpace": "This space has been flagged as potentially malicious. Proceed with caution.",
"version": "Versione",
"timeline": "Cronologia",
"ended": "ended",
"started": "started",
"filters": "Filtri",
"allSpaces": "Tutti gli spazi",
"submitOnchain": "Invia on-chain",
"inSpaces": "In {0} spazio(i)",
"votes": "Voti",
"seeMore": "Mostra altro",
"seeAll": "See all",
"network": "Rete",
"networks": "Reti",
"skins": "Temi",
"spaceMembers": "Members",
"members": "Nessun membro | {count} membro | {count} membri",
"editStrategy": "Modifica strategia",
"invalidProposals": "Proposta non valida",
"account": "Profilo",
"create3box": "Crea profilo su 3Box",
"view3box": "Visualizza il profilo su 3Box",
"edit3box": "Modifica profilo su 3Box",
"connectWallet": "Connetti wallet",
"toggleSkin": "Toggle skin",
"about": "Chi siamo",
"license": "Licenza",
"showMore": "Show more",
"voted": "Voted",
"reload": "Reload",
"ipfsServer": "Server IPFS",
"hub": "Hub",
"cancel": "Annulla",
"delete": "Delete",
"demoSite": "Questo è il sito demo, fai una prova!",
"removeDelegation": "Rimuovi delega",
"confirmRemove": "Sei sicuro di voler rimuovere la delega a",
"removeSpace": "per lo spazio {0}",
"noVotingPower": "Oops, it seems you don't have any voting power at block {blockNumber}.",
"quorumReached": "quorum reached",
"options": "Opzioni",
"votingPower": "Il tuo potere di voto",
"comment": {
"placeholder": "Share your reason (optional)"
},
"receipt": "Ricevuta",
"relayer": "Relè",
"verifyOnMycrypto": "Verifica ricevuta su MyCrypto",
"verifyOnSignatorio": "Verificati su Signator.io",
"isCore": "Core",
"notificationsBlocked": "Your browser is blocking notifications",
"notificationsNotSupported": "Your browser does not support notifications",
"walletNotSupported": "Wallet is not supported",
"seeInExplorer": "See explorer",
"learnMore": "Scopri di più",
"logout": "Disconnetti",
"continue": "Continua",
"add": "Aggiungi",
"edit": "Modifica",
"strategyParameters": "Parametri strategia",
"addAction": "Aggiungi un'azione",
"removeAction": "Rimuovi azione",
"yourChoice": "Scelta {0}",
"targetAddress": "Indirizzo del destinatario",
"value": "Valore",
"date": "Dati",
"marketDetails": "Dettagli di mercato",
"addMarket": "Aggiungi mercato",
"selectNetwork": "Seleziona la rete",
"conditionId": "Condizione ID",
"basetokenAddress": "Indirizzo del token di base",
"quoteAddress": "Indirizzo valuta citata",
"removeMarket": "Rimuovi mercato",
"back": "Indietro",
"loading": "Caricamento...",
"predictedImpact": "Impatto previsto",
"marketSymbol": "{0} mercati",
"twoChoicesRequired": "Per questo plugin sono necessarie due scelte.",
"noResultsFound": "Oops, non riusciamo a trovare alcun risultato",
"createFirstProposal": "Creiamo la tua prima proposta",
"noSpacesJoined": "Spiacenti, non ti sei ancora iscritto a nessuno spazio",
"addFavorites": "Aggiungi preferiti",
"createdBy": "Da {0}",
"startIn": "inizio {0}",
"endIn": "fine {0}",
"proposalTimeLeft": "{0} left",
"endedAgo": "ended {0}",
"proposalBy": "di {0}",
"endDate": "termina {0}",
"defaultSkin": "Tema predefinito",
"select": "Seleziona",
"language": "Lingua",
"agree": "Accetto",
"moderators": "Moderators",
"playground": "Playground",
"strategyParams": "Parametri strategia",
"addresses": "Indirizzi",
"networkErrorPlayground": "Errore di rete: aprire la console del browser per ulteriori informazioni",
"upload": "Caricare",
"join": "Entra",
"joined": "Iscritto",
"leave": "Abbandona",
"subspaces": "Sub-spaces",
"mainspace": "Main space",
"copyLink": "Copia link",
"duplicate": "Duplicate",
"joinedSpaces": "Iscritto a uno spazio",
"joinSpaces": "Iscritto a uno spazio",
"setDelegationToSpace": "Limit delegation to a specific space",
"theCurrentNetwork": "the current network",
"optional": "(optional)",
"homeLoadmore": "Load more",
"confirmAction": "Confirm action",
"or": "or",
"share": "Share",
"shareOnTwitter": "Share on Twitter",
"shareOnLenster": "Share on Lenster",
"createButton": "Create",
"discussion": "Discussion",
"changeWallet": "Change wallet",
"createASpace": "Create a space",
"getStarted": "Get started",
"skip": "Skip",
"newSpaceNotice": {
"header": "Your space is live!",
"mainText": "You can change how voting power is calculated via strategies in your {settings}. Changes to your settings will only affect new proposals, existing proposals can not be changed.",
"learnMore": "Learn more in the {documentation} or join Snapshot {discord} for help.",
"gotIt": "Got it!"
},
"errors": {
"required": "Campo necessario",
"minLength": "Field is required",
"maxLength": "La lunghezza massima è {0}",
"pattern": "Carattere non valido",
"minItems": "Minimo {0} elementi richiesti",
"maxItems": "Maximum {0} item(s) allowed",
"minStrategy": "È necessaria almeno una strategia.",
"website": "URL should be in the format https://www.example.com",
"format": "Formato non valido",
"type": "Tipologia non valida",
"unsupportedImageType": "File type not supported, Supported formats are jpeg, jpg and png",
"invalidAddress": "Invalid address"
},
"create": {
"proposalTitle": "Title",
"discussion": "Discussion (optional)",
"categorie(s)": "Select up to 2 categorie(s)",
"proposalDescription": "Description (optional)",
"preview": "Anteprima",
"choices": "Scelte",
"addChoice": "Aggiungi scelta",
"startDate": "Seleziona una data di inizio",
"endDate": "Selezionare la data di fine",
"startTime": "Selezionare un orario di inizio",
"endTime": "Selezionare un orario di conclusione",
"publish": "Pubblica",
"untitled": "Untitled",
"snapshotBlock": "Numero blocco istantanea",
"voting": "Voting",
"votingSystem": "Voting system",
"choice": "Choice {0}",
"period": "Voting period",
"start": "Start",
"end": "End",
"days": "Days",
"hours": "Hours",
"minutes": "Minutes",
"schedule": "Schedule proposal",
"delayEnforced": "The space enforces a delay until voting can start",
"periodEnforced": "The space enforces the duration of the voting period",
"typeEnforced": "{type} is enforced by the space",
"privacyEnforced": "{type} is enforced by the space",
"edit": "Edit",
"continue": "Continue",
"now": "Now",
"votingPeriodExplainer": "This is the time period in which users will be able to vote. The proposal will be visible and pending before the start of the voting period.",
"uploadImageExplainer": "Attach images by dragging & dropping, selecting or pasting them.",
"uploading": "Uploading image",
"markdown": "Styling with Markdown is supported",
"validationWarning": {
"basic": {
"member": "Devi essere un membro dello spazio per inviare una proposta.",
"minScore": "Devi avere un minimo di {0} {1} per inviare una proposta."
},
"customValidation": "È necessario superare la convalida della proposta per inviare una proposta.",
"executionError": "Verifying your eligibility to create proposals in this space has failed. This is likely due to a misconfigured strategy."
},
"errorGettingSnapshot": "We encountered an error while fetching the snapshot block number which is needed to calculate your voting power. Please try again later."
},
"delegate": {
"header": "Delega",
"selectDelegate": "Seleziona una delega",
"to": "To",
"addressPlaceholder": "Indirizzo o nome ENS",
"delegations": "Le tue deleghe",
"allSpaces": "Per tutti gli spazi",
"delegated": "Delegate a te",
"pendingTransaction": "nessuna transazione in sospeso |1 transazione in attesa {count} transazioni in attesa",
"topDelegates": "Migliori delegati",
"noDelegatesFoundFor": "No delegates found for {0}",
"noValidEns": "Not a valid ENS address.",
"noValidAddress": "Not a valid address",
"delegateToSelf": "You cannot delegate to yourself",
"delegateToSelfAddress": "You cannot delegate to your own ENS address",
"noValidSpaceId": "Not a valid space ID",
"noDelegationsAndDelegates": "Can't find your delegations and delegates? Make sure you are connected to the correct network.",
"delegateNotSupported": "Delegation is currently not supported for {network}."
},
"proposal": {
"castVote": "Esprimi il tuo voto",
"vote": "Vota",
"startDate": "Data di inizio",
"endDate": "Data di fine",
"votingSystem": "Meccanismo di voto:",
"privacy": "Privacy",
"invalidChoice": "Invalid choice",
"postVoteModal": {
"defaultTitle": "Your vote is in!",
"gnosisSafeTitle": "Your vote is pending...",
"gnosisSafeDescription": " Votes with a Safe require additional signers and will be visible once the transaction is confirmed",
"seeQueue": "See queued transactions",
"tips": {
"1": "Votes can be changed while the proposal is active"
}
}
},
"proposals": {
"header": "Proposte",
"new": "Nuova proposta",
"noProposals": "Non ci sono ancora proposte qui!",
"createProposal": "Crea proposta",
"showMore": "Show more",
"showLess": "Show less",
"states": {
"all": "Tutte",
"core": "Core",
"community": "Community",
"active": "Attive",
"pending": "In sospeso",
"closed": "Chiuse"
}
},
"notifications": {
"header": "Notifications",
"noNotifications": "You have no notifications",
"proposalStarted": "proposal has started:",
"proposalEnded": "proposal has ended:",
"markAllAsRead": "Mark all as read",
"all": "All",
"unread": "Unread"
},
"modalTerms": {
"mustAgreeTo": "To {action} this space, you must agree to the {spaceName} terms of service.",
"actionJoin": "join",
"actionCreate": "create a proposal in",
"actionVote": "vote in"
},
"settings": {
"header": "Impostazioni",
"editController": "Edit controller",
"connectWithSpaceOwner": "You are in view only mode, to modify space settings connect with a controller or admin wallet.",
"gnosisWrongNetwork": {
"base": "Your Gnosis Safe is on the wrong network. Please connect to {network} to {action}.",
"settings": "edit the space settings",
"create": "create a proposal",
"vote": "vote on this proposal"
},
"currentSpaceControllerIs": "The current space controller is {address}",
"newController": "New controller",
"noRecord": "No text-record found. Make sure you have registered {id} domain on {network}, then edit the controller text-record to regain access to the space settings.",
"set": "Set",
"profile": "Profilo",
"avatar": "Avatar",
"name": {
"label": "Name",
"placeholder": "e.g. Yam Network"
},
"about": {
"label": "About",
"placeholder": "What is your organisation about?"
},
"categories": {
"label": "Categorie(s)",
"select": "Select categorie(s)"
},
"terms": {
"label": "Terms of service",
"information": "Users will be required to accept these terms once before they can create a proposal or cast a vote"
},
"hideSpace": "Nascondi lo spazio dalla pagina iniziale",
"links": "Social accounts",
"subspaces": {
"label": "Sub-spaces",
"information": "Sub-spaces will only be shown once configured on both the main space and the sub-space(s). {docs}",
"parent": {
"label": "Main space",
"placeholder": "pistachiodao.eth",
"information": "The space that this space is a sub-space of will be displayed on the space page"
},
"children": {
"label": "Sub-spaces",
"placeholder": "pistachiodao.eth",
"information": "Related Sub-spaces listed here will be displayed on the space page"
}
},
"website": "Website",
"strategies": {
"label": "Strategie(s)",
"information": "Strategies are used to determine voting power or whether a user is eligible to create a proposal"
},
"network": {
"label": "Network",
"information": "The defaul network used for this space. Networks can also be specified in individual strategies"
},
"symbol": {
"label": "Symbol",
"information": "The default symbol used for this space, usually the token symbol i.e. BAL for Balancer"
},
"strategiesList": "Select up to 8 strategies",
"votingPowerIsCumulative": "Voting power is cumulative",
"addStrategy": "Aggiungi strategia",
"testInPlayground": "Test in playground",
"admins": {
"label": "Admins",
"information": "Admins are able to modify the space settings and manage the space's proposals"
},
"authors": {
"label": "Authors",
"information": "Authors are always able to create proposals"
},
"proposalValidation": "Validazione proposta",
"validation": "Type",
"proposalThreshold": {
"label": "Threshold",
"information": "The minimum amount of voting power required to create a proposal"
},
"allowOnlyAuthors": "Permetti solo ai membri di presentare una proposta",
"editValidation": "Modifica validatore",
"selectValidation": "Seleziona validazione",
"validationParameters": "Parametri di validazione",
"voting": "Voti",
"votingDelay": "Ritardo delle votazioni",
"votingPeriod": "Periodo di votazione",
"hours": "Hours",
"days": "Days",
"quorum": {
"label": "Quorum",
"information": "The minimum amount of voting power required for the proposal to pass"
},
"type": {
"label": "Type",
"information": "The type of voting system used for this space. (Enforced on all future proposals)"
},
"anyType": "Qualunque",
"hideAbstain": "Ignore abstain votes in basic voting results",
"customDomain": "Dominio personalizzato",
"domain": {
"label": "Domain name",
"placeholder": "e.g. vote.balancer.fi",
"info": "To setup a custom domain you additionally need to open a pull request on github after you have created the space. {docs}"
},
"skin": "Tema",
"treasuries": {
"label": "Treasury",
"add": "Add treasury",
"edit": "Edit treasury",
"information": "Add treasuries of your organization to show them in your space"
},
"addPlugin": "Aggiungi plugin",
"editPlugin": "Modifica plugin",
"pluginParameters": "Parametri plugin",
"proposal": {
"title": "Proposal",
"guidelines": {
"title": "Guidelines",
"information": "Display a link to your guidelines on proposal creation to help users understand what constitutes a good/valid proposal"
},
"template": {
"title": "Template",
"information": "Start every proposal with a template to help users understand what information is required"
}
}
},
"setup": {
"example": "e.g. yam.eth",
"chooseExistingEns": "Choose one of your existing ENS domains to create a space with:",
"useSingleExistingEns": "Use your existing ENS domain:",
"orRegisterNewEns": "Or register a new domain:",
"demoTestnetEnsMessage": "To create a test space you need an ENS domain on {network}.",
"toCreateASpace": "To create a space, you first need an ENS domain. Enter one below and follow the ENS registration instructions.",
"createASpace": "Crea uno spazio",
"registerEnsButton": "Register",
"supportedEnsTLDs": "Supported domain endings",
"helpDocsAndDiscordLinks": "Not sure how to setup your space? Learn more in the {docs} or join Snapshot {discord}.",
"setSpaceController": "Space controller",
"setSpaceControllerExists": "The snapshot text-record for this domain has already been set. Choose edit to change it, otherwise you can skip to the next step.",
"setSpaceControllerInfo": " The space controller is the account that will be able to manage the space settings. Additional space controllers (admins) can be added later.",
"setSpaceControllerInfoGnosisSafe": "When creating a space with a Gnosis Safe it's recommended to set the safe address as the space controller. If you don't, you need to follow some additional steps. {link}",
"editSpaceController": "Edit controller on ENS",
"setController": "Set controller",
"explainControllerAndEns": "Setting the controller requires a transaction on the {network} which will add a \"snapshot\" TEXT record to your ENS domain.",
"confirmToSetAddress": "Are you sure you want to set {address} as the controller of the space?",
"controllerHasAuthority": "The controller has full authority over the space settings",
"controller": "Controller",
"selectEnsForSpace": "Choose ENS address",
"spaceOwnerAddressPlaceHolder": "e.g. {address}",
"controllerAddress": "Controller address",
"updateController": "Update controller",
"seeOnEns": "See on ENS",
"goToSettings": "Go to settings",
"setSpaceProfile": "Customize your space",
"waitForTransaction": "The transaction need to confirm before you can create your space. {txUrl}",
"pleaseWaitMessage": "This can take a few minutes, please wait while the transaction is being confirmed",
"notControllerAddress": "Please connect with the controller address {wallet} to create the space.",
"fillCurrentAccount": "Use currently logged in account",
"domain": {
"title": "Setup your space domain",
"ensMessage": " One thing you need before you can create your own space, is an ENS domain on Ethereum mainnet.",
"ensMessageTestnet": "You can also {link} on the Goerli testnet and mess with things there first.",
"tryDemo": "try the demo",
"yourExistingSpaces": "Your existing spaces",
"invalidEns": "This ENS name is invalid. Usually this is due the use of invalid characters during registration."
},
"strategy": {
"title": "How would you like to setup your voting strategy?",
"subtitle": "You can change your strategy settings any time.",
"blockTitle": "Setup voting strategy",
"onePersonOneVote": {
"title": "One person, one vote",
"description": "Manage a whitelist of people who can vote or simply allow any address to vote. Every vote is equal and no token is required",
"whitelistInformation": "Specify a number of accounts that can vote",
"ticketInformation": "Any account can vote",
"votesEqualInfo": "Each vote is equal and no token is required"
},
"tokenVoting": {
"title": "Token weighted voting",
"description": "Votes are weighted by a token. The token can be an ERC-20, ERC-721 or ERC-1155 token standard",
"tokenNotFound": "Token not found",
"seeOnEtherscan": "See on Etherscan"
},
"advanced": {
"title": "Custom setup",
"description": "Select up to 8 strategies with a wide range of options. If you can't find the right strategy for your use case, you can create your own"
}
},
"validationTitle": "Who can manage this space and create proposals?"
},
"profile": {
"buttonEdit": "Edit profile",
"viewProfile": "View profile",
"about": {
"header": "About",
"joinedSpaces": "Joined spaces",
"createdSpaces": "Created spaces",
"biography": "Bio",
"notJoinSpacesYet": "Hasn't joined any spaces yet",
"notCreatedSpacesYet": "Hasn't created any spaces yet",
"delegatorNetworkInfo": "Change by switching network in your wallet",
"delegate": "Delegate",
"delegated": "Delegated",
"delegateTo": "Delegate to",
"delegateFor": "Delegator for",
"noDelegatorsMessage": "No delegators on {network}",
"notSupportedNetwork": "Delegation currently isn't supported on {network} "
},
"activity": {
"header": "Activity",
"votedFor": "Voted {choice}",
"today": "Today",
"thisWeek": "This week",
"olderThanWeek": "Older than a week",
"noActivity": "No activity yet"
},
"settings": {
"header": "Edit profile",
"name": "Name",
"biography": "Bio",
"namePlaceholder": "Enter name",
"bioPlaceholder": "Tell your story",
"change": "Change",
"remove": "Remove"
}
},
"notify": {
"youDidIt": "Hai finito!",
"copied": "Copiato!",
"proposalDeleted": "Proposta eliminata",
"somethingWentWrong": "Oops, qualcosa è andato storto!",
"saved": "Salvato",
"delegationSuccess": "Delega riuscita",
"delegationRemoved": "Delega rimossa",
"proposalCreated": "Proposta creata",
"voteSuccessful": "Il tuo voto è dentro!",
"ensSet": "ENS text record was successfully set",
"transactionSent": "Transaction sent"
},
"explore": {
"createStrategy": "Creare strategia",
"createSkin": "Creare skin",
"addNetwork": "Aggiungi rete",
"createPlugin": "Crea Plugin",
"strategies": "strategie",
"skins": "skin(s)",
"networks": "rete(e)",
"plugins": "plugin(s)",
"results": "risultato/i",
"category": "Category",
"categories": {
"all": "All",
"protocol": "Protocol",
"social": "Social",
"investment": "Investment",
"grant": "Grant",
"service": "Service",
"media": "Media",
"creator": "Creator",
"collector": "Collector"
}
},
"voting": {
"selectVoting": "Seleziona sistema di voto",
"single-choice": "Voto a scelta unica",
"approval": "Voto di approvazione",
"quadratic": "Votazione quadratica",
"ranked-choice": "Voto di scelta classificato",
"weighted": "Votazioni ponderate",
"basic": "Votazione di base",
"description": {
"single-choice": "Ogni elettore può selezionare una sola opzione.",
"approval": "Ogni elettore può selezionare un numero qualsiasi di scelte.",
"quadratic": "Ogni elettore può distribuire il potere di voto su un numero qualsiasi di scelte. I risultati sono calcolati in termini quadratici.",
"ranked-choice": "Ogni elettore può selezionare e classificare un numero qualsiasi di scelte. I risultati sono calcolati con il metodo di conteggio del runoff istantaneo.",
"weighted": "Ogni elettore può distribuire il potere di voto su qualsiasi numero di scelte.",
"basic": "Voto a scelta singola con tre scelte: Per, Contro o Astenuto\n"
}
},
"privacy": {
"label": "Privacy",
"title": "Select voting privacy",
"information": "The type of privacy used on proposals. (Enforced on all future proposals)",
"any": "Any",
"none": "None",
"shutter": {
"label": "Shutter",
"description": "Choices are encrypted and only visible once the voting period is over",
"tooltip": "This proposal has Shutter privacy enabled. All votes will be encrypted until the voting period has ended and the final score is calculated",
"url": "https://blog.shutter.network/shielded-voting/"
}
},
"validation": {
"label": "Validation",
"title": "Select voting validation",
"information": "The type of validation used to determine if a user can vote. (Enforced on all future proposals)",
"any": {
"label": "Anyone can vote",
"description": "Anyone with voting power can cast a vote."
},
"basic": {
"label": "Basic",
"description": "Use any strategy to determine if a user can vote.",
"invalidMessage": "You do not meet the minimum balance requirement to vote on this proposal. "
},
"passport-gated": {
"label": "Gitcoin Passport gated",
"description": "Protect your proposals from spam and vote manipulation by requiring users to have a Gitcoin Passport.",
"invalidMessage": "You need a Gitcoin Passport with score above {scoreThreshold} and {operator} of the following stamps to vote on this proposal: {stamps}. "
}
},
"safeSnap": {
"currentOutcome": "Risultato attuale",
"currentBond": "Titolo attuale",
"finalizedIn": "Finalizzata {0}",
"executableIn": "Eseguibile {0}",
"finalOutcome": "Esito",
"nextBond": "Legame da impostare",
"setOutcomeTo": "Imposta il risultato a",
"claimBond": "Rivendica obbligazione",
"addBatch": "Aggiungere una linea di transazione",
"batch": "Gruppo di transazioni",
"transactions": "Transazioni",
"to": "To (address)",
"invalidAddress": "Indirizzo non valido",
"invalidAmount": "Importo non valido",
"invalidValue": "Valore errato",
"invalidAbi": "Abi Non Valido",
"invalidData": "Dati non validi",
"value": "Valore (wei)",
"data": "Dati",
"noCollectibles": "Ancora nessuna raccolta",
"asset": "Risorsa",
"amount": "Importo",
"type": "Tipo",
"transferFunds": "Trasferisci fondi",
"transferNFT": "Trasferisci NFT",
"contractInteraction": "Interazione contrattuale",
"rawTransaction": "Transazione",
"addTransaction": "Aggiungi transazione",
"transactionLabels": {
"contractInteraction": "{functionName}() - {amount} wei a {address}",
"transferFunds": "Trasferisci {amount} {tokenSymbol} a {address}",
"transferNFT": "Invia {name} #{id} a {address}",
"raw": "Invia {name} #{id} a {address}"
},
"labels": {
"request": "Richiesta esecuzione",
"setOutcome": "Imposta il risultato a",
"changeOutcome": "Cambiare risultato",
"executeTxs": "Esegui il batch di transazione {0} di {1}",
"executed": "Tutti gli ordini sono stati eseguiti",
"noTransactions": "There are no transactions to execute",
"rejected": "Proposal rejected",
"error": "Qualcosa è andato storto",
"connectWallet": "Connetti il portafoglio per vedere i dettagli dell'esecuzione",
"switchChain": "Passa il tuo portafoglio a {0} per richiedere l'esecuzione",
"question": "Questa proposta è stata approvata e soddisfa i",
"criteria": "criteri di accettazione?",
"proposalPassed": "Did the proposal pass?",
"expired": "The proposal has expired",
"approveBond": "Approve bond",
"confirmVoteResults": "Click to confirm the proposal passed",
"executeTxsUma": "Execute transaction batch",
"deleteDisputedProposal": "Delete disputed proposal",
"confirmVoteResultsToolTip": "Make sure this proposal was approved by this Snapshot vote before proposing on-chain. If the Snapshot vote rejected the proposal, the on-chain proposal will be rejected as well and you will lose your bond.",
"approveBondToolTip": "On-chain proposals require a bond from the proposer. This will approve tokens from your wallet to be posted as a bond. If you make an invalid proposal, it will be disputed and you will lose your bond. If the proposal is valid, your bond will be returned when the transactions are executed.",
"requestToolTip": "This will propose the transactions from this Snapshot vote on-chain. After a challenge window, if the proposal is valid, the transactions can be executed and your bond will be returned.",
"executeToolTip": "This will execute the transactions from this proposal and return the proposer's bond."
}
},
"poap": {
"no_poap_header": "Non è stato ancora stabilito un POAP per questa proposta: '(",
"no_voted_header": "Vota per ottenere questo POAP",
"unclaimed_header": "Mint your I voted POAP",
"claimed_header": "Congratulations! The POAP has been minted to your collection",
"loading_header": "The POAP is being minted to your collection",
"button_claim": "Mint",
"button_show": "Sfoglia la nostra collezione",
"success_claim": "The POAP has been minted to your collection",
"error_claim": "Ci sono stati errori durante la votazione del commento."
},
"progress": {
"progress": "Progress",
"inProgress": "In Progress",
"completed": "Completed",
"complete": "Complete",
"newStep": "New Step",
"description": "Description",
"add": "Add",
"deleteStep": "Delete Step",
"deleteConfirm": "Are you sure you want to delete?",
"delete": "Delete",
"cancel": "Cancel",
"comeBack": "Come back after voting is complete to see how this proposal is progressing!",
"confirmSignature": "Signing this message will allow us to authorize your request to update the progress of your proposal.",
"wentWrong": "Oops something went wrong.",
"voting": "Voting",
"soon": "Soon"
},
"charts": {
"charts": "Charts",
"noVotesYet": "There are no votes to visualize yet.",
"totalVotesPerDay": "Total votes per day",
"shareOfVotingPower": "Share of voting power",
"votingPowerPerDay": "Voting power per day"
},
"comment_box": {
"title": "Casella dei commenti",
"add": "Aggiungi i tuoi commenti qui",
"submit": "Invia",
"preview": "Anteprima",
"continue_editing": "Continua le modifiche",
"edit": "Scrivi la tua risposta qui",
"edit_button": "Modifica",
"dismiss": "Respingi",
"delete": "Cancella",
"add_reply": "Scrivi la tua risposta qui",
"edit_comment": "Modifica Commento",
"edit_modal": "Sei sicuro di voler uscire?",
"yes": "Si",
"no": "no",
"delete_comment": "Cancella commento.",
"delete_modal": "Sei sicuro di volerla eliminare?",
"error": "Oops, qualcosa è andato storto",
"replies": "Risposte",
"hide": "Nascondi",
"show": "Mostra",
"reply": "Rispondi",
"load_more": "Visualizza altri"
},
"page": {
"title": {
"home": "Snapshot",
"setup": "Create a space",
"timeline": "Timeline",
"notifications": "Notifications",
"explore": "Explore",
"playground": "Playground",
"space": {
"create": "Create {space} proposal",
"about": "About {space}",
"proposals": "{space} Proposals",
"proposal": "{space} proposal: {proposal}",
"settings": "{space} Settings"
},
"strategy": "{key} strategy",
"delegate": "Delegate",
"ranking": "Ranking"
}
},
"hal": {
"title": "Track proposals for {spaceName}",
"text": "Receive notifications every time a new proposal is created or ends"
},
"timeUnits": {
"second": "1 second | {n} seconds",
"minute": "1 minute | {n} minutes",
"hour": "1 hour | {n} hours",
"day": "1 day | {n} days",
"week": "1 week | {n} weeks",
"month": "1 month | {n} months",
"year": "1 year | {n} years"
},
"unsupportedNetwork": {
"unsupportedNetwork": "Unsupported network",
"switchNetworkToNetwork": "To continue, you need to change the network in your wallet to {network}.",
"switchToNetwork": "Switch to {network}",
"goToDemoSite": "Go to demo website"
},
"treasury": {
"title": "Treasury",
"wallets": {
"title": "Wallets",
"empty": "This space doesn't have a treasury yet",
"addTreasury": "Add a treasury"
},
"assets": {
"title": "Assets",
"empty": "There are no assets in this contract"
},
"24hChange": "24h change"
},
"newsletter": {
"yourEmail": "Your email",
"title": "Get the latest Snapshot updates"
},
"joinCommunity": "Join Snapshot community",
"header": {
"title": "Where decisions get made",
"description": "Snapshot is a free, open-source platform for community governance. Create your own space now and start making decisions!"
},
"aboutPage": {
"description": "Snapshot is a decentralized governance platform that makes it easy to create and vote on proposals - all without spending a fortune on gas fees! Plus, our flexible system supports various voting types and strategies, so you can tailor the voting process to your needs.",
"subHeader": "Governance should be a snap",
"subDescription": "Web3 governance doesn't have to be complicated. Snapshot is the perfect solution for organizations looking for an easy and efficient way to govern their community or organization."
},
"footerView": {
"resources": "Resources",
"about": "About",
"blog": "Blog",
"jobs": "Jobs",
"discussions": "Discussions",
"github": "GitHub",
"docs": "Docs",
"support": "Support",
"hiring": "Join us!"
}
}
================================================
FILE: src/locales/ja-JP.json
================================================
{
"searchPlaceholder": "検索",
"spaceCount": "{0} スペース",
"createSpace": "スペースを作成する",
"backToHome": "ホーム",
"actions": "アクション",
"poweredBy": "Powered by",
"results": "投票結果",
"resultsError": "結果を計算できませんでした。戦略が誤って設定されていたり、または戦略に対してRPCノードが無反応であることが原因かもしれません。",
"resultsCalculating": "最終結果を計算中です。数分後にもこのメッセージが表示される場合は、スペースの管理者に連絡してください。",
"votingPowerFailedMessage": "投票力を計算できませんでした。これは、誤って設定された戦略や、戦略に関与しているRPCノードが応答しないことが原因の場合があります。",
"votingValidationFailedMessage": "当社側でエラーが発生し、あなたが投票に資格があるかどうかを検証できませんでした。これは、投票検証が誤って設定されたか、検証に関与しているAPIが応答しないことが原因の場合があります。",
"notValidVoterMessage": "おっと、あなたはこの提案に投票する資格がないようです。",
"getHelp": "ヘルプ",
"retry": "リトライ",
"currentResults": "現在の投票状況",
"reset": "リセット",
"close": "閉じる",
"save": "保存",
"author": "作成者",
"next": "次へ",
"choice": "選択",
"submit": "提出する",
"plugins": "プラグイン",
"information": "情報",
"confirm": "確定する",
"snapshot": "Snapshot",
"strategies": "ストラテジー",
"strategiesPage": "ストラテジー",
"space": "スペース",
"spaces": "スペース",
"verifiedSpace": "認証済みのスペース",
"warningSpace": "このスペースは潜在的な悪意があるとフラグが立てられています。注意して進めてください。",
"version": "バージョン",
"timeline": "タイムライン",
"ended": "が終了しました。",
"started": "が始まりました。",
"filters": "フィルター",
"allSpaces": "全てのスペース",
"submitOnchain": "パブリックチェーンへ送信する",
"inSpaces": "{0} 個のスペースが使用",
"votes": "投票数",
"seeMore": "もっと見る",
"seeAll": "すべて見る",
"network": "ネットワーク",
"networks": "ネットワーク",
"skins": "スキン",
"spaceMembers": "メンバー",
"members": "メンバーなし | {count} メンバー | {count} メンバー",
"editStrategy": "ストラテジーを編集",
"invalidProposals": "無効な提案",
"account": "アカウント",
"create3box": "3Box上にプロフィールを作成する",
"view3box": "3Box上のプロフィールを見る",
"edit3box": "3Box上のプロフィールを編集する",
"connectWallet": "ウォレットを接続する",
"toggleSkin": "スキンの切り替え",
"about": "Snapshotについて",
"license": "ライセンス",
"showMore": "もっと表示する",
"voted": "投票済み",
"reload": "リロード",
"ipfsServer": "IPFSサーバ",
"hub": "ハブ",
"cancel": "キャンセル",
"delete": "削除",
"demoSite": "これはデモサイトです。気軽にお試しください!",
"removeDelegation": "投票権の委任を削除する",
"confirmRemove": "以下のアドレスへの投票権の委任を削除してもよろしいですか?",
"removeSpace": "{0} スペースへの委任を削除しますか?",
"noVotingPower": "ブロック{blockNumber}には投票権がありません。",
"quorumReached": "クオーラムが達成されました",
"options": "オプション",
"votingPower": "あなたの投票力",
"comment": {
"placeholder": "理由を共有する(任意)"
},
"receipt": "レシート",
"relayer": "リレーヤー",
"verifyOnMycrypto": "MyCryptoでレシートを検証する",
"verifyOnSignatorio": "Signator.io で検証する",
"isCore": "コア",
"notificationsBlocked": "お使いのブラウザは通知を無効にしています",
"notificationsNotSupported": "お使いのブラウザは通知をサポートしていません",
"walletNotSupported": "ウォレットがサポートされていません",
"seeInExplorer": "エクスプローラーを参照する",
"learnMore": "もっと詳しく知る",
"logout": "ログアウト",
"continue": "続行する",
"add": "追加",
"edit": "編集",
"strategyParameters": "ストラテジーのパラメータ",
"addAction": "アクションを追加する",
"removeAction": "アクションを削除する",
"yourChoice": "選択肢 {0}",
"targetAddress": "ターゲットアドレス",
"value": "値",
"date": "日付",
"marketDetails": "マーケット詳細",
"addMarket": "マーケットを追加する",
"selectNetwork": "ネットワークを選択する",
"conditionId": "コンディションのID",
"basetokenAddress": "ベーストークンのアドレス",
"quoteAddress": "Quote currency address",
"removeMarket": "マーケットを削除する",
"back": "戻る",
"loading": "読み込み中...",
"predictedImpact": "予想される影響",
"marketSymbol": "{0} market",
"twoChoicesRequired": "このプラグインは2つ選択肢が必要です。",
"noResultsFound": "おっと、何もみつかりませんでした。",
"createFirstProposal": "最初の提案を作成しましょう",
"noSpacesJoined": "おっと、まだスペースに参加していません",
"addFavorites": "お気に入りに追加",
"createdBy": "{0} が作成",
"startIn": "{0} 後に開始",
"endIn": "{0} 後に終了",
"proposalTimeLeft": "残り{0}",
"endedAgo": "{0} に終了しました",
"proposalBy": "{0} が作成",
"endDate": "end {0}",
"defaultSkin": "デフォルトのスキン",
"select": "選択する",
"language": "言語",
"agree": "同意する",
"moderators": "モデレーター",
"playground": "プレイグラウンド",
"strategyParams": "ストラテジーのパラメータ",
"addresses": "アドレス",
"networkErrorPlayground": "ネットワークエラーが発生しました。 詳細については、ブラウザのコンソールを開いてください。",
"upload": "アップロード",
"join": "参加する",
"joined": "参加中",
"leave": "退出する",
"subspaces": "サブスペース",
"mainspace": "メインスペース",
"copyLink": "リンクをコピーする",
"duplicate": "複製する",
"joinedSpaces": "参加中のスペース",
"joinSpaces": "スペースに参加する",
"setDelegationToSpace": "委任を特定のスペースに限定する",
"theCurrentNetwork": "現在のネットワーク",
"optional": "(オプショナル)",
"homeLoadmore": "さらに読み込む",
"confirmAction": "アクションを確定する",
"or": "または",
"share": "共有する",
"shareOnTwitter": "Twitterで共有する",
"shareOnLenster": "Lensterで共有する",
"createButton": "作成する",
"discussion": "ディスカッション",
"changeWallet": "ウォレットを変更する",
"createASpace": "スペースを作成する",
"getStarted": "始める",
"skip": "スキップ",
"newSpaceNotice": {
"header": "スペースが 進行中です!",
"mainText": "あなたは、{settings}内のストラテジーを使用して投票力の計算方法を変更できます。設定の変更は、新しい提案にのみ影響します。既存の提案は変更できません。",
"learnMore": "より詳細については{documentation} をご覧ください。またはSnapshotの {discord} に参加してください。",
"gotIt": "了解"
},
"errors": {
"required": "フィールドが必要です",
"minLength": "最小文字数は {0} です",
"maxLength": "最大文字数は {0} です",
"pattern": "無効な文字",
"minItems": "最低 {0} 個のアイテムが必要",
"maxItems": "最大 {0} 個のアイテムが許可されます",
"minStrategy": "少なくとも1つストラテジーが必要です",
"website": "URLはhttps://www.example.com の形式でなければなりません",
"format": "無効な形式",
"type": "無効なタイプ",
"unsupportedImageType": "このファイル形式はサポートされていません。サポートされているファイル形式はjpeg、jpg、そしてpngです。",
"invalidAddress": "無効なアドレス"
},
"create": {
"proposalTitle": "タイトル",
"discussion": "ディスカッション(任意)",
"categorie(s)": "最大2つのカテゴリを選択してください",
"proposalDescription": "概要 (任意)",
"preview": "プレビュー",
"choices": "選択肢",
"addChoice": "選択肢を追加する",
"startDate": "開始日を選択する",
"endDate": "終了日を選択する",
"startTime": "開始時刻を選択する",
"endTime": "終了時刻を選択する",
"publish": "公開する",
"untitled": "タイトル未設定",
"snapshotBlock": "Snapshotのブロック番号",
"voting": "投票",
"votingSystem": "投票システム",
"choice": "選択肢 {0}",
"period": "投票期間",
"start": "開始",
"end": "終了",
"days": "日間",
"hours": "時間",
"minutes": "分間",
"schedule": "提案を予定する",
"delayEnforced": "スペースは投票が開始されるまで遅延を強制する",
"periodEnforced": "スペースは投票期間を強制します",
"typeEnforced": "{type} はスペースによって強制されます",
"privacyEnforced": "{type} はスペースによって強制されます",
"edit": "編集",
"continue": "続行する",
"now": "今すぐ",
"votingPeriodExplainer": "これはユーザーが投票できる期間です。提案は投票期間が始まる前に表示され、投票が開始されるまで保留状態となります。",
"uploadImageExplainer": "ドラッグ&ドロップ、選択、または貼り付けで画像を添付します。",
"uploading": "画像をアップロード中",
"markdown": "マークダウン形式がサポートされています",
"validationWarning": {
"basic": {
"member": "提案を提出するには、このスペースの作成者である必要があります。",
"minScore": "提案を提出するためには、少なくとも {0} {1} を保有する必要があります。"
},
"customValidation": "提案を提出するには、提案検証をクリアする必要があります。",
"executionError": "このスペースで提案を作成する資格を確認することができませんでした。これはおそらくストラテジーの誤設定によるものです。"
},
"errorGettingSnapshot": "投票権を計算するために必要なスナップショットブロック番号を取得する際にエラーが発生しました。後でもう一度お試しください。"
},
"delegate": {
"header": "委任",
"selectDelegate": "委任先を選択する",
"to": "宛先",
"addressPlaceholder": "アドレスまたはENSの名前",
"delegations": "あなたの委任者",
"allSpaces": "すべてのスペース用",
"delegated": "あなたへの委任",
"pendingTransaction": "保留中の取引がありません | 保留中の取引が1件あります | {count} 件の保留中の取引があります",
"topDelegates": "トップの委任先",
"noDelegatesFoundFor": "{0} の委任が見つかりません。",
"noValidEns": "ENSのアドレスが有効ではありません。",
"noValidAddress": "アドレスが有効ではありません",
"delegateToSelf": "自分自身に委任することはできません",
"delegateToSelfAddress": "自分のENSアドレスに委任することはできません",
"noValidSpaceId": "スペースIDが有効ではありません",
"noDelegationsAndDelegates": "委任と委任代理人が見つかりませんか?正しいネットワークに接続しているか確認してください。",
"delegateNotSupported": "現在、委任は{network} でサポートされていません。"
},
"proposal": {
"castVote": "投票する",
"vote": "投票する",
"startDate": "開始日",
"endDate": "終了日",
"votingSystem": "投票のシステム",
"privacy": "プライバシー",
"invalidChoice": "無効な選択",
"postVoteModal": {
"defaultTitle": "あなたの投票が完了しました!",
"gnosisSafeTitle": "あなたの投票は保留中です...",
"gnosisSafeDescription": " Safeを使用した投票には追加の署名者が必要で、取引が確認された後に表示されます",
"seeQueue": "キューに入っている取引を見る",
"tips": {
"1": "提案がアクティブの間は投票を変更することができます"
}
}
},
"proposals": {
"header": "提案",
"new": "新しい提案",
"noProposals": "まだ提案はありません",
"createProposal": "提案を作成する",
"showMore": "もっと見る",
"showLess": "表示を減らす",
"states": {
"all": "すべて",
"core": "コア",
"community": "コミュニティ",
"active": "アクティブ",
"pending": "保留中",
"closed": "終了"
}
},
"notifications": {
"header": "通知",
"noNotifications": "通知がありません。",
"proposalStarted": "提案が開始されました:",
"proposalEnded": "提案が終了しました:",
"markAllAsRead": "すべて既読にする",
"all": "すべて",
"unread": "未読"
},
"modalTerms": {
"mustAgreeTo": "{action} このスペースをするには、{spaceName} 利用規約に同意する必要があります。",
"actionJoin": "参加する",
"actionCreate": "提案を作成する",
"actionVote": "投票する"
},
"settings": {
"header": "設定",
"editController": "コントローラの編集",
"connectWithSpaceOwner": "閲覧専用モードです。スペースの設定を変更するには、コントローラまたは管理者ウォレットと接続してください。",
"gnosisWrongNetwork": {
"base": "Gnosis Safeが間違ったネットワーク上にあります。 {action} するには、{network} に接続してください。",
"settings": "スペースの設定を編集する",
"create": "提案を作成する",
"vote": "この提案に投票する"
},
"currentSpaceControllerIs": "現在のスペースコントローラは{address} です。",
"newController": "新しいコントローラ",
"noRecord": "{id} ドメインを {network} に登録していることを確認してください。その後、コントローラテキストレコードを編集して、スペース設定へのアクセス権を回復してください。",
"set": "設定",
"profile": "プロフィール",
"avatar": "アバター",
"name": {
"label": "名前",
"placeholder": "例:Yam Network"
},
"about": {
"label": "概要",
"placeholder": "あなたの組織について何ですか?"
},
"categories": {
"label": "カテゴリー",
"select": "カテゴリーを選択"
},
"terms": {
"label": "利用規約",
"information": "ユーザーは、提案を作成したり投票したりする前に、これらの利用規約に1度だけ同意する必要があります"
},
"hideSpace": "ホームページからスペースを隠す",
"links": "ソーシャルアカウント",
"subspaces": {
"label": "サブスペース",
"information": "サブスペースは、メインスペースとサブスペースの両方で設定された場合にのみ表示されます。 {docs}",
"parent": {
"label": "メインスペース",
"placeholder": "pistachiodao.eth",
"information": "このスペースがサブスペースであるメインスペースがスペースページに表示されます"
},
"children": {
"label": "サブスペース",
"placeholder": "pistachiodao.eth",
"information": "ここにリストされた関連サブスペースは、スペースページに表示されます"
}
},
"website": "ウェブサイト",
"strategies": {
"label": "戦略(複数可)",
"information": "戦略は、投票権またはユーザーが提案を作成する資格があるかどうかを決定するために使用されます"
},
"network": {
"label": "ネットワーク",
"information": "このスペースに使用されるデフォルトのネットワーク。 ネットワークは個々の戦略でも指定できます"
},
"symbol": {
"label": "シンボル",
"information": "このスペースに使用されるデフォルトのシンボル、通常はトークンシンボルであるBAL(Balancer)など"
},
"strategiesList": "最大8つの戦略を選択してください",
"votingPowerIsCumulative": "投票権は累積的です",
"addStrategy": "ストラテジーを追加",
"testInPlayground": "プレイグラウンドでテスト",
"admins": {
"label": "管理者",
"information": "管理者はスペースの設定を変更し、スペースの提案を管理できます"
},
"authors": {
"label": "作者",
"information": "作者は常に提案を作成できます"
},
"proposalValidation": "提案の検証",
"validation": "タイプ",
"proposalThreshold": {
"label": "閾値",
"information": "提案を作成するために必要な最小の投票権"
},
"allowOnlyAuthors": "作成者のみが提案を提出できます",
"editValidation": "検証を編集",
"selectValidation": "検証を選択",
"validationParameters": "検証パラメータ",
"voting": "投票",
"votingDelay": "投票の遅延",
"votingPeriod": "投票期間",
"hours": "時間",
"days": "日",
"quorum": {
"label": "クオーラム",
"information": "提案が承認されるために必要な最小の投票権"
},
"type": {
"label": "タイプ",
"information": "このスペースで使用される投票システムのタイプ(将来の提案に強制されます)"
},
"anyType": "すべて",
"hideAbstain": "基本的な投票結果において棄権投票を無視する",
"customDomain": "カスタムドメイン",
"domain": {
"label": "ドメイン名",
"placeholder": "例:vote.balancer.fi",
"info": "カスタムドメインを設定するには、スペースを作成した後にGitHub上でプルリクエストを開く必要があります。 {docs}"
},
"skin": "スキン",
"treasuries": {
"label": "財務省",
"add": "財務省を追加",
"edit": "財務省を編集",
"information": "組織の財務省を追加して、スペース内に表示します"
},
"addPlugin": "プラグインを追加",
"editPlugin": "プラグインを編集",
"pluginParameters": "プラグインパラメータ",
"proposal": {
"title": "提案",
"guidelines": {
"title": "ガイドライン",
"information": "提案作成のガイドラインへのリンクを表示して、ユーザーが良好/有効な提案を構成するかどうかを理解するのに役立ちます"
},
"template": {
"title": "テンプレート",
"information": "テンプレートで始めて、ユーザーが必要な情報を理解するのに役立ちます"
}
}
},
"setup": {
"example": "例 yam.eth",
"chooseExistingEns": "作成するスペースと紐付ける既存のENSドメインを選択してください。",
"useSingleExistingEns": "既存のENSドメインを使用してください:",
"orRegisterNewEns": "または新しいドメインを登録する:",
"demoTestnetEnsMessage": "{network}上でテストスペースを作成するには、ENSドメインが必要です。",
"toCreateASpace": "スペースを作成するためには、まずENSドメインが必要です。ドメインを入力し、ENS登録の指示に従ってください。",
"createASpace": "スペースを作成する",
"registerEnsButton": "登録する",
"supportedEnsTLDs": "サポートされているドメインの拡張子",
"helpDocsAndDiscordLinks": "スペースの設定方法がわからない場合は、{docs}を参照してください。Snapshot {discord}に参加してください。",
"setSpaceController": "スペースコントローラー",
"setSpaceControllerExists": "このドメインのsnapshotテキストレコードはすでに設定されています。変更する場合は編集を選択し、次のステップに進むことができます。",
"setSpaceControllerInfo": "スペースコントローラーは、スペースの設定を管理できるアカウントです。後で追加のスペースコントローラー(管理者)を追加できます。",
"setSpaceControllerInfoGnosisSafe": "Gnosis Safeを使用してスペースを作成する場合は、安全なアドレスをスペースコントローラーとして設定することをお勧めします。しない場合は、いくつかの追加ステップを実行する必要があります。 {link}",
"editSpaceController": "ENSのコントローラーを編集する",
"setController": "コントローラーを設定する",
"explainControllerAndEns": "{network}上で取引を行うことで、ENSドメインに「snapshot」TEXTレコードが追加されます。",
"confirmToSetAddress": "{address} をスペースのコントローラーとして設定してもよろしいですか?",
"controllerHasAuthority": "コントローラーはスペースの設定に対して完全な権限を持っています",
"controller": "コントローラー",
"selectEnsForSpace": "ENSアドレスを選択する",
"spaceOwnerAddressPlaceHolder": "例:{address}",
"controllerAddress": "コントローラーのアドレス",
"updateController": "コントローラーを更新する",
"seeOnEns": "ENS上で見る",
"goToSettings": "設定に移動する",
"setSpaceProfile": "スペースをカスタマイズする",
"waitForTransaction": "スペースを作成する前に取引の確定が必要です。{txUrl}",
"pleaseWaitMessage": "取引が確定されるまで少々お待ちください",
"notControllerAddress": "スペースを作成するには、コントローラーアドレス {wallet} に接続してください。",
"fillCurrentAccount": "現在ログインしているアカウントを使用する",
"domain": {
"title": "スペースドメインを設定する",
"ensMessage": "スペースを作成する前に必要なものの1つは、Ethereum mainnet上のENSドメインです。",
"ensMessageTestnet": "また、Goerliテストネット上で{link}することもできます。",
"tryDemo": "デモを試す",
"yourExistingSpaces": "既存のスペース",
"invalidEns": "このENS名は無効です。通常、登録時に無効な文字を使用したためです。"
},
"strategy": {
"title": "投票戦略をどのように設定しますか?",
"subtitle": "戦略の設定はいつでも変更できます。",
"blockTitle": "投票戦略を設定する",
"onePersonOneVote": {
"title": "1人1票",
"description": "投票できるホワイトリストを管理するか、単にすべてのアドレスが投票できるようにすることができます。すべての投票は等しく、トークンは必要ありません",
"whitelistInformation": "投票できるアカウントの数を指定する",
"ticketInformation": "すべてのアカウントが投票できます",
"votesEqualInfo": "各投票は等しく、トークンは必要ありません"
},
"tokenVoting": {
"title": "トークン加重投票",
"description": "投票はトークンで重み付けされます。トークンはERC-20、ERC-721、またはERC-1155トークンスタンダードです",
"tokenNotFound": "トークンが見つかりません",
"seeOnEtherscan": "Etherscanで見る"
},
"advanced": {
"title": "カスタムセットアップ",
"description": "多くのオプションから最大8つの戦略を選択できます。あなたの用途に合った戦略が見つからない場合は、独自の戦略を作成することもできます"
}
},
"validationTitle": "誰がこのスペースを管理して提案を作成できますか?"
},
"profile": {
"buttonEdit": "プロフィールを編集",
"viewProfile": "プロフィールを表示",
"about": {
"header": "情報",
"joinedSpaces": "参加したスペース",
"createdSpaces": "作成したスペース",
"biography": "バイオグラフィー",
"notJoinSpacesYet": "まだスペースに参加していません",
"notCreatedSpacesYet": "まだスペースを作成していません",
"delegatorNetworkInfo": "ウォレット内のネットワークを切り替えて変更します",
"delegate": "委任",
"delegated": "委任された",
"delegateTo": "に委任する",
"delegateFor": "の委任者",
"noDelegatorsMessage": "{network}上に委任者はいません",
"notSupportedNetwork": "{network}では現在委任はサポートされていません"
},
"activity": {
"header": "活動",
"votedFor": "{choice}に投票しました",
"today": "今日",
"thisWeek": "今週",
"olderThanWeek": "1週間以上前",
"noActivity": "まだ活動はありません"
},
"settings": {
"header": "プロフィールを編集",
"name": "名前",
"biography": "自己紹介",
"namePlaceholder": "名前を入力",
"bioPlaceholder": "あなたの物語を話してください",
"change": "変更",
"remove": "削除"
}
},
"notify": {
"youDidIt": "おつかれさまです!",
"copied": "コピーしました!",
"proposalDeleted": "提案が削除されました",
"somethingWentWrong": "おっと、何らかの問題が発生しました!",
"saved": "保存しました!",
"delegationSuccess": "委任されました",
"delegationRemoved": "委任が削除されました",
"proposalCreated": "提案が作成されました",
"voteSuccessful": "投票が完了しました!",
"ensSet": "ENSテキストレコードが正常に設定されました",
"transactionSent": "取引が送付されました"
},
"explore": {
"createStrategy": "ストラテジーを作成する",
"createSkin": "スキンを作成する",
"addNetwork": "ネットワークを追加する",
"createPlugin": "プラグインを作成する",
"strategies": "ストラテジー",
"skins": "スキン",
"networks": "ネットワーク",
"plugins": "プラグイン",
"results": "結果",
"category": "カテゴリー",
"categories": {
"all": "全て",
"protocol": "プロトコル",
"social": "ソーシャル",
"investment": "投資",
"grant": "グラント",
"service": "サービス",
"media": "メディア",
"creator": "クリエイター",
"collector": "コレクター"
}
},
"voting": {
"selectVoting": "投票システムを選択する",
"single-choice": "単一選択の投票",
"approval": "承認投票",
"quadratic": "二次投票",
"ranked-choice": "優先順位付投票",
"weighted": "加重投票",
"basic": "ベーシックな投票",
"description": {
"single-choice": "各投票者は一つの選択のみを選択することができます。",
"approval": "各投票者は任意の数の選択肢を選択することができます。",
"quadratic": "各投票者は、任意の数の選択肢に投票力を分散させることができます。結果は二次的に計算されます。",
"ranked-choice": "各投票者は、任意の数の選択肢を選択してランク付けすることができます。結果は、インスタント流出カウント法によって計算されます。",
"weighted": "各投票者は、任意の数の選択肢に投票力を分散させることができます。",
"basic": "3つの選択肢(賛成または 反対または棄権)の中から一つを選択する投票"
}
},
"privacy": {
"label": "プライバシー",
"title": "投票のプライバシーを選択",
"information": "提案に使用されるプライバシーの種類(今後のすべての提案で強制されます)",
"any": "すべて",
"none": "なし",
"shutter": {
"label": "シャッター",
"description": "選択肢は暗号化され、投票期間が終了して最終スコアが計算されるまで表示されません",
"tooltip": "この提案にはShutterプライバシーが有効になっています。すべての投票は、投票期間が終了して最終スコアが計算されるまで暗号化されます",
"url": "https://blog.shutter.network/shielded-voting/"
}
},
"validation": {
"label": "検証",
"title": "投票検証を選択",
"information": "ユーザーが投票できるかどうかを判断するために使用される検証の種類(今後のすべての提案に強制されます)",
"any": {
"label": "誰でも投票可能",
"description": "投票権を持つ誰でも投票できます。"
},
"basic": {
"label": "基本",
"description": "ユーザーが投票できるかどうかを判断するためのあらゆる戦略を使用します。",
"invalidMessage": "この提案に投票するための最低限のバランス要件を満たしていません。"
},
"passport-gated": {
"label": "Gitcoin Passport ゲート付き",
"description": "Gitcoin Passportを必要とすることで、スパムや投票操作からあなたの提案を保護します。",
"invalidMessage": "この提案に投票するためには、{amount} 以下の切手を持つGitcoin Passportが必要です:{stamps}。"
}
},
"safeSnap": {
"currentOutcome": "現在の結果",
"currentBond": "現在のボンド",
"finalizedIn": "{0}に最終的に決定されました",
"executableIn": "{0}で実行可能",
"finalOutcome": "最終結果",
"nextBond": "次のボンドを設定する",
"setOutcomeTo": "アウトカムを設定する",
"claimBond": "ボンドを受け取る",
"addBatch": "トランザクションを追加する",
"batch": "バッチを追加する",
"transactions": "トランザクション",
"to": "宛先(アドレス)",
"invalidAddress": "無効なアドレス",
"invalidAmount": "無効な金額",
"invalidValue": "無効な値",
"invalidAbi": "無効なABI",
"invalidData": "無効なデータ",
"value": "値(wei)",
"data": "データ",
"noCollectibles": "コレクティブルがありません",
"asset": "資産",
"amount": "量",
"type": "タイプ",
"transferFunds": "送金する",
"transferNFT": "NFTを転送する",
"contractInteraction": "コントラクトとの対話",
"rawTransaction": "生トランザクション",
"addTransaction": "トランザクションを追加する",
"transactionLabels": {
"contractInteraction": "{functionName}() - {amount}weiを{address}に",
"transferFunds": "{amount}{tokenSymbol}を{address}に送金",
"transferNFT": "{name} #{id}を{address}に送信",
"raw": "{amount}weiを{address}に送信"
},
"labels": {
"request": "リクエストの実行",
"setOutcome": "結果を設定",
"changeOutcome": "結果を変更する",
"executeTxs": "{1}の{0}番目のトランザクションバッチを実行する",
"executed": "すべてのトランザクションが実行されました",
"noTransactions": "実行するトランザクションがありません",
"rejected": "提案が拒否されました",
"error": "何らかの問題が発生しました",
"connectWallet": "実行の詳細を表示するにはウォレットに接続してください",
"switchChain": "{0}に切り替えて実行をリクエストしてください",
"question": "この提案が承認され、条件を満たしていますか?",
"criteria": "受け入れ基準?",
"proposalPassed": "提案が承認されましたか?",
"expired": "提案の有効期限が切れました",
"approveBond": "保証金を承認する",
"confirmVoteResults": "提案が承認されたことを確認するにはクリックしてください",
"executeTxsUma": "トランザクションバッチを実行する",
"deleteDisputedProposal": "紛争中の提案を削除する",
"confirmVoteResultsToolTip": "この提案がこのスナップショット投票で承認されたことを確認してから、チェーン上で提案する前に。もしスナップショット投票で提案が拒否された場合、チェーン上の提案も拒否され、保証金を失うことになります。",
"approveBondToolTip": "チェーン上の提案には提案者からの保証金が必要です。これは、あなたのウォレットからトークンを承認することになります。無効な提案を行った場合、それは紛争となり、保証金を失うことになります。提案が有効な場合、トランザクションが実行されるときに保証金が返却されます。",
"requestToolTip": "これは、このスナップショット投票からのトランザクションをチェーン上で提案します。チャレンジウィンドウの後、提案が有効な場合、トランザクションを実行して保証金が返却されます。",
"executeToolTip": "これは、この提案からのトランザクションを実行し、提案者の保証金を返却します。"
}
},
"poap": {
"no_poap_header": "POAP はまだこの提案のためにセットアップされていません :(",
"no_voted_header": "投票してこのPOAPを獲得する",
"unclaimed_header": "投票の証にPOAPを発行する",
"claimed_header": "おめでとうございます!POAPがあなたのコレクションに追加されました。",
"loading_header": "POAPをあなたのコレクションに追加しています",
"button_claim": "発行する",
"button_show": "コレクションを閲覧する",
"success_claim": "POAP があなたのコレクションに追加されました",
"error_claim": "トークンを発行する際に問題が発生しました。"
},
"progress": {
"progress": "進捗",
"inProgress": "進行中",
"completed": "完了しました",
"complete": "完了",
"newStep": "新しいステップ",
"description": "説明",
"add": "追加",
"deleteStep": "ステップを削除する",
"deleteConfirm": "本当に削除しますか?",
"delete": "削除",
"cancel": "キャンセル",
"comeBack": "投票が完了したら、この提案の進捗状況を確認するために戻ってきてください!",
"confirmSignature": "このメッセージの署名を行うと、提案の進捗状況を更新するリクエストを承認できます。",
"wentWrong": "何かが間違っています。",
"voting": "投票",
"soon": "まもなく"
},
"charts": {
"charts": "チャート",
"noVotesYet": "表示する投票がまだありません。",
"totalVotesPerDay": "1日あたりの合計投票数",
"shareOfVotingPower": "Share of voting power",
"votingPowerPerDay": "1日あたりの投票力"
},
"comment_box": {
"title": "コメント欄",
"add": "コメントを入力",
"submit": "提出する",
"preview": "プレビュー",
"continue_editing": "編集を続ける",
"edit": "ここにあなたの返信を書いてください",
"edit_button": "編集",
"dismiss": "閉じる",
"delete": "削除",
"add_reply": "返信を入力",
"edit_comment": "コメントの編集",
"edit_modal": "編集してもよろしいですか?",
"yes": "はい",
"no": "いいえ",
"delete_comment": "コメントを削除",
"delete_modal": "削除してもよろしいですか?",
"error": "おっと、何らかの問題が発生しました",
"replies": "返信",
"hide": "非表示",
"show": "表示:",
"reply": "返信",
"load_more": "さらに表示"
},
"page": {
"title": {
"home": "Snapshot",
"setup": "スペースを作成する",
"timeline": "タイムライン",
"notifications": "お知らせ",
"explore": "Explore",
"playground": "プレイグラウンド",
"space": {
"create": "{space} の提案を作成する",
"about": "{space} について",
"proposals": "{space} の提案",
"proposal": "{space} の提案: {proposal}",
"settings": "{space} の設定"
},
"strategy": "{key} のストラテジー",
"delegate": "委任",
"ranking": "Ranking"
}
},
"hal": {
"title": "{spaceName} の提案を追跡する",
"text": "新しい提案が作成または終了するたびに通知を受け取る"
},
"timeUnits": {
"second": "1 秒 | {n} 秒",
"minute": "1 分 | {n} 分",
"hour": "1 時間 | {n} 時間",
"day": "1 日 | {n} 日",
"week": "1 週間 | {n} 週間",
"month": "1ヶ月 | {n} ヶ月",
"year": "1 年 | {n} 年"
},
"unsupportedNetwork": {
"unsupportedNetwork": "対応していないネットワーク",
"switchNetworkToNetwork": "続行するには、ウォレット内のネットワークを{network}に変更する必要があります。",
"switchToNetwork": "{network}に切り替える",
"goToDemoSite": "デモサイトに行く"
},
"treasury": {
"title": "財務",
"wallets": {
"title": "ウォレット",
"empty": "このスペースにはまだ財務がありません",
"addTreasury": "財務を追加する"
},
"assets": {
"title": "資産",
"empty": "この契約には資産がありません"
},
"24hChange": "24時間変化"
},
"newsletter": {
"yourEmail": "あなたのメールアドレス",
"title": "Snapshotの最新情報を受け取る"
},
"joinCommunity": "コミュニティに参加",
"header": {
"title": "決定が行われる場所",
"description": "Snapshotは、コミュニティガバナンスのための無料でオープンソースのプラットフォームです。今すぐ自分のスペースを作成して決定を開始しましょう!"
},
"aboutPage": {
"description": "Snapshotは、投票を行うのにガス料金を大金を使わなくても簡単に作成し投票できる分散型ガバナンスプラットフォームです。さらに、柔軟なシステムは様々な投票タイプと戦略をサポートしているので、投票プロセスをニーズに合わせて調整できます。",
"subHeader": "ガバナンスはスナップで行う",
"subDescription": "Web3ガバナンスは複雑である必要はありません。Snapshotは、コミュニティや組織を管理するための簡単で効率的な方法を探している組織にとって完璧なソリューションです。"
},
"footerView": {
"resources": "リソース",
"about": "情報",
"blog": "ブログ",
"jobs": "求人",
"discussions": "ディスカッション",
"github": "GitHub",
"docs": "ドキュメント",
"support": "サポート",
"hiring": "私たちと一緒に!"
}
}
================================================
FILE: src/locales/ko-KR.json
================================================
{
"searchPlaceholder": "검색",
"spaceCount": "{0} 공간(들)",
"createSpace": "공간 만들기",
"backToHome": "홈",
"actions": "액션",
"poweredBy": "Powered by",
"results": "결과",
"resultsError": "결과를 계산할 수 없습니다. 이는 종종 잘못 구성된 전략이나 전략에 포함된 RPC 노드가 응답하지 않아서 일 수 있습니다.",
"resultsCalculating": "Final results are being calculated. If you still see this message after a few minutes contact the space admin.",
"votingPowerFailedMessage": "Your voting power could not be calculated. This is often due to a misconfigured strategy or an unresponsive RPC node involved in the strategy.",
"votingValidationFailedMessage": "There was an error on our side and we could not verify if you are eligible to vote. This is often due to a misconfigured voting validation or an unresponsive API involved in the validation.",
"notValidVoterMessage": "Oops, you don't seem to be eligible to vote on this proposal.",
"getHelp": "도움 받기",
"retry": "다시하기",
"currentResults": "현재 결과",
"reset": "초기화",
"close": "Close",
"save": "저장",
"author": "저자",
"next": "다음",
"choice": "Choice",
"submit": "제출",
"plugins": "플러그인",
"information": "정보",
"confirm": "확인",
"snapshot": "스냅샷",
"strategies": "전략(들)",
"strategiesPage": "전략",
"space": "공간",
"spaces": "공간들",
"verifiedSpace": "검증된 공간",
"warningSpace": "This space has been flagged as potentially malicious. Proceed with caution.",
"version": "버전",
"timeline": "타임라인",
"ended": "ended",
"started": "started",
"filters": "필터",
"allSpaces": "모든 공간",
"submitOnchain": "온체인 제출",
"inSpaces": "{0} 공간(들)",
"votes": "투표",
"seeMore": "더보기",
"seeAll": "See all",
"network": "네트워크",
"networks": "네트워크들",
"skins": "스킨",
"spaceMembers": "Members",
"members": "회원 없음 | {count} 회원 | {count} 구성원",
"editStrategy": "전략 수정",
"invalidProposals": "유효하지 않는 제안",
"account": "계정",
"create3box": "3Box에서 프로필 만들기",
"view3box": "3Box에서 프로필 보기",
"edit3box": "3Box에서 프로필 수정",
"connectWallet": "지갑 연결",
"toggleSkin": "스킨 토글",
"about": "소개",
"license": "라이센스",
"showMore": "Show more",
"voted": "Voted",
"reload": "Reload",
"ipfsServer": "IPFS 서버",
"hub": "허브",
"cancel": "취소",
"delete": "Delete",
"demoSite": "데모 사이트입니다. 시도해 보세요!",
"removeDelegation": "위임 제거",
"confirmRemove": "위임을 제거하시겠습니까",
"removeSpace": "{0} 공간",
"noVotingPower": "Oops, it seems you don't have any voting power at block {blockNumber}.",
"quorumReached": "quorum reached",
"options": "옵션",
"votingPower": "당신의 투표권",
"comment": {
"placeholder": "Share your reason (optional)"
},
"receipt": "처리 결과",
"relayer": "중계자",
"verifyOnMycrypto": "MyCrypto에서 처리결과 확인",
"verifyOnSignatorio": "Signator.io에서 확인",
"isCore": "코어",
"notificationsBlocked": "브라우저가 알림을 차단하고 있습니다",
"notificationsNotSupported": "브라우저는 알림을 지원하지 않습니다",
"walletNotSupported": "지갑이 지원되지 않음",
"seeInExplorer": "See explorer",
"learnMore": "더 알아보기",
"logout": "로그아웃",
"continue": "계속",
"add": "추가",
"edit": "편집",
"strategyParameters": "전략 매개변수",
"addAction": "액션 추가",
"removeAction": "액션 제거",
"yourChoice": "{0} 선택",
"targetAddress": "대상 주소",
"value": "값",
"date": "데이터",
"marketDetails": "시장 세부 정보",
"addMarket": "시장 추가",
"selectNetwork": "네트워크 선택",
"conditionId": "조건 ID",
"basetokenAddress": "기본 토큰 주소",
"quoteAddress": "견적 통화 주소",
"removeMarket": "시장 제거",
"back": "뒤로",
"loading": "로딩 중",
"predictedImpact": "예상 영향",
"marketSymbol": "{0} 시장",
"twoChoicesRequired": "이 플러그인을 위해 두 가지 선택이 요구됩니다.",
"noResultsFound": "일치하는 결과가 없습니다.",
"createFirstProposal": "첫 번째 제안을 작성해 보세요.",
"noSpacesJoined": "아직 어떤 공간에도 가입하지 않았습니다.",
"addFavorites": "즐겨찾기 추가",
"createdBy": "{0} 에 의해",
"startIn": "시작 {0}",
"endIn": "종료 {0}",
"proposalTimeLeft": "{0} 남음",
"endedAgo": "{0} 에 종료됨",
"proposalBy": "{0} 에 의해",
"endDate": "말단 {0}",
"defaultSkin": "기본 스킨",
"select": "선택",
"language": "언어",
"agree": "동의함",
"moderators": "Moderators",
"playground": "플레이그라운드",
"strategyParams": "전략 매개변수",
"addresses": "주소",
"networkErrorPlayground": "네트워크 오류 - 자세한 내용을 보려면 브라우저 콘솔을 여십시오",
"upload": "업로드",
"join": "참여",
"joined": "참여함",
"leave": "나가기",
"subspaces": "Sub-spaces",
"mainspace": "Main space",
"copyLink": "링크 복사",
"duplicate": "Duplicate",
"joinedSpaces": "참여한 공간",
"joinSpaces": "공간 참여",
"setDelegationToSpace": "특정 공간으로 위임 제한",
"theCurrentNetwork": "현재 네트워크",
"optional": "(선택 사항)",
"homeLoadmore": "더 불러오기",
"confirmAction": "액션 확인",
"or": "또는",
"share": "Share",
"shareOnTwitter": "Share on Twitter",
"shareOnLenster": "Share on Lenster",
"createButton": "Create",
"discussion": "Discussion",
"changeWallet": "Change wallet",
"createASpace": "Create a space",
"getStarted": "Get started",
"skip": "Skip",
"newSpaceNotice": {
"header": "Your space is live!",
"mainText": "You can change how voting power is calculated via strategies in your {settings}. Changes to your settings will only affect new proposals, existing proposals can not be changed.",
"learnMore": "Learn more in the {documentation} or join Snapshot {discord} for help.",
"gotIt": "Got it!"
},
"errors": {
"required": "필드가 필요합니다",
"minLength": "Field is required",
"maxLength": "최대 길이: {0}",
"pattern": "잘못된 문자",
"minItems": "최소 {0} 개 항목 필요",
"maxItems": "Maximum {0} item(s) allowed",
"minStrategy": "적어도 한 개의 전략이 필요합니다.",
"website": "URL should be in the format https://www.example.com",
"format": "잘못된 형식",
"type": "잘못된 유형",
"unsupportedImageType": "File type not supported, Supported formats are jpeg, jpg and png",
"invalidAddress": "Invalid address"
},
"create": {
"proposalTitle": "Title",
"discussion": "Discussion (optional)",
"categorie(s)": "최대 2개의 카테고리 선택",
"proposalDescription": "Description (optional)",
"preview": "미리보기",
"choices": "선택지",
"addChoice": "선택지 추가",
"startDate": "시작일 선택",
"endDate": "종료일 선택",
"startTime": "시작 시간 선택",
"endTime": "종료 시간 선택",
"publish": "게시",
"untitled": "제목 없음",
"snapshotBlock": "스냅샷 블록 번호",
"voting": "투표",
"votingSystem": "투표 유형",
"choice": "{0} 선택",
"period": "투표 기간",
"start": "시작",
"end": "종료",
"days": "일",
"hours": "시",
"minutes": "분",
"schedule": "일정 제안",
"delayEnforced": "투표가 시작될 때까지 공간이 지연됩니다.",
"periodEnforced": "공간은 투표 기간을 적용합니다.",
"typeEnforced": "{type} is enforced by the space",
"privacyEnforced": "{type} is enforced by the space",
"edit": "편집",
"continue": "계속",
"now": "현재",
"votingPeriodExplainer": "사용자가 투표할 수 있는 기간입니다. 제안은 투표 기간 시작 전에 대기 중으로 보여질 것입니다.",
"uploadImageExplainer": "Attach images by dragging & dropping, selecting or pasting them.",
"uploading": "Uploading image",
"markdown": "Styling with Markdown is supported",
"validationWarning": {
"basic": {
"member": "제안서를 제출하려면 공간의 저자여야 합니다.",
"minScore": "당신은 제안서를 제출하기 위해 {0} {1} 의 최소가 필요합니다."
},
"customValidation": "당신은 제안서를 제출하기 위해 제안의 유효성 검사를 통과해야합니다.",
"executionError": "제안 생성에 대한 자격이 있는지 확인하는 데 실패했습니다. 이는 잘못 구성된 전략 때문일 수 있습니다."
},
"errorGettingSnapshot": "We encountered an error while fetching the snapshot block number which is needed to calculate your voting power. Please try again later."
},
"delegate": {
"header": "대리자",
"selectDelegate": "대리인 선택",
"to": "누구에게",
"addressPlaceholder": "주소 또는 ENS 이름",
"delegations": "당신의 대리인",
"allSpaces": "모든 공간",
"delegated": "당신에게 위임됨",
"pendingTransaction": "보류 중인 트랜잭션 없음 | 1 보류 중인 트랜잭션 | {count} 보류 중인 트랜잭션",
"topDelegates": "상위 대리자",
"noDelegatesFoundFor": "{0} 에 대한 대리자를 찾을 수 없습니다.",
"noValidEns": "올바른 ENS 주소 아님",
"noValidAddress": "올바르지 않은 주소",
"delegateToSelf": "자기 자신에게는 위임할 수 없음",
"delegateToSelfAddress": "자기 자신의 ENS 주소로는 위임할 수 없슴",
"noValidSpaceId": "올바르지 않은 공간 ID",
"noDelegationsAndDelegates": "대리자를 찾을 수 없습니까? 올바른 네트워크에 연결되어 있는지 확인하십시오.",
"delegateNotSupported": "위임은 현재 {network} 에서 지원되지 않습니다."
},
"proposal": {
"castVote": "투표하기",
"vote": "투표",
"startDate": "시작일",
"endDate": "종료일",
"votingSystem": "투표 유형",
"privacy": "Privacy",
"invalidChoice": "Invalid choice",
"postVoteModal": {
"defaultTitle": "Your vote is in!",
"gnosisSafeTitle": "Your vote is pending...",
"gnosisSafeDescription": " Votes with a Safe require additional signers and will be visible once the transaction is confirmed",
"seeQueue": "See queued transactions",
"tips": {
"1": "Votes can be changed while the proposal is active"
}
}
},
"proposals": {
"header": "제안",
"new": "새로운 제안 만들기",
"noProposals": "아직 새로운 제안이 없어요!",
"createProposal": "제안 만들기",
"showMore": "더 보기",
"showLess": "적게 보기",
"states": {
"all": "모두",
"core": "코어",
"community": "커뮤니티",
"active": "진행 중",
"pending": "보류 중",
"closed": "종료됨"
}
},
"notifications": {
"header": "Notifications",
"noNotifications": "You have no notifications",
"proposalStarted": "proposal has started:",
"proposalEnded": "proposal has ended:",
"markAllAsRead": "Mark all as read",
"all": "All",
"unread": "Unread"
},
"modalTerms": {
"mustAgreeTo": "To {action} this space, you must agree to the {spaceName} terms of service.",
"actionJoin": "join",
"actionCreate": "create a proposal in",
"actionVote": "vote in"
},
"settings": {
"header": "설정",
"editController": "관리자 설정",
"connectWithSpaceOwner": "You are in view only mode, to modify space settings connect with a controller or admin wallet.",
"gnosisWrongNetwork": {
"base": "Your Gnosis Safe is on the wrong network. Please connect to {network} to {action}.",
"settings": "edit the space settings",
"create": "create a proposal",
"vote": "vote on this proposal"
},
"currentSpaceControllerIs": "The current space controller is {address}",
"newController": "New controller",
"noRecord": "No text-record found. Make sure you have registered {id} domain on {network}, then edit the controller text-record to regain access to the space settings.",
"set": "Set",
"profile": "프로필",
"avatar": "아바타",
"name": {
"label": "Name",
"placeholder": "e.g. Yam Network"
},
"about": {
"label": "About",
"placeholder": "What is your organisation about?"
},
"categories": {
"label": "Categorie(s)",
"select": "Select categorie(s)"
},
"terms": {
"label": "Terms of service",
"information": "Users will be required to accept these terms once before they can create a proposal or cast a vote"
},
"hideSpace": "홈페이지에서 공간 숨기기",
"links": "Social accounts",
"subspaces": {
"label": "Sub-spaces",
"information": "Sub-spaces will only be shown once configured on both the main space and the sub-space(s). {docs}",
"parent": {
"label": "Main space",
"placeholder": "pistachiodao.eth",
"information": "The space that this space is a sub-space of will be displayed on the space page"
},
"children": {
"label": "Sub-spaces",
"placeholder": "pistachiodao.eth",
"information": "Related Sub-spaces listed here will be displayed on the space page"
}
},
"website": "Website",
"strategies": {
"label": "Strategie(s)",
"information": "Strategies are used to determine voting power or whether a user is eligible to create a proposal"
},
"network": {
"label": "Network",
"information": "The defaul network used for this space. Networks can also be specified in individual strategies"
},
"symbol": {
"label": "Symbol",
"information": "The default symbol used for this space, usually the token symbol i.e. BAL for Balancer"
},
"strategiesList": "Select up to 8 strategies",
"votingPowerIsCumulative": "Voting power is cumulative",
"addStrategy": "전략 추가",
"testInPlayground": "Test in playground",
"admins": {
"label": "Admins",
"information": "Admins are able to modify the space settings and manage the space's proposals"
},
"authors": {
"label": "Authors",
"information": "Authors are always able to create proposals"
},
"proposalValidation": "제안 검증",
"validation": "Type",
"proposalThreshold": {
"label": "Threshold",
"information": "The minimum amount of voting power required to create a proposal"
},
"allowOnlyAuthors": "작성자만 제안서를 제출할 수 있도록 허용",
"editValidation": "유효성 검사 편집",
"selectValidation": "유효성 검사 선택",
"validationParameters": "검증 매개변수",
"voting": "투표",
"votingDelay": "투표 지연",
"votingPeriod": "투표 기간",
"hours": "Hours",
"days": "Days",
"quorum": {
"label": "Quorum",
"information": "The minimum amount of voting power required for the proposal to pass"
},
"type": {
"label": "Type",
"information": "The type of voting system used for this space. (Enforced on all future proposals)"
},
"anyType": "어떤",
"hideAbstain": "기본 투표 결과에서 기권 투표 무시",
"customDomain": "사용자 도메인",
"domain": {
"label": "Domain name",
"placeholder": "e.g. vote.balancer.fi",
"info": "To setup a custom domain you additionally need to open a pull request on github after you have created the space. {docs}"
},
"skin": "스킨",
"treasuries": {
"label": "Treasury",
"add": "Add treasury",
"edit": "Edit treasury",
"information": "Add treasuries of your organization to show them in your space"
},
"addPlugin": "플러그인 추가",
"editPlugin": "편집 플러그인",
"pluginParameters": "플러그인 매개변수",
"proposal": {
"title": "Proposal",
"guidelines": {
"title": "Guidelines",
"information": "Display a link to your guidelines on proposal creation to help users understand what constitutes a good/valid proposal"
},
"template": {
"title": "Template",
"information": "Start every proposal with a template to help users understand what information is required"
}
}
},
"setup": {
"example": "예시 yam.eth",
"chooseExistingEns": "공간을 만드는데 사용할 ENS 도메인 선택:",
"useSingleExistingEns": "당신의 ENS 도메인을 사용하세요:",
"orRegisterNewEns": "또는 새로운 도메인을 등록하세요:",
"demoTestnetEnsMessage": "To create a test space you need an ENS domain on {network}.",
"toCreateASpace": "공간을 만들려면, 먼저 ENS 도메인이 필요합니다. 아래에 하나를 작성하고 ENS 등록 지침을 따라주세요.",
"createASpace": "공간 만들기",
"registerEnsButton": "등록",
"supportedEnsTLDs": "지원되는 도메인 끝자리",
"helpDocsAndDiscordLinks": "Not sure how to setup your space? Learn more in the {docs} or join Snapshot {discord}.",
"setSpaceController": "Space controller",
"setSpaceControllerExists": "The snapshot text-record for this domain has already been set. Choose edit to change it, otherwise you can skip to the next step.",
"setSpaceControllerInfo": " The space controller is the account that will be able to manage the space settings. Additional space controllers (admins) can be added later.",
"setSpaceControllerInfoGnosisSafe": "When creating a space with a Gnosis Safe it's recommended to set the safe address as the space controller. If you don't, you need to follow some additional steps. {link}",
"editSpaceController": "ENS 관리자 수정",
"setController": "관리자 설정",
"explainControllerAndEns": "Setting the controller requires a transaction on the {network} which will add a \"snapshot\" TEXT record to your ENS domain.",
"confirmToSetAddress": "{address} 를 공간 관리자로 설정하겠습니까?",
"controllerHasAuthority": "관리자는 공간 설정에 대한 전체 권한을 갖습니다.",
"controller": "관리자",
"selectEnsForSpace": "ENS 주소 선택",
"spaceOwnerAddressPlaceHolder": "e.g. {address}",
"controllerAddress": "관리자 주소",
"updateController": "관리자 업데이트",
"seeOnEns": "ENS 보기",
"goToSettings": "설정으로 이동",
"setSpaceProfile": "Customize your space",
"waitForTransaction": "The transaction need to confirm before you can create your space. {txUrl}",
"pleaseWaitMessage": "This can take a few minutes, please wait while the transaction is being confirmed",
"notControllerAddress": "Please connect with the controller address {wallet} to create the space.",
"fillCurrentAccount": "Use currently logged in account",
"domain": {
"title": "Setup your space domain",
"ensMessage": " One thing you need before you can create your own space, is an ENS domain on Ethereum mainnet.",
"ensMessageTestnet": "You can also {link} on the Goerli testnet and mess with things there first.",
"tryDemo": "try the demo",
"yourExistingSpaces": "Your existing spaces",
"invalidEns": "This ENS name is invalid. Usually this is due the use of invalid characters during registration."
},
"strategy": {
"title": "How would you like to setup your voting strategy?",
"subtitle": "You can change your strategy settings any time.",
"blockTitle": "Setup voting strategy",
"onePersonOneVote": {
"title": "One person, one vote",
"description": "Manage a whitelist of people who can vote or simply allow any address to vote. Every vote is equal and no token is required",
"whitelistInformation": "Specify a number of accounts that can vote",
"ticketInformation": "Any account can vote",
"votesEqualInfo": "Each vote is equal and no token is required"
},
"tokenVoting": {
"title": "Token weighted voting",
"description": "Votes are weighted by a token. The token can be an ERC-20, ERC-721 or ERC-1155 token standard",
"tokenNotFound": "Token not found",
"seeOnEtherscan": "See on Etherscan"
},
"advanced": {
"title": "Custom setup",
"description": "Select up to 8 strategies with a wide range of options. If you can't find the right strategy for your use case, you can create your own"
}
},
"validationTitle": "Who can manage this space and create proposals?"
},
"profile": {
"buttonEdit": "Edit profile",
"viewProfile": "View profile",
"about": {
"header": "About",
"joinedSpaces": "Joined spaces",
"createdSpaces": "Created spaces",
"biography": "Bio",
"notJoinSpacesYet": "Hasn't joined any spaces yet",
"notCreatedSpacesYet": "Hasn't created any spaces yet",
"delegatorNetworkInfo": "Change by switching network in your wallet",
"delegate": "Delegate",
"delegated": "Delegated",
"delegateTo": "Delegate to",
"delegateFor": "Delegator for",
"noDelegatorsMessage": "No delegators on {network}",
"notSupportedNetwork": "Delegation currently isn't supported on {network} "
},
"activity": {
"header": "Activity",
"votedFor": "Voted {choice}",
"today": "Today",
"thisWeek": "This week",
"olderThanWeek": "Older than a week",
"noActivity": "No activity yet"
},
"settings": {
"header": "Edit profile",
"name": "Name",
"biography": "Bio",
"namePlaceholder": "Enter name",
"bioPlaceholder": "Tell your story",
"change": "Change",
"remove": "Remove"
}
},
"notify": {
"youDidIt": "해냈어요!",
"copied": "복사되었습니다!",
"proposalDeleted": "제안이 삭제됨",
"somethingWentWrong": "문제가 발생했습니다.",
"saved": "저장됨!",
"delegationSuccess": "위임 성공",
"delegationRemoved": "위임 제거됨",
"proposalCreated": "제안 생성됨",
"voteSuccessful": "투표가 완료되었습니다!",
"ensSet": "ENS 텍스트 레코드가 성공적으로 설정되었습니다.",
"transactionSent": "Transaction sent"
},
"explore": {
"createStrategy": "전략 만들기",
"createSkin": "스킨 생성",
"addNetwork": "네트워크 추가",
"createPlugin": "플러그인 만들기",
"strategies": "멜라 (들)",
"skins": "스킨",
"networks": "네트워크",
"plugins": "플러그인",
"results": "결과(에스)",
"category": "Category",
"categories": {
"all": "모두",
"protocol": "프로토콜",
"social": "소셜",
"investment": "투자",
"grant": "그랜트",
"service": "서비스",
"media": "미디어",
"creator": "크리에이터",
"collector": "컬렉터"
}
},
"voting": {
"selectVoting": "투표 유형 선택",
"single-choice": "단일 선택 투표",
"approval": "승인 투표",
"quadratic": "2차 투표",
"ranked-choice": "순위 선택 투표",
"weighted": "가중 투표",
"basic": "기본 투표",
"description": {
"single-choice": "각 유권자는 하나의 선택만 선택할 수 있습니다.",
"approval": "각 유권자는 선택의 수만큼 선택할 수 있습니다.",
"quadratic": "각 유권자는 선택의 수에 관계없이 투표권을 분산할 수 있습니다. 결과는 2차로 계산됩니다.",
"ranked-choice": "각 유권자는 원하는 수만큼 선택하고 순위를 매길 수 있습니다. 결과는 즉시 결선 투표 계수 방법으로 계산됩니다.",
"weighted": "각 유권자는 원하는 선택지들에 투표권을 행사 할 수있습니다.",
"basic": "세 가지 선택이 있는 단일 선택 투표: 찬성, 반대 또는 기권"
}
},
"privacy": {
"label": "Privacy",
"title": "Select voting privacy",
"information": "The type of privacy used on proposals. (Enforced on all future proposals)",
"any": "Any",
"none": "None",
"shutter": {
"label": "Shutter",
"description": "Choices are encrypted and only visible once the voting period is over",
"tooltip": "This proposal has Shutter privacy enabled. All votes will be encrypted until the voting period has ended and the final score is calculated",
"url": "https://blog.shutter.network/shielded-voting/"
}
},
"validation": {
"label": "Validation",
"title": "Select voting validation",
"information": "The type of validation used to determine if a user can vote. (Enforced on all future proposals)",
"any": {
"label": "Anyone can vote",
"description": "Anyone with voting power can cast a vote."
},
"basic": {
"label": "Basic",
"description": "Use any strategy to determine if a user can vote.",
"invalidMessage": "You do not meet the minimum balance requirement to vote on this proposal. "
},
"passport-gated": {
"label": "Gitcoin Passport gated",
"description": "Protect your proposals from spam and vote manipulation by requiring users to have a Gitcoin Passport.",
"invalidMessage": "You need a Gitcoin Passport with score above {scoreThreshold} and {operator} of the following stamps to vote on this proposal: {stamps}. "
}
},
"safeSnap": {
"currentOutcome": "현재 결과",
"currentBond": "현재 채권",
"finalizedIn": "확정 {0}",
"executableIn": "{0} 안에 실행가능함",
"finalOutcome": "결과",
"nextBond": "결과를 설정하기 위한 채권",
"setOutcomeTo": "결과를 다음으로 설정",
"claimBond": "채권 청구",
"addBatch": "트랜잭션 일괄 처리 추가",
"batch": "트랜잭션 일괄처리",
"transactions": "트랜잭션",
"to": "To (address)",
"invalidAddress": "잘못된 주소",
"invalidAmount": "잘못된 금액",
"invalidValue": "잘못된 값",
"invalidAbi": "잘못된 ABI",
"invalidData": "잘못된 데이터",
"value": "값 (wei)",
"data": "데이터",
"noCollectibles": "컬렉션 없음",
"asset": "자산",
"amount": "수량",
"type": "유형",
"transferFunds": "자금 전송",
"transferNFT": "NFT 전송",
"contractInteraction": "컨트랙트 상호 작용",
"rawTransaction": "로우 트랜잭션",
"addTransaction": "트랜잭션 추가",
"transactionLabels": {
"contractInteraction": "{functionName}() - {amount} wei에 {address}",
"transferFunds": "{amount}{tokenSymbol} 을 {address} 로 전송",
"transferNFT": "{name}#{id} 를 {address} 로 보내기",
"raw": "{amount} wei를 {address} 로 보내기"
},
"labels": {
"request": "요청 실행",
"setOutcome": "결과 설정",
"changeOutcome": "결과 변경",
"executeTxs": "{1} 의 트랜잭션 배치 {0} 실행",
"executed": "모든 트랜잭션이 실행되었습니다.",
"noTransactions": "There are no transactions to execute",
"rejected": "Proposal rejected",
"error": "뭔가 잘못되었습니다.",
"connectWallet": "실행 세부 정보를 보려면 지갑을 연결합니다.",
"switchChain": "실행 요청을 위해 지갑을 {0} 로 바꾸세요.",
"question": "이 제안이 통과되었을 때 다음을 충족",
"criteria": "통과 기준?",
"proposalPassed": "제안이 통과되었습니까?",
"expired": "The proposal has expired",
"approveBond": "Approve bond",
"confirmVoteResults": "Click to confirm the proposal passed",
"executeTxsUma": "Execute transaction batch",
"deleteDisputedProposal": "Delete disputed proposal",
"confirmVoteResultsToolTip": "Make sure this proposal was approved by this Snapshot vote before proposing on-chain. If the Snapshot vote rejected the proposal, the on-chain proposal will be rejected as well and you will lose your bond.",
"approveBondToolTip": "On-chain proposals require a bond from the proposer. This will approve tokens from your wallet to be posted as a bond. If you make an invalid proposal, it will be disputed and you will lose your bond. If the proposal is valid, your bond will be returned when the transactions are executed.",
"requestToolTip": "This will propose the transactions from this Snapshot vote on-chain. After a challenge window, if the proposal is valid, the transactions can be executed and your bond will be returned.",
"executeToolTip": "This will execute the transactions from this proposal and return the proposer's bond."
}
},
"poap": {
"no_poap_header": "이 제안에 대한 POAP이 아직 설정되지 않았습니다.",
"no_voted_header": "POAP를 얻으려면 투표하세요",
"unclaimed_header": "POAP에 투표했습니다.",
"claimed_header": "축하합니다! POAP이 당신의 컬렉션에 발행되었습니다.",
"loading_header": "POAP이 당신의 컬렉션에 발행되고 있습니다.",
"button_claim": "발행",
"button_show": "컬렉션 보기",
"success_claim": "POAP이 컬렉션에 발행되었습니다.",
"error_claim": "토큰 발행에 문제가 발생했습니다."
},
"progress": {
"progress": "Progress",
"inProgress": "In Progress",
"completed": "Completed",
"complete": "Complete",
"newStep": "New Step",
"description": "Description",
"add": "Add",
"deleteStep": "Delete Step",
"deleteConfirm": "Are you sure you want to delete?",
"delete": "Delete",
"cancel": "Cancel",
"comeBack": "Come back after voting is complete to see how this proposal is progressing!",
"confirmSignature": "Signing this message will allow us to authorize your request to update the progress of your proposal.",
"wentWrong": "Oops something went wrong.",
"voting": "Voting",
"soon": "Soon"
},
"charts": {
"charts": "차트",
"noVotesYet": "아직 시각화할 투표가 없습니다.",
"totalVotesPerDay": "하루 총 투표수",
"shareOfVotingPower": "의결권 점유율",
"votingPowerPerDay": "하루 투표권"
},
"comment_box": {
"title": "주석 상자",
"add": "여기에 의견 추가",
"submit": "제출",
"preview": "미리보기",
"continue_editing": "편집 계속",
"edit": "댓글 편집",
"edit_button": "편집",
"dismiss": "취소",
"delete": "삭제",
"add_reply": "댓글 추가",
"edit_comment": "댓글 편집",
"edit_modal": "수정하시겠습니까?",
"yes": "예",
"no": "아니오",
"delete_comment": "댓글 삭제",
"delete_modal": "삭제 하시겠습니까?",
"error": "문제가 발생했습니다.",
"replies": "답글",
"hide": "숨김",
"show": "표시",
"reply": "답변",
"load_more": "더 불러오기"
},
"page": {
"title": {
"home": "스냅샷",
"setup": "공간 만들기",
"timeline": "타임라인",
"notifications": "Notifications",
"explore": "탐색",
"playground": "플레이그라운드",
"space": {
"create": "{space} 제안 만들기",
"about": "{space} 정보",
"proposals": "{space} 제안",
"proposal": "{space} 제안: {proposal}",
"settings": "{space} 설정"
},
"strategy": "{key} 전략",
"delegate": "위임",
"ranking": "Ranking"
}
},
"hal": {
"title": "{spaceName} 에서 제안 추적",
"text": "새 제안이 생성되거나 종료될 때마다 알림 수신"
},
"timeUnits": {
"second": "1 초 | {n} 초",
"minute": "1 분 | {n} 분",
"hour": "1 시간 | {n} 시간",
"day": "1 일 | {n} 일",
"week": "1 주 | {n} 주",
"month": "1 개월 | {n} 개월",
"year": "1 년 | {n} 년"
},
"unsupportedNetwork": {
"unsupportedNetwork": "지원되지 않는 네트워크",
"switchNetworkToNetwork": "To continue, you need to change the network in your wallet to {network}.",
"switchToNetwork": "Switch to {network}",
"goToDemoSite": "Go to demo website"
},
"treasury": {
"title": "Treasury",
"wallets": {
"title": "Wallets",
"empty": "This space doesn't have a treasury yet",
"addTreasury": "Add a treasury"
},
"assets": {
"title": "Assets",
"empty": "There are no assets in this contract"
},
"24hChange": "24h change"
},
"newsletter": {
"yourEmail": "Your email",
"title": "Get the latest Snapshot updates"
},
"joinCommunity": "Join Snapshot community",
"header": {
"title": "Where decisions get made",
"description": "Snapshot is a free, open-source platform for community governance. Create your own space now and start making decisions!"
},
"aboutPage": {
"description": "Snapshot is a decentralized governance platform that makes it easy to create and vote on proposals - all without spending a fortune on gas fees! Plus, our flexible system supports various voting types and strategies, so you can tailor the voting process to your needs.",
"subHeader": "Governance should be a snap",
"subDescription": "Web3 governance doesn't have to be complicated. Snapshot is the perfect solution for organizations looking for an easy and efficient way to govern their community or organization."
},
"footerView": {
"resources": "Resources",
"about": "About",
"blog": "Blog",
"jobs": "Jobs",
"discussions": "Discussions",
"github": "GitHub",
"docs": "Docs",
"support": "Support",
"hiring": "Join us!"
}
}
================================================
FILE: src/locales/languages.json
================================================
{
"en-US": {
"name": "English"
},
"zh-CN": {
"name": "Chinese",
"nativeName": "简体中文"
},
"ja-JP": {
"name": "Japanese",
"nativeName": "日本語"
},
"ko-KR": {
"name": "Korean",
"nativeName": "한국어"
},
"fr-FR": {
"name": "French",
"nativeName": "Français"
}
}
================================================
FILE: src/locales/pt-PT.json
================================================
{
"searchPlaceholder": "Pesquisar",
"spaceCount": "{0} comunidade(s)",
"createSpace": "Criar comunidade",
"backToHome": "Início",
"actions": "Ações",
"poweredBy": "Powered by",
"results": "Resultados",
"resultsError": "Results could not be calculated. This is often due to a misconfigured strategy or an unresponsive RPC node involved in the strategy.",
"resultsCalculating": "Final results are being calculated. If you still see this message after a few minutes contact the space admin.",
"votingPowerFailedMessage": "Your voting power could not be calculated. This is often due to a misconfigured strategy or an unresponsive RPC node involved in the strategy.",
"votingValidationFailedMessage": "There was an error on our side and we could not verify if you are eligible to vote. This is often due to a misconfigured voting validation or an unresponsive API involved in the validation.",
"notValidVoterMessage": "Oops, you don't seem to be eligible to vote on this proposal.",
"getHelp": "Get help",
"retry": "Retry",
"currentResults": "Resultados Atuais",
"reset": "Restaurar",
"close": "Close",
"save": "Guardar",
"author": "Autor",
"next": "Seguinte",
"choice": "Choice",
"submit": "Enviar",
"plugins": "Plugins",
"information": "Informações",
"confirm": "Confirmar",
"snapshot": "Snapshot",
"strategies": "Estratégia(s)",
"strategiesPage": "Estratégias",
"space": "Espaço",
"spaces": "Espaços",
"verifiedSpace": "Verified space",
"warningSpace": "This space has been flagged as potentially malicious. Proceed with caution.",
"version": "Versão",
"timeline": "Cronologia",
"ended": "ended",
"started": "started",
"filters": "Filtros",
"allSpaces": "Todos os espaços",
"submitOnchain": "Enviar na cadeia",
"inSpaces": "Em {0} comunidade(s)",
"votes": "Votos",
"seeMore": "Ver mais",
"seeAll": "See all",
"network": "Rede",
"networks": "Redes",
"skins": "Skins",
"spaceMembers": "Members",
"members": "Sem membros, {count} membro, {count} membros",
"editStrategy": "Editar estratégia",
"invalidProposals": "Propostas inválidas",
"account": "Conta",
"create3box": "Criar perfil no 3Box",
"view3box": "Ver perfil no 3Box",
"edit3box": "Editar perfil no 3Box",
"connectWallet": "Conectar carteira",
"toggleSkin": "Toggle skin",
"about": "Sobre nós",
"license": "Licença",
"showMore": "Show more",
"voted": "Voted",
"reload": "Reload",
"ipfsServer": "Protocolo IPFS",
"hub": "Hub",
"cancel": "Cancelar",
"delete": "Delete",
"demoSite": "Este é o site de demonstração, experimente!",
"removeDelegation": "Remover delegação",
"confirmRemove": "Tem certeza que deseja remover sua delegação a",
"removeSpace": "para a comunidade {0}",
"noVotingPower": "Oops, it seems you don't have any voting power at block {blockNumber}.",
"quorumReached": "quorum reached",
"options": "Opções",
"votingPower": "Seu poder de voto",
"comment": {
"placeholder": "Share your reason (optional)"
},
"receipt": "Comprovativo",
"relayer": "Retransmissor",
"verifyOnMycrypto": "Verificar comprovativo no MyCrypto",
"verifyOnSignatorio": "Verify on Signator.io",
"isCore": "Core",
"notificationsBlocked": "Your browser is blocking notifications",
"notificationsNotSupported": "Your browser does not support notifications",
"walletNotSupported": "Wallet is not supported",
"seeInExplorer": "See explorer",
"learnMore": "Saber mais",
"logout": "Terminar sessão",
"continue": "Continuar",
"add": "Adicionar",
"edit": "Editar",
"strategyParameters": "Parâmetros de estratégia",
"addAction": "Adicionar ação",
"removeAction": "Remover ação",
"yourChoice": "Opção {0}",
"targetAddress": "Endereço de destino",
"value": "Valor",
"date": "Dados",
"marketDetails": "Detalhes do mercado",
"addMarket": "Adicionar mercado",
"selectNetwork": "Selecionar rede",
"conditionId": "Condition ID",
"basetokenAddress": "Endereço base do token",
"quoteAddress": "Cotar endereço da moeda",
"removeMarket": "Excluir mercado",
"back": "Anterior",
"loading": "A carregar...",
"predictedImpact": "Impacto previsto",
"marketSymbol": "{0} mercado",
"twoChoicesRequired": "Duas opções são necessárias para este plugin.",
"noResultsFound": "Ops, não encontramos nenhum resultado",
"createFirstProposal": "Let's create your first proposal",
"noSpacesJoined": "Ops, você ainda não entrou em nenhuma comunidade",
"addFavorites": "Adicionar favoritos",
"createdBy": "Por {0}",
"startIn": "iniciar: {0}",
"endIn": "terminar {0}",
"proposalTimeLeft": "{0} esquerda",
"endedAgo": "finalizado {0}",
"proposalBy": "por {0}",
"endDate": "termina {0}",
"contentHash": "Hash do conteúdo",
"defaultSkin": "Skin padrão",
"select": "Selecionar",
"language": "Idioma",
"agree": "Eu concordo",
"moderators": "Moderators",
"playground": "Parque infantil",
"strategyParams": "Parâmetros de estratégia",
"addresses": "Moradas",
"networkErrorPlayground": "Erro de rede - por favor, abra o console do seu navegador para mais informações",
"upload": "Upload",
"join": "Junte-se",
"joined": "Juntou-se",
"leave": "Sair",
"subspaces": "Sub-spaces",
"mainspace": "Main space",
"copyLink": "Copiar link",
"duplicate": "Duplicate",
"joinedSpaces": "Juntou espaços",
"joinSpaces": "Juntar-se aos espaços",
"setDelegationToSpace": "Limit delegation to a specific space",
"theCurrentNetwork": "the current network",
"optional": "(optional)",
"homeLoadmore": "Load more",
"confirmAction": "Confirm action",
"or": "or",
"share": "Share",
"shareOnTwitter": "Share on Twitter",
"shareOnLenster": "Share on Lenster",
"createButton": "Create",
"discussion": "Discussion",
"changeWallet": "Change wallet",
"createASpace": "Create a space",
"getStarted": "Get started",
"skip": "Skip",
"newSpaceNotice": {
"header": "Your space is live!",
"mainText": "You can change how voting power is calculated via strategies in your {settings}. Changes to your settings will only affect new proposals, existing proposals can not be changed.",
"learnMore": "Learn more in the {documentation} or join Snapshot {discord} for help.",
"gotIt": "Got it!"
},
"errors": {
"required": "O campo é obrigatório",
"minLength": "Field is required",
"maxLength": "O comprimento máximo é {0}",
"pattern": "Caracteres Inválidos",
"minItems": "Mínimo {0} item(s) obrigatório(s)",
"maxItems": "Maximum {0} item(s) allowed",
"minStrategy": "É necessária pelo menos uma estratégia.",
"website": "URL should be in the format https://www.example.com",
"format": "Formato inválido",
"type": "Contexto",
"unsupportedImageType": "File type not supported, Supported formats are jpeg, jpg and png",
"invalidAddress": "Invalid address"
},
"create": {
"proposalTitle": "Title",
"discussion": "Discussion (optional)",
"categorie(s)": "Select up to 2 categorie(s)",
"proposalDescription": "Description (optional)",
"preview": "Pré-visualizar",
"choices": "Opções",
"addChoice": "Adicionar opção",
"startDate": "Seleciona uma data de início",
"endDate": "Selecionar Data Final",
"startTime": "Selecionar horário de início",
"endTime": "Selecionar horário de término",
"publish": "Publicar",
"untitled": "Untitled",
"snapshotBlock": "Número de bloco Snapshot",
"voting": "Voting",
"votingSystem": "Voting system",
"choice": "Choice {0}",
"period": "Voting period",
"start": "Start",
"end": "End",
"days": "Days",
"hours": "Hours",
"minutes": "Minutes",
"schedule": "Schedule proposal",
"delayEnforced": "The space enforces a delay until voting can start",
"periodEnforced": "The space enforces the duration of the voting period",
"typeEnforced": "{type} is enforced by the space",
"privacyEnforced": "{type} is enforced by the space",
"edit": "Edit",
"continue": "Continue",
"now": "Now",
"votingPeriodExplainer": "This is the time period in which users will be able to vote. The proposal will be visible and pending before the start of the voting period.",
"uploadImageExplainer": "Attach images by dragging & dropping, selecting or pasting them.",
"uploading": "Uploading image",
"markdown": "Styling with Markdown is supported",
"validationWarning": {
"basic": {
"member": "Você precisa ser um autor deste espaço para poder submeter uma proposta.",
"minScore": "Você precisa ter um mínimo de {0} {1} para submeter uma proposta."
},
"customValidation": "Você precisa passar a validação da proposta para submeter uma proposta.",
"executionError": "Verifying your eligibility to create proposals in this space has failed. This is likely due to a misconfigured strategy."
},
"errorGettingSnapshot": "We encountered an error while fetching the snapshot block number which is needed to calculate your voting power. Please try again later."
},
"delegate": {
"header": "Delegar",
"selectDelegate": "Selecionar delegado",
"to": "Para",
"addressPlaceholder": "Nome do endereço ou ENS",
"delegations": "Sua(s) delegação(ões)",
"allSpaces": "Para todos os espaços",
"delegated": "Delegado a você",
"pendingTransaction": "sem transações pendentes | 1 transação pendente | {count} transações pendentes",
"topDelegates": "Principais delegados",
"noDelegatesFoundFor": "No delegates found for {0}",
"noValidEns": "Not a valid ENS address.",
"noValidAddress": "Not a valid address",
"delegateToSelf": "You cannot delegate to yourself",
"delegateToSelfAddress": "You cannot delegate to your own ENS address",
"noValidSpaceId": "Not a valid space ID",
"noDelegationsAndDelegates": "Can't find your delegations and delegates? Make sure you are connected to the correct network.",
"delegateNotSupported": "Delegation is currently not supported for {network}."
},
"proposal": {
"castVote": "Votar",
"vote": "Votar",
"startDate": "Data de início",
"endDate": "Data final",
"votingSystem": "Sistema de votação",
"privacy": "Privacy",
"invalidChoice": "Invalid choice",
"postVoteModal": {
"defaultTitle": "Your vote is in!",
"gnosisSafeTitle": "Your vote is pending...",
"gnosisSafeDescription": " Votes with a Safe require additional signers and will be visible once the transaction is confirmed",
"seeQueue": "See queued transactions",
"tips": {
"1": "Votes can be changed while the proposal is active"
}
}
},
"proposals": {
"header": "Propostas",
"new": "Nova proposta",
"noProposals": "Ainda não há nenhuma proposta aqui!",
"createProposal": "Create proposal",
"showMore": "Show more",
"showLess": "Show less",
"states": {
"all": "Todas",
"core": "Core",
"community": "Comunidade",
"active": "Ativas",
"pending": "Pendente",
"closed": "Encerradas"
}
},
"notifications": {
"header": "Notifications",
"noNotifications": "You have no notifications",
"proposalStarted": "proposal has started:",
"proposalEnded": "proposal has ended:",
"markAllAsRead": "Mark all as read",
"all": "All",
"unread": "Unread"
},
"modalTerms": {
"mustAgreeTo": "To {action} this space, you must agree to the {spaceName} terms of service.",
"actionJoin": "join",
"actionCreate": "create a proposal in",
"actionVote": "vote in"
},
"settings": {
"header": "Definições",
"editController": "Edit controller",
"connectWithSpaceOwner": "You are in view only mode, to modify space settings connect with a controller or admin wallet.",
"gnosisWrongNetwork": {
"base": "Your Gnosis Safe is on the wrong network. Please connect to {network} to {action}.",
"settings": "edit the space settings",
"create": "create a proposal",
"vote": "vote on this proposal"
},
"currentSpaceControllerIs": "The current space controller is {address}",
"newController": "New controller",
"noRecord": "No text-record found. Make sure you have registered {id} domain on {network}, then edit the controller text-record to regain access to the space settings.",
"set": "Set",
"profile": "Perfil",
"avatar": "Imagem de Perfil",
"name": {
"label": "Name",
"placeholder": "e.g. Yam Network"
},
"about": {
"label": "About",
"placeholder": "What is your organisation about?"
},
"categories": {
"label": "Categorie(s)",
"select": "Select categorie(s)"
},
"terms": {
"label": "Terms of service",
"information": "Users will be required to accept these terms once before they can create a proposal or cast a vote"
},
"hideSpace": "Ocultar comunidade da página inicial",
"links": "Social accounts",
"subspaces": {
"label": "Sub-spaces",
"information": "Sub-spaces will only be shown once configured on both the main space and the sub-space(s). {docs}",
"parent": {
"label": "Main space",
"placeholder": "pistachiodao.eth",
"information": "The space that this space is a sub-space of will be displayed on the space page"
},
"children": {
"label": "Sub-spaces",
"placeholder": "pistachiodao.eth",
"information": "Related Sub-spaces listed here will be displayed on the space page"
}
},
"website": "Website",
"strategies": {
"label": "Strategie(s)",
"information": "Strategies are used to determine voting power or whether a user is eligible to create a proposal"
},
"network": {
"label": "Network",
"information": "The defaul network used for this space. Networks can also be specified in individual strategies"
},
"symbol": {
"label": "Symbol",
"information": "The default symbol used for this space, usually the token symbol i.e. BAL for Balancer"
},
"strategiesList": "Select up to 8 strategies",
"votingPowerIsCumulative": "Voting power is cumulative",
"addStrategy": "Adicionar estratégia",
"testInPlayground": "Test in playground",
"admins": {
"label": "Admins",
"information": "Admins are able to modify the space settings and manage the space's proposals"
},
"authors": {
"label": "Authors",
"information": "Authors are always able to create proposals"
},
"proposalValidation": "Validação da proposta",
"validation": "Type",
"proposalThreshold": {
"label": "Threshold",
"information": "The minimum amount of voting power required to create a proposal"
},
"allowOnlyAuthors": "Permitir apenas autores para submeter a proposta",
"editValidation": "Editar validação",
"selectValidation": "Selecionar validação",
"validationParameters": "Parâmetros de validação",
"voting": "Votar",
"votingDelay": "Atraso na votação",
"votingPeriod": "Período de votação",
"hours": "Hours",
"days": "Days",
"quorum": {
"label": "Quorum",
"information": "The minimum amount of voting power required for the proposal to pass"
},
"type": {
"label": "Type",
"information": "The type of voting system used for this space. (Enforced on all future proposals)"
},
"anyType": "Qualquer",
"hideAbstain": "Ignore abstain votes in basic voting results",
"customDomain": "Domínio Personalizado",
"domain": {
"label": "Domain name",
"placeholder": "e.g. vote.balancer.fi",
"info": "To setup a custom domain you additionally need to open a pull request on github after you have created the space. {docs}"
},
"skin": "Skin",
"treasuries": {
"label": "Treasury",
"add": "Add treasury",
"edit": "Edit treasury",
"information": "Add treasuries of your organization to show them in your space"
},
"addPlugin": "Adicionar plugin",
"editPlugin": "Editar plugin",
"pluginParameters": "Parâmetros do plugin",
"proposal": {
"title": "Proposal",
"guidelines": {
"title": "Guidelines",
"information": "Display a link to your guidelines on proposal creation to help users understand what constitutes a good/valid proposal"
},
"template": {
"title": "Template",
"information": "Start every proposal with a template to help users understand what information is required"
}
}
},
"setup": {
"example": "p. ex. yam.eth",
"chooseExistingEns": "Choose one of your existing ENS domains to create a space with:",
"useSingleExistingEns": "Use your existing ENS domain:",
"orRegisterNewEns": "Or register a new domain:",
"demoTestnetEnsMessage": "To create a test space you need an ENS domain on {network}.",
"toCreateASpace": "To create a space, you first need an ENS domain. Enter one below and follow the ENS registration instructions.",
"createASpace": "Criar comunidade",
"registerEnsButton": "Register",
"supportedEnsTLDs": "Supported domain endings",
"helpDocsAndDiscordLinks": "Not sure how to setup your space? Learn more in the {docs} or join Snapshot {discord}.",
"setSpaceController": "Space controller",
"setSpaceControllerExists": "The snapshot text-record for this domain has already been set. Choose edit to change it, otherwise you can skip to the next step.",
"setSpaceControllerInfo": " The space controller is the account that will be able to manage the space settings. Additional space controllers (admins) can be added later.",
"setSpaceControllerInfoGnosisSafe": "When creating a space with a Gnosis Safe it's recommended to set the safe address as the space controller. If you don't, you need to follow some additional steps. {link}",
"editSpaceController": "Edit controller on ENS",
"setController": "Set controller",
"explainControllerAndEns": "Setting the controller requires a transaction on the {network} which will add a \"snapshot\" TEXT record to your ENS domain.",
"confirmToSetAddress": "Are you sure you want to set {address} as the controller of the space?",
"controllerHasAuthority": "The controller has full authority over the space settings",
"controller": "Controller",
"selectEnsForSpace": "Choose ENS address",
"spaceOwnerAddressPlaceHolder": "e.g. {address}",
"controllerAddress": "Controller address",
"updateController": "Update controller",
"seeOnEns": "See on ENS",
"goToSettings": "Go to settings",
"setSpaceProfile": "Customize your space",
"waitForTransaction": "The transaction need to confirm before you can create your space. {txUrl}",
"pleaseWaitMessage": "This can take a few minutes, please wait while the transaction is being confirmed",
"notControllerAddress": "Please connect with the controller address {wallet} to create the space.",
"fillCurrentAccount": "Use currently logged in account",
"domain": {
"title": "Setup your space domain",
"ensMessage": " One thing you need before you can create your own space, is an ENS domain on Ethereum mainnet.",
"ensMessageTestnet": "You can also {link} on the Goerli testnet and mess with things there first.",
"tryDemo": "try the demo",
"yourExistingSpaces": "Your existing spaces",
"invalidEns": "This ENS name is invalid. Usually this is due the use of invalid characters during registration."
},
"strategy": {
"title": "How would you like to setup your voting strategy?",
"subtitle": "You can change your strategy settings any time.",
"blockTitle": "Setup voting strategy",
"onePersonOneVote": {
"title": "One person, one vote",
"description": "Manage a whitelist of people who can vote or simply allow any address to vote. Every vote is equal and no token is required",
"whitelistInformation": "Specify a number of accounts that can vote",
"ticketInformation": "Any account can vote",
"votesEqualInfo": "Each vote is equal and no token is required"
},
"tokenVoting": {
"title": "Token weighted voting",
"description": "Votes are weighted by a token. The token can be an ERC-20, ERC-721 or ERC-1155 token standard",
"tokenNotFound": "Token not found",
"seeOnEtherscan": "See on Etherscan"
},
"advanced": {
"title": "Custom setup",
"description": "Select up to 8 strategies with a wide range of options. If you can't find the right strategy for your use case, you can create your own"
}
},
"validationTitle": "Who can manage this space and create proposals?"
},
"profile": {
"buttonEdit": "Edit profile",
"viewProfile": "View profile",
"about": {
"header": "About",
"joinedSpaces": "Joined spaces",
"createdSpaces": "Created spaces",
"biography": "Bio",
"notJoinSpacesYet": "Hasn't joined any spaces yet",
"notCreatedSpacesYet": "Hasn't created any spaces yet",
"delegatorNetworkInfo": "Change by switching network in your wallet",
"delegate": "Delegate",
"delegated": "Delegated",
"delegateTo": "Delegate to",
"delegateFor": "Delegator for",
"noDelegatorsMessage": "No delegators on {network}",
"notSupportedNetwork": "Delegation currently isn't supported on {network} "
},
"activity": {
"header": "Activity",
"votedFor": "Voted {choice}",
"today": "Today",
"thisWeek": "This week",
"olderThanWeek": "Older than a week",
"noActivity": "No activity yet"
},
"settings": {
"header": "Edit profile",
"name": "Name",
"biography": "Bio",
"namePlaceholder": "Enter name",
"bioPlaceholder": "Tell your story",
"change": "Change",
"remove": "Remove"
}
},
"notify": {
"youDidIt": "Conseguiu!",
"copied": "Copiado!",
"proposalDeleted": "Proposta excluída",
"somethingWentWrong": "Ops, algo correu mal!",
"saved": "Guardado!",
"delegationSuccess": "Delegação bem sucedida",
"delegationRemoved": "Delegação removida",
"proposalCreated": "Proposta criada",
"voteSuccessful": "Seu voto foi processado!",
"ensSet": "ENS text record was successfully set",
"transactionSent": "Transaction sent"
},
"explore": {
"createStrategy": "Criar estratégia",
"createSkin": "Criar skin",
"addNetwork": "Adicionar rede",
"createPlugin": "Criar plug-in",
"strategies": "estratégia(s)",
"skins": "skin(s)",
"networks": "rede(s)",
"plugins": "plugin(s)",
"results": "resultado(s)",
"category": "Category",
"categories": {
"all": "All",
"protocol": "Protocol",
"social": "Social",
"investment": "Investment",
"grant": "Grant",
"service": "Service",
"media": "Media",
"creator": "Creator",
"collector": "Collector"
}
},
"voting": {
"selectVoting": "Selecionar sistema de votação",
"single-choice": "Voto de escolha única",
"approval": "Votação de aprovação",
"quadratic": "Voto em Quadrático",
"ranked-choice": "Votação por escolha ranqueada",
"weighted": "Votação ponderada",
"basic": "Basic voting",
"description": {
"single-choice": "Cada eleitor pode selecionar apenas uma opção.",
"approval": "Cada eleitor pode selecionar qualquer número de opções.",
"quadratic": "Cada eleitor pode distribuir o poder de voto por qualquer número de escolhas. Os resultados são calculados quadraticamente.",
"ranked-choice": "Cada eleitor pode selecionar e classificar qualquer número de escolhas. Os resultados são calculados pelo método de contagem instantânea.",
"weighted": "Cada eleitor pode espalhar o poder de voto em qualquer número de escolhas.",
"basic": "Single choice voting with three choices: For, Against or Abstain"
}
},
"privacy": {
"label": "Privacy",
"title": "Select voting privacy",
"information": "The type of privacy used on proposals. (Enforced on all future proposals)",
"any": "Any",
"none": "None",
"shutter": {
"label": "Shutter",
"description": "Choices are encrypted and only visible once the voting period is over",
"tooltip": "This proposal has Shutter privacy enabled. All votes will be encrypted until the voting period has ended and the final score is calculated",
"url": "https://blog.shutter.network/shielded-voting/"
}
},
"validation": {
"label": "Validation",
"title": "Select voting validation",
"information": "The type of validation used to determine if a user can vote. (Enforced on all future proposals)",
"any": {
"label": "Anyone can vote",
"description": "Anyone with voting power can cast a vote."
},
"basic": {
"label": "Basic",
"description": "Use any strategy to determine if a user can vote.",
"invalidMessage": "You do not meet the minimum balance requirement to vote on this proposal. "
},
"passport-gated": {
"label": "Gitcoin Passport gated",
"description": "Protect your proposals from spam and vote manipulation by requiring users to have a Gitcoin Passport.",
"invalidMessage": "You need a Gitcoin Passport with score above {scoreThreshold} and {operator} of the following stamps to vote on this proposal: {stamps}. "
}
},
"safeSnap": {
"currentOutcome": "Resultado atual",
"currentBond": "Vínculo atual",
"finalizedIn": "{0} finalizado",
"executableIn": "Executável {0}",
"finalOutcome": "Resultado",
"nextBond": "Contexto",
"setOutcomeTo": "Definir resultado para",
"claimBond": "Alegar bond",
"addBatch": "Adicionar lote de transação",
"batch": "Lote de transação",
"transactions": "Transações",
"to": "To (address)",
"invalidAddress": "Adresso inválido",
"invalidAmount": "Quantia inválida",
"invalidValue": "Valor inválido",
"invalidAbi": "ABI inválido",
"invalidData": "Dados Inválidos",
"value": "Valor (wei)",
"data": "Dados",
"noCollectibles": "Não colecionáveis",
"asset": "Ativo",
"amount": "Contexto",
"type": "Tipo",
"transferFunds": "Transferir Fundos",
"transferNFT": "Transferir NFT",
"contractInteraction": "interação do contrato",
"rawTransaction": "Raw transaction",
"addTransaction": "Add transaction",
"transactionLabels": {
"contractInteraction": "{functionName}() - {amount} wei to {address}",
"transferFunds": "Transfer {amount} {tokenSymbol} to {address}",
"transferNFT": "Send {name} #{id} to {address}",
"raw": "Send {amount} wei to {address}"
},
"labels": {
"request": "Requisitar execução",
"setOutcome": "Definir resultado para",
"changeOutcome": "Alterar resultado",
"executeTxs": "Execute transaction batch {0} of {1}",
"executed": "All transactions have been executed",
"noTransactions": "There are no transactions to execute",
"rejected": "Proposal rejected",
"error": "Something went wrong",
"connectWallet": "Connect wallet to see execution details",
"switchChain": "Switch your wallet to {0} to request execution",
"question": "Esta proposta foi aprovada e atende aos",
"criteria": "Critérios de Aceitação?",
"proposalPassed": "Did the proposal pass?",
"expired": "The proposal has expired",
"approveBond": "Approve bond",
"confirmVoteResults": "Click to confirm the proposal passed",
"executeTxsUma": "Execute transaction batch",
"deleteDisputedProposal": "Delete disputed proposal",
"confirmVoteResultsToolTip": "Make sure this proposal was approved by this Snapshot vote before proposing on-chain. If the Snapshot vote rejected the proposal, the on-chain proposal will be rejected as well and you will lose your bond.",
"approveBondToolTip": "On-chain proposals require a bond from the proposer. This will approve tokens from your wallet to be posted as a bond. If you make an invalid proposal, it will be disputed and you will lose your bond. If the proposal is valid, your bond will be returned when the transactions are executed.",
"requestToolTip": "This will propose the transactions from this Snapshot vote on-chain. After a challenge window, if the proposal is valid, the transactions can be executed and your bond will be returned.",
"executeToolTip": "This will execute the transactions from this proposal and return the proposer's bond."
}
},
"poap": {
"no_poap_header": "Um POAP ainda não foi configurado para esta proposta :'(",
"no_voted_header": "Vote para obter este POAP",
"unclaimed_header": "Mint your I voted POAP",
"claimed_header": "Congratulations! The POAP has been minted to your collection",
"loading_header": "The POAP is being minted to your collection",
"button_claim": "Mint",
"button_show": "Browse collection",
"success_claim": "The POAP has been minted to your collection",
"error_claim": "Ocorreu um problema ao cunhar o token"
},
"progress": {
"progress": "Progress",
"inProgress": "In Progress",
"completed": "Completed",
"complete": "Complete",
"newStep": "New Step",
"description": "Description",
"add": "Add",
"deleteStep": "Delete Step",
"deleteConfirm": "Are you sure you want to delete?",
"delete": "Delete",
"cancel": "Cancel",
"comeBack": "Come back after voting is complete to see how this proposal is progressing!",
"confirmSignature": "Signing this message will allow us to authorize your request to update the progress of your proposal.",
"wentWrong": "Oops something went wrong.",
"voting": "Voting",
"soon": "Soon"
},
"charts": {
"charts": "Charts",
"noVotesYet": "There are no votes to visualize yet.",
"totalVotesPerDay": "Total votes per day",
"shareOfVotingPower": "Share of voting power",
"votingPowerPerDay": "Voting power per day"
},
"comment_box": {
"title": "Comment box",
"add": "Add your comment here",
"submit": "Submit",
"preview": "Preview",
"continue_editing": "Continue editing",
"edit": "Edit your reply here",
"edit_button": "Edit",
"dismiss": "Dismiss",
"delete": "Delete",
"add_reply": "Add your reply here",
"edit_comment": "Edit comment",
"edit_modal": "Are you sure you want to edit?",
"yes": "Yes",
"no": "No",
"delete_comment": "Delete comment",
"delete_modal": "Are you sure you want to delete?",
"error": "Oops, something went wrong",
"replies": "replies",
"hide": "Hide",
"show": "Show",
"reply": "Reply",
"load_more": "Load more"
},
"page": {
"title": {
"home": "Snapshot",
"setup": "Create a space",
"timeline": "Timeline",
"notifications": "Notifications",
"explore": "Explore",
"playground": "Playground",
"space": {
"create": "Create {space} proposal",
"about": "About {space}",
"proposals": "{space} Proposals",
"proposal": "{space} proposal: {proposal}",
"settings": "{space} Settings"
},
"strategy": "{key} strategy",
"delegate": "Delegate",
"ranking": "Ranking"
}
},
"hal": {
"title": "Track proposals for {spaceName}",
"text": "Receive notifications every time a new proposal is created or ends"
},
"timeUnits": {
"second": "1 segundo | {n} segundos",
"minute": "1 minuto | {n} minutos",
"hour": "1 hour | {n} horas",
"day": "1 dia | {n} dias",
"week": "1 semana | {n} semanas",
"month": "1 mês | {n} meses",
"year": "1 ano | {n} anos"
},
"unsupportedNetwork": {
"unsupportedNetwork": "Unsupported network",
"switchNetworkToNetwork": "To continue, you need to change the network in your wallet to {network}.",
"switchToNetwork": "Switch to {network}",
"goToDemoSite": "Go to demo website"
},
"treasury": {
"title": "Treasury",
"wallets": {
"title": "Wallets",
"empty": "This space doesn't have a treasury yet",
"addTreasury": "Add a treasury"
},
"assets": {
"title": "Assets",
"empty": "There are no assets in this contract"
},
"24hChange": "24h change"
},
"newsletter": {
"yourEmail": "Your email",
"title": "Get the latest Snapshot updates"
},
"joinCommunity": "Join Snapshot community",
"header": {
"title": "Where decisions get made",
"description": "Snapshot is a free, open-source platform for community governance. Create your own space now and start making decisions!"
},
"aboutPage": {
"description": "Snapshot is a decentralized governance platform that makes it easy to create and vote on proposals - all without spending a fortune on gas fees! Plus, our flexible system supports various voting types and strategies, so you can tailor the voting process to your needs.",
"subHeader": "Governance should be a snap",
"subDescription": "Web3 governance doesn't have to be complicated. Snapshot is the perfect solution for organizations looking for an easy and efficient way to govern their community or organization."
},
"footerView": {
"resources": "Resources",
"about": "About",
"blog": "Blog",
"jobs": "Jobs",
"discussions": "Discussions",
"github": "GitHub",
"docs": "Docs",
"support": "Support",
"hiring": "Join us!"
}
}
================================================
FILE: src/locales/ro-RO.json
================================================
{
"searchPlaceholder": "Caută",
"spaceCount": "{0} spațiu(i)",
"createSpace": "Creează un spațiu",
"backToHome": "Prima Pagină",
"actions": "Acţiuni",
"poweredBy": "Powered by",
"results": "Rezultate",
"resultsError": "Results could not be calculated. This is often due to a misconfigured strategy or an unresponsive RPC node involved in the strategy.",
"resultsCalculating": "Final results are being calculated. If you still see this message after a few minutes contact the space admin.",
"votingPowerFailedMessage": "Your voting power could not be calculated. This is often due to a misconfigured strategy or an unresponsive RPC node involved in the strategy.",
"votingValidationFailedMessage": "There was an error on our side and we could not verify if you are eligible to vote. This is often due to a misconfigured voting validation or an unresponsive API involved in the validation.",
"notValidVoterMessage": "Oops, you don't seem to be eligible to vote on this proposal.",
"getHelp": "Solicită ajutor",
"retry": "Retry",
"currentResults": "Rezultatele actuale",
"reset": "Resetează",
"close": "Close",
"save": "Salvează",
"author": "Autor",
"next": "Continuare",
"choice": "Choice",
"submit": "Trimite",
"plugins": "Plugin-uri",
"information": "Informaţii",
"confirm": "Confirmă",
"snapshot": "Snapshot",
"strategies": "Strategie(i)",
"strategiesPage": "Strategii",
"space": "Spaţiu",
"spaces": "Spaţii",
"verifiedSpace": "Spațiu verificat",
"warningSpace": "This space has been flagged as potentially malicious. Proceed with caution.",
"version": "Versiune",
"timeline": "Cronologie",
"ended": "ended",
"started": "started",
"filters": "Filtre",
"allSpaces": "Toate spațiile",
"submitOnchain": "Trimite in lanț",
"inSpaces": "În {0} spațiu(i)",
"votes": "Voturi",
"seeMore": "Vezi mai multe",
"seeAll": "See all",
"network": "Rețea",
"networks": "Reţele",
"skins": "Skin-uri",
"spaceMembers": "Members",
"members": "Niciun membru | {count} membru |\n {count} membri",
"editStrategy": "Modifică strategia",
"invalidProposals": "Propuneri invalide",
"account": "Cont",
"create3box": "Crează profil în 3Box",
"view3box": "Vezi profilul în 3Box",
"edit3box": "Modifică profilul în 3Box",
"connectWallet": "Conectează portofel",
"toggleSkin": "Comută skin",
"about": "Despre",
"license": "Licență",
"showMore": "Show more",
"voted": "Voted",
"reload": "Reload",
"ipfsServer": "Server IPFS",
"hub": "Hub",
"cancel": "Anulează",
"delete": "Delete",
"demoSite": "Acesta este site-ul demo, încercă-l!",
"removeDelegation": "Elimină delegația",
"confirmRemove": "Ești sigur că vrei să elimini delegația către",
"removeSpace": "pentru spațiul {0}",
"noVotingPower": "Oops, it seems you don't have any voting power at block {blockNumber}.",
"quorumReached": "quorum reached",
"options": "Opțiunea(i)",
"votingPower": "Puterea ta de vot",
"comment": {
"placeholder": "Share your reason (optional)"
},
"receipt": "Chitanță",
"relayer": "Relayer",
"verifyOnMycrypto": "Verifică chitanța pe MyCrypto",
"verifyOnSignatorio": "Verifică pe Signator.io",
"isCore": "Nucleu",
"notificationsBlocked": "Browserul tău blochează notificările",
"notificationsNotSupported": "Browserul tău nu acceptă notificări",
"walletNotSupported": "Portofelul nu este suportat",
"seeInExplorer": "See explorer",
"learnMore": "Află mai multe",
"logout": "Deconectează-te",
"continue": "Continuă",
"add": "Adaugă",
"edit": "Modifică",
"strategyParameters": "Parametrii strategiei",
"addAction": "Adaugă acţiune",
"removeAction": "Elimină acțiune",
"yourChoice": "Alege {0}",
"targetAddress": "Adresa destinației",
"value": "Valoare",
"date": "Date",
"marketDetails": "Detalii piață",
"addMarket": "Adaugă piață",
"selectNetwork": "Selectează rețea",
"conditionId": "Condiție ID",
"basetokenAddress": "Adresa token de bază",
"quoteAddress": "Adresa monedei de ofertă",
"removeMarket": "Elimină piață",
"back": "Înapoi",
"loading": "Se încarcă...",
"predictedImpact": "Impact estimat",
"marketSymbol": "{0} piață",
"twoChoicesRequired": "Pentru acest plugin sunt necesare două opţiuni.",
"noResultsFound": "Ups, nu găsim niciun rezultat",
"createFirstProposal": "Să creăm prima ta propunere",
"noSpacesJoined": "Ups, nu te-ai alăturat încă niciunui spațiu",
"addFavorites": "Adaugă la favorite",
"createdBy": "De către {0}",
"startIn": "pornește {0}",
"endIn": "sfârșit {0}",
"proposalTimeLeft": "{0} rămas",
"endedAgo": "încheiat {0}",
"proposalBy": "de către {0}",
"endDate": "sfârșit {0}",
"contentHash": "Hash conținut",
"defaultSkin": "Skin implicit",
"select": "Selectează",
"language": "Limbă",
"agree": "Sunt de acord",
"moderators": "Moderators",
"playground": "Teren de joacă",
"strategyParams": "Parametrii strategiei",
"addresses": "Adrese",
"networkErrorPlayground": "Eroare de rețea - te rugam sa deschizi consola browser-ului pentru mai multe informatii",
"upload": "Încarcă",
"join": "Alătură-te",
"joined": "Alăturat(ă)",
"leave": "Părăsește",
"subspaces": "Sub-spaces",
"mainspace": "Main space",
"copyLink": "Copiază link",
"duplicate": "Duplicate",
"joinedSpaces": "Spații alăturate",
"joinSpaces": "Alătura-te la spații",
"setDelegationToSpace": "Limitarea delegării către un anumit spațiu",
"theCurrentNetwork": "rețeaua actuală",
"optional": "(optional)",
"homeLoadmore": "Încarcă mai multe",
"confirmAction": "Confirm action",
"or": "or",
"share": "Share",
"shareOnTwitter": "Share on Twitter",
"shareOnLenster": "Share on Lenster",
"createButton": "Create",
"discussion": "Discussion",
"changeWallet": "Change wallet",
"createASpace": "Create a space",
"getStarted": "Get started",
"skip": "Skip",
"newSpaceNotice": {
"header": "Your space is live!",
"mainText": "You can change how voting power is calculated via strategies in your {settings}. Changes to your settings will only affect new proposals, existing proposals can not be changed.",
"learnMore": "Learn more in the {documentation} or join Snapshot {discord} for help.",
"gotIt": "Got it!"
},
"errors": {
"required": "Câmp obligatoriu",
"minLength": "Field is required",
"maxLength": "Lungime maximă de {0}",
"pattern": "Caracter invalid",
"minItems": "Minim {0} caracter(e) necesar(e)",
"maxItems": "Maximum {0} item(s) allowed",
"minStrategy": "Este necesară cel puţin o strategie.",
"website": "URL should be in the format https://www.example.com",
"format": "Formatul nu este valid",
"type": "Tipul fişierului nu ese valid",
"unsupportedImageType": "File type not supported, Supported formats are jpeg, jpg and png",
"invalidAddress": "Invalid address"
},
"create": {
"proposalTitle": "Title",
"discussion": "Discussion (optional)",
"categorie(s)": "Selectează până la 2 categorii(e)",
"proposalDescription": "Description (optional)",
"preview": "Previzualizează",
"choices": "Alegeri",
"addChoice": "Adaugă alegere",
"startDate": "Selectează data de începere",
"endDate": "Selectează data de sfârșit",
"startTime": "Selectează ora de start",
"endTime": "Selectează ora de start",
"publish": "Publică",
"untitled": "Untitled",
"snapshotBlock": "Block-number la snapshot",
"voting": "Voting",
"votingSystem": "Voting system",
"choice": "Choice {0}",
"period": "Voting period",
"start": "Start",
"end": "End",
"days": "Days",
"hours": "Hours",
"minutes": "Minutes",
"schedule": "Schedule proposal",
"delayEnforced": "The space enforces a delay until voting can start",
"periodEnforced": "The space enforces the duration of the voting period",
"typeEnforced": "{type} is enforced by the space",
"privacyEnforced": "{type} is enforced by the space",
"edit": "Edit",
"continue": "Continue",
"now": "Now",
"votingPeriodExplainer": "This is the time period in which users will be able to vote. The proposal will be visible and pending before the start of the voting period.",
"uploadImageExplainer": "Attach images by dragging & dropping, selecting or pasting them.",
"uploading": "Uploading image",
"markdown": "Styling with Markdown is supported",
"validationWarning": {
"basic": {
"member": "Trebuie sa fii autor al spațiu-lui pentru a înregistra o propunere.",
"minScore": "Trebuie sa ai un minim de {0} {1} pentru a înscrie o propunere."
},
"customValidation": "Trebuie sa treci de validarea propunerii pentru a o înscrie.",
"executionError": "Verifying your eligibility to create proposals in this space has failed. This is likely due to a misconfigured strategy."
},
"errorGettingSnapshot": "We encountered an error while fetching the snapshot block number which is needed to calculate your voting power. Please try again later."
},
"delegate": {
"header": "Deleghează",
"selectDelegate": "Selectează delegat",
"to": "Către",
"addressPlaceholder": "Adresă sau nume ENS",
"delegations": "Delegațiile tale",
"allSpaces": "Pentru toate spațiile",
"delegated": "Delegat ție",
"pendingTransaction": "nicio tranzacție în așteptare | 1 tranzacție în așteptare | {count} tranzacții în așteptare",
"topDelegates": "Cei mai buni delegați",
"noDelegatesFoundFor": "Nici un delegat găsit pentru {0}",
"noValidEns": "Nu este o adresă ENS validă.",
"noValidAddress": "Nu este o adresă validă",
"delegateToSelf": "Nu poți delega către tine însuți",
"delegateToSelfAddress": "Nu poți delega la propria adresă ENS",
"noValidSpaceId": "Nu este un ID de spațiu valid",
"noDelegationsAndDelegates": "Nu iți găsești delegațiile și delegații? Asigură-te că ești conectat la rețeaua corectă.",
"delegateNotSupported": "Delegația nu este acceptată în prezent pentru {network}."
},
"proposal": {
"castVote": "Votează",
"vote": "Vot",
"startDate": "Data începerii",
"endDate": "Dată de sfârşit",
"votingSystem": "Sistem de votare",
"privacy": "Privacy",
"invalidChoice": "Invalid choice",
"postVoteModal": {
"defaultTitle": "Your vote is in!",
"gnosisSafeTitle": "Your vote is pending...",
"gnosisSafeDescription": " Votes with a Safe require additional signers and will be visible once the transaction is confirmed",
"seeQueue": "See queued transactions",
"tips": {
"1": "Votes can be changed while the proposal is active"
}
}
},
"proposals": {
"header": "Propuneri",
"new": "Propunere nouă",
"noProposals": "Incă nu sunt propuneri!",
"createProposal": "Crează propunere",
"showMore": "Arată mai multe",
"showLess": "Arată mai puține",
"states": {
"all": "Toate",
"core": "Nucleu",
"community": "Comunitate",
"active": "Activ",
"pending": "În așteptare",
"closed": "Închis"
}
},
"notifications": {
"header": "Notifications",
"noNotifications": "You have no notifications",
"proposalStarted": "proposal has started:",
"proposalEnded": "proposal has ended:",
"markAllAsRead": "Mark all as read",
"all": "All",
"unread": "Unread"
},
"modalTerms": {
"mustAgreeTo": "To {action} this space, you must agree to the {spaceName} terms of service.",
"actionJoin": "join",
"actionCreate": "create a proposal in",
"actionVote": "vote in"
},
"settings": {
"header": "Setări",
"editController": "Edit controller",
"connectWithSpaceOwner": "You are in view only mode, to modify space settings connect with a controller or admin wallet.",
"gnosisWrongNetwork": {
"base": "Your Gnosis Safe is on the wrong network. Please connect to {network} to {action}.",
"settings": "edit the space settings",
"create": "create a proposal",
"vote": "vote on this proposal"
},
"currentSpaceControllerIs": "The current space controller is {address}",
"newController": "New controller",
"noRecord": "No text-record found. Make sure you have registered {id} domain on {network}, then edit the controller text-record to regain access to the space settings.",
"set": "Set",
"profile": "Profil",
"avatar": "Avatar",
"name": {
"label": "Name",
"placeholder": "e.g. Yam Network"
},
"about": {
"label": "About",
"placeholder": "What is your organisation about?"
},
"categories": {
"label": "Categorie(s)",
"select": "Select categorie(s)"
},
"terms": {
"label": "Terms of service",
"information": "Users will be required to accept these terms once before they can create a proposal or cast a vote"
},
"hideSpace": "Ascunde spațiu de pe pagina principală",
"links": "Social accounts",
"subspaces": {
"label": "Sub-spaces",
"information": "Sub-spaces will only be shown once configured on both the main space and the sub-space(s). {docs}",
"parent": {
"label": "Main space",
"placeholder": "pistachiodao.eth",
"information": "The space that this space is a sub-space of will be displayed on the space page"
},
"children": {
"label": "Sub-spaces",
"placeholder": "pistachiodao.eth",
"information": "Related Sub-spaces listed here will be displayed on the space page"
}
},
"website": "Website",
"strategies": {
"label": "Strategie(s)",
"information": "Strategies are used to determine voting power or whether a user is eligible to create a proposal"
},
"network": {
"label": "Network",
"information": "The defaul network used for this space. Networks can also be specified in individual strategies"
},
"symbol": {
"label": "Symbol",
"information": "The default symbol used for this space, usually the token symbol i.e. BAL for Balancer"
},
"strategiesList": "Select up to 8 strategies",
"votingPowerIsCumulative": "Voting power is cumulative",
"addStrategy": "Adaugă strategie",
"testInPlayground": "Test in playground",
"admins": {
"label": "Admins",
"information": "Admins are able to modify the space settings and manage the space's proposals"
},
"authors": {
"label": "Authors",
"information": "Authors are always able to create proposals"
},
"proposalValidation": "Validare propunere",
"validation": "Type",
"proposalThreshold": {
"label": "Threshold",
"information": "The minimum amount of voting power required to create a proposal"
},
"allowOnlyAuthors": "Permite doar autorilor să înregistreze o propunere",
"editValidation": "Modifică validarea",
"selectValidation": "Selectează validarea",
"validationParameters": "Parametrii de validare",
"voting": "Votare",
"votingDelay": "Intârziere vot",
"votingPeriod": "Perioada de vot",
"hours": "Hours",
"days": "Days",
"quorum": {
"label": "Quorum",
"information": "The minimum amount of voting power required for the proposal to pass"
},
"type": {
"label": "Type",
"information": "The type of voting system used for this space. (Enforced on all future proposals)"
},
"anyType": "Oricare",
"hideAbstain": "Ignoră voturile de abținere în rezultatele votului de bază",
"customDomain": "Domeniu personalizat",
"domain": {
"label": "Domain name",
"placeholder": "e.g. vote.balancer.fi",
"info": "To setup a custom domain you additionally need to open a pull request on github after you have created the space. {docs}"
},
"skin": "Skin",
"treasuries": {
"label": "Treasury",
"add": "Add treasury",
"edit": "Edit treasury",
"information": "Add treasuries of your organization to show them in your space"
},
"addPlugin": "Adaugă extensie",
"editPlugin": "Modifică extensie",
"pluginParameters": "Parametrii extensie",
"proposal": {
"title": "Proposal",
"guidelines": {
"title": "Guidelines",
"information": "Display a link to your guidelines on proposal creation to help users understand what constitutes a good/valid proposal"
},
"template": {
"title": "Template",
"information": "Start every proposal with a template to help users understand what information is required"
}
}
},
"setup": {
"example": "e.g. yam.eth",
"chooseExistingEns": "Alege unul dintre domeniile tale ENS existente pentru a crea un spațiu:",
"useSingleExistingEns": "Utilizează domeniul tău ENS existent:",
"orRegisterNewEns": "Sau înregistrează un nou domeniu:",
"demoTestnetEnsMessage": "To create a test space you need an ENS domain on {network}.",
"toCreateASpace": "Pentru a crea un spațiu, ai nevoie mai întâi de un domeniu ENS. Introdu unul mai jos și urmează instrucțiunile de înregistrare ENS.",
"createASpace": "Crează un spațiu",
"registerEnsButton": "Înregistrează-te",
"supportedEnsTLDs": "Sufix domenii suportate",
"helpDocsAndDiscordLinks": "Not sure how to setup your space? Learn more in the {docs} or join Snapshot {discord}.",
"setSpaceController": "Space controller",
"setSpaceControllerExists": "The snapshot text-record for this domain has already been set. Choose edit to change it, otherwise you can skip to the next step.",
"setSpaceControllerInfo": " The space controller is the account that will be able to manage the space settings. Additional space controllers (admins) can be added later.",
"setSpaceControllerInfoGnosisSafe": "When creating a space with a Gnosis Safe it's recommended to set the safe address as the space controller. If you don't, you need to follow some additional steps. {link}",
"editSpaceController": "Edit controller on ENS",
"setController": "Set controller",
"explainControllerAndEns": "Setting the controller requires a transaction on the {network} which will add a \"snapshot\" TEXT record to your ENS domain.",
"confirmToSetAddress": "Are you sure you want to set {address} as the controller of the space?",
"controllerHasAuthority": "The controller has full authority over the space settings",
"controller": "Controller",
"selectEnsForSpace": "Choose ENS address",
"spaceOwnerAddressPlaceHolder": "e.g. {address}",
"controllerAddress": "Controller address",
"updateController": "Update controller",
"seeOnEns": "See on ENS",
"goToSettings": "Go to settings",
"setSpaceProfile": "Customize your space",
"waitForTransaction": "The transaction need to confirm before you can create your space. {txUrl}",
"pleaseWaitMessage": "This can take a few minutes, please wait while the transaction is being confirmed",
"notControllerAddress": "Please connect with the controller address {wallet} to create the space.",
"fillCurrentAccount": "Use currently logged in account",
"domain": {
"title": "Setup your space domain",
"ensMessage": " One thing you need before you can create your own space, is an ENS domain on Ethereum mainnet.",
"ensMessageTestnet": "You can also {link} on the Goerli testnet and mess with things there first.",
"tryDemo": "try the demo",
"yourExistingSpaces": "Your existing spaces",
"invalidEns": "This ENS name is invalid. Usually this is due the use of invalid characters during registration."
},
"strategy": {
"title": "How would you like to setup your voting strategy?",
"subtitle": "You can change your strategy settings any time.",
"blockTitle": "Setup voting strategy",
"onePersonOneVote": {
"title": "One person, one vote",
"description": "Manage a whitelist of people who can vote or simply allow any address to vote. Every vote is equal and no token is required",
"whitelistInformation": "Specify a number of accounts that can vote",
"ticketInformation": "Any account can vote",
"votesEqualInfo": "Each vote is equal and no token is required"
},
"tokenVoting": {
"title": "Token weighted voting",
"description": "Votes are weighted by a token. The token can be an ERC-20, ERC-721 or ERC-1155 token standard",
"tokenNotFound": "Token not found",
"seeOnEtherscan": "See on Etherscan"
},
"advanced": {
"title": "Custom setup",
"description": "Select up to 8 strategies with a wide range of options. If you can't find the right strategy for your use case, you can create your own"
}
},
"validationTitle": "Who can manage this space and create proposals?"
},
"profile": {
"buttonEdit": "Edit profile",
"viewProfile": "View profile",
"about": {
"header": "About",
"joinedSpaces": "Joined spaces",
"createdSpaces": "Created spaces",
"biography": "Bio",
"notJoinSpacesYet": "Hasn't joined any spaces yet",
"notCreatedSpacesYet": "Hasn't created any spaces yet",
"delegatorNetworkInfo": "Change by switching network in your wallet",
"delegate": "Delegate",
"delegated": "Delegated",
"delegateTo": "Delegate to",
"delegateFor": "Delegator for",
"noDelegatorsMessage": "No delegators on {network}",
"notSupportedNetwork": "Delegation currently isn't supported on {network} "
},
"activity": {
"header": "Activity",
"votedFor": "Voted {choice}",
"today": "Today",
"thisWeek": "This week",
"olderThanWeek": "Older than a week",
"noActivity": "No activity yet"
},
"settings": {
"header": "Edit profile",
"name": "Name",
"biography": "Bio",
"namePlaceholder": "Enter name",
"bioPlaceholder": "Tell your story",
"change": "Change",
"remove": "Remove"
}
},
"notify": {
"youDidIt": "Ai reușit!",
"copied": "Copiat!",
"proposalDeleted": "Propunere ştearsă",
"somethingWentWrong": "Ups, ceva nu a mers bine!",
"saved": "Salvează!",
"delegationSuccess": "Delegare reușită",
"delegationRemoved": "Delegare eliminată",
"proposalCreated": "Propunere creată",
"voteSuccessful": "Ai votat cu succes!",
"ensSet": "ENS text record was successfully set",
"transactionSent": "Transaction sent"
},
"explore": {
"createStrategy": "Crează strategie",
"createSkin": "Crează skin",
"addNetwork": "Adaugă rețea",
"createPlugin": "Crează plugin",
"strategies": "strategie(i)",
"skins": "skin(uri)",
"networks": "rețea(le)",
"plugins": "plugin(uri)",
"results": "rezultat(e)",
"category": "Category",
"categories": {
"all": "Toate",
"protocol": "Protocol",
"social": "Social",
"investment": "Investiţie",
"grant": "Subvenție",
"service": "Serviciu",
"media": "Media",
"creator": "Autorul",
"collector": "Colecționar"
}
},
"voting": {
"selectVoting": "Selectează sistemul de votare",
"single-choice": "Votare cu alegere unică",
"approval": "Votare cu aprobare",
"quadratic": "Votare cuadratica",
"ranked-choice": "Votare prin clasament",
"weighted": "Votare cu pondere",
"basic": "Votare de bază",
"description": {
"single-choice": "Fiecare votant poate selecta o singură alegere.",
"approval": "Fiecare alegător poate selecta orice număr de alegeri.",
"quadratic": "Fiecare alegător poate răspândi puterea de vot în orice număr de alegeri. Rezultatele sunt calculate cuadratic.",
"ranked-choice": "Fiecare alegător poate selecta şi clasifica orice număr de alegeri. Rezultatele sunt calculate prin metoda de numărare cu balotaj instantaneu.",
"weighted": "Fiecare alegator poate raspândi puterea de vot la oricâte alegeri.",
"basic": "Votul unic cu trei opțiuni: Pentru, Împotriva sau Abținere"
}
},
"privacy": {
"label": "Privacy",
"title": "Select voting privacy",
"information": "The type of privacy used on proposals. (Enforced on all future proposals)",
"any": "Any",
"none": "None",
"shutter": {
"label": "Shutter",
"description": "Choices are encrypted and only visible once the voting period is over",
"tooltip": "This proposal has Shutter privacy enabled. All votes will be encrypted until the voting period has ended and the final score is calculated",
"url": "https://blog.shutter.network/shielded-voting/"
}
},
"validation": {
"label": "Validation",
"title": "Select voting validation",
"information": "The type of validation used to determine if a user can vote. (Enforced on all future proposals)",
"any": {
"label": "Anyone can vote",
"description": "Anyone with voting power can cast a vote."
},
"basic": {
"label": "Basic",
"description": "Use any strategy to determine if a user can vote.",
"invalidMessage": "You do not meet the minimum balance requirement to vote on this proposal. "
},
"passport-gated": {
"label": "Gitcoin Passport gated",
"description": "Protect your proposals from spam and vote manipulation by requiring users to have a Gitcoin Passport.",
"invalidMessage": "You need a Gitcoin Passport with score above {scoreThreshold} and {operator} of the following stamps to vote on this proposal: {stamps}. "
}
},
"safeSnap": {
"currentOutcome": "Rezultat curent",
"currentBond": "Bond curent",
"finalizedIn": "Finalizat {0}",
"executableIn": "Executabil {0}",
"finalOutcome": "Rezultat",
"nextBond": "Bond pentru a seta rezultatul",
"setOutcomeTo": "Setează rezultatul la",
"claimBond": "Revendică bond",
"addBatch": "Adaugă manunchi de tranzactii",
"batch": "Manunchi de tranzacții",
"transactions": "Tranzacții",
"to": "To (address)",
"invalidAddress": "Adresă invalidă",
"invalidAmount": "Sumă invalidă",
"invalidValue": "Valoare invalidă",
"invalidAbi": "ABI invalid",
"invalidData": "Date necorespunzătoare",
"value": "Valoare (wei)",
"data": "Date",
"noCollectibles": "Nu sunt collectibles",
"asset": "Asset",
"amount": "Sumă",
"type": "Tip",
"transferFunds": "Transferă fonduri",
"transferNFT": "Transferă NFT",
"contractInteraction": "Interacțiune contract",
"rawTransaction": "Tranzacție brută",
"addTransaction": "Adaugă tranzacție",
"transactionLabels": {
"contractInteraction": "{functionName}() - {amount} wei catre {address}",
"transferFunds": "Transferă {amount} {tokenSymbol} către {address}",
"transferNFT": "Trimite {name} #{id} către {address}",
"raw": "Trimite {amount} wei către {address}"
},
"labels": {
"request": "Solicită executare",
"setOutcome": "Setează rezultat",
"changeOutcome": "Modifică rezultat",
"executeTxs": "Execută manunchiul de tranzacții {0} din {1}",
"executed": "Toate tranzacțiile au fost executate",
"noTransactions": "There are no transactions to execute",
"rejected": "Proposal rejected",
"error": "Ceva nu a funcţionat",
"connectWallet": "Conectează portofelul pentru a vedea detaliile de execuție",
"switchChain": "Comută portofelul la {0} pentru a solicita execuția",
"question": "A trecut această propunere si îndeplinește",
"criteria": "criterii de acceptare?",
"proposalPassed": "Propunerea a fost aprobată?",
"expired": "The proposal has expired",
"approveBond": "Approve bond",
"confirmVoteResults": "Click to confirm the proposal passed",
"executeTxsUma": "Execute transaction batch",
"deleteDisputedProposal": "Delete disputed proposal",
"confirmVoteResultsToolTip": "Make sure this proposal was approved by this Snapshot vote before proposing on-chain. If the Snapshot vote rejected the proposal, the on-chain proposal will be rejected as well and you will lose your bond.",
"approveBondToolTip": "On-chain proposals require a bond from the proposer. This will approve tokens from your wallet to be posted as a bond. If you make an invalid proposal, it will be disputed and you will lose your bond. If the proposal is valid, your bond will be returned when the transactions are executed.",
"requestToolTip": "This will propose the transactions from this Snapshot vote on-chain. After a challenge window, if the proposal is valid, the transactions can be executed and your bond will be returned.",
"executeToolTip": "This will execute the transactions from this proposal and return the proposer's bond."
}
},
"poap": {
"no_poap_header": "Un POAP nu a fost configurat pentru această propunere încă :'(",
"no_voted_header": "Votează pentru a obține acest POAP",
"unclaimed_header": "Revendică-ți POAP-ul Am votat",
"claimed_header": "Felicitări! POAP-ul a fost adăugat la colecția ta",
"loading_header": "POAP-ul este în curs de adăugare la colecția ta",
"button_claim": "Mint",
"button_show": "Răsfoiește colecția",
"success_claim": "POAP-ul a fost adăugat la colecția ta",
"error_claim": "A apărut o problemă la mintarea acestui token"
},
"progress": {
"progress": "Progress",
"inProgress": "In Progress",
"completed": "Completed",
"complete": "Complete",
"newStep": "New Step",
"description": "Description",
"add": "Add",
"deleteStep": "Delete Step",
"deleteConfirm": "Are you sure you want to delete?",
"delete": "Delete",
"cancel": "Cancel",
"comeBack": "Come back after voting is complete to see how this proposal is progressing!",
"confirmSignature": "Signing this message will allow us to authorize your request to update the progress of your proposal.",
"wentWrong": "Oops something went wrong.",
"voting": "Voting",
"soon": "Soon"
},
"charts": {
"charts": "Grafice",
"noVotesYet": "Nu există voturi pentru vizualizare încă.",
"totalVotesPerDay": "Total voturi pe zi",
"shareOfVotingPower": "Proporția puterii de vot",
"votingPowerPerDay": "Putere de vot pe zi"
},
"comment_box": {
"title": "Căsuță de comentarii",
"add": "Adaugă comentariul tau aici",
"submit": "Trimite",
"preview": "Previzualizează",
"continue_editing": "Continuă editarea",
"edit": "Modifică-ți răspunsul aici",
"edit_button": "Editează",
"dismiss": "Renunță",
"delete": "Șterge",
"add_reply": "Adaugă răspunsul tău aici",
"edit_comment": "Editează comentariu",
"edit_modal": "Ești sigur că vrei să editezi?",
"yes": "Da",
"no": "Nu",
"delete_comment": "Șterge comentariu",
"delete_modal": "Eşti sigur că vrei să ştergi?",
"error": "Ups, ceva nu a mers bine",
"replies": "răspunsuri",
"hide": "Ascunde",
"show": "Afișează",
"reply": "Răspunde",
"load_more": "Încarcă mai mult"
},
"page": {
"title": {
"home": "Snapshot",
"setup": "Crează un spațiu",
"timeline": "Cronologie",
"notifications": "Notifications",
"explore": "Explorează",
"playground": "Teren de joacă",
"space": {
"create": "Crează {space} propunere",
"about": "Despre {space}",
"proposals": "{space} Propuneri",
"proposal": "Propunere {space}: {proposal}",
"settings": "Setări {space}"
},
"strategy": "Strategie {key}",
"delegate": "Deleghează",
"ranking": "Ranking"
}
},
"hal": {
"title": "Urmărește propuneri pentru {spaceName}",
"text": "Primește notificări de fiecare dată când o nouă propunere este creată sau se termină"
},
"timeUnits": {
"second": "1 secundă | {n} secunde",
"minute": "1 minut | {n} minute",
"hour": "1 oră | {n} ore",
"day": "1 zi | {n} zile",
"week": "1 săptămână | {n} săptămâni",
"month": "1 lună | {n} luni",
"year": "1 an | {n} ani"
},
"unsupportedNetwork": {
"unsupportedNetwork": "Unsupported network",
"switchNetworkToNetwork": "To continue, you need to change the network in your wallet to {network}.",
"switchToNetwork": "Switch to {network}",
"goToDemoSite": "Go to demo website"
},
"treasury": {
"title": "Treasury",
"wallets": {
"title": "Wallets",
"empty": "This space doesn't have a treasury yet",
"addTreasury": "Add a treasury"
},
"assets": {
"title": "Assets",
"empty": "There are no assets in this contract"
},
"24hChange": "24h change"
},
"newsletter": {
"yourEmail": "Your email",
"title": "Get the latest Snapshot updates"
},
"joinCommunity": "Join Snapshot community",
"header": {
"title": "Where decisions get made",
"description": "Snapshot is a free, open-source platform for community governance. Create your own space now and start making decisions!"
},
"aboutPage": {
"description": "Snapshot is a decentralized governance platform that makes it easy to create and vote on proposals - all without spending a fortune on gas fees! Plus, our flexible system supports various voting types and strategies, so you can tailor the voting process to your needs.",
"subHeader": "Governance should be a snap",
"subDescription": "Web3 governance doesn't have to be complicated. Snapshot is the perfect solution for organizations looking for an easy and efficient way to govern their community or organization."
},
"footerView": {
"resources": "Resources",
"about": "About",
"blog": "Blog",
"jobs": "Jobs",
"discussions": "Discussions",
"github": "GitHub",
"docs": "Docs",
"support": "Support",
"hiring": "Join us!"
}
}
================================================
FILE: src/locales/ru-RU.json
================================================
{
"searchPlaceholder": "Поиск",
"spaceCount": "{0} сообщество(сообществ)",
"createSpace": "Создать сообщество",
"backToHome": "Главная",
"actions": "Действия",
"poweredBy": "Powered by",
"results": "Результаты",
"resultsError": "Не удалось подсчитать результаты. Часто это связано с неправильной конфигурацией стратегии или не отвечающим на запросы ноды RPC, задействованным в стратегии.",
"resultsCalculating": "Final results are being calculated. If you still see this message after a few minutes contact the space admin.",
"votingPowerFailedMessage": "Your voting power could not be calculated. This is often due to a misconfigured strategy or an unresponsive RPC node involved in the strategy.",
"votingValidationFailedMessage": "There was an error on our side and we could not verify if you are eligible to vote. This is often due to a misconfigured voting validation or an unresponsive API involved in the validation.",
"notValidVoterMessage": "Oops, you don't seem to be eligible to vote on this proposal.",
"getHelp": "Получить помощь",
"retry": "Повторить",
"currentResults": "Текущий результат",
"reset": "Сброс",
"close": "Close",
"save": "Сохранить",
"author": "Автор",
"next": "Далее",
"choice": "Choice",
"submit": "Отправить",
"plugins": "Плагины",
"information": "Информация",
"confirm": "Подтвердить",
"snapshot": "Snapshot",
"strategies": "Стратегия(и)",
"strategiesPage": "Стратегии",
"space": "Сообщество",
"spaces": "Сообщества",
"verifiedSpace": "Проверенное сообщество",
"warningSpace": "This space has been flagged as potentially malicious. Proceed with caution.",
"version": "Версия",
"timeline": "Хронология",
"ended": "завершился",
"started": "начался",
"filters": "Фильтры",
"allSpaces": "Все сообщества",
"submitOnchain": "Отправить в блокчейн",
"inSpaces": "В {0} сообществе(ах)",
"votes": "Голоса",
"seeMore": "Подробнее",
"seeAll": "See all",
"network": "Сеть",
"networks": "Сети",
"skins": "Темы",
"spaceMembers": "Members",
"members": "Нет участников | {count} участник | {count} участников",
"editStrategy": "Изменить стратегию",
"invalidProposals": "Недействительные предложения",
"account": "Учетная запись",
"create3box": "Создать профиль на 3Box",
"view3box": "Просмотреть профиль на 3Box",
"edit3box": "Редактировать профиль на 3Box",
"connectWallet": "Подключить кошелек",
"toggleSkin": "Переключить тему",
"about": "О нас",
"license": "Лицензия",
"showMore": "Show more",
"voted": "Voted",
"reload": "Reload",
"ipfsServer": "IPFS сервер",
"hub": "Хаб",
"cancel": "Отменить",
"delete": "Delete",
"demoSite": "Это демо сайт, попробуйте!",
"removeDelegation": "Убрать делегирование",
"confirmRemove": "Вы уверены, что хотите отменить свое делегирование в",
"removeSpace": "для сообщества {0}",
"noVotingPower": "Oops, it seems you don't have any voting power at block {blockNumber}.",
"quorumReached": "quorum reached",
"options": "Вариант(ы)",
"votingPower": "Ваше право голоса",
"comment": {
"placeholder": "Share your reason (optional)"
},
"receipt": "Квитанция",
"relayer": "Передатчик",
"verifyOnMycrypto": "Подтвердите получение на MyCrypto",
"verifyOnSignatorio": "Верификация на Signator.io",
"isCore": "Основные",
"notificationsBlocked": "Ваш браузер блокирует уведомления",
"notificationsNotSupported": "Ваш браузер не поддерживает уведомления",
"walletNotSupported": "Кошелёк не поддерживается",
"seeInExplorer": "See explorer",
"learnMore": "Подробнее",
"logout": "Выйти",
"continue": "Продолжить",
"add": "Добавить",
"edit": "Изменить",
"strategyParameters": "Параметры стратегии",
"addAction": "Добавить действие",
"removeAction": "Удалить действие",
"yourChoice": "Вариант {0}",
"targetAddress": "Адрес назначения",
"value": "Значение",
"date": "Данные",
"marketDetails": "Информация о рынке",
"addMarket": "Добавить рынок",
"selectNetwork": "Выбрать сеть",
"conditionId": "Условия ID",
"basetokenAddress": "Адрес основного токена",
"quoteAddress": "Адрес котировки валюты",
"removeMarket": "Удалить рынок",
"back": "Назад",
"loading": "Загрузка...",
"predictedImpact": "Прогнозируемое влияние",
"marketSymbol": "{0} рынок",
"twoChoicesRequired": "Для этого плагина необходимо выбрать два варианта.",
"noResultsFound": "Упс, мы не можем найти результатов",
"createFirstProposal": "Давайте создадим ваше первое предложение",
"noSpacesJoined": "Упс, вы еще не присоединились ни к одному сообществу",
"addFavorites": "Добавить в избранное",
"createdBy": "От: {0}",
"startIn": "начать {0}",
"endIn": "по {0}",
"proposalTimeLeft": "{0} осталось",
"endedAgo": "завершился {0}",
"proposalBy": "от: {0}",
"endDate": "по {0}",
"contentHash": "Хэш содержимого",
"defaultSkin": "Тема по умолчанию",
"select": "Выбрать",
"language": "Язык",
"agree": "Я принимаю",
"moderators": "Moderators",
"playground": "Игровая площадка",
"strategyParams": "Параметры стратегии",
"addresses": "Адреса",
"networkErrorPlayground": "Ошибка сети — пожалуйста, откройте панель браузера для получения дополнительной информации",
"upload": "Загрузить",
"join": "Присоединиться",
"joined": "Присоединился",
"leave": "Покинуть",
"subspaces": "Sub-spaces",
"mainspace": "Main space",
"copyLink": "Копировать ссылку",
"duplicate": "Duplicate",
"joinedSpaces": "Вступившие сообщества",
"joinSpaces": "Вступить в сообщества",
"setDelegationToSpace": "Ограничить делегирование полномочий определенным сообществам",
"theCurrentNetwork": "текущая сеть",
"optional": "(необязательно)",
"homeLoadmore": "Загрузить больше",
"confirmAction": "Подтвердить действие",
"or": "или",
"share": "Поделиться",
"shareOnTwitter": "Share on Twitter",
"shareOnLenster": "Share on Lenster",
"createButton": "Создать",
"discussion": "Discussion",
"changeWallet": "Change wallet",
"createASpace": "Create a space",
"getStarted": "Get started",
"skip": "Skip",
"newSpaceNotice": {
"header": "Ваше сообщество запущено!",
"mainText": "You can change how voting power is calculated via strategies in your {settings}. Changes to your settings will only affect new proposals, existing proposals can not be changed.",
"learnMore": "Узнайте больше в {documentation} или присоединитесь к {discord} Snapshot для получения помощи.",
"gotIt": "Понятно!"
},
"errors": {
"required": "Обязательное поле",
"minLength": "Обязательное поле",
"maxLength": "Максимальная длина: {0}",
"pattern": "Неверный символ",
"minItems": "Требуется минимум {0} предмета(ов)",
"maxItems": "Maximum {0} item(s) allowed",
"minStrategy": "Необходимо наличие хотя бы одной стратегии.",
"website": "Адрес должен быть в формате https://www.example.com",
"format": "Неверный формат",
"type": "Неверный тип",
"unsupportedImageType": "Тип файла не поддерживается, Поддерживаемые форматы: jpeg, jpg и png",
"invalidAddress": "Invalid address"
},
"create": {
"proposalTitle": "Название",
"discussion": "Discussion (optional)",
"categorie(s)": "Выберите до 2 категорий",
"proposalDescription": "Описание (необязательно)",
"preview": "Предпросмотр",
"choices": "Варианты ответов",
"addChoice": "Добавить вариант ответа",
"startDate": "Выбрать дату начала",
"endDate": "Выбрать дату окончания",
"startTime": "Выбрать время начала",
"endTime": "Выбрать время окончания",
"publish": "Опубликовать",
"untitled": "Без названия",
"snapshotBlock": "Номер блока snapshot",
"voting": "Голосование",
"votingSystem": "Система голосования",
"choice": "Выбор {0}",
"period": "Период голосования",
"start": "Начало",
"end": "Конец",
"days": "Дней",
"hours": "Часов",
"minutes": "Минут",
"schedule": "График предложения",
"delayEnforced": "Сообщество вводит отсрочку до начала голосования",
"periodEnforced": "Сообщество устанавливает продолжительность периода голосования",
"typeEnforced": "{type} is enforced by the space",
"privacyEnforced": "{type} is enforced by the space",
"edit": "Изменить",
"continue": "Продолжить",
"now": "Сейчас",
"votingPeriodExplainer": "Период времени, в течение которого пользователи могут голосовать. Предложение будет видно и отложено до начала периода голосования.",
"uploadImageExplainer": "Attach images by dragging & dropping, selecting or pasting them.",
"uploading": "Uploading image",
"markdown": "Поддерживается обработка стилей с помощью Markdown",
"validationWarning": {
"basic": {
"member": "Чтобы внести предложение, необходимо быть автором сообщества.",
"minScore": "Для подачи заявки необходимо иметь минимум {0} {1}."
},
"customValidation": "Чтобы подать предложение, необходимо пройти проверку предложения.",
"executionError": "Проверка вашего права на создание предложений в этом сообществе не удалась. Вероятно, это связано с неправильной конфигурацией стратегии."
},
"errorGettingSnapshot": "We encountered an error while fetching the snapshot block number which is needed to calculate your voting power. Please try again later."
},
"delegate": {
"header": "Делегировать",
"selectDelegate": "Выбрать делегата",
"to": "Кому",
"addressPlaceholder": "Адрес или название ENS",
"delegations": "Ваша(и) делегация (делегации)",
"allSpaces": "Для всех сообществ",
"delegated": "Делегировано вам",
"pendingTransaction": "нет незавершенных транзакций | 1 незавершенная транзакция | {count} незавершенных транзакций",
"topDelegates": "Лучшие делегаты",
"noDelegatesFoundFor": "Для {0} делегатов не найдено",
"noValidEns": "Не действительный адрес ENS.",
"noValidAddress": "Недействительный адрес",
"delegateToSelf": "Вы не можете делегировать сам себя",
"delegateToSelfAddress": "Вы не можете делегировать на свой собственный адрес ENS",
"noValidSpaceId": "Не действительное ID сообщества",
"noDelegationsAndDelegates": "Не можете найти свои делегации и делегатов? Убедитесь, что вы подключены к правильной сети.",
"delegateNotSupported": "Делегирование в настоящее время не поддерживается для {сети}."
},
"proposal": {
"castVote": "Проголосуйте",
"vote": "Голосовать",
"startDate": "Дата начала",
"endDate": "Дата окончания",
"votingSystem": "Система голосования",
"privacy": "Privacy",
"invalidChoice": "Invalid choice",
"postVoteModal": {
"defaultTitle": "Your vote is in!",
"gnosisSafeTitle": "Your vote is pending...",
"gnosisSafeDescription": " Votes with a Safe require additional signers and will be visible once the transaction is confirmed",
"seeQueue": "See queued transactions",
"tips": {
"1": "Votes can be changed while the proposal is active"
}
}
},
"proposals": {
"header": "Предложения",
"new": "Новые предложения",
"noProposals": "Здесь еще нет ни одного предложения!",
"createProposal": "Создать предложение",
"showMore": "Показать больше",
"showLess": "Показывать меньше",
"states": {
"all": "Все",
"core": "Основные",
"community": "Сообщество",
"active": "Активные",
"pending": "В процессе",
"closed": "Закрытые"
}
},
"notifications": {
"header": "Уведомление",
"noNotifications": "У вас нет уведомлений",
"proposalStarted": "предложение началось:",
"proposalEnded": "предложение закончилось:",
"markAllAsRead": "Отметить всё как прочитанное",
"all": "Все",
"unread": "Непрочитанные"
},
"modalTerms": {
"mustAgreeTo": "To {action} this space, you must agree to the {spaceName} terms of service.",
"actionJoin": "join",
"actionCreate": "create a proposal in",
"actionVote": "vote in"
},
"settings": {
"header": "Настройки",
"editController": "Изменить виджеты",
"connectWithSpaceOwner": "You are in view only mode, to modify space settings connect with a controller or admin wallet.",
"gnosisWrongNetwork": {
"base": "Your Gnosis Safe is on the wrong network. Please connect to {network} to {action}.",
"settings": "edit the space settings",
"create": "create a proposal",
"vote": "vote on this proposal"
},
"currentSpaceControllerIs": "Текущий контроллер сообщества - {адрес}",
"newController": "Новый контроллер",
"noRecord": "No text-record found. Make sure you have registered {id} domain on {network}, then edit the controller text-record to regain access to the space settings.",
"set": "Установить",
"profile": "Профиль",
"avatar": "Аватар",
"name": {
"label": "Name",
"placeholder": "e.g. Yam Network"
},
"about": {
"label": "About",
"placeholder": "What is your organisation about?"
},
"categories": {
"label": "Categorie(s)",
"select": "Select categorie(s)"
},
"terms": {
"label": "Terms of service",
"information": "Users will be required to accept these terms once before they can create a proposal or cast a vote"
},
"hideSpace": "Скрыть сообщество с главной страницы",
"links": "Social accounts",
"subspaces": {
"label": "Sub-spaces",
"information": "Sub-spaces will only be shown once configured on both the main space and the sub-space(s). {docs}",
"parent": {
"label": "Main space",
"placeholder": "pistachiodao.eth",
"information": "The space that this space is a sub-space of will be displayed on the space page"
},
"children": {
"label": "Sub-spaces",
"placeholder": "pistachiodao.eth",
"information": "Related Sub-spaces listed here will be displayed on the space page"
}
},
"website": "Website",
"strategies": {
"label": "Strategie(s)",
"information": "Strategies are used to determine voting power or whether a user is eligible to create a proposal"
},
"network": {
"label": "Network",
"information": "The defaul network used for this space. Networks can also be specified in individual strategies"
},
"symbol": {
"label": "Symbol",
"information": "The default symbol used for this space, usually the token symbol i.e. BAL for Balancer"
},
"strategiesList": "Select up to 8 strategies",
"votingPowerIsCumulative": "Voting power is cumulative",
"addStrategy": "Добавить стратегию",
"testInPlayground": "Test in playground",
"admins": {
"label": "Admins",
"information": "Admins are able to modify the space settings and manage the space's proposals"
},
"authors": {
"label": "Authors",
"information": "Authors are always able to create proposals"
},
"proposalValidation": "Проверка предложения",
"validation": "Type",
"proposalThreshold": {
"label": "Threshold",
"information": "The minimum amount of voting power required to create a proposal"
},
"allowOnlyAuthors": "Разрешить подавать предложения только авторам",
"editValidation": "Редактировать проверку",
"selectValidation": "Выбрать проверку",
"validationParameters": "Параметры проверки",
"voting": "Голосование",
"votingDelay": "Задержка голосования",
"votingPeriod": "Период голосования",
"hours": "Hours",
"days": "Days",
"quorum": {
"label": "Quorum",
"information": "The minimum amount of voting power required for the proposal to pass"
},
"type": {
"label": "Type",
"information": "The type of voting system used for this space. (Enforced on all future proposals)"
},
"anyType": "Любой",
"hideAbstain": "Игнорировать голоса воздержавшихся в основных результатах голосования",
"customDomain": "Пользовательский домен",
"domain": {
"label": "Domain name",
"placeholder": "e.g. vote.balancer.fi",
"info": "To setup a custom domain you additionally need to open a pull request on github after you have created the space. {docs}"
},
"skin": "Тема",
"treasuries": {
"label": "Treasury",
"add": "Add treasury",
"edit": "Edit treasury",
"information": "Add treasuries of your organization to show them in your space"
},
"addPlugin": "Добавить плагин",
"editPlugin": "Редактировать плагин",
"pluginParameters": "Настройки плагина",
"proposal": {
"title": "Proposal",
"guidelines": {
"title": "Guidelines",
"information": "Display a link to your guidelines on proposal creation to help users understand what constitutes a good/valid proposal"
},
"template": {
"title": "Template",
"information": "Start every proposal with a template to help users understand what information is required"
}
}
},
"setup": {
"example": "например, yam.eth",
"chooseExistingEns": "Выберите один из существующих доменов ENS для создания сообщества:",
"useSingleExistingEns": "Используйте свой существующий домен ENS:",
"orRegisterNewEns": "Или зарегистрируйте новый домен:",
"demoTestnetEnsMessage": "To create a test space you need an ENS domain on {network}.",
"toCreateASpace": "Чтобы создать сообщество, вам сначала нужен домен ENS. Введите его ниже и следуйте инструкциям по регистрации ENS.",
"createASpace": "Создать сообщество",
"registerEnsButton": "Зарегистрироваться",
"supportedEnsTLDs": "Поддерживаемые доменные окончания",
"helpDocsAndDiscordLinks": "Not sure how to setup your space? Learn more in the {docs} or join Snapshot {discord}.",
"setSpaceController": "Space controller",
"setSpaceControllerExists": "The snapshot text-record for this domain has already been set. Choose edit to change it, otherwise you can skip to the next step.",
"setSpaceControllerInfo": " The space controller is the account that will be able to manage the space settings. Additional space controllers (admins) can be added later.",
"setSpaceControllerInfoGnosisSafe": "When creating a space with a Gnosis Safe it's recommended to set the safe address as the space controller. If you don't, you need to follow some additional steps. {link}",
"editSpaceController": "Изменить контроллер на ENS",
"setController": "Настроить виджеты",
"explainControllerAndEns": "Setting the controller requires a transaction on the {network} which will add a \"snapshot\" TEXT record to your ENS domain.",
"confirmToSetAddress": "Вы уверены, что хотите установить {address} в качестве управляющего сообществом?",
"controllerHasAuthority": "Контроллер имеет полное право управлять настройками сообщества",
"controller": "Контроллер",
"selectEnsForSpace": "Выберите ENS-адрес",
"spaceOwnerAddressPlaceHolder": "e.g. {address}",
"controllerAddress": "Адрес контроллера",
"updateController": "Обновить контроллер",
"seeOnEns": "Смотреть на ENS",
"goToSettings": "Перейти в настройки",
"setSpaceProfile": "Настроить свое сообщество",
"waitForTransaction": "Подтвердите транзакцию, чтобы создать свое сообщество.{txUrl}",
"pleaseWaitMessage": "Это может занять несколько минут, пожалуйста, подождите, подтверждения трансакции",
"notControllerAddress": "Please connect with the controller address {wallet} to create the space.",
"fillCurrentAccount": "Use currently logged in account",
"domain": {
"title": "Setup your space domain",
"ensMessage": " One thing you need before you can create your own space, is an ENS domain on Ethereum mainnet.",
"ensMessageTestnet": "You can also {link} on the Goerli testnet and mess with things there first.",
"tryDemo": "try the demo",
"yourExistingSpaces": "Your existing spaces",
"invalidEns": "This ENS name is invalid. Usually this is due the use of invalid characters during registration."
},
"strategy": {
"title": "How would you like to setup your voting strategy?",
"subtitle": "You can change your strategy settings any time.",
"blockTitle": "Setup voting strategy",
"onePersonOneVote": {
"title": "One person, one vote",
"description": "Manage a whitelist of people who can vote or simply allow any address to vote. Every vote is equal and no token is required",
"whitelistInformation": "Specify a number of accounts that can vote",
"ticketInformation": "Any account can vote",
"votesEqualInfo": "Each vote is equal and no token is required"
},
"tokenVoting": {
"title": "Token weighted voting",
"description": "Votes are weighted by a token. The token can be an ERC-20, ERC-721 or ERC-1155 token standard",
"tokenNotFound": "Token not found",
"seeOnEtherscan": "See on Etherscan"
},
"advanced": {
"title": "Custom setup",
"description": "Select up to 8 strategies with a wide range of options. If you can't find the right strategy for your use case, you can create your own"
}
},
"validationTitle": "Who can manage this space and create proposals?"
},
"profile": {
"buttonEdit": "Edit profile",
"viewProfile": "View profile",
"about": {
"header": "About",
"joinedSpaces": "Joined spaces",
"createdSpaces": "Created spaces",
"biography": "Bio",
"notJoinSpacesYet": "Hasn't joined any spaces yet",
"notCreatedSpacesYet": "Hasn't created any spaces yet",
"delegatorNetworkInfo": "Change by switching network in your wallet",
"delegate": "Delegate",
"delegated": "Delegated",
"delegateTo": "Delegate to",
"delegateFor": "Delegator for",
"noDelegatorsMessage": "No delegators on {network}",
"notSupportedNetwork": "Delegation currently isn't supported on {network} "
},
"activity": {
"header": "Activity",
"votedFor": "Voted {choice}",
"today": "Today",
"thisWeek": "This week",
"olderThanWeek": "Older than a week",
"noActivity": "No activity yet"
},
"settings": {
"header": "Edit profile",
"name": "Name",
"biography": "Bio",
"namePlaceholder": "Enter name",
"bioPlaceholder": "Tell your story",
"change": "Change",
"remove": "Remove"
}
},
"notify": {
"youDidIt": "У вас получилось!",
"copied": "Скопировано!",
"proposalDeleted": "Предложение удалено",
"somethingWentWrong": "Ой, что-то пошло не так!",
"saved": "Сохранено!",
"delegationSuccess": "Делегирование прошло успешно",
"delegationRemoved": "Делегирование удалено",
"proposalCreated": "Предложение создано",
"voteSuccessful": "Ваш голос принят!",
"ensSet": "Текстовая запись ENS успешно установлена",
"transactionSent": "Транзакция отправлена"
},
"explore": {
"createStrategy": "Создать стратегию",
"createSkin": "Создать тему",
"addNetwork": "Добавить сеть",
"createPlugin": "Создать плагин",
"strategies": "стратегия(и)",
"skins": "тема(ы)",
"networks": "сеть(и)",
"plugins": "плагин(ы)",
"results": "результат(ы)",
"category": "Category",
"categories": {
"all": "Все",
"protocol": "Протоколы",
"social": "Социальные",
"investment": "Инвестиционные",
"grant": "Гранты",
"service": "Услуги",
"media": "Медиа",
"creator": "Создатели",
"collector": "Коллекционеры"
}
},
"voting": {
"selectVoting": "Выберите систему голосования",
"single-choice": "Голосование с одним вариантом ответа",
"approval": "Утверждающее голосование",
"quadratic": "Квадратичное голосование",
"ranked-choice": "Голосование по рейтингу",
"weighted": "Взвешенное голосование",
"basic": "Основное голосование",
"description": {
"single-choice": "Каждый участник голосования может выбрать только один вариант.",
"approval": "Каждый избиратель может выбрать любое количество вариантов.",
"quadratic": "Каждый участник может распределить право голоса между любым количеством вариантов. Результаты подсчитываются квадратично.",
"ranked-choice": "Каждый участник может выбрать и расставить любое количество вариантов. Результаты вычисляются методом мгновенного подсчета голосов.",
"weighted": "Каждый участник голосования может распределить право голоса между любым количеством вариантов.",
"basic": "Голосование с одним вариантом выбора с тремя вариантами: За, Против или Воздержался"
}
},
"privacy": {
"label": "Privacy",
"title": "Select voting privacy",
"information": "The type of privacy used on proposals. (Enforced on all future proposals)",
"any": "Any",
"none": "None",
"shutter": {
"label": "Shutter",
"description": "Choices are encrypted and only visible once the voting period is over",
"tooltip": "This proposal has Shutter privacy enabled. All votes will be encrypted until the voting period has ended and the final score is calculated",
"url": "https://blog.shutter.network/shielded-voting/"
}
},
"validation": {
"label": "Validation",
"title": "Select voting validation",
"information": "The type of validation used to determine if a user can vote. (Enforced on all future proposals)",
"any": {
"label": "Anyone can vote",
"description": "Anyone with voting power can cast a vote."
},
"basic": {
"label": "Basic",
"description": "Use any strategy to determine if a user can vote.",
"invalidMessage": "You do not meet the minimum balance requirement to vote on this proposal. "
},
"passport-gated": {
"label": "Gitcoin Passport gated",
"description": "Protect your proposals from spam and vote manipulation by requiring users to have a Gitcoin Passport.",
"invalidMessage": "You need a Gitcoin Passport with score above {scoreThreshold} and {operator} of the following stamps to vote on this proposal: {stamps}. "
}
},
"safeSnap": {
"currentOutcome": "Текущий исход",
"currentBond": "Текущая связь",
"finalizedIn": "Завершено {0}",
"executableIn": "Исполняемый {0}",
"finalOutcome": "Итог",
"nextBond": "Связать для установления результата",
"setOutcomeTo": "Установить результат в",
"claimBond": "Получить облигацию",
"addBatch": "Добавить пакет транзакций",
"batch": "Пакет транзакций",
"transactions": "Транзакции",
"to": "На (адрес)",
"invalidAddress": "Неверный адрес",
"invalidAmount": "Недопустимая сумма",
"invalidValue": "Недопустимое значение",
"invalidAbi": "Недопустимый ABI",
"invalidData": "Неверные данные",
"value": "Ценность (wei)",
"data": "Данные",
"noCollectibles": "Нет Коллекций",
"asset": "Актив",
"amount": "Сумма",
"type": "Тип",
"transferFunds": "Перевести средства",
"transferNFT": "Передать NFT",
"contractInteraction": "Взаимодействие с контрактом",
"rawTransaction": "Транзакция в исходном виде",
"addTransaction": "Добавить транзакцию",
"transactionLabels": {
"contractInteraction": "{functionName}() - {amount} wei to {address}",
"transferFunds": "Перевести {amount} {tokenSymbol} на {address}",
"transferNFT": "Отправить {name} #{id} на {address}",
"raw": "Отправить {amount} wei на {address}"
},
"labels": {
"request": "Выполнение запроса",
"setOutcome": "Задать результат",
"changeOutcome": "Изменить результат",
"executeTxs": "Выполнить пакет транзакций {0} из {1}",
"executed": "Все транзакции выполнены",
"noTransactions": "There are no transactions to execute",
"rejected": "Proposal rejected",
"error": "Что-то пошло не так",
"connectWallet": "Подключите кошелек, чтобы увидеть детали выполнения",
"switchChain": "Переключите ваш кошелек на {0} для выполнения запроса",
"question": "Принято ли это предложение и соответствует ли оно",
"criteria": "Критерии одобрения?",
"proposalPassed": "Did the proposal pass?",
"expired": "The proposal has expired",
"approveBond": "Approve bond",
"confirmVoteResults": "Click to confirm the proposal passed",
"executeTxsUma": "Execute transaction batch",
"deleteDisputedProposal": "Delete disputed proposal",
"confirmVoteResultsToolTip": "Make sure this proposal was approved by this Snapshot vote before proposing on-chain. If the Snapshot vote rejected the proposal, the on-chain proposal will be rejected as well and you will lose your bond.",
"approveBondToolTip": "On-chain proposals require a bond from the proposer. This will approve tokens from your wallet to be posted as a bond. If you make an invalid proposal, it will be disputed and you will lose your bond. If the proposal is valid, your bond will be returned when the transactions are executed.",
"requestToolTip": "This will propose the transactions from this Snapshot vote on-chain. After a challenge window, if the proposal is valid, the transactions can be executed and your bond will be returned.",
"executeToolTip": "This will execute the transactions from this proposal and return the proposer's bond."
}
},
"poap": {
"no_poap_header": "POAP еще не был настроен для этого предложения:'(",
"no_voted_header": "Проголосуйте, чтобы получить этот POAP",
"unclaimed_header": "Я проголосовал, забрать свой POAP",
"claimed_header": "Поздравляем! POAP добавлен в вашу коллекцию",
"loading_header": "POAP добавляется в вашу коллекцию",
"button_claim": "Минт",
"button_show": "Посмотреть коллекцию",
"success_claim": "POAP добавлен в вашу коллекцию",
"error_claim": "При создании токена возникла проблема"
},
"progress": {
"progress": "Progress",
"inProgress": "In Progress",
"completed": "Completed",
"complete": "Complete",
"newStep": "New Step",
"description": "Description",
"add": "Add",
"deleteStep": "Delete Step",
"deleteConfirm": "Are you sure you want to delete?",
"delete": "Delete",
"cancel": "Cancel",
"comeBack": "Come back after voting is complete to see how this proposal is progressing!",
"confirmSignature": "Signing this message will allow us to authorize your request to update the progress of your proposal.",
"wentWrong": "Oops something went wrong.",
"voting": "Voting",
"soon": "Soon"
},
"charts": {
"charts": "Графики",
"noVotesYet": "Голосов за визуализацию пока нет.",
"totalVotesPerDay": "Общее количество голосов за день",
"shareOfVotingPower": "Поделиться правом голоса",
"votingPowerPerDay": "Доля голосов в день"
},
"comment_box": {
"title": "Comment box",
"add": "Add your comment here",
"submit": "Submit",
"preview": "Preview",
"continue_editing": "Continue editing",
"edit": "Edit your reply here",
"edit_button": "Edit",
"dismiss": "Dismiss",
"delete": "Delete",
"add_reply": "Add your reply here",
"edit_comment": "Edit comment",
"edit_modal": "Are you sure you want to edit?",
"yes": "Yes",
"no": "No",
"delete_comment": "Delete comment",
"delete_modal": "Are you sure you want to delete?",
"error": "Oops, something went wrong",
"replies": "replies",
"hide": "Hide",
"show": "Show",
"reply": "Reply",
"load_more": "Load more"
},
"page": {
"title": {
"home": "Snapshot",
"setup": "Создать сообщество",
"timeline": "Хронологическая лента",
"notifications": "Уведомления",
"explore": "Изучить",
"playground": "Рабочая площадь",
"space": {
"create": "Создайте предложение {space}",
"about": "О {space}",
"proposals": "Предложения {space}",
"proposal": "предложение {space}: {proposal}",
"settings": "Настройки {space}"
},
"strategy": "{key} стратегия",
"delegate": "Делегировать",
"ranking": "Ranking"
}
},
"hal": {
"title": "Отслеживать предложения для {spaceName}",
"text": "Получать уведомления каждый раз, когда создается или завершается новое предложение"
},
"timeUnits": {
"second": "1 секунда | {n} секунд(-ы)",
"minute": "1 минута | {n} минут(-ы)",
"hour": "1 час | {n} часов(а)",
"day": "1 день | {n} дней(я)",
"week": "1 неделя | {n} недель(и)",
"month": "1 месяц | {n} месяцев(а)",
"year": "1 год | {n} лет(года)"
},
"unsupportedNetwork": {
"unsupportedNetwork": "Неподдерживаемая сеть",
"switchNetworkToNetwork": "To continue, you need to change the network in your wallet to {network}.",
"switchToNetwork": "Switch to {network}",
"goToDemoSite": "Go to demo website"
},
"treasury": {
"title": "Treasury",
"wallets": {
"title": "Wallets",
"empty": "This space doesn't have a treasury yet",
"addTreasury": "Add a treasury"
},
"assets": {
"title": "Assets",
"empty": "There are no assets in this contract"
},
"24hChange": "24h change"
},
"newsletter": {
"yourEmail": "Your email",
"title": "Get the latest Snapshot updates"
},
"joinCommunity": "Join Snapshot community",
"header": {
"title": "Where decisions get made",
"description": "Snapshot is a free, open-source platform for community governance. Create your own space now and start making decisions!"
},
"aboutPage": {
"description": "Snapshot is a decentralized governance platform that makes it easy to create and vote on proposals - all without spending a fortune on gas fees! Plus, our flexible system supports various voting types and strategies, so you can tailor the voting process to your needs.",
"subHeader": "Governance should be a snap",
"subDescription": "Web3 governance doesn't have to be complicated. Snapshot is the perfect solution for organizations looking for an easy and efficient way to govern their community or organization."
},
"footerView": {
"resources": "Resources",
"about": "About",
"blog": "Blog",
"jobs": "Jobs",
"discussions": "Discussions",
"github": "GitHub",
"docs": "Docs",
"support": "Support",
"hiring": "Join us!"
}
}
================================================
FILE: src/locales/tr-TR.json
================================================
{
"searchPlaceholder": "Ara",
"spaceCount": "adet",
"createSpace": "Alan oluştur",
"backToHome": "Anasayfa",
"actions": "İşlemler",
"poweredBy": "Powered by",
"results": "Sonuçlar",
"resultsError": "Results could not be calculated. This is often due to a misconfigured strategy or an unresponsive RPC node involved in the strategy.",
"resultsCalculating": "Final results are being calculated. If you still see this message after a few minutes contact the space admin.",
"votingPowerFailedMessage": "Your voting power could not be calculated. This is often due to a misconfigured strategy or an unresponsive RPC node involved in the strategy.",
"votingValidationFailedMessage": "There was an error on our side and we could not verify if you are eligible to vote. This is often due to a misconfigured voting validation or an unresponsive API involved in the validation.",
"notValidVoterMessage": "Oops, you don't seem to be eligible to vote on this proposal.",
"getHelp": "Get help",
"retry": "Retry",
"currentResults": "Güncel Sonuçlar",
"reset": "Sıfırla",
"close": "Close",
"save": "Kaydet",
"author": "Yazar",
"next": "Sonraki",
"choice": "Choice",
"submit": "Gönder",
"plugins": "Eklentiler",
"information": "Bilgi",
"confirm": "Onayla",
"snapshot": "Anlık Görüntü",
"strategies": "Strateji",
"strategiesPage": "Stratejiler",
"space": "Boşluk",
"spaces": "Aralık",
"verifiedSpace": "Verified space",
"warningSpace": "This space has been flagged as potentially malicious. Proceed with caution.",
"version": "Sürüm",
"timeline": "Zaman çizelgesi",
"ended": "ended",
"started": "started",
"filters": "Filtreler",
"allSpaces": "Tümü",
"submitOnchain": "Zincirde onayla",
"inSpaces": "{0} Adet alan içinde",
"votes": "Oylar",
"seeMore": "Daha fazla göster",
"seeAll": "See all",
"network": "Ağ",
"networks": "Ağlar",
"skins": "Ciltler",
"spaceMembers": "Members",
"members": "Üye yok | {count} Üye | {count} Üyeler",
"editStrategy": "Stratejiyi Düzenle",
"invalidProposals": "Geçersiz teklifler",
"account": "Hesap",
"create3box": "Profili 3Box'da oluştur",
"view3box": "Profili 3Box'da görüntüle",
"edit3box": "Profili 3Box'da düzenle",
"connectWallet": "Cüzdanı bağlayın",
"toggleSkin": "Toggle skin",
"about": "Hakkında",
"license": "Lisans",
"showMore": "Show more",
"voted": "Voted",
"reload": "Reload",
"ipfsServer": "IPFS protokolü",
"hub": "Hub",
"cancel": "İptal",
"delete": "Delete",
"demoSite": "Bu site şuan sadece demo, bize şans ver!",
"removeDelegation": "Delegasyonları Kaldır",
"confirmRemove": "Yetkilendirmenizi kaldırmak istediğinizden emin misiniz",
"removeSpace": "{0} alanı için",
"noVotingPower": "Oops, it seems you don't have any voting power at block {blockNumber}.",
"quorumReached": "quorum reached",
"options": "Seçenekler",
"votingPower": "Oy Verme Gücünüz",
"comment": {
"placeholder": "Share your reason (optional)"
},
"receipt": "Dekont",
"relayer": "Aktaran",
"verifyOnMycrypto": "MyCrypto'da dekontu doğrulayın",
"verifyOnSignatorio": "Signator.io'da doğrula",
"isCore": "Genel",
"notificationsBlocked": "Tarayıcın bildirimleri engelliyor",
"notificationsNotSupported": "Your browser does not support notifications",
"walletNotSupported": "Wallet is not supported",
"seeInExplorer": "See explorer",
"learnMore": "Daha fazla bilgi edinin",
"logout": "Çıkış",
"continue": "Devam et",
"add": "Ekle",
"edit": "Düzenle",
"strategyParameters": "Strateji parametreleri",
"addAction": "İşlem Ekle",
"removeAction": "İşlemi kaldırın",
"yourChoice": "Seçenek {0}",
"targetAddress": "Hedef adresi",
"value": "Fiyat",
"date": "Veri",
"marketDetails": "Market detayları",
"addMarket": "Market ekle",
"selectNetwork": "Ağ seçin",
"conditionId": "Durum ID'si",
"basetokenAddress": "Ana token adresi",
"quoteAddress": "Karşıt cüzdan adresi",
"removeMarket": "Marketi kaldır",
"back": "Geri",
"loading": "Yükleniyor...",
"predictedImpact": "Öngörülen etki",
"marketSymbol": "{0} market",
"twoChoicesRequired": "Bu eklenti için iki seçenek gereklidir.",
"noResultsFound": "Hata, herhangi bir sonuç bulamıyoruz",
"createFirstProposal": "İlk teklifinizi oluşturalım",
"noSpacesJoined": "Oops, henüz herhangi bir alana katılmadın",
"addFavorites": "Favorileri ekleyin",
"createdBy": "{0} Tarafından",
"startIn": "başlangıç {0}",
"endIn": "bitiş {0}",
"proposalTimeLeft": "{0} left",
"endedAgo": "ended {0}",
"proposalBy": "{0} tarafından",
"endDate": "bitiş {0}",
"contentHash": "İçerik sağlama",
"defaultSkin": "Varsayılan seçim",
"select": "Seçin",
"language": "Dil",
"agree": "Kabul Ediyorum",
"moderators": "Moderators",
"playground": "Oyun Alanı",
"strategyParams": "Strateji parametreleri",
"addresses": "Adresler",
"networkErrorPlayground": "Ağ hatası - daha fazla bilgi için lütfen tarayıcı konsolunuzu açın",
"upload": "Yükle",
"join": "Katıl",
"joined": "Katıldın",
"leave": "Ayrıl",
"subspaces": "Sub-spaces",
"mainspace": "Main space",
"copyLink": "Linki kopyala",
"duplicate": "Duplicate",
"joinedSpaces": "Alana katıldın",
"joinSpaces": "Alanlara katıl",
"setDelegationToSpace": "Limit delegation to a specific space",
"theCurrentNetwork": "the current network",
"optional": "(optional)",
"homeLoadmore": "Load more",
"confirmAction": "Confirm action",
"or": "or",
"share": "Share",
"shareOnTwitter": "Share on Twitter",
"shareOnLenster": "Share on Lenster",
"createButton": "Create",
"discussion": "Discussion",
"changeWallet": "Change wallet",
"createASpace": "Create a space",
"getStarted": "Get started",
"skip": "Skip",
"newSpaceNotice": {
"header": "Your space is live!",
"mainText": "You can change how voting power is calculated via strategies in your {settings}. Changes to your settings will only affect new proposals, existing proposals can not be changed.",
"learnMore": "Learn more in the {documentation} or join Snapshot {discord} for help.",
"gotIt": "Got it!"
},
"errors": {
"required": "Bu alan zorunludur",
"minLength": "Field is required",
"maxLength": "Maksimum Uzunluk: {0}",
"pattern": "Geçersiz karakter",
"minItems": "Minimum {0} öğe gerekli",
"maxItems": "Maximum {0} item(s) allowed",
"minStrategy": "En az bir strateji gerekiyor.",
"website": "URL should be in the format https://www.example.com",
"format": "Geçersiz format",
"type": "Geçersiz tür",
"unsupportedImageType": "File type not supported, Supported formats are jpeg, jpg and png",
"invalidAddress": "Invalid address"
},
"create": {
"proposalTitle": "Title",
"discussion": "Discussion (optional)",
"categorie(s)": "Select up to 2 categorie(s)",
"proposalDescription": "Description (optional)",
"preview": "Önizleme",
"choices": "Seçenekler",
"addChoice": "Seçim ekle",
"startDate": "Başlangıç zamanını seçin",
"endDate": "Bitiş zamanını seçin",
"startTime": "Başlangıç zamanını seçin",
"endTime": "Bitiş zamanını seçin",
"publish": "Yayımla",
"untitled": "Untitled",
"snapshotBlock": "Ekran görüntüsü alınma blok numarası",
"voting": "Voting",
"votingSystem": "Voting system",
"choice": "Choice {0}",
"period": "Voting period",
"start": "Start",
"end": "End",
"days": "Days",
"hours": "Hours",
"minutes": "Minutes",
"schedule": "Schedule proposal",
"delayEnforced": "The space enforces a delay until voting can start",
"periodEnforced": "The space enforces the duration of the voting period",
"typeEnforced": "{type} is enforced by the space",
"privacyEnforced": "{type} is enforced by the space",
"edit": "Edit",
"continue": "Continue",
"now": "Now",
"votingPeriodExplainer": "This is the time period in which users will be able to vote. The proposal will be visible and pending before the start of the voting period.",
"uploadImageExplainer": "Attach images by dragging & dropping, selecting or pasting them.",
"uploading": "Uploading image",
"markdown": "Styling with Markdown is supported",
"validationWarning": {
"basic": {
"member": "Bir teklif göndermek için alanın yazarı olman gerekir.",
"minScore": "Bir teklif gönderebilmek için minimum {0} {1} sahip olunması gerekir."
},
"customValidation": "Bir teklif göndermek için teklif doğrulamasını geçmeniz gerekir.",
"executionError": "Verifying your eligibility to create proposals in this space has failed. This is likely due to a misconfigured strategy."
},
"errorGettingSnapshot": "We encountered an error while fetching the snapshot block number which is needed to calculate your voting power. Please try again later."
},
"delegate": {
"header": "Temsilci",
"selectDelegate": "Temsilci seç",
"to": "Alıcı",
"addressPlaceholder": "Adres veya ENS adı",
"delegations": "Temsilcilik(ler)iniz",
"allSpaces": "Tüm alanlar için",
"delegated": "Görevlendir",
"pendingTransaction": "bekleyen işlem yok | 1 bekleyen işlem | {count} bekleyen işlem",
"topDelegates": "Top delegates",
"noDelegatesFoundFor": "No delegates found for {0}",
"noValidEns": "Not a valid ENS address.",
"noValidAddress": "Not a valid address",
"delegateToSelf": "You cannot delegate to yourself",
"delegateToSelfAddress": "You cannot delegate to your own ENS address",
"noValidSpaceId": "Not a valid space ID",
"noDelegationsAndDelegates": "Can't find your delegations and delegates? Make sure you are connected to the correct network.",
"delegateNotSupported": "Delegation is currently not supported for {network}."
},
"proposal": {
"castVote": "Oyunu kullan",
"vote": "Oyla",
"startDate": "Başlangıç tarihi",
"endDate": "Bitiş tarihi",
"votingSystem": "Oylama sistemi",
"privacy": "Privacy",
"invalidChoice": "Invalid choice",
"postVoteModal": {
"defaultTitle": "Your vote is in!",
"gnosisSafeTitle": "Your vote is pending...",
"gnosisSafeDescription": " Votes with a Safe require additional signers and will be visible once the transaction is confirmed",
"seeQueue": "See queued transactions",
"tips": {
"1": "Votes can be changed while the proposal is active"
}
}
},
"proposals": {
"header": "Teklifler",
"new": "Yeni teklif",
"noProposals": "Henüz yeni bir teklif yok!",
"createProposal": "Teklif oluştur",
"showMore": "Show more",
"showLess": "Show less",
"states": {
"all": "Tümü",
"core": "Genel",
"community": "Topluluk",
"active": "Aktif",
"pending": "Beklemede",
"closed": "Kapandı"
}
},
"notifications": {
"header": "Notifications",
"noNotifications": "You have no notifications",
"proposalStarted": "proposal has started:",
"proposalEnded": "proposal has ended:",
"markAllAsRead": "Mark all as read",
"all": "All",
"unread": "Unread"
},
"modalTerms": {
"mustAgreeTo": "To {action} this space, you must agree to the {spaceName} terms of service.",
"actionJoin": "join",
"actionCreate": "create a proposal in",
"actionVote": "vote in"
},
"settings": {
"header": "Ayarlar",
"editController": "Edit controller",
"connectWithSpaceOwner": "You are in view only mode, to modify space settings connect with a controller or admin wallet.",
"gnosisWrongNetwork": {
"base": "Your Gnosis Safe is on the wrong network. Please connect to {network} to {action}.",
"settings": "edit the space settings",
"create": "create a proposal",
"vote": "vote on this proposal"
},
"currentSpaceControllerIs": "The current space controller is {address}",
"newController": "New controller",
"noRecord": "No text-record found. Make sure you have registered {id} domain on {network}, then edit the controller text-record to regain access to the space settings.",
"set": "Set",
"profile": "Profil",
"avatar": "Profil Resmi",
"name": {
"label": "Name",
"placeholder": "e.g. Yam Network"
},
"about": {
"label": "About",
"placeholder": "What is your organisation about?"
},
"categories": {
"label": "Categorie(s)",
"select": "Select categorie(s)"
},
"terms": {
"label": "Terms of service",
"information": "Users will be required to accept these terms once before they can create a proposal or cast a vote"
},
"hideSpace": "Ana sayfadan alanı gizle",
"links": "Social accounts",
"subspaces": {
"label": "Sub-spaces",
"information": "Sub-spaces will only be shown once configured on both the main space and the sub-space(s). {docs}",
"parent": {
"label": "Main space",
"placeholder": "pistachiodao.eth",
"information": "The space that this space is a sub-space of will be displayed on the space page"
},
"children": {
"label": "Sub-spaces",
"placeholder": "pistachiodao.eth",
"information": "Related Sub-spaces listed here will be displayed on the space page"
}
},
"website": "Website",
"strategies": {
"label": "Strategie(s)",
"information": "Strategies are used to determine voting power or whether a user is eligible to create a proposal"
},
"network": {
"label": "Network",
"information": "The defaul network used for this space. Networks can also be specified in individual strategies"
},
"symbol": {
"label": "Symbol",
"information": "The default symbol used for this space, usually the token symbol i.e. BAL for Balancer"
},
"strategiesList": "Select up to 8 strategies",
"votingPowerIsCumulative": "Voting power is cumulative",
"addStrategy": "Strateji ekle",
"testInPlayground": "Test in playground",
"admins": {
"label": "Admins",
"information": "Admins are able to modify the space settings and manage the space's proposals"
},
"authors": {
"label": "Authors",
"information": "Authors are always able to create proposals"
},
"proposalValidation": "Teklif doğrulama",
"validation": "Type",
"proposalThreshold": {
"label": "Threshold",
"information": "The minimum amount of voting power required to create a proposal"
},
"allowOnlyAuthors": "Yalnızca yazarların teklif göndermesine izin ver",
"editValidation": "Doğrulamaları düzenle",
"selectValidation": "Doğrulamayı seçin",
"validationParameters": "Doğrulama parametreleri",
"voting": "Oylama",
"votingDelay": "Oylama gecikmesi",
"votingPeriod": "Oylama periyodu",
"hours": "Hours",
"days": "Days",
"quorum": {
"label": "Quorum",
"information": "The minimum amount of voting power required for the proposal to pass"
},
"type": {
"label": "Type",
"information": "The type of voting system used for this space. (Enforced on all future proposals)"
},
"anyType": "Hiç",
"hideAbstain": "Ignore abstain votes in basic voting results",
"customDomain": "Özel alanadı",
"domain": {
"label": "Domain name",
"placeholder": "e.g. vote.balancer.fi",
"info": "To setup a custom domain you additionally need to open a pull request on github after you have created the space. {docs}"
},
"skin": "Birim",
"treasuries": {
"label": "Treasury",
"add": "Add treasury",
"edit": "Edit treasury",
"information": "Add treasuries of your organization to show them in your space"
},
"addPlugin": "Eklenti ekle",
"editPlugin": "Eklentiyi düzenle",
"pluginParameters": "Eklenti Parametreleri",
"proposal": {
"title": "Proposal",
"guidelines": {
"title": "Guidelines",
"information": "Display a link to your guidelines on proposal creation to help users understand what constitutes a good/valid proposal"
},
"template": {
"title": "Template",
"information": "Start every proposal with a template to help users understand what information is required"
}
}
},
"setup": {
"example": "örneğin. yam.eth",
"chooseExistingEns": "Choose one of your existing ENS domains to create a space with:",
"useSingleExistingEns": "Use your existing ENS domain:",
"orRegisterNewEns": "Or register a new domain:",
"demoTestnetEnsMessage": "To create a test space you need an ENS domain on {network}.",
"toCreateASpace": "To create a space, you first need an ENS domain. Enter one below and follow the ENS registration instructions.",
"createASpace": "Oluştur",
"registerEnsButton": "Kaydol",
"supportedEnsTLDs": "Supported domain endings",
"helpDocsAndDiscordLinks": "Not sure how to setup your space? Learn more in the {docs} or join Snapshot {discord}.",
"setSpaceController": "Space controller",
"setSpaceControllerExists": "The snapshot text-record for this domain has already been set. Choose edit to change it, otherwise you can skip to the next step.",
"setSpaceControllerInfo": " The space controller is the account that will be able to manage the space settings. Additional space controllers (admins) can be added later.",
"setSpaceControllerInfoGnosisSafe": "When creating a space with a Gnosis Safe it's recommended to set the safe address as the space controller. If you don't, you need to follow some additional steps. {link}",
"editSpaceController": "Edit controller on ENS",
"setController": "Set controller",
"explainControllerAndEns": "Setting the controller requires a transaction on the {network} which will add a \"snapshot\" TEXT record to your ENS domain.",
"confirmToSetAddress": "Are you sure you want to set {address} as the controller of the space?",
"controllerHasAuthority": "The controller has full authority over the space settings",
"controller": "Controller",
"selectEnsForSpace": "Choose ENS address",
"spaceOwnerAddressPlaceHolder": "e.g. {address}",
"controllerAddress": "Controller address",
"updateController": "Update controller",
"seeOnEns": "See on ENS",
"goToSettings": "Go to settings",
"setSpaceProfile": "Customize your space",
"waitForTransaction": "The transaction need to confirm before you can create your space. {txUrl}",
"pleaseWaitMessage": "This can take a few minutes, please wait while the transaction is being confirmed",
"notControllerAddress": "Please connect with the controller address {wallet} to create the space.",
"fillCurrentAccount": "Use currently logged in account",
"domain": {
"title": "Setup your space domain",
"ensMessage": " One thing you need before you can create your own space, is an ENS domain on Ethereum mainnet.",
"ensMessageTestnet": "You can also {link} on the Goerli testnet and mess with things there first.",
"tryDemo": "try the demo",
"yourExistingSpaces": "Your existing spaces",
"invalidEns": "This ENS name is invalid. Usually this is due the use of invalid characters during registration."
},
"strategy": {
"title": "How would you like to setup your voting strategy?",
"subtitle": "You can change your strategy settings any time.",
"blockTitle": "Setup voting strategy",
"onePersonOneVote": {
"title": "One person, one vote",
"description": "Manage a whitelist of people who can vote or simply allow any address to vote. Every vote is equal and no token is required",
"whitelistInformation": "Specify a number of accounts that can vote",
"ticketInformation": "Any account can vote",
"votesEqualInfo": "Each vote is equal and no token is required"
},
"tokenVoting": {
"title": "Token weighted voting",
"description": "Votes are weighted by a token. The token can be an ERC-20, ERC-721 or ERC-1155 token standard",
"tokenNotFound": "Token not found",
"seeOnEtherscan": "See on Etherscan"
},
"advanced": {
"title": "Custom setup",
"description": "Select up to 8 strategies with a wide range of options. If you can't find the right strategy for your use case, you can create your own"
}
},
"validationTitle": "Who can manage this space and create proposals?"
},
"profile": {
"buttonEdit": "Edit profile",
"viewProfile": "View profile",
"about": {
"header": "About",
"joinedSpaces": "Joined spaces",
"createdSpaces": "Created spaces",
"biography": "Bio",
"notJoinSpacesYet": "Hasn't joined any spaces yet",
"notCreatedSpacesYet": "Hasn't created any spaces yet",
"delegatorNetworkInfo": "Change by switching network in your wallet",
"delegate": "Delegate",
"delegated": "Delegated",
"delegateTo": "Delegate to",
"delegateFor": "Delegator for",
"noDelegatorsMessage": "No delegators on {network}",
"notSupportedNetwork": "Delegation currently isn't supported on {network} "
},
"activity": {
"header": "Activity",
"votedFor": "Voted {choice}",
"today": "Today",
"thisWeek": "This week",
"olderThanWeek": "Older than a week",
"noActivity": "No activity yet"
},
"settings": {
"header": "Edit profile",
"name": "Name",
"biography": "Bio",
"namePlaceholder": "Enter name",
"bioPlaceholder": "Tell your story",
"change": "Change",
"remove": "Remove"
}
},
"notify": {
"youDidIt": "Başardınız!",
"copied": "Kopyalandı!",
"proposalDeleted": "Teklif silindi",
"somethingWentWrong": "Hoop! Birşeyler yanlış gitti!",
"saved": "Kaydedildi!",
"delegationSuccess": "Delegasyon Başarılı",
"delegationRemoved": "Delegation removed",
"proposalCreated": "Proposal created",
"voteSuccessful": "Your vote is in!",
"ensSet": "ENS text record was successfully set",
"transactionSent": "Transaction sent"
},
"explore": {
"createStrategy": "Strateji oluştur",
"createSkin": "Yeni bir tema oluştur",
"addNetwork": "Ağ ekle",
"createPlugin": "Eklenti oluştur",
"strategies": "strateji(ler)",
"skins": "birim(ler)",
"networks": "ağ(lar)",
"plugins": "eklenti(ler)",
"results": "sonuç(lar)",
"category": "Category",
"categories": {
"all": "All",
"protocol": "Protocol",
"social": "Social",
"investment": "Investment",
"grant": "Grant",
"service": "Service",
"media": "Media",
"creator": "Creator",
"collector": "Collector"
}
},
"voting": {
"selectVoting": "Oylama sistemini seçiniz",
"single-choice": "Tek seçenekli oylama",
"approval": "Oy onayı",
"quadratic": "İkinci dereceden(quadratic) oylama",
"ranked-choice": "Dereceli seçim oylaması",
"weighted": "Ağırlıklı oylama",
"basic": "Basic voting",
"description": {
"single-choice": "Her seçmen sadece bir seçim yapabilir.",
"approval": "Her seçmen istediği sayıda seçim yapabilir.",
"quadratic": "Her seçmen, oy verme gücünü herhangi bir sayıda seçeneğe yayabilir. Sonuçlar kuadratik olarak hesaplanır.",
"ranked-choice": "Her seçmen istediği sayıda seçeneği seçebilir ve sıralayabilir. Sonuçlar anlık akış sayma yöntemiyle hesaplanır.",
"weighted": "Her seçmen, oy verme gücünü herhangi bir sayıda seçeneğe yayabilir.",
"basic": "Single choice voting with three choices: For, Against or Abstain"
}
},
"privacy": {
"label": "Privacy",
"title": "Select voting privacy",
"information": "The type of privacy used on proposals. (Enforced on all future proposals)",
"any": "Any",
"none": "None",
"shutter": {
"label": "Shutter",
"description": "Choices are encrypted and only visible once the voting period is over",
"tooltip": "This proposal has Shutter privacy enabled. All votes will be encrypted until the voting period has ended and the final score is calculated",
"url": "https://blog.shutter.network/shielded-voting/"
}
},
"validation": {
"label": "Validation",
"title": "Select voting validation",
"information": "The type of validation used to determine if a user can vote. (Enforced on all future proposals)",
"any": {
"label": "Anyone can vote",
"description": "Anyone with voting power can cast a vote."
},
"basic": {
"label": "Basic",
"description": "Use any strategy to determine if a user can vote.",
"invalidMessage": "You do not meet the minimum balance requirement to vote on this proposal. "
},
"passport-gated": {
"label": "Gitcoin Passport gated",
"description": "Protect your proposals from spam and vote manipulation by requiring users to have a Gitcoin Passport.",
"invalidMessage": "You need a Gitcoin Passport with score above {scoreThreshold} and {operator} of the following stamps to vote on this proposal: {stamps}. "
}
},
"safeSnap": {
"currentOutcome": "Current outcome",
"currentBond": "Current bond",
"finalizedIn": "Sonuçlandı {0}",
"executableIn": "Çalıştırılabilir {0}",
"finalOutcome": "Outcome",
"nextBond": "Bond to set outcome",
"setOutcomeTo": "Set outcome to",
"claimBond": "Teminat talebi",
"addBatch": "Add transaction batch",
"batch": "Transaction batch",
"transactions": "Transactions",
"to": "To (address)",
"invalidAddress": "Invalid address",
"invalidAmount": "Invalid amount",
"invalidValue": "Invalid value",
"invalidAbi": "Invalid ABI",
"invalidData": "Invalid data",
"value": "Value (wei)",
"data": "Data",
"noCollectibles": "No collectibles",
"asset": "Asset",
"amount": "Amount",
"type": "Type",
"transferFunds": "Transfer funds",
"transferNFT": "Transfer NFT",
"contractInteraction": "Contract interaction",
"rawTransaction": "Raw transaction",
"addTransaction": "Add transaction",
"transactionLabels": {
"contractInteraction": "{functionName}() - {amount} wei to {address}",
"transferFunds": "Transfer {amount} {tokenSymbol} to {address}",
"transferNFT": "Send {name} #{id} to {address}",
"raw": "Send {amount} wei to {address}"
},
"labels": {
"request": "Yürütme isteğinde bulun",
"setOutcome": "Sonucu ayarla",
"changeOutcome": "Sonucu değiştir",
"executeTxs": "Execute transaction batch {0} of {1}",
"executed": "All transactions have been executed",
"noTransactions": "There are no transactions to execute",
"rejected": "Proposal rejected",
"error": "Something went wrong",
"connectWallet": "Connect wallet to see execution details",
"switchChain": "Switch your wallet to {0} to request execution",
"question": "Bu teklif kabul edildi mi ve beklentileri karşılıyor mu",
"criteria": "kabul şartları?",
"proposalPassed": "Did the proposal pass?",
"expired": "The proposal has expired",
"approveBond": "Approve bond",
"confirmVoteResults": "Click to confirm the proposal passed",
"executeTxsUma": "Execute transaction batch",
"deleteDisputedProposal": "Delete disputed proposal",
"confirmVoteResultsToolTip": "Make sure this proposal was approved by this Snapshot vote before proposing on-chain. If the Snapshot vote rejected the proposal, the on-chain proposal will be rejected as well and you will lose your bond.",
"approveBondToolTip": "On-chain proposals require a bond from the proposer. This will approve tokens from your wallet to be posted as a bond. If you make an invalid proposal, it will be disputed and you will lose your bond. If the proposal is valid, your bond will be returned when the transactions are executed.",
"requestToolTip": "This will propose the transactions from this Snapshot vote on-chain. After a challenge window, if the proposal is valid, the transactions can be executed and your bond will be returned.",
"executeToolTip": "This will execute the transactions from this proposal and return the proposer's bond."
}
},
"poap": {
"no_poap_header": "Bu teklif için henüz bir POAP kurulmadı :'(",
"no_voted_header": "Bu POAP'ı almak için oy verin",
"unclaimed_header": "Mint your I voted POAP",
"claimed_header": "Congratulations! The POAP has been minted to your collection",
"loading_header": "The POAP is being minted to your collection",
"button_claim": "Mint",
"button_show": "Browse collection",
"success_claim": "The POAP has been minted to your collection",
"error_claim": "Jeton basılırken bir hata oluştu"
},
"progress": {
"progress": "Progress",
"inProgress": "In Progress",
"completed": "Completed",
"complete": "Complete",
"newStep": "New Step",
"description": "Description",
"add": "Add",
"deleteStep": "Delete Step",
"deleteConfirm": "Are you sure you want to delete?",
"delete": "Delete",
"cancel": "Cancel",
"comeBack": "Come back after voting is complete to see how this proposal is progressing!",
"confirmSignature": "Signing this message will allow us to authorize your request to update the progress of your proposal.",
"wentWrong": "Oops something went wrong.",
"voting": "Voting",
"soon": "Soon"
},
"charts": {
"charts": "Charts",
"noVotesYet": "There are no votes to visualize yet.",
"totalVotesPerDay": "Total votes per day",
"shareOfVotingPower": "Share of voting power",
"votingPowerPerDay": "Voting power per day"
},
"comment_box": {
"title": "Comment box",
"add": "Add your comment here",
"submit": "Submit",
"preview": "Preview",
"continue_editing": "Continue editing",
"edit": "Edit your reply here",
"edit_button": "Edit",
"dismiss": "Dismiss",
"delete": "Delete",
"add_reply": "Add your reply here",
"edit_comment": "Edit comment",
"edit_modal": "Are you sure you want to edit?",
"yes": "Yes",
"no": "No",
"delete_comment": "Delete comment",
"delete_modal": "Are you sure you want to delete?",
"error": "Oops, something went wrong",
"replies": "replies",
"hide": "Hide",
"show": "Show",
"reply": "Reply",
"load_more": "Load more"
},
"page": {
"title": {
"home": "Snapshot",
"setup": "Create a space",
"timeline": "Timeline",
"notifications": "Notifications",
"explore": "Explore",
"playground": "Playground",
"space": {
"create": "Create {space} proposal",
"about": "About {space}",
"proposals": "{space} Proposals",
"proposal": "{space} proposal: {proposal}",
"settings": "{space} Settings"
},
"strategy": "{key} strategy",
"delegate": "Delegate",
"ranking": "Ranking"
}
},
"hal": {
"title": "Track proposals for {spaceName}",
"text": "Receive notifications every time a new proposal is created or ends"
},
"timeUnits": {
"second": "1 second | {n} seconds",
"minute": "1 minute | {n} minutes",
"hour": "1 hour | {n} hours",
"day": "1 day | {n} days",
"week": "1 week | {n} weeks",
"month": "1 month | {n} months",
"year": "1 year | {n} years"
},
"unsupportedNetwork": {
"unsupportedNetwork": "Unsupported network",
"switchNetworkToNetwork": "To continue, you need to change the network in your wallet to {network}.",
"switchToNetwork": "Switch to {network}",
"goToDemoSite": "Go to demo website"
},
"treasury": {
"title": "Treasury",
"wallets": {
"title": "Wallets",
"empty": "This space doesn't have a treasury yet",
"addTreasury": "Add a treasury"
},
"assets": {
"title": "Assets",
"empty": "There are no assets in this contract"
},
"24hChange": "24h change"
},
"newsletter": {
"yourEmail": "Your email",
"title": "Get the latest Snapshot updates"
},
"joinCommunity": "Join Snapshot community",
"header": {
"title": "Where decisions get made",
"description": "Snapshot is a free, open-source platform for community governance. Create your own space now and start making decisions!"
},
"aboutPage": {
"description": "Snapshot is a decentralized governance platform that makes it easy to create and vote on proposals - all without spending a fortune on gas fees! Plus, our flexible system supports various voting types and strategies, so you can tailor the voting process to your needs.",
"subHeader": "Governance should be a snap",
"subDescription": "Web3 governance doesn't have to be complicated. Snapshot is the perfect solution for organizations looking for an easy and efficient way to govern their community or organization."
},
"footerView": {
"resources": "Resources",
"about": "About",
"blog": "Blog",
"jobs": "Jobs",
"discussions": "Discussions",
"github": "GitHub",
"docs": "Docs",
"support": "Support",
"hiring": "Join us!"
}
}
================================================
FILE: src/locales/uk-UA.json
================================================
{
"searchPlaceholder": "Пошук",
"spaceCount": "{0} розділ(и)",
"createSpace": "Створити розділ",
"backToHome": "Головна",
"actions": "Дії",
"results": "Результат",
"resultsError": "Results could not be calculated. This is often due to a misconfigured strategy or an unresponsive RPC node involved in the strategy.",
"vpError": "Your voting power could not be calculated. This is often due to a misconfigured strategy or an unresponsive RPC node involved in the strategy.",
"getHelp": "Get help",
"retry": "Retry",
"currentResults": "Поточні результати",
"reset": "Повернути налаштування",
"save": "Зберегти",
"author": "Автор",
"next": "Далі",
"submit": "Надіслати",
"plugins": "Модулі",
"information": "Інформація",
"confirm": "Підтвердити",
"snapshot": "Snapshot",
"strategies": "Стратегія(ї)",
"strategiesPage": "Стратегії",
"space": "Сховище",
"spaces": "Сховища",
"verifiedSpace": "Verified space",
"version": "Версія ",
"timeline": "Часова шкала",
"filters": "Фільтри",
"allSpaces": "Всі області",
"submitOnchain": "Надіслати в ланцюжок",
"inSpaces": "У {0} сховищах",
"votes": "Голоси",
"seeMore": "Показати більше",
"network": "Мережа",
"networks": "Мережі",
"skins": "Теми оформлення",
"members": "No members | {count} member | {count} members",
"editStrategy": "Редагувати стратегію",
"invalidProposals": "Недійсні пропозиції",
"account": "Мій профіль",
"create3box": "Створити профіль у 3Box",
"view3box": "Переглянути профіль у 3Box",
"edit3box": "Редагувати профіль у 3Box",
"connectWallet": "Підключити гаманець",
"toggleSkin": "Toggle skin",
"about": "Про",
"license": "Ліцензія",
"ipfsServer": "IPFS сервер",
"hub": "Хаб",
"cancel": "Скасувати",
"deleteProposal": "Видалити пропозицію",
"demoSite": "Це демо-сайт, спробуйте!",
"removeDelegation": "Видалити делегацію",
"confirmRemove": "Ви впевнені, що хочете видалити делегування?",
"removeSpace": "для простору {0}",
"confirmVote": "Підтвердити свій голос",
"sureToVote": "Are you sure you want to cast this vote?",
"cannotBeUndone": "Цю дію не можна скасувати.",
"options": "Параметри",
"votingPower": "Ваша сила голосу",
"receipt": "Квитанція",
"relayer": "Ретранслятор",
"verifyOnMycrypto": "Перевірити чек на MyCrypto",
"verifyOnSignatorio": "Verify on Signator.io",
"isCore": "Ядро",
"notificationsBlocked": "Your browser is blocking notifications",
"notificationsNotSupported": "Your browser does not support notifications",
"walletNotSupported": "Wallet is not supported",
"seeInExplorer": "Дивитися в провіднику",
"learnMore": "Дізнатися більше",
"logout": "Вийти",
"continue": "Продовжити",
"add": "Додати",
"edit": "Редагувати",
"strategyParameters": "Параметри стратегії",
"addAction": "Додати дію",
"removeAction": "Видалити дію",
"yourChoice": "Вибір {0}",
"targetAddress": "Адреса призначення",
"value": "Значення",
"date": "Дані",
"marketDetails": "Деталі ринку",
"addMarket": "Додати ринок",
"selectNetwork": "Обрати мережу",
"conditionId": "Стан ID:",
"basetokenAddress": "Основна адреса токена",
"quoteAddress": "Укажіть адресу валюти",
"removeMarket": "Видалити ринок",
"back": "Назад",
"loading": "Завантаження...",
"predictedImpact": "Передбачуваний вплив",
"marketSymbol": "{0} ринок",
"twoChoicesRequired": "Потрібно два варіанти для цього плагіна.",
"noResultsFound": "Ой! Нічого не знайдено",
"createFirstProposal": "Let's create your first proposal",
"noSpacesJoined": "Oops, you haven't joined any spaces yet",
"addFavorites": "Додати улюблені",
"createdBy": "Від {0}",
"startIn": "початок {0}",
"endIn": "кінець {0}",
"proposalTimeLeft": "{0} left",
"endedAgo": "ended {0}",
"proposalBy": "від {0}",
"endDate": "кінець {0}",
"contentHash": "Хеш контенту",
"defaultSkin": "Тема оформлення за замовчуванням",
"select": "Вибрати",
"language": "Мова",
"agree": "Я згоден",
"mustAgreeToTerms": "Перш ніж ви зможете завершити цю дію, ви повинні погодитися з {0} умовами використання:",
"playground": "Ігровий майданчик",
"strategyParams": "Параметри стратегії",
"addresses": "Адреси",
"networkErrorPlayground": "Помилка мережі - будь ласка, відкрийте ваш браузер для отримання додаткової інформації",
"upload": "Завантажити",
"join": "Join",
"joined": "Joined",
"leave": "Leave",
"copyLink": "Скопіювати посилання",
"duplicateProposal": "Дублювати пропозицію",
"joinedSpaces": "Joined spaces",
"joinSpaces": "Join spaces",
"setDelegationToSpace": "Limit delegation to a specific space",
"theCurrentNetwork": "the current network",
"optional": "(optional)",
"homeLoadmore": "Load more",
"confirmAction": "Confirm action",
"or": "or",
"share": "Share",
"errors": {
"required": "Обов'язкове поле",
"minLength": "Мінімальна довжина: {0}",
"maxLength": "Максимальна довжина: {0}",
"pattern": "Неприпустимий символ:",
"minItems": "Потрібно мінімум {0} елементів",
"minStrategy": "Потрібна принаймні одна стратегія.",
"format": "Невірний формат",
"type": "Invalid type"
},
"create": {
"question": "Ask a question...",
"categorie(s)": "Select up to 2 categorie(s)",
"content": "Tell us more about your proposal (optional)",
"preview": "Попередній перегляд",
"choices": "Вибір",
"addChoice": "Додати вибір",
"startDate": "Виберіть дату початку",
"endDate": "Оберіть дату",
"startTime": "Оберіть час початку",
"endTime": "Оберіть час закінчення",
"publish": "Публікувати",
"untitled": "Untitled",
"snapshotBlock": "Номер блоку знімка",
"voting": "Voting",
"votingSystem": "Voting system",
"choice": "Choice {0}",
"period": "Voting period",
"start": "Start",
"end": "End",
"days": "Days",
"hours": "Hours",
"minutes": "Minutes",
"schedule": "Schedule proposal",
"delayEnforced": "The space enforces a delay until voting can start",
"periodEnforced": "The space enforces the duration of the voting period",
"edit": "Edit",
"continue": "Continue",
"now": "Now",
"votingPeriodExplainer": "This is the time period in which users will be able to vote. The proposal will be visible and pending before the start of the voting period.",
"validationWarning": {
"basic": {
"member": "You need to be an author of the space in order to submit a proposal.",
"minScore": "Вам потрібно мати мінімум {0} {1} для того, щоб зробити пропозицію."
},
"customValidation": "Вам необхідно пройти перевірку пропозиції, щоб запропонувати пропозицію.",
"executionError": "Verifying your eligibility to create proposals in this space has failed. This is likely due to a misconfigured strategy."
}
},
"delegate": {
"header": "Делегувати",
"selectDelegate": "Вибрати делегата",
"to": "Для",
"addressPlaceholder": "Адреса або ім'я ENS",
"delegations": "Ваша делегація(ї)",
"allSpaces": "Для всіх сховищ",
"delegated": "Делеговано вам",
"pendingTransaction": "no pending transaction | 1 pending transaction | {count} pending transactions",
"topDelegates": "Top delegates",
"noDelegatesFoundFor": "No delegates found for {0}",
"noValidEns": "Not a valid ENS address.",
"noValidAddress": "Not a valid address",
"delegateToSelf": "You cannot delegate to yourself",
"delegateToSelfAddress": "You cannot delegate to your own ENS address",
"noValidSpaceId": "Not a valid space ID",
"noDelegationsAndDelegates": "Can't find your delegations and delegates? Make sure you are connected to the correct network.",
"delegateNotSupported": "Delegation is currently not supported for {network}."
},
"proposal": {
"castVote": "Віддайте свій голос",
"vote": "Голосувати",
"startDate": "Дата початку",
"endDate": "Дата завершення",
"votingSystem": "Система голосування"
},
"proposals": {
"header": "Пропозиції",
"new": "Нова пропозиція",
"noProposals": "Тут поки що немає пропозицій!",
"createProposal": "Create proposal",
"showMore": "Show more",
"showLess": "Show less",
"states": {
"all": "Всі",
"core": "Ядро",
"community": "Спільнота",
"active": "Активний",
"pending": "Очікує підтвердження",
"closed": "Закрито"
}
},
"settings": {
"header": "Налаштування",
"setEnsTextRecord": "Set ENS text record",
"editController": "Edit controller",
"needToSetEnsText": "Your space settings will be stored in a file on IPFS. Before you can edit them, you need to set an ENS text record pointing to that file.",
"profile": "Профіль",
"avatar": "Зображення користувача",
"name": "Ім’я",
"about": "Про",
"terms": "Умови",
"network": "Мережа",
"symbol": "Символ",
"skin": "Тема оформлення",
"customDomain": "Custom domain",
"domain": "Ім'я домену",
"strategies": "Стратегія(ї)",
"categories": "Categorie(s)",
"selectCategories": "Select categories",
"hideSpace": "Сховати простір з головної сторінки",
"addStrategy": "Додати стратегію",
"authors": "Authors",
"admins": "Адмінстратори",
"defaultTab": "Закладка за замовчуванням",
"proposalThreshold": "Поріг пропозиції",
"allowOnlyAuthors": "Allow only authors to submit a proposal",
"editPlugin": "Редагувати плагін",
"addPlugin": "Додати плагін",
"pluginParameters": "Параметри плагіну",
"proposalValidation": "Перевірка пропозиції",
"validation": "Перевірка",
"editValidation": "Редагувати перевірку",
"selectValidation": "Виберіть перевірку",
"validationParameters": "Параметри перевірки",
"warningTextRecord": "Ви повинні встановити текстовий запис з домену ENS.",
"votingDelay": "Voting delay",
"votingPeriod": "Voting period",
"voting": "Voting",
"quorum": "Quorum",
"type": "Type",
"anyType": "Any",
"hideAbstain": "Ignore abstain votes in basic voting results",
"connectWithSpaceOwner": "To modify space settings, connect with a controller or admin wallet."
},
"setup": {
"example": "наприклад yam.eth",
"chooseExistingEns": "Choose one of your existing ENS domains to create a space with:",
"useSingleExistingEns": "Use your existing ENS domain:",
"orRegisterNewEns": "Or register a new domain:",
"toCreateASpace": "To create a space, you first need an ENS domain. Enter one below and follow the ENS registration instructions.",
"createASpace": "Створити області",
"registerEnsButton": "Register",
"supportedEnsTLDs": "Supported domain endings",
"helpDocsAndDiscordLinks": "Not sure how to create a space? Learn more in the documentation or join Snapshot Discord .",
"setSpaceController": "Set space controller address on ENS",
"editSpaceController": "Edit controller on ENS",
"setController": "Set controller",
"explainControllerAndEns": "This action requires a transaction on the Ethereum Mainnet which will add a \"snapshot\" TEXT record to your ENS domain.",
"confirmToSetAddress": "Are you sure you want to set {address} as the controller of the space?",
"controllerHasAuthority": "The controller has full authority over the space settings",
"controller": "Controller",
"selectEnsForSpace": "Choose ENS address",
"textRecordExists": "Currently the space controller is {address}. You can update the space controller by using the form below.",
"spaceOwnerAddressPlaceHolder": "e.g. 0xF78108c9BBaF466dd96BE41be728Fe3220b37119",
"controllerAddress": "Controller address",
"updateController": "Update controller",
"connectWithEnsController": "To modify the space controller you need to connect with the wallet that owns {ens}",
"seeOnEns": "See on ENS",
"goToSettings": "Go to settings"
},
"notify": {
"youDidIt": "Ви це зробили!",
"copied": "Скопійовано!",
"proposalDeleted": "Пропозицію видалено",
"somethingWentWrong": "Ой, щось пішло не так!",
"saved": "Saved!",
"delegationSuccess": "Delegation successful",
"delegationRemoved": "Delegation removed",
"proposalCreated": "Proposal created",
"voteSuccessful": "Your vote is in!",
"ensSet": "ENS text record was successfully set"
},
"explore": {
"createStrategy": "Створити стратегію",
"createSkin": "Створити тему",
"addNetwork": "Додати мережу",
"createPlugin": "Створити плагін",
"strategies": "стратегія(ї)",
"skins": "тема(и) оформлення",
"networks": "мережа(і)",
"plugins": "плагін(и)",
"results": "результат(и)",
"categories": {
"all": "All",
"protocol": "Protocol",
"social": "Social",
"investment": "Investment",
"grant": "Grant",
"service": "Service",
"media": "Media",
"creator": "Creator",
"collector": "Collector"
}
},
"voting": {
"selectVoting": "Виберіть систему голосування",
"single-choice": "Голосування з одним вибором",
"approval": "Затвердження голосування",
"quadratic": "Квадратичне голосування",
"ranked-choice": "Голосування за ранговий вибір",
"weighted": "Взважене голосування",
"basic": "Basic voting",
"description": {
"single-choice": "Кожен учасник голосування може зробити лише один вибір.",
"approval": "Кожен учасник може вибрати будь-яку кількість варіантів.",
"quadratic": "Кожен учасник голосування може поширювати свій голос на будь -яку кількість варіантів. Результати розраховуються квадратично.",
"ranked-choice": "Кожен учасник голосування може обрати та оцінити будь -яку кількість варіантів. Результати розраховуються миттєво",
"weighted": "Кожен учасник голосування може розподіляти своє право голосу на будь -яку кількість варіантів.",
"basic": "Single choice voting with three choices: For, Against or Abstain"
},
"choices": {
"for": "For",
"against": "Against",
"abstain": "Abstain"
}
},
"safeSnap": {
"currentOutcome": "Current outcome",
"currentBond": "Current bond",
"finalizedIn": "Завершено {0}",
"executableIn": "Виконано {0}",
"finalOutcome": "Outcome",
"nextBond": "Bond to set outcome",
"setOutcomeTo": "Set outcome to",
"claimBond": "Отримати облігації",
"addBatch": "Add transaction batch",
"batch": "Transaction batch",
"transactions": "Transactions",
"to": "To (address)",
"invalidAddress": "Invalid address",
"invalidAmount": "Invalid amount",
"invalidValue": "Invalid value",
"invalidAbi": "Invalid ABI",
"invalidData": "Invalid data",
"value": "Value (wei)",
"data": "Data",
"noCollectibles": "No collectibles",
"asset": "Asset",
"amount": "Amount",
"type": "Type",
"transferFunds": "Transfer funds",
"transferNFT": "Transfer NFT",
"contractInteraction": "Contract interaction",
"rawTransaction": "Raw transaction",
"addTransaction": "Add transaction",
"transactionLabels": {
"contractInteraction": "{functionName}() - {amount} wei to {address}",
"transferFunds": "Transfer {amount} {tokenSymbol} to {address}",
"transferNFT": "Send {name} #{id} to {address}",
"raw": "Send {amount} wei to {address}"
},
"labels": {
"request": "Виконання запиту",
"setOutcome": "Встановіть результат",
"changeOutcome": "Змінити результат",
"executeTxs": "Execute transaction batch {0} of {1}",
"executed": "All transactions have been executed",
"rejected": "Proposal rejected",
"error": "Something went wrong",
"connectWallet": "Connect wallet to see execution details",
"switchChain": "Switch your wallet to {0} to request execution",
"question": "Чи пройшла ця пропозиція і чи відповідає вона",
"criteria": "критерії приймання?",
"proposalPassed": "Did the proposal pass?"
}
},
"poap": {
"no_poap_header": "POAP ще не налаштований для цієї пропозиції :'(",
"no_voted_header": "Проголосуйте, щоб отримати POAP",
"unclaimed_header": "Mint your I voted POAP",
"claimed_header": "Congratulations! The POAP has been minted to your collection",
"loading_header": "The POAP is being minted to your collection",
"button_claim": "Mint",
"button_show": "Browse collection",
"success_claim": "The POAP has been minted to your collection",
"error_claim": "Під створюванні токену виникла проблема"
},
"charts": {
"charts": "Charts",
"noVotesYet": "There are no votes to visualize yet.",
"totalVotesPerDay": "Total votes per day",
"shareOfVotingPower": "Share of voting power",
"votingPowerPerDay": "Voting power per day"
},
"comment_box": {
"title": "Comment box",
"add": "Add your comment here",
"submit": "Submit",
"preview": "Preview",
"continue_editing": "Continue editing",
"edit": "Edit your reply here",
"edit_button": "Edit",
"dismiss": "Dismiss",
"delete": "Delete",
"add_reply": "Add your reply here",
"edit_comment": "Edit comment",
"edit_modal": "Are you sure you want to edit?",
"yes": "Yes",
"no": "No",
"delete_comment": "Delete comment",
"delete_modal": "Are you sure you want to delete?",
"error": "Oops, something went wrong",
"replies": "replies",
"hide": "Hide",
"show": "Show",
"reply": "Reply",
"load_more": "Load more"
},
"page": {
"title": {
"home": "Snapshot",
"setup": "Create a space",
"timeline": "Timeline",
"explore": "Explore",
"playground": "Playground",
"space": {
"create": "Create {space} proposal",
"about": "About {space}",
"proposals": "{space} Proposals",
"proposal": "{space} proposal: {proposal}",
"settings": "{space} Settings"
},
"strategy": "{key} strategy",
"delegate": "Delegate"
}
},
"hal": {
"title": "Track proposals for {spaceName}",
"text": "Receive notifications every time a new proposal is created or ends"
},
"timeUnits": {
"second": "1 second | {n} seconds",
"minute": "1 minute | {n} minutes",
"hour": "1 hour | {n} hours",
"day": "1 day | {n} days",
"week": "1 week | {n} weeks",
"month": "1 month | {n} months",
"year": "1 year | {n} years"
},
"unsupportedNetwork": {
"unsupportedNetwork": "Unsupported network",
"ensOnlyMainnet": "Snapshot only supports ENS on the ethereum mainnet. Once the space is created, you can use it across multiple chains.",
"switchNetworkToMainnet": "To continue, you need to change the network in your wallet to Ethereum Mainnet.",
"switchToMainnet": "Switch to Mainnet"
}
}
================================================
FILE: src/locales/vi-VN.json
================================================
{
"searchPlaceholder": "Tìm kiếm",
"spaceCount": "{0} khoảng trống(s)",
"createSpace": "Tạo khoảng trống",
"backToHome": "Nhà",
"actions": "Hành động",
"results": "Kết quả",
"resultsError": "Results could not be calculated. This is often due to a misconfigured strategy or an unresponsive RPC node involved in the strategy.",
"vpError": "Your voting power could not be calculated. This is often due to a misconfigured strategy or an unresponsive RPC node involved in the strategy.",
"getHelp": "Get help",
"retry": "Retry",
"currentResults": "Kết quả hiện tại",
"reset": "Cài lại",
"save": "Lưu lại",
"author": "Tác giả",
"next": "Tiếp theo",
"submit": "Nộp",
"plugins": "Bổ sung",
"information": "Thông tin",
"confirm": "Xác nhận",
"snapshot": "Chụp ảnh",
"strategies": "Chiến lược(s)",
"strategiesPage": "Chiến lược",
"space": "Khoảng trống",
"spaces": "Những khoảng trống",
"verifiedSpace": "Verified space",
"version": "Phiên bản",
"timeline": "Mốc thời gian",
"filters": "Bộ lọc",
"allSpaces": "Tất cả các khoảng trống",
"submitOnchain": "Nộp on chain",
"inSpaces": "Trong {0} khoảng trống(s)",
"votes": "Bầu",
"seeMore": "Xem nhiều hơn",
"network": "Mạng",
"networks": "Mạng",
"skins": "Giao diện",
"members": "Chưa có thành viên | {count} thành viên} | {count} thành viên",
"editStrategy": "Chỉnh sửa chiến lược",
"invalidProposals": "Đề xuất không hợp lệ",
"account": "Tài khoản",
"create3box": "Tạo hồ sơ trên 3Box",
"view3box": "Xem hồ sơ trên 3Box",
"edit3box": "Chỉnh sửa hồ sơ trên 3Box",
"connectWallet": "Kết nối ví",
"toggleSkin": "Toggle skin",
"about": "Về",
"license": "Giấy phép",
"ipfsServer": "Máy chủ IPFS",
"hub": "Trung tâm",
"cancel": "Hủy bỏ",
"deleteProposal": "Xóa đề xuất",
"demoSite": "Đây là trang demo, hãy thử!",
"removeDelegation": "Gỡ bỏ ủy quyền",
"confirmRemove": "Bạn chắc chắn muốn xóa ủy quyền cho",
"removeSpace": "cho khoảng trống {0}",
"confirmVote": "Xác nhận phiếu bầu",
"sureToVote": "Are you sure you want to cast this vote?",
"cannotBeUndone": "Hành động này không thể hoàn tác.",
"options": "Các lựa chọn(s)",
"votingPower": "Quyền biểu quyết của bạn",
"receipt": "Nhận",
"relayer": "Lặp lại",
"verifyOnMycrypto": "Xác minh biên nhận trên MyCrypto",
"verifyOnSignatorio": "Verify on Signator.io",
"isCore": "Cốt lõi",
"notificationsBlocked": "Your browser is blocking notifications",
"notificationsNotSupported": "Your browser does not support notifications",
"walletNotSupported": "Wallet is not supported",
"seeInExplorer": "Xem trên trình duyệt",
"learnMore": "Tìm hiểu thêm",
"logout": "Đăng xuất",
"continue": "Tiếp tục",
"add": "Thêm vào",
"edit": "Chỉnh sửa",
"strategyParameters": "Thông số chiến lược",
"addAction": "Thêm hành động",
"removeAction": "Xóa hành động",
"yourChoice": "Lựa chọn {0}",
"targetAddress": "Địa chỉ mục tiêu",
"value": "Giá trị",
"date": "Dữ liệu",
"marketDetails": "Chi tiết thị trường",
"addMarket": "Thêm thị trường",
"selectNetwork": "Chọn mạng",
"conditionId": "Điều kiện ID",
"basetokenAddress": "Địa chỉ mã thông báo cơ sở",
"quoteAddress": "Trích dẫn địa chỉ tiền tệ",
"removeMarket": "Xóa thị trường",
"back": "Trở lại",
"loading": "Tải...",
"predictedImpact": "Tác động dự đoán",
"marketSymbol": "{0} thị trường",
"twoChoicesRequired": "Cần có hai lựa chọn cho plugin này.",
"noResultsFound": "Rất tiếc, chúng tôi không thể tìm thấy bất kỳ kết quả nào",
"createFirstProposal": "Let's create your first proposal",
"noSpacesJoined": "Bạn chưa tham gia một nhóm nào cả",
"addFavorites": "Thêm yêu thích",
"createdBy": "Bởi {0}",
"startIn": "bắt đầu {0}",
"endIn": "kết thúc {0}",
"proposalTimeLeft": "{0} left",
"endedAgo": "ended {0}",
"proposalBy": "bởi {0}",
"endDate": "kết thúc {0}",
"contentHash": "Nội dung băm",
"defaultSkin": "Skin mặc định",
"select": "Lựa chọn",
"language": "Ngôn ngữ",
"agree": "Tôi đồng ý",
"mustAgreeToTerms": "Trước khi có thể hoàn tất hành động này, bạn phải đồng ý với {0} điều khoản dịch vụ tại đây:",
"playground": "Sân chơi",
"strategyParams": "Thông số chiến lược",
"addresses": "Địa chỉ",
"networkErrorPlayground": "Lỗi mạng - vui lòng mở bảng điều khiển trình duyệt của bạn để biết thêm thông tin",
"upload": "Tải lên",
"join": "Tham gia",
"joined": "Đã tham gia",
"leave": "Rời nhóm",
"copyLink": "Sao chép đường dẫn",
"duplicateProposal": "Sao chép đề xuất",
"joinedSpaces": "Các nhóm đã tham gia",
"joinSpaces": "Tham gia nhóm",
"setDelegationToSpace": "Limit delegation to a specific space",
"theCurrentNetwork": "the current network",
"optional": "(optional)",
"homeLoadmore": "Load more",
"confirmAction": "Confirm action",
"or": "or",
"share": "Share",
"errors": {
"required": "Trường này là bắt buộc",
"minLength": "Độ dài tối thiểu là {0}",
"maxLength": "Độ dài tối đa là {0}",
"pattern": "Ký tự không hợp lệ",
"minItems": "Yêu cầu tối thiểu {0} mục(s)",
"minStrategy": "Ít nhất một chiến lược là bắt buộc.",
"format": "Định dạng không hợp lệ",
"type": "Kiểu dữ liệu không hợp lệ"
},
"create": {
"question": "Ask a question...",
"categorie(s)": "Select up to 2 categorie(s)",
"content": "Tell us more about your proposal (optional)",
"preview": "Xem trước",
"choices": "Lựa chọn",
"addChoice": "Thêm lựa chọn",
"startDate": "Chọn ngày bắt đầu",
"endDate": "Chọn ngày kết thúc",
"startTime": "Chọn thời gian bắt đầu",
"endTime": "Chọn thời gian kết thúc",
"publish": "Xuất bản",
"untitled": "Untitled",
"snapshotBlock": "Số khối chụp ảnh nhanh",
"voting": "Voting",
"votingSystem": "Voting system",
"choice": "Choice {0}",
"period": "Voting period",
"start": "Start",
"end": "End",
"days": "Days",
"hours": "Hours",
"minutes": "Minutes",
"schedule": "Schedule proposal",
"delayEnforced": "The space enforces a delay until voting can start",
"periodEnforced": "The space enforces the duration of the voting period",
"edit": "Edit",
"continue": "Continue",
"now": "Now",
"votingPeriodExplainer": "This is the time period in which users will be able to vote. The proposal will be visible and pending before the start of the voting period.",
"validationWarning": {
"basic": {
"member": "You need to be an author of the space in order to submit a proposal.",
"minScore": "Bạn cần có ít nhất {0} {1} để khởi tạo một đề xuất."
},
"customValidation": "You need to pass the proposal validation in order to submit a proposal.",
"executionError": "Verifying your eligibility to create proposals in this space has failed. This is likely due to a misconfigured strategy."
}
},
"delegate": {
"header": "Ủy thác",
"selectDelegate": "Select delegate",
"to": "Đến",
"addressPlaceholder": "Địa chỉ hay ENS",
"delegations": "Ủy quyền của bạn(s)",
"allSpaces": "Cho tất cả không gian",
"delegated": "Ủy quyền cho bạn",
"pendingTransaction": "no pending transaction | 1 pending transaction | {count} pending transactions",
"topDelegates": "Top delegates",
"noDelegatesFoundFor": "No delegates found for {0}",
"noValidEns": "Not a valid ENS address.",
"noValidAddress": "Not a valid address",
"delegateToSelf": "You cannot delegate to yourself",
"delegateToSelfAddress": "You cannot delegate to your own ENS address",
"noValidSpaceId": "Not a valid space ID",
"noDelegationsAndDelegates": "Can't find your delegations and delegates? Make sure you are connected to the correct network.",
"delegateNotSupported": "Delegation is currently not supported for {network}."
},
"proposal": {
"castVote": "Hãy bình chọn",
"vote": "Bầu",
"startDate": "Ngày bắt đầu",
"endDate": "Ngày kết thúc",
"votingSystem": "Hệ thống bầu cử"
},
"proposals": {
"header": "Đề xuất",
"new": "Đề xuất mới",
"noProposals": "Không có bất kỳ đề xuất nào ở đây!",
"createProposal": "Create proposal",
"showMore": "Show more",
"showLess": "Show less",
"states": {
"all": "Tất cả",
"core": "Cốt lõi",
"community": "Cộng đồng",
"active": "Hoạt động",
"pending": "Đang chờ xử lý",
"closed": "Đã đóng"
}
},
"settings": {
"header": "Cài đặt",
"setEnsTextRecord": "Set ENS text record",
"editController": "Edit controller",
"needToSetEnsText": "Your space settings will be stored in a file on IPFS. Before you can edit them, you need to set an ENS text record pointing to that file.",
"profile": "Hồ sơ",
"avatar": "Hình đại diện",
"name": "Tên",
"about": "Về",
"terms": "Kỳ hạn",
"network": "Mạng",
"symbol": "Biểu tượng",
"skin": "Skin",
"customDomain": "Custom domain",
"domain": "Tên miền",
"strategies": "Chiến lược(s)",
"categories": "Categorie(s)",
"selectCategories": "Select categories",
"hideSpace": "Ẩn không gian khỏi trang chủ",
"addStrategy": "Thêm chiến lược",
"authors": "Authors",
"admins": "Admins",
"defaultTab": "Tab mặc định",
"proposalThreshold": "Ngưỡng đề xuất",
"allowOnlyAuthors": "Allow only authors to submit a proposal",
"editPlugin": "Chỉnh sửa plugin",
"addPlugin": "Thêm plugin",
"pluginParameters": "Thông số plugin",
"proposalValidation": "Proposal validation",
"validation": "Validation",
"editValidation": "Edit validation",
"selectValidation": "Select validation",
"validationParameters": "Validation parameters",
"warningTextRecord": "You must set a text record on the ENS domain.",
"votingDelay": "Voting delay",
"votingPeriod": "Voting period",
"voting": "Voting",
"quorum": "Quorum",
"type": "Type",
"anyType": "Any",
"hideAbstain": "Ignore abstain votes in basic voting results",
"connectWithSpaceOwner": "To modify space settings, connect with a controller or admin wallet."
},
"setup": {
"example": "e.g.yam.eth",
"chooseExistingEns": "Choose one of your existing ENS domains to create a space with:",
"useSingleExistingEns": "Use your existing ENS domain:",
"orRegisterNewEns": "Or register a new domain:",
"toCreateASpace": "To create a space, you first need an ENS domain. Enter one below and follow the ENS registration instructions.",
"createASpace": "Tạo một không gian",
"registerEnsButton": "Register",
"supportedEnsTLDs": "Supported domain endings",
"helpDocsAndDiscordLinks": "Not sure how to create a space? Learn more in the documentation or join Snapshot Discord .",
"setSpaceController": "Set space controller address on ENS",
"editSpaceController": "Edit controller on ENS",
"setController": "Set controller",
"explainControllerAndEns": "This action requires a transaction on the Ethereum Mainnet which will add a \"snapshot\" TEXT record to your ENS domain.",
"confirmToSetAddress": "Are you sure you want to set {address} as the controller of the space?",
"controllerHasAuthority": "The controller has full authority over the space settings",
"controller": "Controller",
"selectEnsForSpace": "Choose ENS address",
"textRecordExists": "Currently the space controller is {address}. You can update the space controller by using the form below.",
"spaceOwnerAddressPlaceHolder": "e.g. 0xF78108c9BBaF466dd96BE41be728Fe3220b37119",
"controllerAddress": "Controller address",
"updateController": "Update controller",
"connectWithEnsController": "To modify the space controller you need to connect with the wallet that owns {ens}",
"seeOnEns": "See on ENS",
"goToSettings": "Go to settings"
},
"notify": {
"youDidIt": "Bạn đã thực hiện nó!",
"copied": "Đã sao chép!",
"proposalDeleted": "Đề xuất bị xóa",
"somethingWentWrong": "Rất tiếc, có gì đó không ổn!",
"saved": "Đã lưu!",
"delegationSuccess": "Delegation successful",
"delegationRemoved": "Delegation removed",
"proposalCreated": "Đề xuất đã được khởi tạo",
"voteSuccessful": "Phiếu bầu của bạn đã được lưu lại!",
"ensSet": "ENS text record was successfully set"
},
"explore": {
"createStrategy": "Tạo chiến lược",
"createSkin": "Tạo skin",
"addNetwork": "Thêm mạng",
"createPlugin": "Tạo plugin",
"strategies": "chiến lược(s)",
"skins": "skin(s)",
"networks": "mạng(s)",
"plugins": "plugin(s)",
"results": "kết quả(s)",
"categories": {
"all": "All",
"protocol": "Protocol",
"social": "Social",
"investment": "Investment",
"grant": "Grant",
"service": "Service",
"media": "Media",
"creator": "Creator",
"collector": "Collector"
}
},
"voting": {
"selectVoting": "Chọn hệ thống bầu cử",
"single-choice": "Biểu quyết một lựa chọn",
"approval": "Phê duyệt bầu cử",
"quadratic": "Biểu quyết bậc hai",
"ranked-choice": "Ranked choice voting",
"weighted": "Weighted voting",
"basic": "Basic voting",
"description": {
"single-choice": "Mỗi cử tri chỉ được chọn một sự lựa chọn.",
"approval": "Mỗi cử tri có thể chọn bất kỳ số lượng lựa chọn nào.",
"quadratic": "Each voter may spread voting power across any number of choices. Results are calculated quadratically.",
"ranked-choice": "Each voter may select and rank any number of choices. Results are calculated by instant-runoff counting method.",
"weighted": "Each voter may spread voting power across any number of choices.",
"basic": "Single choice voting with three choices: For, Against or Abstain"
},
"choices": {
"for": "For",
"against": "Against",
"abstain": "Abstain"
}
},
"safeSnap": {
"currentOutcome": "Current outcome",
"currentBond": "Current bond",
"finalizedIn": "Đã hoàn thành {0}",
"executableIn": "Thực thi {0}",
"finalOutcome": "Outcome",
"nextBond": "Bond to set outcome",
"setOutcomeTo": "Set outcome to",
"claimBond": "Yêu cầu đặt cược",
"addBatch": "Add transaction batch",
"batch": "Transaction batch",
"transactions": "Các giao dịch",
"to": "To (address)",
"invalidAddress": "Địa chỉ không hợp lệ",
"invalidAmount": "Invalid amount",
"invalidValue": "Invalid value",
"invalidAbi": "Invalid ABI",
"invalidData": "Invalid data",
"value": "Value (wei)",
"data": "Data",
"noCollectibles": "No collectibles",
"asset": "Asset",
"amount": "Amount",
"type": "Type",
"transferFunds": "Transfer funds",
"transferNFT": "Transfer NFT",
"contractInteraction": "Contract interaction",
"rawTransaction": "Raw transaction",
"addTransaction": "Add transaction",
"transactionLabels": {
"contractInteraction": "{functionName}() - {amount} wei to {address}",
"transferFunds": "Gửi {amount} {tokenSymbol} tới địa chỉ {address}",
"transferNFT": "Gửi {name} #{id} tới địa chỉ {address}",
"raw": "Gửi {amount} wei tới địa chỉ {address}"
},
"labels": {
"request": "Yêu cầu thực hiện",
"setOutcome": "Đặt kết quả",
"changeOutcome": "Thay đổi kết quả",
"executeTxs": "Thực hiện đa giao dịch {0} của {1}",
"executed": "Giao dịch thành công",
"rejected": "Proposal rejected",
"error": "Đã xảy ra lỗi",
"connectWallet": "Kết nối ví",
"switchChain": "Vui lòng chọn mạng lưới {0} để thực hiện giao dịch",
"question": "Đề xuất này có được thông qua không và nó có đáp ứng được",
"criteria": "tiêu chí chấp nhận?",
"proposalPassed": "Did the proposal pass?"
}
},
"poap": {
"no_poap_header": "POAP chưa được thiết lập cho đề xuất này: '(",
"no_voted_header": "Bỏ phiếu để nhận POAP này",
"unclaimed_header": "Mint your I voted POAP",
"claimed_header": "Congratulations! The POAP has been minted to your collection",
"loading_header": "The POAP is being minted to your collection",
"button_claim": "Mint",
"button_show": "Bộ sưu tập",
"success_claim": "The POAP has been minted to your collection",
"error_claim": "Đã xảy ra sự cố khi in mã thông báo"
},
"charts": {
"charts": "Charts",
"noVotesYet": "There are no votes to visualize yet.",
"totalVotesPerDay": "Total votes per day",
"shareOfVotingPower": "Share of voting power",
"votingPowerPerDay": "Voting power per day"
},
"comment_box": {
"title": "Khung bình luận",
"add": "Nhập bình luận",
"submit": "Gửi",
"preview": "Xem trước",
"continue_editing": "Tiếp tục chỉnh sửa",
"edit": "Sửa bình luận",
"edit_button": "Chỉnh sửa",
"dismiss": "Hủy",
"delete": "Xóa",
"add_reply": "Thêm bình luận",
"edit_comment": "Sửa bình luận",
"edit_modal": "Bạn có muốn chỉnh sửa bình luận không?",
"yes": "Đồng ý",
"no": "Không",
"delete_comment": "Xóa bình luận",
"delete_modal": "Xác nhận xóa bình luận?",
"error": "Đã xảy ra lỗi",
"replies": "các bình luận",
"hide": "Ẩn",
"show": "Hiển thị",
"reply": "Bình luận",
"load_more": "Hiển thị thêm"
},
"page": {
"title": {
"home": "Snapshot",
"setup": "Create a space",
"timeline": "Timeline",
"explore": "Explore",
"playground": "Playground",
"space": {
"create": "Create {space} proposal",
"about": "About {space}",
"proposals": "{space} Proposals",
"proposal": "{space} proposal: {proposal}",
"settings": "{space} Settings"
},
"strategy": "{key} strategy",
"delegate": "Delegate"
}
},
"hal": {
"title": "Track proposals for {spaceName}",
"text": "Receive notifications every time a new proposal is created or ends"
},
"timeUnits": {
"second": "1 second | {n} seconds",
"minute": "1 minute | {n} minutes",
"hour": "1 hour | {n} hours",
"day": "1 day | {n} days",
"week": "1 week | {n} weeks",
"month": "1 month | {n} months",
"year": "1 year | {n} years"
},
"unsupportedNetwork": {
"unsupportedNetwork": "Unsupported network",
"ensOnlyMainnet": "Snapshot only supports ENS on the ethereum mainnet. Once the space is created, you can use it across multiple chains.",
"switchNetworkToMainnet": "To continue, you need to change the network in your wallet to Ethereum Mainnet.",
"switchToMainnet": "Switch to Mainnet"
}
}
================================================
FILE: src/locales/zh-CN.json
================================================
{
"searchPlaceholder": "搜索",
"spaceCount": "{0} 个空间",
"createSpace": "创建空间",
"backToHome": "主页",
"actions": "操作",
"poweredBy": "技术支持",
"results": "结果",
"resultsError": "结果无法计算。这通常是由于策略配置错误或者策略中涉及到了无反应的RPC节点。",
"resultsCalculating": "正在计算最终结果。如果您在几分钟后仍然看到这个消息,请联系空间管理员。",
"votingPowerFailedMessage": "您的投票权无法计算。这通常是由于策略配置错误或者策略中涉及到了无反应的RPC节点。",
"votingValidationFailedMessage": "我们一方出现了一个错误,我们无法核实你是否有资格投票。 这常常是由于错误配置的投票验证或在验证中包含一个不适应的 API。",
"notValidVoterMessage": "很抱歉,您似乎没有资格对此提案进行投票。",
"getHelp": "获取帮助",
"retry": "重试",
"currentResults": "当前结果",
"reset": "重置",
"close": "关闭",
"save": "保存",
"author": "作者",
"next": "下一页",
"choice": "选项",
"submit": "提交",
"plugins": "插件",
"information": "信息",
"confirm": "确认",
"snapshot": "Snapshot",
"strategies": "策略",
"strategiesPage": "策略",
"space": "空间",
"spaces": "空间",
"verifiedSpace": "已认证的空间",
"warningSpace": "这个空间已被标记为潜在的恶意空间,请谨慎行事。",
"version": "版本",
"timeline": "时间线",
"ended": "已结束",
"started": "已开始",
"filters": "过滤器",
"allSpaces": "所有空间",
"submitOnchain": "链上提交",
"inSpaces": "在 {0} 空间中",
"votes": "投票数",
"seeMore": "查看更多",
"seeAll": "查看所有",
"network": "网络",
"networks": "网络",
"skins": "皮肤",
"spaceMembers": "成员",
"members": "没有成员 | {count} 个成员 | {count} 个成员",
"editStrategy": "编辑策略",
"invalidProposals": "无效提案",
"account": "账户",
"create3box": "在 3Box 上创建个人资料",
"view3box": "在 3Box 上查看个人资料",
"edit3box": "在 3Box 上编辑个人资料",
"connectWallet": "连接钱包",
"toggleSkin": "切换皮肤",
"about": "关于",
"license": "许可证",
"showMore": "显示更多",
"voted": "已投票",
"reload": "刷新",
"ipfsServer": "IPFS 服务器",
"hub": "中心",
"cancel": "取消",
"delete": "删除",
"demoSite": "这是一个演示站点,请试试看!",
"removeDelegation": "移除委托",
"confirmRemove": "您确定要移除对的委托吗?",
"removeSpace": "用于空间 {0}",
"noVotingPower": "欧,看起来你在区块 blockNumber 没有任何投票权。",
"quorumReached": "已达到既定人数",
"options": "选项",
"votingPower": "您的投票权",
"comment": {
"placeholder": "分享您的理由(可选)"
},
"receipt": "凭证",
"relayer": "中继器",
"verifyOnMycrypto": "在 MyCrypto 上验证凭证",
"verifyOnSignatorio": "在 Signator.io 上验证",
"isCore": "核心",
"notificationsBlocked": "你的浏览器正在屏蔽通知",
"notificationsNotSupported": "你的浏览器不支持接收通知",
"walletNotSupported": "不支持该钱包",
"seeInExplorer": "查看浏览器",
"learnMore": "了解更多",
"logout": "登出",
"continue": "继续",
"add": "添加",
"edit": "编辑",
"strategyParameters": "策略参数",
"addAction": "添加操作",
"removeAction": "移除操作",
"yourChoice": "选择 {0}",
"targetAddress": "目标地址",
"value": "数值",
"date": "数据",
"marketDetails": "市场信息",
"addMarket": "添加市场",
"selectNetwork": "选择网络",
"conditionId": "状况 ID",
"basetokenAddress": "基础代币地址",
"quoteAddress": "引用货币地址",
"removeMarket": "移除市场",
"back": "返回",
"loading": "正在加载…",
"predictedImpact": "预计影响",
"marketSymbol": "{0} 市场",
"twoChoicesRequired": "此插件需要两个选项。",
"noResultsFound": "哎呀,我们找不到任何结果",
"createFirstProposal": "开始创建您的第一个提案",
"noSpacesJoined": "哎呀,您还没有加入任何空间",
"addFavorites": "添加收藏",
"createdBy": "由 {0}",
"startIn": "从 {0} 开始",
"endIn": "结束 {0}",
"proposalTimeLeft": "还剩 {0}",
"endedAgo": "结束 {0}",
"proposalBy": "由 {0}",
"endDate": "结束 {0}",
"contentHash": "内容哈希",
"defaultSkin": "默认皮肤",
"select": "选择",
"language": "语言",
"agree": "我同意",
"moderators": "版主",
"playground": "练习场",
"strategyParams": "策略参数",
"addresses": "地址",
"networkErrorPlayground": "网络错误 - 请打开您的浏览器控制台获取更多信息",
"upload": "上传",
"join": "加入",
"joined": "已加入",
"leave": "离开",
"subspaces": "子空间",
"mainspace": "主空间",
"copyLink": "复制链接",
"duplicate": "复制",
"joinedSpaces": "已加入的空间",
"joinSpaces": "加入空间",
"setDelegationToSpace": "将委托限制在一个指定空间内",
"theCurrentNetwork": "当前网络",
"optional": "(选填)",
"homeLoadmore": "加载更多",
"confirmAction": "确认操作",
"or": "或者",
"share": "分享",
"shareOnTwitter": "分享到 Twitter",
"shareOnLenster": "分享到 Lenster",
"createButton": "创建",
"discussion": "讨论",
"changeWallet": "更换钱包",
"createASpace": "创建空间",
"getStarted": "开始",
"skip": "略过",
"newSpaceNotice": {
"header": "您的空间已经上线了!",
"mainText": "您可以通过 {settings} 中的策略更改投票权的计算方式。 更改您的设置只会影响新的提案,不会更改现有的提案。",
"learnMore": "在 {documentation} 获得更多信息,或加入Snapshot {discord} 寻求帮助。",
"gotIt": "了解!"
},
"errors": {
"required": "此项为必填项",
"minLength": "此项为必填项",
"maxLength": "最大长度为 {0}",
"pattern": "无效字符",
"minItems": "最少需要 {0} 个项目",
"maxItems": "最多允许 0个项目 ",
"minStrategy": "至少需要一个策略",
"website": "URL 格式应为 https://www.example.com",
"format": "格式无效",
"type": "无效的类型",
"unsupportedImageType": "不支持该文件类型,支持的格式是 jpeg、 jpg 和 png",
"invalidAddress": "无效地址"
},
"create": {
"proposalTitle": "标题",
"discussion": "讨论 (可选)",
"categorie(s)": "最多选择2个类别",
"proposalDescription": "描述(可选)",
"preview": "预览",
"choices": "选项",
"addChoice": "添加选项",
"startDate": "选择开始日期",
"endDate": "选择结束日期",
"startTime": "选择开始时间",
"endTime": "选择结束时间",
"publish": "发布",
"untitled": "未命名",
"snapshotBlock": "快照区块号",
"voting": "投票",
"votingSystem": "投票制度",
"choice": "选项 {0}",
"period": "投票时长",
"start": "开始",
"end": "结束",
"days": "天",
"hours": "小时",
"minutes": "分",
"schedule": "计划提案",
"delayEnforced": "空间强制执行直到投票可以开始",
"periodEnforced": "空间强制执行投票期",
"typeEnforced": "{type}由空间强制执行",
"privacyEnforced": "{type} 由空间强制执行",
"edit": "编辑",
"continue": "继续",
"now": "当前",
"votingPeriodExplainer": "这是用户可以投票的时间段。 该提案将在投票期开始之前可见并处于待定状态。",
"uploadImageExplainer": "通过拖放、选择或粘贴来附加文件。",
"uploading": "正在上传图片",
"markdown": "支持Markdown样式",
"validationWarning": {
"basic": {
"member": "您需要成为空间的作者才能提交提案。",
"minScore": "您至少需要有 {0} {1} 才能提交提案。"
},
"customValidation": "您需要通过提案验证后才能提交提案。",
"executionError": "您在此空间中创建提案的资格验证失败。这可能是由于配置了错误的策略。"
},
"errorGettingSnapshot": "我们在获取计算您投票能力所需的快照区块时遇到了一个错误。请稍后再试。"
},
"delegate": {
"header": "委托",
"selectDelegate": "选择委托",
"to": "给",
"addressPlaceholder": "地址或ENS域名",
"delegations": "您的授权",
"allSpaces": "用于所有空间",
"delegated": "已委托给您",
"pendingTransaction": "没有待处理交易 | 1 个待处理交易 | {count} 个待处理交易",
"topDelegates": "顶级委托",
"noDelegatesFoundFor": "没有找到 {0} 的委托",
"noValidEns": "此ENS地址无效",
"noValidAddress": "此地址无效",
"delegateToSelf": "不能将自己选为委托人",
"delegateToSelfAddress": "您不能委托给您自己的ENS地址",
"noValidSpaceId": "此空间ID无效",
"noDelegationsAndDelegates": "找不到您的委托人和代委托?请确保您已经连接到正确的网络。",
"delegateNotSupported": "目前 {network} 不支持委托。"
},
"proposal": {
"castVote": "投出您的票",
"vote": "投票",
"startDate": "开始日期",
"endDate": "结束日期",
"votingSystem": "投票制度",
"privacy": "隐私",
"invalidChoice": "无效的选择",
"postVoteModal": {
"defaultTitle": "投票成功!",
"gnosisSafeTitle": "您的投票正在进行中...",
"gnosisSafeDescription": " 使用Safe进行的投票需要额外的签名者,并在交易确认后可见",
"seeQueue": "查看队列中的交易",
"tips": {
"1": "投票可以在提案处于活动状态时更改"
}
}
},
"proposals": {
"header": "提案",
"new": "新提案",
"noProposals": "这里还没有任何提案!",
"createProposal": "创建提案",
"showMore": "显示更多",
"showLess": "收起",
"states": {
"all": "所有",
"core": "核心",
"community": "社区",
"active": "活跃",
"pending": "待开始",
"closed": "已关闭"
}
},
"notifications": {
"header": "通知",
"noNotifications": "您没有通知",
"proposalStarted": "提案已开始:",
"proposalEnded": "提案已结束:",
"markAllAsRead": "全部标记为已读",
"all": "全部",
"unread": "未读"
},
"modalTerms": {
"mustAgreeTo": "要{action} 此空间,您必须同意{spaceName} 的服务条款。",
"actionJoin": "加入",
"actionCreate": "创建提案",
"actionVote": "投票"
},
"settings": {
"header": "设置",
"editController": "编辑控制人",
"connectWithSpaceOwner": "您处于只读模式,要修改空间设置,请连接控制人或管理员的钱包。",
"gnosisWrongNetwork": {
"base": "您的Gnosis Safe 在错误的网络上。请链接到网络来action。",
"settings": "编辑空间设置",
"create": "创建提案",
"vote": "对此提案进行投票"
},
"currentSpaceControllerIs": "当前空间控制人是 {address}",
"newController": "新的控制人",
"noRecord": "未找到文本记录。请确保 id 已经在 network注册了域名,然后编辑控制人的文本记录来重新设置空间。",
"set": "设置",
"profile": "用户信息",
"avatar": "头像",
"name": {
"label": "名称",
"placeholder": "例如 Yam Network"
},
"about": {
"label": "关于",
"placeholder": "介绍您的组织"
},
"categories": {
"label": "类别",
"select": "选择类别"
},
"terms": {
"label": "服务条款",
"information": "用户在创建提案或投票前需要接受一次这些条款"
},
"hideSpace": "从主页隐藏空间",
"links": "社交账户",
"subspaces": {
"label": "子空间",
"information": "子空间仅在主空间和子空间都配置后才会显示。 ",
"parent": {
"label": "主空间",
"placeholder": "pistachiodao.eth",
"information": "此空间作为子空间的空间将显示在空间页面上"
},
"children": {
"label": "子空间",
"placeholder": "pistachiodao.eth",
"information": "此处列出的相关子空间将显示在空间页面上"
}
},
"website": "网站",
"strategies": {
"label": "策略",
"information": "策略用于确定投票权或用户是否有资格创建提案。"
},
"network": {
"label": "网络",
"information": "用于此空间的默认网络。网络也可以在单独的策略中指定。"
},
"symbol": {
"label": "符号",
"information": "此空间使用的默认符号,通常是代币的符号。例如BAL代表Balancer"
},
"strategiesList": "最多选择 8 个策略",
"votingPowerIsCumulative": "投票权是累积的",
"addStrategy": "添加策略",
"testInPlayground": "在playground中测试",
"admins": {
"label": "管理员",
"information": "管理员能够修改空间设置和管理空间提案"
},
"authors": {
"label": "作者",
"information": "创建者能够创建提案"
},
"proposalValidation": "提案验证",
"validation": "类型",
"proposalThreshold": {
"label": "阈值",
"information": "创建提案所需的最低投票权"
},
"allowOnlyAuthors": "仅允许作者提交提案",
"editValidation": "编辑验证",
"selectValidation": "选择验证",
"validationParameters": "验证参数",
"voting": "投票",
"votingDelay": "投票延迟开始",
"votingPeriod": "投票时长",
"hours": "小时",
"days": "天",
"quorum": {
"label": "提案人数",
"information": "提案通过所需的最低投票权"
},
"type": {
"label": "类型",
"information": "这个空间所使用的投票系统(未来的所有提案也照此执行)"
},
"anyType": "任意",
"hideAbstain": "忽略基本投票结果中的弃权票",
"customDomain": "自定义域名",
"domain": {
"label": "域名",
"placeholder": "例如 vote.balancer.fi",
"info": "要设置一个自定义域名,您另外需要在github上开启一个pull request {docs}。"
},
"skin": "皮肤",
"treasuries": {
"label": "金库",
"add": "添加金库",
"edit": "编辑金库",
"information": "添加您的组织的金库以在您的空间中展示它们"
},
"addPlugin": "添加插件",
"editPlugin": "编辑插件",
"pluginParameters": "插件参数",
"proposal": {
"title": "提案",
"guidelines": {
"title": "指南",
"information": "在提案创建指南放置一个链接,以帮助用户了解什么是一个好的/有效的提案"
},
"template": {
"title": "模板",
"information": "以模板启动每个提案,以帮助用户了解需要的信息"
}
}
},
"setup": {
"example": "例如 yam.eth",
"chooseExistingEns": "选择您现有的一个ENS域名来创建空间:",
"useSingleExistingEns": "使用您现有的ENS域名:",
"orRegisterNewEns": "或者注册一个新的域名:",
"demoTestnetEnsMessage": "若要创建测试空间,您需要在 {network} 上的 ENS 域名。",
"toCreateASpace": "要创建空间,您首先需要一个ENS域名。在下面输入一个,并按照ENS的注册说明进行操作。",
"createASpace": "创建空间",
"registerEnsButton": "注册",
"supportedEnsTLDs": "支持的域名结尾",
"helpDocsAndDiscordLinks": "不知道如何创建空间?在 docs了解更多信息或加入Snapshot的Discord。",
"setSpaceController": "空间控制人",
"setSpaceControllerExists": "此域名的snapshot文本记录已被设置。您可以选择编辑更改它,您也可以跳到下一步。",
"setSpaceControllerInfo": " 空间控制人是能够管理空间设置的帐户。 稍后可以添加其他空间控制人(管理员)。",
"setSpaceControllerInfoGnosisSafe": "使用Gnosis Safe创建空间时,建议将安全地址设置为空间控制人。 如果您不这样做,则需要遵循一些额外的步骤。{link}",
"editSpaceController": "在 ENS 上编辑控制人",
"setController": "设置控制人",
"explainControllerAndEns": "设置控制人需要在 {network} 上进行交易,该交易会将“snapshot”文本记录添加到您的 ENS 域。",
"confirmToSetAddress": "您确定要设置 {address} 作为空间的控制人吗?",
"controllerHasAuthority": "管理员对空间设置拥有所有权限",
"controller": "控制人",
"selectEnsForSpace": "选择ENS地址",
"spaceOwnerAddressPlaceHolder": "例如: {address}",
"controllerAddress": "控制人地址",
"updateController": "更新控制人",
"seeOnEns": "在 ENS 上查看",
"goToSettings": "前往设置",
"setSpaceProfile": "自定义您的空间",
"waitForTransaction": "您需要先确认交易才能创建您的空间。 {txUrl}",
"pleaseWaitMessage": "这可能需要几分钟,请等待交易确认",
"notControllerAddress": "请连接控制人地址 {wallet} 以创建空间。",
"fillCurrentAccount": "使用当前登录的帐户",
"domain": {
"title": "设置空间域名",
"ensMessage": " 在创建自己的空间之前,您需要设置以太坊主网上的 ENS 域名。",
"ensMessageTestnet": "你也可以先在Goerli测试网 {link} 测试并处理问题。",
"tryDemo": "尝试这个 demo",
"yourExistingSpaces": "您已经存在的空间",
"invalidEns": "此ENS名称无效。 通常这是由于在注册时使用了无效字符。"
},
"strategy": {
"title": "您想如何设置您的投票策略?",
"subtitle": "您可以随时改变您的策略设置。",
"blockTitle": "设置投票策略",
"onePersonOneVote": {
"title": "一人,一票",
"description": "管理可以投票或只允许任何地址投票的白名单。每个投票都是平等的,不需要代币。",
"whitelistInformation": "指定一些可以投票的账户",
"ticketInformation": "任何帐户都可以投票",
"votesEqualInfo": "每次投票都是平等的,无代币限制"
},
"tokenVoting": {
"title": "代币加权投票",
"description": "投票用代币作为参考的权重。代币可以是 ERC-20、 ERC-721 或 ERC-115 的代币标准",
"tokenNotFound": "未找到代币",
"seeOnEtherscan": "在Etherscan上查看"
},
"advanced": {
"title": "自定义设置",
"description": "选择最多8个具有多种选项的策略。 如果你找不到正确的策略,你可以创建自己的策略"
}
},
"validationTitle": "谁可以管理这个空间并创建提案?"
},
"profile": {
"buttonEdit": "编辑个人资料",
"viewProfile": "查看个人资料",
"about": {
"header": "关于我们",
"joinedSpaces": "已加入的空间",
"createdSpaces": "创建空间",
"biography": "个人介绍",
"notJoinSpacesYet": "尚未加入任何空间",
"notCreatedSpacesYet": "尚未创建任何空间",
"delegatorNetworkInfo": "通过切换你钱包中的网络进行更改",
"delegate": "委托",
"delegated": "已委托",
"delegateTo": "委托给",
"delegateFor": "代表",
"noDelegatorsMessage": "{network} 上没有代表",
"notSupportedNetwork": "目前在 {network} 上不支持委托 "
},
"activity": {
"header": "活动",
"votedFor": "投了 {choice}",
"today": "今天",
"thisWeek": "本周",
"olderThanWeek": "早于一周前",
"noActivity": "暂无动态"
},
"settings": {
"header": "编辑个人资料",
"name": "姓名",
"biography": "个人介绍",
"namePlaceholder": "输入名字",
"bioPlaceholder": "讲述您的故事",
"change": "修改",
"remove": "删除"
}
},
"notify": {
"youDidIt": "您成功了!",
"copied": "已复制!",
"proposalDeleted": "提案被删除",
"somethingWentWrong": "哎呀,似乎出了点问题!",
"saved": "已保存!",
"delegationSuccess": "委托成功",
"delegationRemoved": "委托已移除",
"proposalCreated": "提案已创建",
"voteSuccessful": "投票成功!",
"ensSet": "ENS 文本记录已成功设置",
"transactionSent": "交易已发送"
},
"explore": {
"createStrategy": "创建策略",
"createSkin": "创建新皮肤",
"addNetwork": "添加网络",
"createPlugin": "创建插件",
"strategies": "策略",
"skins": "皮肤",
"networks": "网络",
"plugins": "插件",
"results": "结果",
"category": "分类",
"categories": {
"all": "所有",
"protocol": "协议",
"social": "社群媒体",
"investment": "投资",
"grant": "拨款项目",
"service": "服务",
"media": "文化传媒",
"creator": "创作者",
"collector": "收藏家"
}
},
"voting": {
"selectVoting": "选择投票系统",
"single-choice": "单选投票",
"approval": "赞成投票",
"quadratic": "二次投票",
"ranked-choice": "排序选择投票",
"weighted": "加权投票",
"basic": "基本投票",
"description": {
"single-choice": "每个投票人只能选择一个选项。",
"approval": "每个投票人可以选择任意数量的选项。",
"quadratic": "每个选民都可以在任何数量的选择中分配投票权。结果按四舍五入计算。",
"ranked-choice": "每个选民都可以选择和排列任何数目的选择。结果是通过即时计算的。",
"weighted": "每个选民都可以在任何选择中分散投票权。",
"basic": "有三种投票单选项:支持,反对,和弃权"
}
},
"privacy": {
"label": "隐私",
"title": "选择投票隐私",
"information": "提案中使用的隐私类型。(在所有未来的提案中强制执行)",
"any": "任何类型",
"none": "未设置",
"shutter": {
"label": "快门",
"description": "选择是加密的,只有在投票期结束后才能看到",
"tooltip": "该提案已启用快门隐私。所有的投票都将被加密,直到投票期结束并计算出最终的分数",
"url": "https://blog.shutter.network/shielded-voting/"
}
},
"validation": {
"label": "验证",
"title": "选择投票验证",
"information": "验证类型用来确定用户是否可以投票。(在所有未来的提案中强制执行)",
"any": {
"label": "任何人都可以投票",
"description": "任何拥有投票权的人都可以投票。"
},
"basic": {
"label": "基础",
"description": "使用任何策略来确定用户是否可以投票。",
"invalidMessage": "您不符合就该提案进行投票的最低余额要求。"
},
"passport-gated": {
"label": "Gitcoin Passport门槛",
"description": "通过要求用户拥有 Gitcoin Passport 来保护您的提案免受垃圾邮件和投票操纵。",
"invalidMessage": "你需要一本带有 amount个如下邮票的Gitcoin Passport才能对这个提案投票: stamps "
}
},
"safeSnap": {
"currentOutcome": "当前结果",
"currentBond": "当前的保证金",
"finalizedIn": "已完成 {0}",
"executableIn": "可执行 {0}",
"finalOutcome": "结果",
"nextBond": "设置结果的绑定",
"setOutcomeTo": "设置结果为",
"claimBond": "认领保证金",
"addBatch": "添加交易批次",
"batch": "交易批次",
"transactions": "交易次数",
"to": "至 (地址)",
"invalidAddress": "无效地址",
"invalidAmount": "无效金额",
"invalidValue": "无效值",
"invalidAbi": "无效的ABI",
"invalidData": "无效数据",
"value": "值 (wei)",
"data": "数据",
"noCollectibles": "暂无收藏",
"asset": "资产",
"amount": "数额",
"type": "类型",
"transferFunds": "发送资金",
"transferNFT": "发送NFT",
"contractInteraction": "智能合约交互",
"rawTransaction": "原始交易记录",
"addTransaction": "添加交易",
"transactionLabels": {
"contractInteraction": "{functionName}() - {amount} wei 到 {address}",
"transferFunds": "将 {amount} {tokenSymbol} 发送到 {address}",
"transferNFT": "发送 {name} #{id} 至 {address}",
"raw": "发送 {amount} 到 {address}"
},
"labels": {
"request": "请求执行",
"setOutcome": "添加结果",
"changeOutcome": "修改结果",
"executeTxs": "执行交易批处理 {0} / {1}",
"executed": "所有交易已执行",
"noTransactions": "没有需要执行的交易。",
"rejected": "提案被否决",
"error": "出了些问题",
"connectWallet": "连接钱包以查看执行详情",
"switchChain": "将你的钱包切换到 {0} 以请求执行",
"question": "该提案是否通过并且是否符合",
"criteria": "接收标准?",
"proposalPassed": "该提案通过了吗?",
"expired": "提案已过期",
"approveBond": "批准保证金",
"confirmVoteResults": "点击确认已通过的提案",
"executeTxsUma": "执行交易批次",
"deleteDisputedProposal": "删除有争议的提案",
"confirmVoteResultsToolTip": "请确保这项提案在提出链上提案之前经快照投票批准。 如果快照投票否决了这项提案,链上的提案也将被否决,你将失去你的保证金。",
"approveBondToolTip": "链上提案需要提案人提供保证金。这将批准你的钱包中的代币作为保证金。如果你做了一个无效的提案,它将受到争议,你将失去你的保证金。如果提案是有效的,你的保证金将在交易执行时被退回。",
"requestToolTip": "这将在链上提议该快照投票的交易。在一个挑战窗口后,如果提案有效,交易可以被执行,你的保证金将被退回。",
"executeToolTip": "这将执行此提案中的交易并退还提议者的保证金。"
}
},
"poap": {
"no_poap_header": "尚未为此提议设置POAP :'(",
"no_voted_header": "投票以获取此POAP",
"unclaimed_header": "铸造您投票本提案的专属POAP",
"claimed_header": "恭喜!POAP 已被铸造到您的收藏中",
"loading_header": "POAP正在被铸造到您的收藏中",
"button_claim": "铸造",
"button_show": "浏览收藏",
"success_claim": "POAP已被铸造到您的收藏中",
"error_claim": "创建令牌时出错"
},
"progress": {
"progress": "进度",
"inProgress": "进行中",
"completed": "已完成",
"complete": "完成",
"newStep": "新步骤",
"description": "说明",
"add": "添加",
"deleteStep": "删除步骤",
"deleteConfirm": "您确定要删除吗?",
"delete": "删除",
"cancel": "取消",
"comeBack": "投票结束后再来看看这个提案的进展情况吧!",
"confirmSignature": "签名此消息将使我们能够授权您的请求来更新您的提案的进度。",
"wentWrong": "哎呀,出了点问题",
"voting": "投票中",
"soon": "即将到来"
},
"charts": {
"charts": "图表",
"noVotesYet": "目前还没有投票可呈现",
"totalVotesPerDay": "每日总票数",
"shareOfVotingPower": "投票权重比例",
"votingPowerPerDay": "每日投票权"
},
"comment_box": {
"title": "评论框",
"add": "在这里添加您的评论",
"submit": "提交",
"preview": "预览",
"continue_editing": "继续编辑",
"edit": "请在此编辑您的回复",
"edit_button": "编辑",
"dismiss": "取消",
"delete": "删除",
"add_reply": "在此添加您的回复",
"edit_comment": "编辑评论",
"edit_modal": "您确定要编辑吗?",
"yes": "是",
"no": "否",
"delete_comment": "删除评论",
"delete_modal": "您确定要删除吗?",
"error": "哎呀,出了些问题",
"replies": "回复",
"hide": "隐藏",
"show": "显示",
"reply": "回复",
"load_more": "显示更多"
},
"page": {
"title": {
"home": "Snapshot",
"setup": "创建空间",
"timeline": "时间线",
"notifications": "通知",
"explore": "浏览",
"playground": "练习场",
"space": {
"create": "创建 {space} 提案",
"about": "关于 {space}",
"proposals": "{space} 提案",
"proposal": "{space} 提案: {proposal}",
"settings": "{space} 设置"
},
"strategy": "{key} 策略",
"delegate": "委托",
"ranking": "排名"
}
},
"hal": {
"title": "追踪 {spaceName} 的提案",
"text": "每次创建新提案或提案结束时都接收通知"
},
"timeUnits": {
"second": "1 秒 | {n} 秒",
"minute": "1 分钟 | {n} 分钟",
"hour": "1 小时 | {n} 小时",
"day": "1 天 | {n} 天",
"week": "1周 | {n} 周",
"month": "1 个月 | {n} 个月",
"year": "1 年 | {n} 年"
},
"unsupportedNetwork": {
"unsupportedNetwork": "不支持的网络",
"switchNetworkToNetwork": "若要继续,您需要将钱包中的网络更改为 {network}。",
"switchToNetwork": "切换到 {network}",
"goToDemoSite": "前往演示网站"
},
"treasury": {
"title": "金库",
"wallets": {
"title": "钱包",
"empty": "此空间还没有金库",
"addTreasury": "添加一个金库"
},
"assets": {
"title": "资产",
"empty": "该合约中没有资产"
},
"24hChange": "24小时变化"
},
"newsletter": {
"yourEmail": "您的电子邮件地址",
"title": "获取最近的Snapshot更新"
},
"joinCommunity": "加入Snapshot社区",
"header": {
"title": "在哪里作出决定",
"description": "Snapshot是一个用于社区治理的免费开放源码平台。现在您可以创建自己的空间并开始决策!"
},
"aboutPage": {
"description": "Snapshot是一个去中心化的治理平台,这使得用户很容易创建提案并就其进行表决,所有这些提案都不需要花费gas! 此外,我们的灵活系统支持各种投票类型和策略,以便您可以根据您的需要调整投票过程。",
"subHeader": "治理应该是一种快照",
"subDescription": "Web3管理不一定要复杂。 Snapshot是各组织寻求一种简单有效的方法来管理其社区或组织的完美解决办法。"
},
"footerView": {
"resources": "资源",
"about": "关于",
"blog": "博客",
"jobs": "工作机会",
"discussions": "讨论",
"github": "GitHub",
"docs": "文档",
"support": "支持",
"hiring": "加入我们!"
}
}
================================================
FILE: src/main.ts
================================================
import { Buffer } from 'buffer';
(window as any).global = window;
(window as any).Buffer = Buffer;
import { LockPlugin } from '@snapshot-labs/lock/plugins/vue3';
import { DefaultApolloClient } from '@vue/apollo-composable';
import { createHead } from '@vueuse/head';
import options from '@/helpers/auth';
import VueTippy from 'vue-tippy';
import VueViewer from 'v-viewer';
import { apolloClient } from '@/helpers/apollo';
// import { initSentry } from '@/sentry';
import { KNOWN_DOMAINS, KNOWN_HOSTS } from '@/helpers/constants';
import i18n from '@/helpers/i18n';
import router from '@/router';
import '@/assets/css/main.scss';
import App from '@/App.vue';
const parentUrl =
window.location != window.parent.location
? document.referrer ||
document.location.ancestorOrigins[
document.location.ancestorOrigins.length - 1
]
: document.location.href;
const parentHost = new URL(parentUrl).host;
if (
window !== window.parent &&
!KNOWN_HOSTS.includes(parentHost) &&
!KNOWN_DOMAINS.includes(parentHost.split('.').slice(-2).join('.'))
) {
document.documentElement.style.display = 'none';
throw new Error(`Unknown host: ${parentHost}`);
}
const head = createHead();
const app = createApp({
setup() {
provide(DefaultApolloClient, apolloClient);
},
render: () => h(App)
});
// initSentry(app, router);
app
.use(head)
.use(i18n)
.use(router)
.use(LockPlugin, options)
.use(VueTippy, {
defaultProps: { delay: [400, null] },
directive: 'tippy' // => v-tippy
})
.use(VueViewer, { defaultOptions: { navbar: true, toolbar: false } });
app.mount('#app');
export default app;
================================================
FILE: src/plugins/README.md
================================================
Discover how to develop a plugin by following our comprehensive documentation at https://docs.snapshot.org/developer-guides/create
================================================
FILE: src/plugins/domino/ProposalSidebar.vue
================================================
================================================
FILE: src/plugins/domino/components/CustomBlock.vue
================================================
================================================
FILE: src/plugins/domino/plugin.json
================================================
{
"name": "Domino",
"version": "1.0.0",
"author": "domino.run",
"website": "https://github.com/snapshot-labs/snapshot/tree/master/src/plugins/domino",
"icon": "ipfs://QmePXkJYHhkSMXi7qpvShtNg8Vhd5kVDYQNcPjAcZv3QdG"
}
================================================
FILE: src/plugins/gnosis/Create.vue
================================================
================================================
FILE: src/plugins/gnosis/ProposalSidebar.vue
================================================
================================================
FILE: src/plugins/gnosis/components/Config.vue
================================================
{{ $t('create.preview') }}
{{ $t('back') }}
================================================
FILE: src/plugins/gnosis/components/CustomBlock.vue
================================================
{{ $t('loading') }}
{{ $t('predictedImpact') }}
{{ predictPriceImpact.toFixed(2) }}%
{{ shorten(choices[0], 'name') }}
1
{{ baseToken.symbol }}
=
{{ priceFirstOption.toFixed(2) }}
{{ quoteToken.symbol }}
{{ $tc('marketSymbol', [baseToken.symbol]) }}
{{ $tc('marketSymbol', [baseToken.symbol]) }}
{{ $t('twoChoicesRequired') }}
================================================
FILE: src/plugins/gnosis/index.ts
================================================
import { getAddress } from '@ethersproject/address';
import snapshot from '@snapshot-labs/snapshot.js';
const UNISWAP_V2_SUBGRAPH_URL = {
'1': 'https://subgrapher.snapshot.org/subgraph/arbitrum/EYCKATKGBKLWvSfwvBjzfCBmGwYNdVkduYXVivCsLRFu',
'100':
'https://subgrapher.snapshot.org/subgraph/arbitrum/F5u74NSaLF92s1qUSacQU4fmWizmK9yqDhXAq6RPtgky'
};
const OMEN_SUBGRAPH_URL = {
'1': 'https://subgrapher.snapshot.org/subgraph/arbitrum/7JbjEfSTme3LioqJ7SJZ9Y1GhSD55X98Btd8fg7iYUPT',
'100':
'https://subgrapher.snapshot.org/subgraph/arbitrum/9fUVQpFwzpdWS9bq5WkAnmKbNNcoBwatMR4yZq81pbbz'
};
const WETH_ADDRESS = {
'1': '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
'4': '0xc778417e063141139fce010982780140aa0cd5ab',
'100': '0x6A023CCd1ff6F2045C3309768eAd9E68F978f6e1'
};
const OMEN_GQL_QUERY = {
condition: {
__args: {
id: undefined
},
id: true,
fixedProductMarketMakers: {
id: true,
collateralToken: true,
outcomeTokenAmounts: true,
outcomeTokenMarginalPrices: true
}
}
};
const UNISWAP_V2_GQL_QUERY = {
pairsTokens: {
__aliasFor: 'pairs',
__args: {
where: {
token0: true,
token1: true
}
},
token0Price: true
},
pairsTokensInverted: {
__aliasFor: 'pairs',
__args: {
where: {
token0: true,
token1: true
}
},
token1Price: true
},
pairsTokens0: {
__aliasFor: 'pairs',
__args: {
where: {
token0: true,
token1: true
}
},
token0Price: true
},
pairsTokens1: {
__aliasFor: 'pairs',
__args: {
where: {
token0: true,
token1: true
}
},
token0Price: true
}
};
const erc20Abi = [
{
constant: true,
inputs: [],
name: 'name',
outputs: [{ name: '', type: 'string' }],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: false,
inputs: [{ name: '_upgradedAddress', type: 'address' }],
name: 'deprecate',
outputs: [],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
},
{
constant: false,
inputs: [
{ name: '_spender', type: 'address' },
{ name: '_value', type: 'uint256' }
],
name: 'approve',
outputs: [],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'deprecated',
outputs: [{ name: '', type: 'bool' }],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: false,
inputs: [{ name: '_evilUser', type: 'address' }],
name: 'addBlackList',
outputs: [],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'totalSupply',
outputs: [{ name: '', type: 'uint256' }],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: false,
inputs: [
{ name: '_from', type: 'address' },
{ name: '_to', type: 'address' },
{ name: '_value', type: 'uint256' }
],
name: 'transferFrom',
outputs: [],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'upgradedAddress',
outputs: [{ name: '', type: 'address' }],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [{ name: '', type: 'address' }],
name: 'balances',
outputs: [{ name: '', type: 'uint256' }],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'decimals',
outputs: [{ name: '', type: 'uint256' }],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'maximumFee',
outputs: [{ name: '', type: 'uint256' }],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [],
name: '_totalSupply',
outputs: [{ name: '', type: 'uint256' }],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: false,
inputs: [],
name: 'unpause',
outputs: [],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
},
{
constant: true,
inputs: [{ name: '_maker', type: 'address' }],
name: 'getBlackListStatus',
outputs: [{ name: '', type: 'bool' }],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [
{ name: '', type: 'address' },
{ name: '', type: 'address' }
],
name: 'allowed',
outputs: [{ name: '', type: 'uint256' }],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'paused',
outputs: [{ name: '', type: 'bool' }],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [{ name: 'who', type: 'address' }],
name: 'balanceOf',
outputs: [{ name: '', type: 'uint256' }],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: false,
inputs: [],
name: 'pause',
outputs: [],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'getOwner',
outputs: [{ name: '', type: 'address' }],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'owner',
outputs: [{ name: '', type: 'address' }],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'symbol',
outputs: [{ name: '', type: 'string' }],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: false,
inputs: [
{ name: '_to', type: 'address' },
{ name: '_value', type: 'uint256' }
],
name: 'transfer',
outputs: [],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
},
{
constant: false,
inputs: [
{ name: 'newBasisPoints', type: 'uint256' },
{ name: 'newMaxFee', type: 'uint256' }
],
name: 'setParams',
outputs: [],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
},
{
constant: false,
inputs: [{ name: 'amount', type: 'uint256' }],
name: 'issue',
outputs: [],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
},
{
constant: false,
inputs: [{ name: 'amount', type: 'uint256' }],
name: 'redeem',
outputs: [],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
},
{
constant: true,
inputs: [
{ name: '_owner', type: 'address' },
{ name: '_spender', type: 'address' }
],
name: 'allowance',
outputs: [{ name: 'remaining', type: 'uint256' }],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'basisPointsRate',
outputs: [{ name: '', type: 'uint256' }],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [{ name: '', type: 'address' }],
name: 'isBlackListed',
outputs: [{ name: '', type: 'bool' }],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: false,
inputs: [{ name: '_clearedUser', type: 'address' }],
name: 'removeBlackList',
outputs: [],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'MAX_UINT',
outputs: [{ name: '', type: 'uint256' }],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: false,
inputs: [{ name: 'newOwner', type: 'address' }],
name: 'transferOwnership',
outputs: [],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
},
{
constant: false,
inputs: [{ name: '_blackListedUser', type: 'address' }],
name: 'destroyBlackFunds',
outputs: [],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
},
{
inputs: [
{ name: '_initialSupply', type: 'uint256' },
{ name: '_name', type: 'string' },
{ name: '_symbol', type: 'string' },
{ name: '_decimals', type: 'uint256' }
],
payable: false,
stateMutability: 'nonpayable',
type: 'constructor'
},
{
anonymous: false,
inputs: [{ indexed: false, name: 'amount', type: 'uint256' }],
name: 'Issue',
type: 'event'
},
{
anonymous: false,
inputs: [{ indexed: false, name: 'amount', type: 'uint256' }],
name: 'Redeem',
type: 'event'
},
{
anonymous: false,
inputs: [{ indexed: false, name: 'newAddress', type: 'address' }],
name: 'Deprecate',
type: 'event'
},
{
anonymous: false,
inputs: [
{ indexed: false, name: 'feeBasisPoints', type: 'uint256' },
{ indexed: false, name: 'maxFee', type: 'uint256' }
],
name: 'Params',
type: 'event'
},
{
anonymous: false,
inputs: [
{ indexed: false, name: '_blackListedUser', type: 'address' },
{ indexed: false, name: '_balance', type: 'uint256' }
],
name: 'DestroyedBlackFunds',
type: 'event'
},
{
anonymous: false,
inputs: [{ indexed: false, name: '_user', type: 'address' }],
name: 'AddedBlackList',
type: 'event'
},
{
anonymous: false,
inputs: [{ indexed: false, name: '_user', type: 'address' }],
name: 'RemovedBlackList',
type: 'event'
},
{
anonymous: false,
inputs: [
{ indexed: true, name: 'owner', type: 'address' },
{ indexed: true, name: 'spender', type: 'address' },
{ indexed: false, name: 'value', type: 'uint256' }
],
name: 'Approval',
type: 'event'
},
{
anonymous: false,
inputs: [
{ indexed: true, name: 'from', type: 'address' },
{ indexed: true, name: 'to', type: 'address' },
{ indexed: false, name: 'value', type: 'uint256' }
],
name: 'Transfer',
type: 'event'
},
{ anonymous: false, inputs: [], name: 'Pause', type: 'event' },
{ anonymous: false, inputs: [], name: 'Unpause', type: 'event' }
];
/**
* Returns the token `name` and `symbol` from a given ERC-20 contract address
* @param web3
* @param tokenAddress
* @param method
*/
const getTokenInfo = async (web3, tokenAddress) => {
return await snapshot.utils.multicall(
web3._network.chainId.toString(),
web3,
erc20Abi,
[
[tokenAddress, 'name'],
[tokenAddress, 'symbol']
]
);
};
export default class Plugin {
public author = 'davidalbela';
public version = '0.0.1';
public name = 'Gnosis Impact';
public website = 'https://gnosis.io';
public options: any;
async getTokenInfo(web3: any, tokenAddress: string) {
try {
const tokenInfo = await getTokenInfo(web3, tokenAddress);
return {
address: tokenAddress,
checksumAddress: getAddress(tokenAddress),
name: tokenInfo[0][0],
symbol: tokenInfo[1][0]
};
} catch (e: any) {
throw new Error(e);
}
}
async getOmenCondition(network: string, conditionId: any) {
try {
const query = OMEN_GQL_QUERY;
query.condition.__args.id = conditionId;
return await snapshot.utils.subgraphRequest(
OMEN_SUBGRAPH_URL[network],
query
);
} catch (e) {
console.error(e);
}
}
async getUniswapPair(network: string, token0: any, token1: any) {
try {
const query = UNISWAP_V2_GQL_QUERY;
query.pairsTokens.__args.where = {
token0: token0.toLowerCase(),
token1: token1.toLowerCase()
};
query.pairsTokensInverted.__args.where = {
token0: token1.toLowerCase(),
token1: token0.toLowerCase()
};
query.pairsTokens0.__args.where = {
token0: token0.toLowerCase(),
token1: WETH_ADDRESS[network]
};
query.pairsTokens1.__args.where = {
token0: token1.toLowerCase(),
token1: WETH_ADDRESS[network]
};
const result = await snapshot.utils.subgraphRequest(
UNISWAP_V2_SUBGRAPH_URL[network],
query
);
if (result.pairsTokens.length > 0) {
return result.pairsTokens[0];
} else if (result.pairsTokensInverted.length > 0) {
return {
token0Price: result.pairsTokensInverted[0].token1Price
};
} else if (
result.pairsTokens0.length > 0 &&
result.pairsTokens1.length > 0
) {
return {
token0Price: (
parseFloat(result.pairsTokens0[0].token0Price) /
parseFloat(result.pairsTokens1[0].token0Price)
).toString()
};
}
throw new Error(
`Does not exist market pairs for ${token0} and ${token1}.`
);
} catch (e) {
console.error(e);
}
}
}
================================================
FILE: src/plugins/gnosis/plugin.json
================================================
{
"name": "Gnosis Impact",
"version": "0.0.1",
"author": "davidalbela",
"website": "https://...",
"icon": "ipfs://QmPhmL1jPjaYKeKeEzyetAueNHCsGgrUVqSJasiTrcPWvx",
"defaults": {
"space": {},
"proposal": {}
}
}
================================================
FILE: src/plugins/oSnap/Create.vue
================================================
Warning: no treasuries
You have installed the oSnap plugin, but you don't have any treasuries.
Please add a Safe as a treasury and enable oSnap on it to use the oSnap
plugin.
Warning: Multiple oSnap enabled plugins detected
For best experience using oSnap, please remove the SafeSnap plugin from
your space.
Loading oSnap Safes
Warning: no oSnap safes found
You have installed the oSnap plugin, but you don't have any oSnap safes.
Please add a Safe as a treasury and enable oSnap on it to use the oSnap
plugin.
Add treasuries to start building
removeSafe(i)"
@update-safe="safe => updateSafe(safe, i)"
/>
Add treasury +
================================================
FILE: src/plugins/oSnap/CreateSafe.vue
================================================
Remove treasury
Add oSnap transactions
Pick a safe
================================================
FILE: src/plugins/oSnap/Proposal.vue
================================================
oSnap Transactions
Transaction #{{ index + 1 }} — {{ type }}
{{ key }}
{{ value }}
There are no transactions associated with this proposal.
================================================
FILE: src/plugins/oSnap/README.md
================================================
# oSnap Snapshot Plugin
This is a Snapshot plugin that facilitates using the Optimistic Governor to execute a set of transactions.
See https://docs.snapshot.org/user-guides/plugins for general info about Snapshot plugin development.
## Terms
There are some terms that can be confusing in this plugin, because they are used to mean different things in different contexts.
* Proposal — in the context of a normal Snapshot vote, regardless of plugins, "Proposal" refers to the set of questions that gets submitted to Snapshot and presented to voters. In the context of the Optimistic Governor, "Proposal" refers to the set of transactions that are submitted to the Optimistic Governor contract to be executed. This can be confusing because a Snapshot "Proposal" that uses the oSnap plugin will itself have an Optimistic Governor "Proposal" for the transactions that it aims to execute. As far as possible we have prefixed Optimistic Governor proposals with "OG" in the code to avoid confusion.
* Assertion — Optimistic Oracle V3 calls a piece of information that is asserted as true an "assertion". In the context of the Optimistic Governor, an assertion is made on the Optimistic Oracle which states that the specified set of transactions is valid and should be executed. Some key information about the Optimistic Governor proposal can only be found in the context of assertions, such as the assertion transaction hash and log index which are used to generate links to the Optimistic Oracle UI.
* SafeSnap and oSnap — oSnap was originally part of the SafeSnap plugin, hence the similar names. SafeSnap uses the Reality oracle, while oSnap provides the option to use the Optimistic Oracle instead. Eventually it was decided that oSnap deserves to be its own plugin, and so it was split off from SafeSnap. However, for legacy support reasons, the oSnap functionality in the SafeSnap plugin is still available.
* Votes — both Snapshot and the Optimistic Oracle use votes for their function. The _Snapshot_ vote takes place first in the context of oSnap. When creating a Snapshot Proposal with oSnap, the Snapshot vote takes place first. Only if the Snapshot vote passes can transactions be proposed to the Optimistic Governor. In fact, if the assertion for an Optimistic Governor proposal is disputed, the Optimistic Governor proposal is immediately deleted by the contract. This means that the Optimistic Oracle vote that proceeds from the dispute has no bearing on the Optimistic Governor proposal. The Optimistic Oracle vote is only used to determine whether the disputer or the proposer loses their bond.
================================================
FILE: src/plugins/oSnap/components/BotSupportWarning.vue
================================================
Automatic
Execution
You are using the default settings. If your proposal passes, your
transaction will be
automatically executed and verified by the UMA
Optimistic Oracle.
Automatic
Execution
You are not using the default settings. If your
proposal passes, you will be required to manually
request transaction execution and post a bond to the UMA Optimistic
Oracle for verification.
Reasons:
Bond Amount (Should be 2)
Bond Token (Should be WETH)
Space URL (only snapshot.org production spaces
are supported with automated execution. There is no bot support
for testnets.)
================================================
FILE: src/plugins/oSnap/components/ExternalLink.vue
================================================
================================================
FILE: src/plugins/oSnap/components/HandleOutcome/HandleOutcome.vue
================================================
Connect your wallet to see execution details
Switch your wallet to {{ networkName }} to request execution
Request transaction execution
================================================
FILE: src/plugins/oSnap/components/HandleOutcome/steps/CanProposeToOG.vue
================================================
Required bond:
{{ formatUnits(minimumBond, decimals) }} {{ symbol }}
Challenge period:
{{ formatDuration(challengePeriod) }}
Quorum did not reach space requirement
Your balance is less than the required bond
On-chain proposals require a bond from the proposer. This will approve
tokens from your wallet to be posted as a bond. If you make an invalid
proposal, it will be disputed and you will lose your bond. If the proposal
is valid, your bond will be returned when the transactions are executed.
Approve bond
Warning: This proposal was disputed on-chain. Exercise caution when
proposing, because your proposal may be disputed too.
Make assertion on Oracle
================================================
FILE: src/plugins/oSnap/components/HandleOutcome/steps/CanRequestTxExecution.vue
================================================
This will execute the transactions from this proposal and return the
asserter's bond.
Execute {{ transactionCount }} transaction{{
transactionCount > 1 ? 's' : ''
}}
================================================
FILE: src/plugins/oSnap/components/HandleOutcome/steps/InOOChallengePeriod.vue
================================================
Transactions can be executed at
{{ expirationDateLocaleString }}
================================================
FILE: src/plugins/oSnap/components/HandleOutcome/steps/RejectedBySnapshotVote.vue
================================================
Transactions cannot be executed because the Snapshot vote resolved as
"against".
================================================
FILE: src/plugins/oSnap/components/HandleOutcome/steps/TallyingSnapshotVotes.vue
================================================
Waiting for Snapshot vote counting to conclude.
================================================
FILE: src/plugins/oSnap/components/HandleOutcome/steps/TransactionsExecuted.vue
================================================
All transactions have been executed
================================================
FILE: src/plugins/oSnap/components/Input/Address.vue
================================================
{{ label }}
================================================
FILE: src/plugins/oSnap/components/Input/Amount.vue
================================================
{{ label }}
================================================
FILE: src/plugins/oSnap/components/Input/FileInput/FileInput.vue
================================================
{{ props.error }}
File type must be {{ props.fileType }} . Please choose another.
Drop only one file at a time
{{
file.name
}}
{{
props.defaultLabel ?? 'Click to select file, or drag n drop'
}}
================================================
FILE: src/plugins/oSnap/components/Input/FileInput/utils.ts
================================================
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;
}
================================================
FILE: src/plugins/oSnap/components/Input/MethodParameter/MethodParameter.vue
================================================
{{ label }}
{{ label }}
true
false
{{ label }}
{{ label }}
{{ label }}
{{ label }}
================================================
FILE: src/plugins/oSnap/components/Input/MethodParameter/utils.ts
================================================
import { InputTypes } from '@/plugins/oSnap/types';
import { MaybeNestedArrays } from '@/plugins/oSnap/utils';
import { ParamType } from '@ethersproject/abi';
export function getTypes(param: ParamType): MaybeNestedArrays {
if (param.baseType === 'array') {
if (param.components) {
return param.components.map(getTypes);
}
if (param.arrayChildren) {
return getTypes(param.arrayChildren);
}
}
if (param.baseType === 'tuple') {
return param.components.map(getTypes);
}
return param.baseType as InputTypes;
}
export function extractTypes(param: ParamType) {
if (param.baseType === 'tuple') {
return {
input: 'tuple',
type: getTypes(param)
} as const;
}
if (param.baseType === 'array') {
return {
input: 'array',
type: getTypes(param)
} as const;
}
return {
input: 'single',
type: getTypes(param) as InputTypes
} as const;
}
const placeholders = {
string: 'a string of text',
address: '0x123...abc',
int: '123',
bytes: '0x123abc',
bytes32: '0x123abc',
bool: 'true'
} as const;
function reduceInt(value: string) {
return value.includes('int') ? 'int' : value;
}
export function getParamPlaceholder(param: ParamType) {
if (param.baseType === 'array') {
if (param.arrayChildren) {
return `[${placeholders[reduceInt(param.arrayChildren.baseType)]}]`;
}
return '[...]';
}
if (param.baseType === 'tuple') {
return '[...]';
}
return placeholders[reduceInt(param.baseType)];
}
export function getParamLabel(param: ParamType) {
const name = param.name?.length ? param.name + ' ' : '';
if (param.baseType === 'array') {
if (param.arrayChildren) {
return `${name} (${param.arrayChildren.baseType}[])`;
}
return `${name} (tuple[])`;
}
if (param.baseType === 'tuple') {
return `${name} (tuple)`;
}
return `${name} (${param.baseType})`;
}
function convertToStrings(value: unknown) {
if (Array.isArray(value)) {
return value.map(item => convertToStrings(item)); // Recursive processing for nested arrays
} else {
return String(value); // Convert other types (number, boolean, hex strings) to string
}
}
export function parseInputArray(
input: string
): MaybeNestedArrays | undefined {
try {
// Step 1: Preprocess the input to ensure it's in a valid JSON format
const validJsonString = preprocessInputToJson(input);
// Step 2: Safely parse the string into an array
const result = JSON.parse(validJsonString);
const toStrings = convertToStrings(result);
return toStrings;
} catch (error) {
return `Error parsing input: ${error}`;
}
}
// This function aims to quote unquoted strings and hex values to make the input JSON-compliant
function preprocessInputToJson(input: string): string {
// Regex to find hex values and unquoted strings
const hexRegex = /(\b0x[0-9A-Fa-f]+\b)/g;
return input
.replace(hexRegex, '"$1"')
.replace(/(['"])?([a-z0-9A-Z_]+)(['"])?:/g, '"$2": ')
.replace(/'/g, '"');
}
================================================
FILE: src/plugins/oSnap/components/Input/ReadOnly.vue
================================================
================================================
FILE: src/plugins/oSnap/components/Input/SelectSafe.vue
================================================
================================================
FILE: src/plugins/oSnap/components/Input/TransactionType.vue
================================================
================================================
FILE: src/plugins/oSnap/components/OsnapMarketingWidget.vue
================================================
Powered by
oSnap
================================================
FILE: src/plugins/oSnap/components/SafeLinkWithAvatar.vue
================================================
{{ safe.safeName }}
{{ shorten(safe.safeAddress) }}
================================================
FILE: src/plugins/oSnap/components/TransactionBuilder/ContractInteraction.vue
================================================
{{ $t('safeSnap.value') }}
ABI
function
{{ method.name }}()
Use Implementation ABI?
This contract looks like a proxy. Would you like to use the
implementation ABI?
Keep proxy ABI
Use Implementation ABI
================================================
FILE: src/plugins/oSnap/components/TransactionBuilder/ModalSafe.vue
================================================
Select Safe
================================================
FILE: src/plugins/oSnap/components/TransactionBuilder/ModalTransactionType.vue
================================================
Select transaction type
================================================
FILE: src/plugins/oSnap/components/TransactionBuilder/RawTransaction.vue
================================================
{{ $t('safeSnap.value') }}
{{ $t('safeSnap.data') }}
================================================
FILE: src/plugins/oSnap/components/TransactionBuilder/SafeImport.vue
================================================
{{
props.transaction?.method?.name
? `Contract interaction (${props.transaction.method.name})`
: 'Native Transfer'
}}
updateFinalTransaction({ to: e })"
:model-value="props.transaction.to"
:label="$t('safeSnap.to')"
:error="!isToValid ? 'Invalid address' : undefined"
/>
updateValue(e)"
>
Value (wei)
Function Parameters
updateParams({ [input.name]: e })
"
/>
================================================
FILE: src/plugins/oSnap/components/TransactionBuilder/TenderlySimulation.vue
================================================
Simulate Transaction
Checking transaction...
{{
simulationState.error
}}
Reset Simulation
Warning:
This transaction will
not be automatically executed by oSnap.
This transaction used
{{ simulationState.gasUsed.toLocaleString() }} gas, which exceeds
oSnap's maximum subsidized amount of
{{ OSNAP_GAS_SUBSIDY.toLocaleString() }}.
================================================
FILE: src/plugins/oSnap/components/TransactionBuilder/TokensModal.vue
================================================
{{ $t('noResultsFound') }}
This token isn't known to us. Please make sure it is the correct address
before proceeding.
{{ $t('learnMore') }}
================================================
FILE: src/plugins/oSnap/components/TransactionBuilder/TokensModalItem.vue
================================================
{{
formatNumber(
Number(token.balance),
getNumberFormatter({ maximumFractionDigits: 6 }).value
)
}}
{{ shorten(token.address) }}
================================================
FILE: src/plugins/oSnap/components/TransactionBuilder/Transaction.vue
================================================
Transaction {{ transactionIndex + 1 }}
================================================
FILE: src/plugins/oSnap/components/TransactionBuilder/TransactionBuilder.vue
================================================
Safe app link
{{ shorten(safe.safeAddress) }}
Module address {{
shorten(safe.moduleAddress)
}}
Number of transactions {{ safe.transactions.length }}
emit('updateTransaction', ...args)"
@remove-transaction="(...args) => emit('removeTransaction', ...args)"
/>
Add transaction +
================================================
FILE: src/plugins/oSnap/components/TransactionBuilder/TransactionImport.vue
================================================
================================================
FILE: src/plugins/oSnap/components/TransactionBuilder/TransferFunds.vue
================================================
{{ $t('safeSnap.asset') }}
{{ selectedToken.symbol }}
{{
selectedToken.address === 'main'
? ''
: `(${shorten(selectedToken.address)})`
}}
================================================
FILE: src/plugins/oSnap/components/TransactionBuilder/TransferNFT.vue
================================================
{{ $t('safeSnap.asset') }}
- {{ $t('safeSnap.noCollectibles') }} -
{{ collectable.name ?? collectable.tokenName }} #{{
shorten(collectable.id, 10)
}}
================================================
FILE: src/plugins/oSnap/constants.ts
================================================
export const safePrefixes = {
1: 'eth',
2: 'exp',
3: 'rop',
4: 'rin',
5: 'gor',
6: 'kot',
7: 'tch',
8: 'ubq',
9: 'tubq',
10: 'oeth',
11: 'meta',
12: 'kal',
13: 'dstg',
14: 'flr',
15: 'diode',
16: 'cflr',
17: 'tfi',
18: 'TST',
19: 'sgb',
20: 'esc',
21: 'esct',
22: 'eladid',
23: 'eladidt',
24: 'kardiachain',
25: 'cro',
26: 'L1test',
27: 'shib',
28: 'BobaRinkeby',
29: 'L1',
30: 'rsk',
31: 'trsk',
32: 'GooDT',
33: 'GooD',
34: 'dth',
35: 'tbwg',
36: 'dx',
37: 'xpla',
38: 'val',
39: 'u2u',
40: 'TelosEVM',
41: 'TelosEVMTestnet',
42: 'lukso',
43: 'pangolin',
44: 'crab',
45: 'pangoro',
46: 'darwinia',
47: 'aic',
48: 'etmp',
49: 'etmpTest',
50: 'xdc',
51: 'txdc',
52: 'cet',
53: 'tcet',
54: 'OP',
55: 'ZYX',
56: 'bnb',
57: 'sys',
58: 'OntologyMainnet',
59: 'eos-legacy',
60: 'go',
61: 'etc',
62: 'tetc',
63: 'metc',
64: 'ellaism',
65: 'tokt',
66: 'okt',
67: 'dbm',
68: 'SO1',
69: 'okov',
70: 'hsc',
71: 'cfxtest',
72: 'dxc',
73: 'FNCY',
74: 'idchain',
75: 'DSC',
76: 'mix',
77: 'spoa',
78: 'primuschain',
79: 'zenith',
80: 'GeneChain',
81: 'joc',
82: 'Meter',
83: 'MeterTest',
84: 'linqto-devnet',
85: 'gttest',
86: 'gt',
87: 'nnw',
88: 'tomo',
89: 'tomot',
90: 'gar-s0',
91: 'gar-s1',
92: 'gar-s2',
93: 'gar-s3',
94: 'sdlt',
95: 'camdl',
96: 'bkc',
97: 'bnbt',
98: 'six',
99: 'poa',
100: 'gno',
101: 'eti',
102: 'tw3g',
103: 'WLC',
104: 'tklc',
105: 'dw3g',
106: 'vlx',
107: 'ntn',
108: 'TT',
109: 'shibariumecosystem',
110: 'xpr',
111: 'ETL',
112: 'coinbit',
113: 'deh',
114: 'c2flr',
115: 'debank-testnet',
116: 'debank-mainnet',
117: 'auptick',
118: 'arcology',
119: 'enuls',
120: 'enulst',
121: 'REAL',
122: 'fuse',
123: 'spark',
124: 'dwu',
125: 'OYchainTestnet',
126: 'OYchainMainnet',
127: 'feth',
128: 'heco',
134: 'rlc',
135: 'AlyxTestnet',
136: 'deam',
137: 'matic',
138: 'dfio-meta-main',
139: 'woop',
141: 'OPtest',
142: 'dax',
144: 'PHI',
148: 'shimmerevm-mainnet',
150: 'sixt',
151: 'rbn',
152: 'rbn-devnet',
153: 'rbn-testnet',
154: 'rbn-tge',
155: 'tenet-testnet',
156: 'obe',
160: 'eva',
161: 'wall-e',
162: 'tpht',
163: 'pht',
165: 'omni_testnet',
167: 'atoshi',
168: 'aioz',
169: 'manta',
170: 'hoosmartchain',
172: 'resil',
180: 'ame',
186: 'Seele',
188: 'BMC',
189: 'BMCT',
193: 'cem',
195: 'tokb',
196: 'okb',
197: 'NEUTR',
198: 'bit',
199: 'BTT',
200: 'aox',
201: 'moactest',
204: 'obnb',
208: 'utx',
210: 'BTN',
211: 'EDI',
212: 'makalu',
217: 'SIN2',
218: 'SO1-old',
222: 'ASK',
225: 'LA',
226: 'TLA',
230: 'SDX',
236: 'deamtest',
242: 'plgchain',
246: 'ewt',
248: 'OAS',
250: 'ftm',
255: 'kroma',
256: 'hecot',
258: 'setm',
259: 'neon',
262: 'SUR',
269: 'hpb',
271: 'EGONm',
274: 'lachain',
280: 'zksync-goerli',
288: 'Boba',
291: 'orderly',
295: 'hedera-mainnet',
296: 'hedera-testnet',
297: 'hedera-previewnet',
298: 'hedera-localnet',
300: 'ogc',
301: 'Bobaopera',
303: 'ncnt',
309: 'wyz',
311: 'omax',
313: 'ncn',
314: 'filecoin',
321: 'kcs',
322: 'kcst',
324: 'zksync',
333: 'w3q',
335: 'DFKTEST',
336: 'sdn',
338: 'tcro',
345: 'YVM',
361: 'theta-mainnet',
363: 'theta-sapphire',
364: 'theta-amber',
365: 'theta-testnet',
369: 'pls',
371: 'tCNT',
385: 'lisinski',
400: 'hpn',
401: 'ozo_tst',
411: 'pepe',
416: 'SX',
418: 'latestnet',
420: 'ogor',
424: 'PGN',
427: 'zeeth',
443: 'obs-testnet',
444: 'synapse-sepolia',
456: 'arzio',
462: 'tarea',
499: 'rupx',
500: 'Camino',
501: 'Columbus',
512: 'aac',
513: 'aact',
516: 'gz-mainnet',
520: 'xt',
529: 'fire',
530: 'FxCore',
534: 'CNDL',
542: 'PAW',
555: 'CLASS',
558: 'tao',
568: 'dct',
570: 'sys-rollux',
588: 'metis-stardust',
592: 'astr',
595: 'maca',
596: 'tkar',
597: 'taca',
599: 'metis-goerli',
600: 'mesh-chain-testnet',
601: 'PEER',
614: 'glq',
634: 'avocado',
647: 'SX-Testnet',
648: 'ace',
666: 'pixie-chain-testnet',
667: 'laos',
668: 'junca',
669: 'juncat',
686: 'kar',
700: 'SNS',
707: 'bcs',
708: 'tbcs',
710: 'fury',
719: 'shibarium',
721: 'LYC',
740: 'tcanto',
741: 'vsct',
742: 'SPAY',
766: 'qom',
776: 'opc',
777: 'cth',
786: 'maal',
787: 'aca',
788: 'taero',
789: 'peth',
800: 'LUCID',
803: 'haic',
808: 'PFTEST',
813: 'meer',
818: 'BOC',
820: 'clo',
821: 'tclo',
841: 'tara',
842: 'taratest',
859: 'zeethdev',
868: 'FSCMainnet',
876: 'BNKEN',
877: 'DXT',
880: 'ambros',
888: 'wan',
900: 'gar-test-s0',
901: 'gar-test-s1',
902: 'gar-test-s2',
903: 'gar-test-s3',
909: 'PF',
910: 'DBONE',
917: 'tfire',
919: 'modesep',
927: 'ydk',
940: 'tpls',
941: 't2bpls',
942: 't3pls',
943: 't4pls',
956: 'munode',
963: 'btc20',
970: 'ccn',
971: 'Huygens',
972: 'Ascraeus',
977: 'yeti',
980: 'top_evm',
985: 'memochain',
989: 'top',
990: 'ELm',
997: '5ire',
998: 'ln',
999: 'twan',
1000: 'gton',
1001: 'Baobab',
1002: 'kai',
1003: 'tet',
1004: 't-ekta',
1007: 'tnew',
1008: 'eun',
1010: 'EVC',
1012: 'new',
1022: 'sku',
1023: 'tclv',
1024: 'clv',
1028: 'tbtt',
1030: 'cfx',
1031: 'prx',
1038: 'bronos-testnet',
1039: 'bronos-mainnet',
1071: 'shimmerevm-testnet-deprecated',
1072: 'shimmerevm-testnet',
1079: 'mintara-testnet',
1080: 'mintara',
1088: 'metis-andromeda',
1089: 'humans',
1099: 'moac',
1101: 'zkevm',
1107: 'tblxq',
1108: 'blxq',
1111: 'wemix',
1112: 'twemix',
1115: 'tcore',
1116: 'core',
1117: 'DOGSm',
1130: 'DFI',
1131: 'DFI-T',
1133: 'changi',
1138: 'ASARt',
1139: 'MATH',
1140: 'tMATH',
1149: 'Plexchain',
1170: 'auoc',
1177: 'sht',
1197: 'iora',
1201: 'avis',
1202: 'wtt',
1213: 'popcat',
1214: 'enter',
1229: 'xzo',
1230: 'UltronTestnet',
1231: 'UtronMainnet',
1234: 'step',
1243: 'ARC',
1244: 'TARC',
1246: 'om',
1252: 'CICT',
1280: 'HO',
1284: 'mbeam',
1285: 'mriver',
1286: 'mrock-old',
1287: 'mbase',
1288: 'mrock',
1291: 'swtr',
1294: 'Bobabeam',
1297: 'Bobabase',
1311: 'TDOS',
1314: 'alyx',
1319: 'aia',
1320: 'aiatestnet',
1337: 'geth',
1338: 'ELST',
1339: 'ELSM',
1353: 'CIC',
1369: 'zafic',
1379: 'KLC',
1388: 'ASAR',
1392: 'mun',
1402: 'zkevmtest',
1422: 'testnet-zkEVM-mango-pre-audit-upgraded',
1433: 'RIK',
1440: 'LAS',
1442: 'testnet-zkEVM-mango',
1452: 'gil',
1455: 'CTEX',
1501: 'chainx',
1506: 'Sherpax',
1507: 'SherpaxTestnet',
1515: 'beagle',
1559: 'tenet',
1618: 'cate',
1620: 'ath',
1657: 'bta',
1662: 'Yuma',
1663: 'Gobi',
1688: 'LUDAN',
1701: 'AnytypeChain',
1707: 'TBSI',
1708: 'tTBSI',
1718: 'PCM',
1773: 'TeaParty',
1777: 'gauss',
1804: 'kerleano',
1807: 'rAna',
1818: 'cube',
1819: 'cubet',
1856: 'tsf',
1875: 'wbt',
1881: 'gitshockchain',
1890: 'lightlink_phoenix',
1891: 'lightlink_pegasus',
1898: 'boya',
1907: 'bitci',
1908: 'tbitci',
1945: 'onus-testnet',
1951: 'dchain-mainnet',
1954: 'Dexilla',
1967: 'mtc',
1969: 'tscs',
1970: 'scs',
1971: 'atlr',
1975: 'onus-mainnet',
1984: 'euntest',
1985: 'satoshie',
1986: 'satoshie_testnet',
1987: 'egem',
1994: 'ekta',
1995: 'edx',
2000: 'dc',
2001: 'milkAda',
2002: 'milkALGO',
2008: 'cloudwalk_testnet',
2009: 'cloudwalk_mainnet',
2016: 'NetZm',
2018: 'pmint_dev',
2019: 'pmint_test',
2020: 'pmint',
2021: 'edg',
2022: 'edgt',
2023: 'taycan-testnet',
2025: 'rpg',
2031: 'cfg',
2032: 'ncfg',
2037: 'kiwi',
2038: 'shraptest',
2043: 'otp',
2044: 'Shrapnel',
2047: 'stos-testnet',
2048: 'stos-mainnet',
2049: 'movo',
2077: 'QKA',
2088: 'air',
2089: 'algl',
2100: 'eco',
2101: 'esp',
2109: 'exn',
2122: 'Metad',
2124: 'MEU',
2137: 'bigsb',
2138: 'dfio-meta-test',
2151: 'boa',
2152: 'fra',
2153: 'findora-testnet',
2154: 'findora-forge',
2199: 'msn',
2202: 'ABNm',
2203: 'BTC',
2213: 'evanesco',
2221: 'tkava',
2222: 'kava',
2223: 'VChain',
2241: 'KRST',
2300: 'bomb',
2309: 'arevia',
2323: 'sma',
2330: 'alt',
2332: 'smam',
2357: 'deprecated-kroma-sepolia',
2358: 'kroma-sepolia',
2399: 'bombt',
2400: 'TCGV',
2415: 'xodex',
2484: 'u2u_nebulas',
2559: 'ktoc',
2569: 'tpc',
2606: 'pocrnet',
2611: 'REDLC',
2612: 'EZChain',
2613: 'Fuji-EZChain',
2625: 'twbt',
2710: 'tmorph',
2888: 'BobaGoerli',
2999: 'bty',
3000: 'cennz-r',
3001: 'cennz-n',
3003: 'cau',
3011: '3ULL',
3031: 'ORL',
3068: 'bfc',
3141: 'filecoin-hyperspace',
3269: 'dubx',
3270: 'testdubx',
3306: 'debounce-devnet',
3331: 'zcrbeach',
3333: 'w3q-t',
3334: 'w3q-g',
3400: 'prb',
3434: 'SCAIt',
3500: 'prbtestnet',
3501: 'jfin',
3601: 'pando-mainnet',
3602: 'pando-testnet',
3636: 'BTCt',
3637: 'BTCm',
3666: 'jouleverse',
3690: 'btx',
3693: 'empire',
3698: 'SPCt',
3699: 'SPCm',
3701: 'xplatest',
3737: 'csb',
3797: 'alv',
3888: 'kalymainnet',
3889: 'kalytestnet',
3912: 'drac',
3939: 'dost',
3966: 'dyno',
3967: 'tdyno',
3999: 'ycc',
4000: 'ozo',
4001: 'PERIUM',
4002: 'tftm',
4051: 'BobaoperaTestnet',
4061: 'Nahmii3Mainnet',
4062: 'Nahmii3Testnet',
4090: 'Oasis',
4096: 'BNIt',
4099: 'BNIm',
4102: 'aioz-testnet',
4139: 'humans_testnet',
4141: 'TPBXt',
4181: 'PHIv1',
4201: 'lukso-testnet',
4242: 'nexi',
4328: 'BobaFujiTestnet',
4337: 'beam',
4444: 'html',
4460: 'orderlyl2',
4689: 'iotex-mainnet',
4690: 'iotex-testnet',
4759: 'TESTMEV',
4777: 'TBXN',
4918: 'txvm',
4919: 'xvm',
4999: 'BXN',
5000: 'mantle',
5001: 'mantle-testnet',
5002: 'treasurenet',
5005: 'tntest',
5165: 'ftn',
5177: 'tlc',
5197: 'es',
5234: 'hmnd',
5290: '_old_fire',
5315: 'UZMI',
5353: 'ttrn',
5522: 'VEX',
5551: 'Nahmii',
5553: 'NahmiiTestnet',
5555: 'cverse',
5611: 'obnbt',
5616: 'ARCT',
5678: 'TanssiCC',
5700: 'tsys',
5729: 'hik',
5758: 'satst',
5777: 'ggui',
5851: 'OntologyTestnet',
5869: 'rbd',
6065: 'TRESTEST',
6066: 'TRESMAIN',
6102: 'cascadia',
6118: 'UPTN-TEST',
6119: 'UPTN',
6502: 'Peerpay',
6552: 'SRC-test',
6565: 'fox',
6626: 'pixie-chain',
6688: 'iris',
6789: 'STANDm',
6969: 'tombchain',
6999: 'psc',
7000: 'zetachain-mainnet',
7001: 'zetachain-athens',
7027: 'ELLA',
7070: 'planq',
7171: 'bitrock',
7331: 'kly',
7332: 'EON',
7341: 'shyft',
7484: 'raba',
7518: 'MEV',
7575: 'tadil',
7576: 'adil',
7668: 'trn-mainnet',
7672: 'trn-porcini',
7700: 'canto',
7701: 'TestnetCanto',
7771: 'tbitrock',
7777: 'RiseOfTheWarbotsTestnet',
7878: 'tscas',
7895: 'ard',
7979: 'dos',
8000: 'teleport',
8001: 'teleport-testnet',
8029: 'mdgl',
12357: 'rei',
7363: 'dnd',
9052: 'acrechain',
8080: 'Liberty10',
8081: 'Liberty20',
8082: 'Sphinx10',
8086: 'BitEth',
8098: 'StreamuX',
8131: 'meertest',
8132: 'meermix',
8133: 'meerpriv',
8134: 'amana',
8135: 'flana',
8136: 'mizana',
8181: 'tBOC',
8217: 'Cypress',
8272: 'BTON',
8285: 'Kortho',
8387: 'fuck',
8453: 'base',
8654: 'toki',
8655: 'toki-testnet',
8723: 'olo',
8724: 'tolo',
8738: 'alph',
8768: 'tmy',
8848: 'maro',
8880: 'unq',
8881: 'qtz',
8882: 'opl',
8883: 'sph',
8888: 'XANAChain',
8889: 'vsc',
8898: 'mmt',
8899: 'jbc',
8989: 'gmmt',
8995: 'berg',
9000: 'evmos-testnet',
9001: 'evmos',
9012: 'brb',
9100: 'GENEC',
9170: '_old_tfire',
9223: 'COF',
9339: 'DOGSt',
9527: 'trpg',
9528: 'QETTest',
9559: 'testneon',
9700: 'MainnetDev',
9728: 'BobaBnbTestnet',
9768: 'NetZt',
9779: 'pn',
9790: 'carbon',
9792: 'carbon-testnet',
9818: 'tIMP',
9819: 'IMP',
9977: 'tMIND',
9990: 'AGNG',
9996: 'MIND',
9997: 'alt-testnet',
9999: 'myn',
10000: 'smartbch',
10001: 'smartbchtest',
10024: 'gon',
10081: 'joct',
10086: 'SJ',
10101: 'GEN',
10200: 'chi',
10201: 'PWR',
10243: 'aa',
10248: '0xt',
10395: 'TWLC',
10507: 'Jade',
10508: 'Snow',
10823: 'CCP',
10946: 'quadrans',
10947: 'quadranstestnet',
11110: 'astra',
11111: 'WAGMI',
11115: 'astra-testnet',
11119: 'hbit',
11235: 'ISLM',
11437: 'shyftt',
11612: 'SRDXt',
11888: 'SAN',
11891: 'Arianee',
12009: 'sats',
12051: 'tZERO',
12052: 'ZERO',
12123: 'BRC',
12306: 'fibo',
12321: 'blgchain',
12345: 'steptest',
12611: 'astrzk',
12715: 'tRIK',
12890: 'tqnet',
13000: 'SPS',
13308: 'Credit',
13337: 'beam-testnet',
13381: 'Phoenix',
13812: 'sus',
14000: 'SPS-Test',
14853: 'hmnd-t5',
15551: 'loop',
15555: 'TrustTestnet',
15557: 'eos-testnet',
16000: 'mtt',
16001: 'mtttest',
16507: 'Genesys',
16688: 'nyancat',
16718: 'airdao',
16888: 'tivar',
17000: 'holesky',
17171: 'G8Cm',
17180: 'PCT',
17777: 'eos',
18000: 'ZKST',
18122: 'STN',
18159: 'pom',
18181: 'G8Ct',
18686: 'MXCzkEVM',
19011: 'HMV',
19845: 'btcix',
20001: 'Camelark',
20729: 'CLOTestnet',
20736: 'p12',
21337: 'cennz-a',
21816: 'omc',
22023: 'SFL',
22040: 'airdao-test',
22222: 'NAUTCHAIN',
22776: 'map',
23006: 'ABNt',
23118: 'opside',
23294: 'sapphire',
23295: 'sapphire-testnet',
24484: 'web',
24734: 'mintme',
25888: 'GOLDT',
25925: 'bkct',
26026: 'frm',
26600: 'HTZ',
26863: 'OAC',
28528: 'obgor',
30067: 'Piece',
30103: 'ceri',
31102: 'esn',
31223: 'CLDTX',
31224: 'CLD',
31337: 'got',
31415: 'filecoin-wallaby',
32520: 'Brise',
32659: 'fsn',
32769: 'zil',
32990: 'zil-isolated-server',
33101: 'zil-testnet',
33333: 'avs',
33385: 'zil-devnet',
33469: 'zq2-devnet',
35011: 'j2o',
35441: 'q',
35443: 'q-testnet',
38400: 'cmrpg',
38401: 'ttrpg',
39797: 'nrg',
39815: 'oho',
41500: 'ox-beta',
42069: 'PC',
42161: 'arb1',
42170: 'arb-nova',
42220: 'celo',
42261: 'emerald-testnet',
42262: 'emerald',
42801: 'GST',
42888: 'keth',
43110: 'avaeth',
43113: 'Fuji',
43114: 'avax',
43288: 'bobaavax',
44444: 'FREN',
44787: 'ALFA',
45000: 'AutobahnNetwork',
46688: 'tfsn',
47805: 'REI',
49049: 'floripa',
49088: 'tbfc',
49797: 'tnrg',
50001: 'LOE',
50021: 'tgton',
51178: 'Opside-Testnet',
51712: 'SRDXm',
53935: 'DFK',
54211: 'ISLMT',
54321: 'ToronetTestnet',
55004: 'teth',
55555: 'reichain',
55556: 'trei',
56288: 'BobaBnb',
56789: 'VELO',
57000: 'tsys-rollux',
58008: 'sepPGN',
59140: 'linea-testnet',
59144: 'linea',
60000: 'TKM-test0',
60001: 'TKM-test1',
60002: 'TKM-test2',
60103: 'TKM-test103',
61800: 'aium-dev',
61803: 'Etica',
61916: 'DoKEN',
62320: 'BKLV',
62621: 'mtv',
63000: 'ecs',
63001: 'ecs-testnet',
65450: 'SRC',
67390: 'mcl',
67588: 'Cosmic',
69420: 'cndr',
70000: 'TKM0',
70001: 'TKM1',
70002: 'TKM2',
70103: 'TKM103',
71111: 'GuapX',
71393: 'ckb',
71401: 'gw-testnet-v1',
71402: 'gw-mainnet-v1',
73799: 'vt',
73927: 'mvm',
75000: 'resin',
77238: 'fnc',
77612: 'vscm',
77777: 'Toronet',
78110: 'firenze',
78281: 'dfly',
78430: 'amplify',
78431: 'bulletin',
78432: 'conduit',
79879: 'STANDt',
80001: 'maticmum',
81341: 'amanatest',
81342: 'amanamix',
81343: 'amanapriv',
81351: 'flanatest',
81352: 'flanamix',
81353: 'flanapriv',
81361: 'mizanatest',
81362: 'mizanamix',
81363: 'mizanapriv',
81720: 'qnet',
84531: 'basegor',
84886: 'Aerie',
85449: 'Cyber',
88002: 'NAUTTest',
88880: 'chz',
88888: 'ivar',
90210: 'bvhl',
91002: 'NAUT',
92001: 'lambda-testnet',
96970: 'mantis',
97288: 'BobaBnbOld',
99099: 'ELt',
99998: 'usctest',
99999: 'usc',
100000: 'qkc-r',
100001: 'qkc-s0',
100002: 'qkc-s1',
100003: 'qkc-s2',
100004: 'qkc-s3',
100005: 'qkc-s4',
100006: 'qkc-s5',
100007: 'qkc-s6',
100008: 'qkc-s7',
100009: 'vechain',
100010: 'vechain-testnet',
100100: 'chi1',
101010: 'SVRNt',
103090: 'CRFI',
108801: 'bro',
110000: 'qkc-d-r',
110001: 'qkc-d-s0',
110002: 'qkc-d-s1',
110003: 'qkc-d-s2',
110004: 'qkc-d-s3',
110005: 'qkc-d-s4',
110006: 'qkc-d-s5',
110007: 'qkc-d-s6',
110008: 'qkc-d-s7',
111000: 'testsbr',
111111: 'sbr',
112358: 'metao',
123456: 'dadil',
131419: 'ETND',
142857: 'ICPlaza',
167004: 'taiko-a2',
167005: 'taiko-l2',
167006: 'taiko-l3',
167007: 'tko-jolnir',
188881: 'condor',
200101: 'milkTAda',
200202: 'milkTAlgo',
200625: 'aka',
201018: 'alaya',
201030: 'alayadev',
201804: 'myth',
202020: 'tDSC',
202624: 'twl-jellie',
210425: 'platon',
220315: 'mas',
221230: 'reap',
221231: 'reap-testnet',
224168: 'TAFECO',
230315: 'hsktest',
234666: 'hym',
246529: 'ats',
246785: 'atstau',
247253: 'saakuru-testnet',
256256: 'cmp-mainnet',
266256: 'gz-testnet',
271271: 'EGONt',
281121: 'SoChain',
314159: 'filecoin-calibration',
330844: 'tc',
333331: 'avst',
333666: 'oonetest',
333777: 'oonedev',
333888: 'sparta',
333999: 'olympus',
355113: 'Bitfinity',
373737: 'hap-testnet',
381931: 'metal',
381932: 'Tahoe',
404040: 'TPBXm',
420420: 'KEK',
420666: 'tKEK',
420692: 'alterium',
421611: 'arb-rinkeby',
421613: 'arb-goerli',
421614: 'arb-sep',
424242: 'fastexTestnet',
431140: 'markr-go',
432201: 'dexalot-testnet',
432204: 'dexalot',
444900: 'wlkt',
471100: 'psep',
474142: 'oc',
512512: 'cmp',
513100: 'ethf',
534351: 'scr-sepolia',
534352: 'scr',
534353: 'scr-alpha',
534354: 'scr-prealpha',
534849: 'shi',
535037: 'BESC',
622277: 'rth',
641230: 'BRNKC',
651940: 'ALL',
666666: 'vpioneer',
751230: 'BRNKCTEST',
761412: 'Miexs',
776877: 'mdlrm',
800001: 'octa',
827431: 'CURVEm',
846000: 'bloqs4good',
888888: 'vision',
900000: 'psc-s0',
910000: 'psc-t-s0',
920000: 'psc-d-s0',
920001: 'psc-d-s1',
923018: 'tFNCY',
955305: 'elv',
1261120: 'azktn',
1313114: 'etho',
1313500: 'xero',
1337702: 'kintsugi',
1337802: 'kiln',
1337803: 'zhejiang',
2021398: 'dbk',
2099156: 'plian-mainnet',
2203181: 'platondev',
2206132: 'platondev2',
3141592: 'filecoin-butterfly',
3441005: 'mantaTestnet',
4000003: 'alt-zerogas',
4281033: 'worldscal',
5167003: 'MXC',
5555555: 'imversed',
5555558: 'imversed-testnet',
7225878: 'saakuru',
7355310: 'vsl',
7668378: 'tqom',
7762959: 'music',
7777777: 'zora',
8007736: 'plian-mainnet-l2',
8794598: 'hap',
8888881: 'quarix-testnet',
8888888: 'quarix',
10067275: 'plian-testnet-l2',
10101010: 'SVRNm',
11155111: 'sep',
13371337: 'tpep',
14288640: 'anduschain-mainnet',
16658437: 'plian-testnet',
18289463: 'ilt',
20180430: 'spectrum',
20181205: 'qki',
20201022: 'pg',
22052002: 'xlon',
27082017: 'exlvolta',
27082022: 'exl',
28945486: 'auxi',
29032022: 'fla',
31415926: 'filecoin-local',
35855456: 'JOYS',
43214913: 'mais',
61717561: 'aqua',
65010000: 'bakerloo-0',
65100000: 'piccadilly-0',
88888888: 'team',
99415706: 'TOYS',
192837465: 'GTH',
222000222: 'kanazawa',
245022926: 'neonevm-devnet',
245022934: 'neonevm-mainnet',
245022940: 'neonevm-testnet',
278611351: 'razor',
311752642: 'oneledger',
333000333: 'meld',
344106930: 'calypso-testnet',
356256156: 'tGTH',
486217935: 'dGTH',
503129905: 'nebula-staging',
1122334455: 'ipos',
1146703430: 'cyb',
1273227453: 'human-mainnet',
1313161554: 'aurora',
1313161555: 'aurora-testnet',
1313161556: 'aurora-betanet',
1351057110: 'chaos-tenet',
1380996178: 'rptr',
1482601649: 'nebula-mainnet',
1564830818: 'calypso-mainnet',
1666600000: 'hmy-s0',
1666600001: 'hmy-s1',
1666600002: 'hmy-s2',
1666600003: 'hmy-s3',
1666700000: 'hmy-b-s0',
1666700001: 'hmy-b-s1',
1666900000: 'hmy-ps-s0',
1666900001: 'hmy-ps-s1',
2021121117: 'hop',
2046399126: 'europa',
2863311531: 'a8',
3125659152: 'pirl',
4216137055: 'frankenstein',
11297108099: 'tpalm',
11297108109: 'palm',
111222333444: 'alphabet',
197710212030: 'ntt',
197710212031: 'ntt-haradev',
383414847825: 'zeniq',
666301171999: 'ipdc',
6022140761023: 'mole',
868455272153094: 'gw-testnet-v1-deprecated'
} as const;
export const EXPLORER_API_URLS = {
'1': 'https://api.etherscan.io/api',
'5': 'https://api-goerli.etherscan.io/api',
'10': 'https://api-optimistic.etherscan.io/api',
'100': 'https://gnosis.blockscout.com/api',
'73799': 'https://volta-explorer.energyweb.org/api',
'246': 'https://explorer.energyweb.org/api',
'137': 'https://api.polygonscan.com/api',
'56': 'https://api.bscscan.com/api',
'42161': 'https://api.arbiscan.io/api',
'8453' : 'https://api.basescan.org/api',
// '1116': Add 'https://openapi.coredao.org/api' if API key requirement is removed
'11155111': 'https://api-sepolia.etherscan.io/api',
} as const;
export const GNOSIS_SAFE_TRANSACTION_API_URLS = {
'1': 'https://safe-transaction-mainnet.safe.global/api',
'5': 'https://safe-transaction-goerli.safe.global/api',
'10': 'https://safe-transaction-optimism.safe.global/api',
'100': 'https://safe-transaction-gnosis-chain.safe.global/api',
'8453': 'https://safe-transaction-base.safe.global/api',
'73799': 'https://safe-transaction-volta.safe.global/api',
'246': 'https://safe-transaction-ewc.safe.global/api',
'137': 'https://safe-transaction-polygon.safe.global/api',
'56': 'https://safe-transaction-bsc.safe.global/api',
'42161': 'https://safe-transaction-arbitrum.safe.global/api',
'1116': 'https://safetx.coredao.org/api',
'11155111': 'https://safe-transaction-sepolia.safe.global/api',
} as const;
export const SAFE_APP_URLS = {
'1': 'https://app.safe.global/apps/open',
'5': 'https://app.safe.global/apps/open',
'100': 'https://app.safe.global/apps/open',
'73799': 'https://app.safe.global/apps/open',
'246': 'https://app.safe.global/apps/open',
'137': 'https://app.safe.global/apps/open',
'56': 'https://app.safe.global/apps/open',
'42161': 'https://app.safe.global/apps/open',
'1116': 'https://safe.coredao.org/apps/open',
'8453': 'https://app.safe.global/apps/open',
'11155111': 'https://app.safe.global/apps/open',
} as const;
// ABIs
export const OPTIMISTIC_GOVERNOR_ABI = [
'constructor(address _finder, address _owner, address _collateral, uint256 _bondAmount, string _rules, bytes32 _identifier, uint64 _liveness)',
'error NotIERC165Compliant(address guard_)',
'event AvatarSet(address indexed previousAvatar, address indexed newAvatar)',
'event ChangedGuard(address guard)',
'event Initialized(uint8 version)',
'event OptimisticGovernorDeployed(address indexed owner, address indexed avatar, address target)',
'event OptimisticOracleChanged(address indexed newOptimisticOracleV3)',
'event OwnershipTransferred(address indexed previousOwner, address indexed newOwner)',
'event ProposalDeleted(bytes32 indexed proposalHash, bytes32 indexed assertionId)',
'event ProposalExecuted(bytes32 indexed proposalHash, bytes32 indexed assertionId)',
'event SetCollateralAndBond(address indexed collateral, uint256 indexed bondAmount)',
'event SetEscalationManager(address indexed escalationManager)',
'event SetIdentifier(bytes32 indexed identifier)',
'event SetLiveness(uint64 indexed liveness)',
'event SetRules(string rules)',
'event TargetSet(address indexed previousTarget, address indexed newTarget)',
'event TransactionExecuted(bytes32 indexed proposalHash, bytes32 indexed assertionId, uint256 indexed transactionIndex)',
'event TransactionsProposed(address indexed proposer, uint256 indexed proposalTime, bytes32 indexed assertionId, tuple(tuple(address to, uint8 operation, uint256 value, bytes data)[] transactions, uint256 requestTime) proposal, bytes32 proposalHash, bytes explanation, string rules, uint256 challengeWindowEnds)',
'function EXPLANATION_KEY() view returns (bytes)',
'function PROPOSAL_HASH_KEY() view returns (bytes)',
'function RULES_KEY() view returns (bytes)',
'function assertionDisputedCallback(bytes32 assertionId)',
'function assertionIds(bytes32) view returns (bytes32)',
'function assertionResolvedCallback(bytes32 assertionId, bool assertedTruthfully)',
'function avatar() view returns (address)',
'function bondAmount() view returns (uint256)',
'function collateral() view returns (address)',
'function deleteProposalOnUpgrade(bytes32 proposalHash)',
'function escalationManager() view returns (address)',
'function executeProposal(tuple(address to, uint8 operation, uint256 value, bytes data)[] transactions)',
'function finder() view returns (address)',
'function getCurrentTime() view returns (uint256)',
'function getGuard() view returns (address _guard)',
'function getProposalBond() view returns (uint256)',
'function guard() view returns (address)',
'function identifier() view returns (bytes32)',
'function liveness() view returns (uint64)',
'function optimisticOracleV3() view returns (address)',
'function owner() view returns (address)',
'function proposalHashes(bytes32) view returns (bytes32)',
'function proposeTransactions(tuple(address to, uint8 operation, uint256 value, bytes data)[] transactions, bytes explanation)',
'function renounceOwnership()',
'function rules() view returns (string)',
'function setAvatar(address _avatar)',
'function setCollateralAndBond(address _collateral, uint256 _bondAmount)',
'function setEscalationManager(address _escalationManager)',
'function setGuard(address _guard)',
'function setIdentifier(bytes32 _identifier)',
'function setLiveness(uint64 _liveness)',
'function setRules(string _rules)',
'function setTarget(address _target)',
'function setUp(bytes initializeParams)',
'function sync()',
'function target() view returns (address)',
'function transferOwnership(address newOwner)'
] as const;
export const OPTIMISTIC_ORACLE_V3_ABI = [
'constructor(address _finder, address _defaultCurrency, uint64 _defaultLiveness)',
'event AdminPropertiesSet(address defaultCurrency, uint64 defaultLiveness, uint256 burnedBondPercentage)',
'event AssertionDisputed(bytes32 indexed assertionId, address indexed caller, address indexed disputer)',
'event AssertionMade(bytes32 indexed assertionId, bytes32 domainId, bytes claim, address indexed asserter, address callbackRecipient, address escalationManager, address caller, uint64 expirationTime, address currency, uint256 bond, bytes32 indexed identifier)',
'event AssertionSettled(bytes32 indexed assertionId, address indexed bondRecipient, bool disputed, bool settlementResolution, address settleCaller)',
'event OwnershipTransferred(address indexed previousOwner, address indexed newOwner)',
'function assertTruth(bytes claim, address asserter, address callbackRecipient, address escalationManager, uint64 liveness, address currency, uint256 bond, bytes32 identifier, bytes32 domainId) returns (bytes32 assertionId)',
'function assertTruthWithDefaults(bytes claim, address asserter) returns (bytes32)',
'function assertions(bytes32) view returns (tuple(bool arbitrateViaEscalationManager, bool discardOracle, bool validateDisputers, address assertingCaller, address escalationManager) escalationManagerSettings, address asserter, uint64 assertionTime, bool settled, address currency, uint64 expirationTime, bool settlementResolution, bytes32 domainId, bytes32 identifier, uint256 bond, address callbackRecipient, address disputer)',
'function burnedBondPercentage() view returns (uint256)',
'function cachedCurrencies(address) view returns (bool isWhitelisted, uint256 finalFee)',
'function cachedIdentifiers(bytes32) view returns (bool)',
'function cachedOracle() view returns (address)',
'function defaultCurrency() view returns (address)',
'function defaultIdentifier() view returns (bytes32)',
'function defaultLiveness() view returns (uint64)',
'function disputeAssertion(bytes32 assertionId, address disputer)',
'function finder() view returns (address)',
'function getAssertion(bytes32 assertionId) view returns (tuple(tuple(bool arbitrateViaEscalationManager, bool discardOracle, bool validateDisputers, address assertingCaller, address escalationManager) escalationManagerSettings, address asserter, uint64 assertionTime, bool settled, address currency, uint64 expirationTime, bool settlementResolution, bytes32 domainId, bytes32 identifier, uint256 bond, address callbackRecipient, address disputer))',
'function getAssertionResult(bytes32 assertionId) view returns (bool)',
'function getCurrentTime() view returns (uint256)',
'function getMinimumBond(address currency) view returns (uint256)',
'function multicall(bytes[] data) returns (bytes[] results)',
'function numericalTrue() view returns (int256)',
'function owner() view returns (address)',
'function renounceOwnership()',
'function setAdminProperties(address _defaultCurrency, uint64 _defaultLiveness, uint256 _burnedBondPercentage)',
'function settleAndGetAssertionResult(bytes32 assertionId) returns (bool)',
'function settleAssertion(bytes32 assertionId)',
'function stampAssertion(bytes32 assertionId) view returns (bytes)',
'function syncUmaParams(bytes32 identifier, address currency)',
'function transferOwnership(address newOwner)'
] as const;
export const VOTING_ABI = [
'constructor(uint128 _emissionRate, uint64 _unstakeCoolDown, uint64 _phaseLength, uint32 _maxRolls, uint32 _maxRequestsPerRound, uint128 _gat, uint64 _spat, address _votingToken, address _finder, address _slashingLibrary, address _previousVotingContract)',
'event DelegateSet(address indexed delegator, address indexed delegate)',
'event DelegatorSet(address indexed delegate, address indexed delegator)',
'event EncryptedVote(address indexed caller, uint32 indexed roundId, bytes32 indexed identifier, uint256 time, bytes ancillaryData, bytes encryptedVote)',
'event ExecutedUnstake(address indexed voter, uint128 tokensSent, uint128 voterStake)',
'event GatAndSpatChanged(uint128 newGat, uint64 newSpat)',
'event MaxRequestsPerRoundChanged(uint32 newMaxRequestsPerRound)',
'event MaxRollsChanged(uint32 newMaxRolls)',
'event OwnershipTransferred(address indexed previousOwner, address indexed newOwner)',
'event RequestAdded(address indexed requester, uint32 indexed roundId, bytes32 indexed identifier, uint256 time, bytes ancillaryData, bool isGovernance)',
'event RequestDeleted(bytes32 indexed identifier, uint256 indexed time, bytes ancillaryData, uint32 rollCount)',
'event RequestResolved(uint32 indexed roundId, uint256 indexed resolvedPriceRequestIndex, bytes32 indexed identifier, uint256 time, bytes ancillaryData, int256 price)',
'event RequestRolled(bytes32 indexed identifier, uint256 indexed time, bytes ancillaryData, uint32 rollCount)',
'event RequestedUnstake(address indexed voter, uint128 amount, uint64 unstakeTime, uint128 voterStake)',
'event SetNewEmissionRate(uint128 newEmissionRate)',
'event SetNewUnstakeCoolDown(uint64 newUnstakeCoolDown)',
'event SlashingLibraryChanged(address newAddress)',
'event Staked(address indexed voter, address indexed from, uint128 amount, uint128 voterStake, uint128 voterPendingUnstake, uint128 cumulativeStake)',
'event UpdatedReward(address indexed voter, uint128 newReward, uint64 lastUpdateTime)',
'event VoteCommitted(address indexed voter, address indexed caller, uint32 roundId, bytes32 indexed identifier, uint256 time, bytes ancillaryData)',
'event VoteRevealed(address indexed voter, address indexed caller, uint32 roundId, bytes32 indexed identifier, uint256 time, bytes ancillaryData, int256 price, uint128 numTokens)',
'event VoterSlashApplied(address indexed voter, int128 slashedTokens, uint128 postStake)',
'event VoterSlashed(address indexed voter, uint256 indexed requestIndex, int128 slashedTokens)',
'event VotingContractMigrated(address newAddress)',
'event WithdrawnRewards(address indexed voter, address indexed delegate, uint128 tokensWithdrawn)',
'function ANCILLARY_BYTES_LIMIT() view returns (uint256)',
'function UINT64_MAX() view returns (uint64)',
'function commitAndEmitEncryptedVote(bytes32 identifier, uint256 time, bytes ancillaryData, bytes32 hash, bytes encryptedVote)',
'function commitVote(bytes32 identifier, uint256 time, bytes ancillaryData, bytes32 hash)',
'function cumulativeStake() view returns (uint128)',
'function currentActiveRequests() view returns (bool)',
'function delegateToStaker(address) view returns (address)',
'function emissionRate() view returns (uint128)',
'function executeUnstake()',
'function finder() view returns (address)',
'function gat() view returns (uint128)',
'function getCurrentRoundId() view returns (uint32)',
'function getCurrentTime() view returns (uint256)',
'function getNumberOfPriceRequests() view returns (uint256 numberPendingPriceRequests, uint256 numberResolvedPriceRequests)',
'function getNumberOfPriceRequestsPostUpdate() returns (uint256 numberPendingPriceRequests, uint256 numberResolvedPriceRequests)',
'function getPendingRequests() view returns (tuple(uint32 lastVotingRound, bool isGovernance, uint64 time, uint32 rollCount, bytes32 identifier, bytes ancillaryData)[])',
'function getPrice(bytes32 identifier, uint256 time, bytes ancillaryData) view returns (int256)',
'function getPrice(bytes32 identifier, uint256 time) view returns (int256)',
'function getPriceRequestStatuses(tuple(bytes32 identifier, uint256 time, bytes ancillaryData)[] requests) view returns (tuple(uint8 status, uint32 lastVotingRound)[])',
'function getRoundEndTime(uint256 roundId) view returns (uint256)',
'function getRoundIdToVoteOnRequest(uint32 targetRoundId) view returns (uint32)',
'function getVotePhase() view returns (uint8)',
'function getVoterFromDelegate(address caller) view returns (address)',
'function getVoterParticipation(uint256 requestIndex, uint32 lastVotingRound, address voter) view returns (uint8)',
'function getVoterPendingStake(address voter, uint32 roundId) view returns (uint128)',
'function getVoterStakePostUpdate(address voter) returns (uint128)',
'function hasPrice(bytes32 identifier, uint256 time) view returns (bool)',
'function hasPrice(bytes32 identifier, uint256 time, bytes ancillaryData) view returns (bool)',
'function lastRoundIdProcessed() view returns (uint32)',
'function lastUpdateTime() view returns (uint64)',
'function maxRequestsPerRound() view returns (uint32)',
'function maxRolls() view returns (uint32)',
'function migratedAddress() view returns (address)',
'function multicall(bytes[] data) returns (bytes[] results)',
'function nextPendingIndexToProcess() view returns (uint64)',
'function outstandingRewards(address voter) view returns (uint256)',
'function owner() view returns (address)',
'function pendingPriceRequestsIds(uint256) view returns (bytes32)',
'function previousVotingContract() view returns (address)',
'function priceRequests(bytes32) view returns (uint32 lastVotingRound, bool isGovernance, uint64 time, uint32 rollCount, bytes32 identifier, bytes ancillaryData)',
'function processResolvablePriceRequests()',
'function processResolvablePriceRequestsRange(uint64 maxTraversals)',
'function renounceOwnership()',
'function requestGovernanceAction(bytes32 identifier, uint256 time, bytes ancillaryData)',
'function requestPrice(bytes32 identifier, uint256 time, bytes ancillaryData)',
'function requestPrice(bytes32 identifier, uint256 time)',
'function requestSlashingTrackers(uint256 requestIndex) view returns (tuple(uint256 wrongVoteSlashPerToken, uint256 noVoteSlashPerToken, uint256 totalSlashed, uint256 totalCorrectVotes, uint32 lastVotingRound))',
'function requestUnstake(uint128 amount)',
'function resolvedPriceRequestIds(uint256) view returns (bytes32)',
'function retrieveRewardsOnMigratedVotingContract(address voter, uint256 roundId, tuple(bytes32 identifier, uint256 time, bytes ancillaryData)[] toRetrieve) returns (uint256)',
'function revealVote(bytes32 identifier, uint256 time, int256 price, bytes ancillaryData, int256 salt)',
'function rewardPerToken() view returns (uint256)',
'function rewardPerTokenStored() view returns (uint128)',
'function rounds(uint256) view returns (address slashingLibrary, uint128 minParticipationRequirement, uint128 minAgreementRequirement, uint128 cumulativeStakeAtRound, uint32 numberOfRequestsToVote)',
'function setDelegate(address delegate)',
'function setDelegator(address delegator)',
'function setEmissionRate(uint128 newEmissionRate)',
'function setGatAndSpat(uint128 newGat, uint64 newSpat)',
'function setMaxRequestPerRound(uint32 newMaxRequestsPerRound)',
'function setMaxRolls(uint32 newMaxRolls)',
'function setMigrated(address newVotingAddress)',
'function setSlashingLibrary(address _newSlashingLibrary)',
'function setUnstakeCoolDown(uint64 newUnstakeCoolDown)',
'function slashingLibrary() view returns (address)',
'function spat() view returns (uint64)',
'function stake(uint128 amount)',
'function stakeTo(address recipient, uint128 amount)',
'function transferOwnership(address newOwner)',
'function unstakeCoolDown() view returns (uint64)',
'function updateTrackers(address voter)',
'function updateTrackersRange(address voter, uint64 maxTraversals)',
'function voteTiming() view returns (uint256 phaseLength)',
'function voterStakes(address) view returns (uint128 stake, uint128 pendingUnstake, uint128 rewardsPaidPerToken, uint128 outstandingRewards, int128 unappliedSlash, uint64 nextIndexToProcess, uint64 unstakeTime, address delegate)',
'function votingToken() view returns (address)',
'function withdrawAndRestake() returns (uint128)',
'function withdrawRewards() returns (uint128)'
] as const;
export const UMA_FINDER_ABI = [
'event InterfaceImplementationChanged(bytes32 indexed interfaceName, address indexed newImplementationAddress)',
'event OwnershipTransferred(address indexed previousOwner, address indexed newOwner)',
'function changeImplementationAddress(bytes32 interfaceName, address implementationAddress)',
'function getImplementationAddress(bytes32 interfaceName) view returns (address)',
'function interfacesImplemented(bytes32) view returns (address)',
'function owner() view returns (address)',
'function renounceOwnership()',
'function transferOwnership(address newOwner)'
] as const;
export const ERC20_ABI = [
//Read functions
'function balanceOf(address account) view returns (uint256)',
'function decimals() view returns (uint32)',
'function symbol() view returns (string)',
'function allowance(address owner, address spender) external view returns (uint256)',
// Write functions
'function approve(address spender, uint256 value) external returns (bool)',
'function transfer(address recipient, uint256 amount) public virtual override returns (bool)'
] as const;
export const ERC721_ABI = [
'function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable'
] as const;
// to potentially cut down on event ranges we query, hard code some deploy blocks for contracts
export type ContractData = {
network: string;
name: string;
address?: string;
deployBlock?: number;
subgraph?: string;
};
// contract addresses pulled from https://github.com/UMAprotocol/protocol/tree/master/packages/core/networks
export const contractData = [
{
// mainnet
network: '1',
name: 'OptimisticOracleV3',
address: '0xfb55F43fB9F48F63f9269DB7Dde3BbBe1ebDC0dE',
subgraph:
'https://subgrapher.snapshot.org/subgraph/arbitrum/Bm3ytsa1YvcyFJahdfQQgscFQVCcMvoXujzkd3Cz6aof',
deployBlock: 16636058
},
{
// goerli
network: '5',
name: 'OptimisticOracleV3',
address: '0x9923D42eF695B5dd9911D05Ac944d4cAca3c4EAB',
subgraph:
'https://api.thegraph.com/subgraphs/name/md0x/goerli-optimistic-oracle-v3',
deployBlock: 8497481
},
{
// optimism
network: '10',
name: 'OptimisticOracleV3',
address: '0x072819Bb43B50E7A251c64411e7aA362ce82803B',
subgraph:
'https://subgrapher.snapshot.org/subgraph/arbitrum/FyJQyV5TLNeowZrL6kLUTB9JNPyWQNCNXJoxJWGEtBcn',
deployBlock: 74537234
},
{
// gnosis
network: '100',
name: 'OptimisticOracleV3',
address: '0x22A9AaAC9c3184f68C7B7C95b1300C4B1D2fB95C',
subgraph:
'https://subgrapher.snapshot.org/subgraph/arbitrum/9K2nctaB2rAh7Cgzx3wKtdHwWoEeEQ9AThGATak6Ngm9',
deployBlock: 27087150
},
{
// polygon
network: '137',
name: 'OptimisticOracleV3',
address: '0x5953f2538F613E05bAED8A5AeFa8e6622467AD3D',
subgraph:
'https://subgrapher.snapshot.org/subgraph/arbitrum/7KWbDhUE5Eqcfn3LXQtLbCfJLkNucnhzJLpi2jKhqNuf',
deployBlock: 39331673
},
{
//arbitrum
network: '42161',
name: 'OptimisticOracleV3',
address: '0xa6147867264374F324524E30C02C331cF28aa879',
subgraph:
'https://subgrapher.snapshot.org/subgraph/arbitrum/9wpkM5tHgJBHYTzKEKk4tK8a7q6MimfS9QnW7Japa8hW',
deployBlock: 61236565
},
{
// avalanche
network: '43114',
name: 'OptimisticOracleV3',
address: '0xa4199d73ae206d49c966cF16c58436851f87d47F',
subgraph:
'https://subgrapher.snapshot.org/subgraph/arbitrum/3k8gzGzTMV2vDZAGBFM2q642SUyVbE31bAUL8SjFQkre',
deployBlock: 27816737
},
{
// core
network: '1116',
name: 'OptimisticOracleV3',
address: '0xD84ACa67d683aF7702705141b3C7E57e4e5e7726',
subgraph:
'https://thegraph.coredao.org/subgraphs/name/umaprotocol/core-optimistic-oracle-v3',
deployBlock: 11341063
},
{
// base
network: '8453',
name: 'OptimisticOracleV3',
address: '0x2aBf1Bd76655de80eDB3086114315Eec75AF500c',
subgraph:
'https://subgrapher.snapshot.org/subgraph/arbitrum/2Q4i8YgVZd6bAmLyDxXgrKPL2B6QwySiEUqbTyQ4vm4C',
deployBlock: 12066343
},
{
// sepolia
network: '11155111',
name: 'OptimisticOracleV3',
address: '0xFd9e2642a170aDD10F53Ee14a93FcF2F31924944',
subgraph:
'https://subgrapher.snapshot.org/subgraph/arbitrum/78JbrMhcC9CVDZHDADvNcyhRrrccTJG4vCVBztyer1Xa',
deployBlock: 5421195
},
{
// mainnet
network: '1',
name: 'OptimisticGovernor',
address: '0x28CeBFE94a03DbCA9d17143e9d2Bd1155DC26D5d',
subgraph:
'https://subgrapher.snapshot.org/subgraph/arbitrum/DQpwhiRSPQJEuc8y6ZBGsFfNpfwFQ8NjmjLmfv8kBkLu',
deployBlock: 16890621
},
// Keep in mind, OG addresses are not the module addresses for each individual space, these addresses typically
// are not used, but are here for reference.
{
//goerli
network: '5',
name: 'OptimisticGovernor',
address: '0x07a7Be7AA4AaD42696A17e974486cb64A4daC47b',
deployBlock: 8700589,
subgraph:
'https://api.thegraph.com/subgraphs/name/md0x/goerli-optimistic-governor'
},
{
// optimism
network: '10',
name: 'OptimisticGovernor',
address: '0x357fe84E438B3150d2F68AB9167bdb8f881f3b9A',
deployBlock: 83168480,
subgraph:
'https://subgrapher.snapshot.org/subgraph/arbitrum/Fd5RvSfkajAJ8Mi9sPxFSMVPFf56SDivDQW3ocqTAW5'
},
{
// gnosis
network: '100',
name: 'OptimisticGovernor',
deployBlock: 27102135,
subgraph:
'https://subgrapher.snapshot.org/subgraph/arbitrum/RrkjZ6wTgLJkcjX68auzrEZHMRYwDx8kR5sFQQy4Phz'
},
{
// polygon
network: '137',
name: 'OptimisticGovernor',
address: '0x3Cc4b597E9c3f51288c6Cd0c087DC14c3FfdD966',
deployBlock: 40677035,
subgraph:
'https://subgrapher.snapshot.org/subgraph/arbitrum/7L2JM14PnZgxGnRX7xaz54zWS6KVK6ZqVRCxEKJrJTDG'
},
{
// arbitrum
network: '42161',
name: 'OptimisticGovernor',
address: '0x30679ca4ea452d3df8a6c255a806e08810321763',
deployBlock: 72850751,
subgraph:
'https://subgrapher.snapshot.org/subgraph/arbitrum/BfK867bnkQhnx1LspA99ypqiqxbAReQ92aZz66Ubv4tz'
},
{
// avalanche
network: '43114',
name: 'OptimisticGovernor',
address: '0xEF8b46765ae805537053C59f826C3aD61924Db45',
deployBlock: 28050250,
subgraph:
'https://subgrapher.snapshot.org/subgraph/arbitrum/5F8875fmvtnv8Vv4aeedUcwNWjuxUg54aTHdapFuMJi3'
},
{
// core
network: '1116',
name: 'OptimisticGovernor',
address: '0x596Fd6A5A185c67aBD1c845b39f593fBA9C233aa',
deployBlock: 11341122,
subgraph:
'https://thegraph.coredao.org/subgraphs/name/umaprotocol/core-optimistic-governor'
},
{
// base
network: '8453',
name: 'OptimisticGovernor',
address: '0x80bCA2E1c272239AdFDCdc87779BC8Af6E12e633',
deployBlock: 13062540,
subgraph:
'https://subgrapher.snapshot.org/subgraph/arbitrum/H1WyWZqh5pHebWRDCXs7GhvGj7XznSP7arPY6pYcCqLn'
},
{
// sepolia
network: '11155111',
name: 'OptimisticGovernor',
address: '0x40153DdFAd90C49dbE3F5c9F96f2a5B25ec67461',
deployBlock: 5421242,
subgraph:
'https://subgrapher.snapshot.org/subgraph/arbitrum/5pwrjCkpcpCd79k9MBS5yVgnsHQiw6afvXUfzqHjdRFw'
},
] as const;
export const transactionTypes = [
'transferFunds',
'transferNFT',
'contractInteraction',
'raw',
'safeImport'
] as const;
export const solidityZeroHexString =
'0x0000000000000000000000000000000000000000000000000000000000000000';
================================================
FILE: src/plugins/oSnap/plugin.json
================================================
{
"name": "oSnap by UMA",
"version": "1.0.0",
"author": "UMA",
"website": "https://uma.xyz/osnap",
"icon": "https://osnap.uma.xyz/osnap-icon.svg",
"defaults": {
"proposal": {
"safe": null
}
}
}
================================================
FILE: src/plugins/oSnap/types.ts
================================================
import { BigNumber } from '@ethersproject/bignumber';
import { Contract, Event } from '@ethersproject/contracts';
import networks from '@snapshot-labs/snapshot.js/src/networks.json';
import { safePrefixes, transactionTypes } from './constants';
import { FunctionFragment } from '@ethersproject/abi';
/**
* Represents details about the chains that snapshot supports as described in the `networks` json file.
*
* This corresponds to one of the chain ids that snapshot supports.
*
* @see `@snapshot-labs/snapshot.js/src/networks.json`.
*/
type Networks = typeof networks;
/**
* One of the supported networks as defined in `@snapshot-labs/snapshot.js/src/networks.json`.
* @see Networks
*/
export type Network = keyof Networks;
/**
* The supported network prefixes as defined in EIP-3770 used by Safe apps.
* @see https://eips.ethereum.org/EIPS/eip-3770
*/
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[keyof SafeNetworkPrefixes];
/**
* Represents the four different types of transactions that oSnap supports.
*
* - Transfer Funds
* - Transfer NFT (also called Collectable)
* - Contract Interaction
* - Raw
*/
export type TransactionTypes = typeof transactionTypes;
/**
* One of the four different types of transactions that oSnap supports.
*
* - Transfer Funds
* - Transfer NFT (also called Collectable)
* - Contract Interaction
* - Raw
*/
export type TransactionType = TransactionTypes[number];
/**
* The Optimistic Governor contract requires transactions that it executes to be formatted like this.
*
* This corresponds to the values found in the `Transaction` struct in the Optimistic Governor contract.
*
* @see https://github.com/UMAprotocol/protocol/blob/master/packages/core/contracts/optimistic-governor/implementation/OptimisticGovernor.sol
*
* NOTE: Since we don't support the contract's `delegatecall` feature in oSnap, the value for `operation` is always 0.
*/
export type OptimisticGovernorTransaction = [
to: string,
operation: 0,
value: string,
data: string
];
/**
* Represents the data associated with the different types of transactions that oSnap supports.
*
* This is a discriminated union of the four object types that represent the data associated with a given transaction. The union discriminates on the `type` property of the objects.
*
* @see TransactionTypes
*/
export type Transaction =
| RawTransaction
| ContractInteractionTransaction
| TransferNftTransaction
| TransferFundsTransaction
| SafeImportTransaction;
/**
* Represents the fields that all transactions share.
*
* All the transaction types inherit from this type, adding their `type` field and any additional fields that they require.
*
* NOTE: the `formatted` field is what is actually sent to the Optimistic Governor contract.
*/
export type BaseTransaction = {
to: string;
value: string;
data: string;
formatted: OptimisticGovernorTransaction;
isValid?: boolean;
};
/**
* Represents a transaction that interacts with an arbitrary contract from safe json file import.
*
* @field `abi` field is the ABI of the contract that the transaction interacts with, represented as a JSON string.
*
* @field `methodName` field is the name of the method on the contract that the transaction calls.
*
* @field `parameters` field is an array of strings that represent the parameters that the method takes. NOTE: some methods take arrays or tuples as arguments, so some of these strings in the array may be JSON formatted arrays or tuples.
*/
export type SafeImportTransaction = BaseTransaction & {
type: 'safeImport';
abi?: string; // represents partial ABI only
method?: FunctionFragment;
parameters?: { [key: string]: string };
};
/**
* Represents a 'raw' transaction that does not have any additional fields.
*/
export type RawTransaction = BaseTransaction & {
type: 'raw';
};
/**
* Represents a transaction that interacts with an arbitrary contract.
*
* @field `abi` field is the ABI of the contract that the transaction interacts with, represented as a JSON string.
*
* @field `methodName` field is the name of the method on the contract that the transaction calls.
*
* @field `parameters` field is an array of strings that represent the parameters that the method takes. NOTE: some methods take arrays or tuples as arguments, so some of these strings in the array may be JSON formatted arrays or tuples.
*/
export type ContractInteractionTransaction = BaseTransaction & {
type: 'contractInteraction';
abi?: string;
methodName?: string;
method?: FunctionFragment;
parameters?: string[];
};
/**
* Represents a transaction that transfers an NFT (also called a Collectable).
*
* @field `recipient` field is the address of the recipient of the NFT.
*
* @field `collectable` field is the NFT that is being transferred.
*/
export type TransferNftTransaction = BaseTransaction & {
type: 'transferNFT';
recipient?: string;
collectable?: NFT;
};
/**
* Represents a transaction that transfers funds.
*
* @field `amount` field is the amount of funds that are being transferred.
*
* @field `recipient` field is the address of the recipient of the funds.
*
* @field `token` field is the token that is being transferred.
*/
export type TransferFundsTransaction = BaseTransaction & {
type: 'transferFunds';
amount?: string;
recipient?: string;
token?: Token;
};
/**
* The base type for assets that can be transferred by a transaction.
*
* Can represent either an NFT or an ERC20 token.
*
* @field `name` field is the name of the asset.
* @field `address` field is the address of the asset.
* @field `logoUri` field is the URI of the logo of the asset, if one exists.
* @field `imageUri` field is the URI of the image of the asset, if one exists.
*
* @see https://miyauchi.dev/posts/typescript-literal-hack/ for details about the `(string & {})` syntax.
*/
export type Asset = {
name: string;
address: 'main' | (string & {});
logoUri?: string;
imageUri?: string;
};
/**
* Represents an ERC20 token.
*
* @field `symbol` field is the symbol of the token.
* @field `decimals` field is the number of decimals that the token has.
* @field `balance` field is the balance of the token that the user has.
* @field `verified` field is whether or not the token is verified by Gnosis or Snapshot contract.
* @field `chainId` field is the chain id of the network that the token is on.
*/
export type Token = Asset & {
symbol: string;
decimals: number;
balance?: string;
verified?: boolean;
chainId?: Network;
};
/**
* Represents an ERC-721 NFT (also called a Collectable).
*
* @field `id` field is the id of the NFT, usually used as the mint number.
* @field `tokenName` field is the name of the NFT.
*/
export type NFT = Asset & {
id: string;
tokenName?: string;
};
/**
* Represents the response from the Gnosis Safe API when fetching the balances of the tokens that the user has. This is immediately transformed after fetching into the `Token` type, which holds both the token and the balance.
*
* @field `tokenAddress` field is the address of the token.
* @field `token` field is the token that the safe has.
* @field `balance` field is the balance of the token that the user has.
*/
export type BalanceResponse = {
tokenAddress: string;
token: {
decimals: number;
logoUri: string;
name: string;
symbol: string;
};
balance: string;
};
/**
* Represents the data associated with a safe.
*
* The plugin persists one object with this shape per proposal created. This object holds the `transactions` that the Optimistic Governor contract will execute.
*
* @field `safeName` field is the name of the safe.
* @field `safeAddress` field is the address of the safe.
* @field `network` field is the id for network that the safe is on.
* @field `moduleAddress` field is the address of the Optimistic Governor contract that was deployed for this safe.
* @field `transactions` field is the list of transactions that the Optimistic Governor contract will execute.
*/
export type GnosisSafe = {
safeName: string;
safeAddress: string;
network: Network;
moduleAddress: string;
transactions: Transaction[];
};
/**
* Represents the data associated with the plugin.
*
* Holds one object with this shape per proposal created. This is the shape of the data that is persisted by the plugin.
*
* `safes` is null when first creating a plugin, but is then immediately populated once the user picks a safe.
*
* @field `safes` field is the array of safes that the plugin is currently working with.
*/
export type OsnapPluginData = MultiSafe;
export type LegacyOsnapPluginData = SingleSafe;
type MultiSafe = {
safes: GnosisSafe[] | null;
};
type SingleSafe = {
safe: GnosisSafe | null;
};
/**
* Represents the data associated with an assertion on the Optimistic Oracle V3 subgraph.
*
* @field `assertionId` field is the id of the assertion.
* @field `expirationTime` field is the time that the assertion's challenge period ends.
* @field `assertionHash` field is the transaction hash from when the assertion was made.
* @field `settlementHash` field is the transaction hash from when the assertion was settled.
* @field `disputeHash` field is the transaction hash from when the assertion was disputed.
* @field `assertionLogIndex` field is the log index of the transaction from when the assertion was made.
* @field `settlementResolution` field is whether or not the assertion was resolved in favor of the asserter.
*/
export type AssertionGql = {
assertionId: string;
expirationTime: string;
assertionHash: string;
settlementHash: string | null;
disputeHash: string | null;
assertionLogIndex: string;
settlementResolution: boolean | null;
};
/**
* Represents the configuration of the Optimistic Governor module contract that was deployed for a given Safe.
*
* @field `moduleAddress` field is the address of the specific Optimistic Governor module contract that was deployed for a given Safe.
* @field `oracleAddress` field is the address of the Optimistic Oracle V3 contract.
* @field `rules` rules for this Optimistic Governor contract.
* @field `minimumBond` field is the minimum bond that is required for an assertion to be made on this Optimistic Governor contract.
* @field `challengePeriod` field is the challenge period that is required for an assertion to be made on this Optimistic Governor contract.
*/
export type OGModuleDetails = {
moduleAddress: string;
oracleAddress: string;
rules: string;
minimumBond: BigNumber;
challengePeriod: BigNumber;
};
/**
* Represents the collateral configuration for a given Optimistic Governor contract.
*
* @field `erc20Contract` field is the ERC20 contract that is used for collateral.
* @field `address` field is the address of the ERC20 contract that is used for collateral.
* @field `symbol` field is the symbol of the ERC20 contract that is used for collateral.
* @field `decimals` field is the number of decimals that the ERC20 contract that is used for collateral has.
*/
export type CollateralDetails = {
erc20Contract: Contract;
address: string;
symbol: string;
decimals: BigNumber;
};
/**
* Event fired when an assertion is made on the Optimistic Oracle V3 contract.
*
* @field `assertionId` field is the id of the assertion.
* @field `domainId` field is the domain id of the assertion.
* @field `claim` field is the claim of the assertion.
* @field `asserter` field is the address of the asserter.
* @field `callbackRecipient` field is the address of the callback recipient.
* @field `escalationManager` field is the address of the escalation manager.
* @field `caller` field is the address of the caller.
* @field `expirationTime` field is the time that the assertion's challenge period ends.
* @field `currency` field is the currency that the assertion is made in.
* @field `bond` field is the bond that is required for the assertion.
* @field `identifier` field is the identifier of the assertion.
*/
export type AssertionMadeEvent = Event & {
args: {
assertionId: string; // indexed
domainId: string;
claim: string;
asserter: string; // indexed
callbackRecipient: string;
escalationManager: string;
caller: string;
expirationTime: BigNumber;
currency: string;
bond: BigNumber;
identifier: string; // indexed
};
};
/**
* Event fired when transactions are proposed on the Optimistic Governor contract.
*
* @field `proposer` field is the address of the proposer.
* @field `proposalTime` field is the time that the proposal was made.
* @field `assertionId` field is the id of the assertion.
* @field `proposal` field is the proposal that was made.
* @field `proposalHash` field is the hash of the proposal.
* @field `explanation` field is the explanation of the proposal, which in the case of oSnap is the ipfs url.
* @field `rules` field is the rules of the proposal.
* @field `challengeWindowEvents` field is the challenge window events of the proposal.
*/
export type TransactionsProposedEvent = Event & {
args: {
proposer: string; // indexed
proposalTime: BigNumber; // indexed
assertionId: string; // indexed
proposal: {
transactions: OptimisticGovernorTransaction[];
requestTime: BigNumber;
};
proposalHash: string;
explanation: string;
rules: string;
challengeWindowEvents: BigNumber;
};
};
/**
* Event fired when an Optimistic Governor proposal's transactions are executed successfully.
*
* @field `proposalHash` field is the hash of the proposal.
* @field `assertionId` field is the id of the assertion.
*/
export type ProposalExecutedEvent = Event & {
args: {
proposalHash: string; // indexed
assertionId: string; // indexed
};
};
/**
* Represents the transaction hash and log index of an `AssertionMade` event.
*
* We need these for generating a link to the assertion on the Optimistic Oracle dapp.
*/
export type AssertionTransactionDetails = {
assertionHash: string;
assertionLogIndex: string;
};
/**
* Represents the state of a proposal on the Optimistic Governor contract. When an assertion is associated with the proposal, we also include the assertion transaction hash and log index so that we can create a link to the assertion on the Optimistic Oracle dapp.
*
* There are four states that a proposal can be in:
*
* - `can-propose-to-og`: The user can propose transactions to the Optimistic Governor contract. This is the initial state of a proposal. We also indicate if this proposal has been disputed in the Oracle, so that we can warn the user to exercise caution and avoid losing their bond.
*
* - `in-oo-challenge-period`: The user has proposed transactions to the Optimistic Governor contract, and the proposal is currently in the challenge period on the Optimistic Oracle contract. We also indicate when the challenge period ends, so that we can warn the user to wait until the challenge period ends before proposing new transactions.
*
* - `can-request-tx-execution`: The user has proposed transactions to the Optimistic Governor contract, and the challenge period has ended. The user can now request that the Optimistic Governor contract execute the transactions.
*
* - `transactions-executed`: The user has proposed transactions to the Optimistic Governor contract, the challenge period has ended, and the transactions have been executed by the Optimistic Governor contract.
*/
export type OGProposalState =
| {
status: 'can-propose-to-og';
isDisputed: boolean;
}
| (AssertionTransactionDetails & {
status: 'in-oo-challenge-period';
expirationTime: number;
})
| (AssertionTransactionDetails & {
status: 'can-request-tx-execution';
})
| (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 & {
message: string;
};
// predicate for better error handling
export function isErrorWithMessage(error: unknown): error is ErrorWithMessage {
return error !== null && typeof error === 'object' && 'message' in error;
}
export const Status = {
IDLE: 'IDLE',
LOADING: 'LOADING',
SUCCESS: 'SUCCESS',
FAIL: 'FAIL',
ERROR: 'ERROR'
} as const;
export type Status = keyof typeof Status;
export type SpaceConfigResponse =
| {
automaticExecution: true;
}
| {
automaticExecution: false;
rules: boolean;
bondToken: boolean;
bondAmount: boolean;
};
export namespace GnosisSafe {
export interface ProposedTransaction {
id: number;
contractInterface: ContractInterface | null;
description: {
to: string;
value: string;
customTransactionData?: string;
contractMethod?: ContractMethod;
contractFieldsValues?: Record;
contractMethodIndex?: string;
nativeCurrencySymbol?: string;
networkPrefix?: string;
};
raw: { to: string; value: string; data: string };
}
export interface ContractInterface {
methods: ContractMethod[];
}
export interface Batch {
id: number | string;
name: string;
transactions: ProposedTransaction[];
}
export interface BatchFile {
version: string;
chainId: string;
createdAt: number;
meta: BatchFileMeta;
transactions: BatchTransaction[];
}
export interface BatchFileMeta {
txBuilderVersion?: string;
checksum?: string;
createdFromSafeAddress?: string;
createdFromOwnerAddress?: string;
name: string;
description?: string;
}
export interface BatchTransaction {
to: string;
value: string;
data?: string;
contractMethod?: ContractMethod;
contractInputsValues?: { [key: string]: string };
}
export interface ContractMethod {
inputs: ContractInput[];
name: string;
payable: boolean;
}
export interface ContractInput {
internalType: string;
name: string;
type: string;
components?: ContractInput[];
}
}
export type InputTypes =
| 'bool'
| 'string'
| 'address'
| Integer
| 'bytes'
| 'bytes32';
export type Integer = `int${number}` | `uint${number}`;
export function isIntegerType(type: InputTypes): type is Integer {
return type.includes('int');
}
export function nonNullable(value: T): value is NonNullable {
return value !== null;
}
// for backwards compatibility
export function isLegacySingleSafe(
pluginData: LegacyOsnapPluginData | OsnapPluginData
): pluginData is LegacyOsnapPluginData {
return 'safe' in pluginData;
}
================================================
FILE: src/plugins/oSnap/utils/abi.ts
================================================
import { FunctionFragment, Interface, ParamType } from '@ethersproject/abi';
import { BigNumberish } from '@ethersproject/bignumber';
import { memoize } from 'lodash';
import { ERC20_ABI, ERC721_ABI, EXPLORER_API_URLS } from '../constants';
import {
mustBeEthereumAddress,
mustBeEthereumContractAddress
} from './validators';
import { GnosisSafe, SafeImportTransaction } from '../types';
import { createSafeImportTransaction } from './transactions';
/**
* Checks if the `parameter` of a contract method `method` takes an array or tuple as input, based on the `baseType` of the parameter.
*
* If this is the case, we must parse the value as JSON and verify that it is valid.
*/
export function isArrayParameter(parameter: string): boolean {
return ['tuple', 'array'].includes(parameter);
}
const fetchContractABI = memoize(
async (url: string, contractAddress: string) => {
const params = new URLSearchParams({
module: 'contract',
action: 'getAbi',
address: contractAddress
});
const response = await fetch(`${url}?${params}`);
if (!response.ok) {
return { status: 0, result: '' };
}
return response.json();
},
(url, contractAddress) => `${url}_${contractAddress}`
);
/**
* Returns the ABI of a contract at the given address
*/
export async function getContractABI(
network: string,
contractAddress: string
): Promise {
const apiUrl = EXPLORER_API_URLS[network as keyof typeof EXPLORER_API_URLS];
if (!apiUrl) {
return '';
}
const isEthereumAddress = mustBeEthereumAddress(contractAddress);
const isEthereumContractAddress = await mustBeEthereumContractAddress(
network,
contractAddress
);
if (!isEthereumAddress || !isEthereumContractAddress) {
return '';
}
try {
const { result, status } = await fetchContractABI(apiUrl, contractAddress);
if (status === '0') {
return '';
}
return result;
} catch (e) {
console.error('Failed to retrieve ABI', e);
return '';
}
}
/**
* Checks if a method is a write function.
*
* Only write functions can be executed by the Optimistic Governor.
*/
export function isWriteFunction(method: FunctionFragment) {
if (!method.stateMutability) return true;
return !['view', 'pure'].includes(method.stateMutability);
}
/**
* Returns the write functions of a contract ABI.
*/
export function getABIWriteFunctions(abi: string) {
const abiInterface = new Interface(abi);
return (
abiInterface.fragments
// Return only contract's functions
.filter(FunctionFragment.isFunctionFragment)
.map(FunctionFragment.fromObject)
// Return only write functions
.filter(isWriteFunction)
// Sort by name
.sort((a, b) => (a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1))
);
}
/**
* Handles the extraction of the method's arguments from the `values` array.
*
* If the parameter is an array or tuple, we parse the value as JSON.
*/
export function extractMethodArgs(values: string[]) {
return (param: ParamType, index: number) => {
const value = values[index];
if (isArrayParameter(param.baseType)) {
return JSON.parse(value);
}
return value;
};
}
/**
* Encodes the method and parameters of a contract interaction.
*/
export function encodeMethodAndParams(
abi: string,
method: FunctionFragment,
values: string[]
) {
const contractInterface = new Interface(abi);
const parameterValues = method.inputs.map(extractMethodArgs(values));
return contractInterface.encodeFunctionData(method, parameterValues);
}
export function transformSafeMethodToFunctionFragment(
method: GnosisSafe.ContractMethod
): FunctionFragment {
const fragment = FunctionFragment.from({
...method,
type: 'function',
stateMutability: method.payable ? 'payable' : 'nonpayable'
});
return fragment;
}
export function initializeSafeImportTransaction(
unprocessedTransactions: GnosisSafe.BatchTransaction
): SafeImportTransaction {
return createSafeImportTransaction({
type: 'safeImport',
to: unprocessedTransactions.to,
value: unprocessedTransactions.value,
data: unprocessedTransactions?.data ?? '',
method: unprocessedTransactions.contractMethod
? transformSafeMethodToFunctionFragment(
unprocessedTransactions.contractMethod
)
: undefined,
parameters: unprocessedTransactions.contractInputsValues,
formatted: ['', 0, '0', '0x']
});
}
export function encodeSafeMethodAndParams(
method: SafeImportTransaction['method'],
params: SafeImportTransaction['parameters']
) {
if (!params || !method) return;
const missingParams = Object.values(params).length !== method.inputs.length;
if (missingParams) {
throw new Error('Some params are missing');
}
const abiSlice = Array(method);
const contractInterface = new Interface(abiSlice);
const parameterValues = method.inputs.map(input => {
const value = params[input.name];
if (isArrayParameter(input.baseType)) {
return JSON.parse(value);
}
return value;
});
return contractInterface.encodeFunctionData(method.name, parameterValues);
}
/**
* Returns the transaction data for an ERC20 transfer.
*/
export function getERC20TokenTransferTransactionData(
recipientAddress: string,
amount: BigNumberish
): string {
const contractInterface = new Interface(ERC20_ABI);
return contractInterface.encodeFunctionData('transfer', [
recipientAddress,
amount
]);
}
/**
* Returns the transaction data for an ERC721 transfer.
*/
export function getERC721TokenTransferTransactionData(
fromAddress: string,
recipientAddress: string,
id: BigNumberish
): string {
const contractInterface = new Interface(ERC721_ABI);
return contractInterface.encodeFunctionData('safeTransferFrom', [
fromAddress,
recipientAddress,
id
]);
}
================================================
FILE: src/plugins/oSnap/utils/coins.ts
================================================
import { Network, Token } from '../types';
export const ETHEREUM_COIN = {
name: 'Ether',
decimals: 18,
symbol: 'ETH',
logoUri:
'https://safe-transaction-assets.safe.global/chains/1/currency_logo.png',
address: 'main'
} as const;
export const MATIC_COIN = {
name: 'MATIC',
decimals: 18,
symbol: 'MATIC',
address: 'main',
logoUri:
'https://safe-transaction-assets.safe.global/chains/137/currency_logo.png'
} as const;
const EWC_COIN = {
name: 'Energy Web Token',
symbol: 'EWT',
address: 'main',
decimals: 18,
logoUri:
'https://safe-transaction-assets.safe.global/chains/246/currency_logo.png'
} as const;
const XDAI_COIN = {
name: 'XDAI',
symbol: 'XDAI',
address: 'main',
decimals: 18,
logoUri:
'https://safe-transaction-assets.safe.global/chains/100/currency_logo.png'
} as const;
const BNB_COIN = {
name: 'BNB',
symbol: 'BNB',
address: 'main',
decimals: 18,
logoUri:
'https://safe-transaction-assets.safe.global/chains/56/currency_logo.png'
} as const;
const CORE_COIN = {
name: 'Core',
symbol: 'CORE',
address: 'main',
decimals: 18,
logoUri:
'https://cloudflare-ipfs.com/ipfs/bafkreigjv5yb7uhlrryzib7j2f73nnwqan2tmfnwjdu26vkk365fyesoiu'
} as const;
const OPTIMISM_COIN = {
name: 'OPTIMISM',
symbol: 'OP',
address: 'main',
decimals: 18,
logoUri:
'https://optimistic.etherscan.io/images/svg/brands/optimism.svg?v=24.3.2.1'
} as const;
export function getNativeAsset(network: Network) {
switch (parseInt(network)) {
case 137:
case 80001:
return MATIC_COIN;
case 10:
return OPTIMISM_COIN;
case 100:
return XDAI_COIN;
case 246:
return EWC_COIN;
case 56:
return BNB_COIN;
case 1116:
return CORE_COIN;
}
return ETHEREUM_COIN;
}
export function isNativeAsset(token: Token | undefined) {
return token ? token.address === 'main' : false;
}
================================================
FILE: src/plugins/oSnap/utils/events.ts
================================================
import { Contract, Event, EventFilter } from '@ethersproject/contracts';
import bb from 'bluebird';
// This state is meant for adjusting a start/end block when querying events. Some apis will fail if the range
// is too big, so the following functions will adjust range dynamically.
export type RangeState = {
startBlock: number;
endBlock: number;
maxRange: number;
currentRange: number;
currentStart: number; // This is the start value you want for your query.
currentEnd: number; // this is the end value you want for your query.
done: boolean; // Signals we successfully queried the entire range.
multiplier?: number; // Multiplier increases or decreases range by this value, depending on success or failure
};
/**
* rangeStart. This starts a new range query and sets defaults for state. Use this as the first call before starting your queries
*
* @param {Pick} state
* @returns {RangeState}
*/
export function rangeStart(
state: Pick & {
maxRange?: number;
}
): RangeState {
const { startBlock, endBlock, multiplier = 2 } = state;
if (state.maxRange && state.maxRange > 0) {
const range = endBlock - startBlock;
if (!(range >= 0)) {
throw new Error('End block must be higher than start block');
}
const currentRange = Math.min(state.maxRange, range);
const currentStart = endBlock - currentRange;
const currentEnd = endBlock;
return {
done: false,
startBlock,
endBlock,
maxRange: state.maxRange,
currentRange,
currentStart,
currentEnd,
multiplier
};
} else {
// the largest range we can have, since this is the users query for start and end
const maxRange = endBlock - startBlock;
if (!(maxRange > 0)) {
throw new Error('End block must be higher than start block');
}
const currentStart = startBlock;
const currentEnd = endBlock;
const currentRange = maxRange;
return {
done: false,
startBlock,
endBlock,
maxRange,
currentRange,
currentStart,
currentEnd,
multiplier
};
}
}
/**
* rangeSuccessDescending. We have 2 ways of querying events, from oldest to newest, or newest to oldest. Typically we want them in order, from
* oldest to newest, but for this particular case we want them newest to oldest, ie descending ( larger timestamp to smaller timestamp).
* This function will increase the range between start/end block and return a new start/end to use since by calling this you are signalling
* that the last range ended in a successful query.
*
* @param {RangeState} state
* @returns {RangeState}
*/
export function rangeSuccessDescending(state: RangeState): RangeState {
const {
startBlock,
currentStart,
maxRange,
currentRange,
multiplier = 2
} = state;
// we are done if we succeeded querying where the currentStart matches are initial start block
const done = currentStart <= startBlock;
// increase range up to max range for every successful query
const nextRange = Math.min(Math.ceil(currentRange * multiplier), maxRange);
// move our end point to the previously successful start, ie moving from newest to oldest
const nextEnd = currentStart;
// move our start block to the next range down
const nextStart = Math.max(nextEnd - nextRange, startBlock);
return {
...state,
currentStart: nextStart,
currentEnd: nextEnd,
currentRange: nextRange,
done
};
}
/**
* rangeFailureDescending. Like the previous function, this will decrease the range between start/end for your query, because you are signalling
* that the last query failed. It will also keep the end of your range the same, while moving the start range up. This is why
* its considered descending, it will attempt to move from end to start, rather than start to end.
*
* @param {RangeState} state
* @returns {RangeState}
*/
export function rangeFailureDescending(state: RangeState): RangeState {
const { startBlock, currentEnd, currentRange, multiplier = 2 } = state;
const nextRange = Math.floor(currentRange / multiplier);
// this will eventually throw an error if you keep calling this function, which protects us against re-querying a broken api in a loop
if (currentRange <= 0) throw new Error('Current range must be above 0');
if (!(nextRange > 0)) throw new Error('Range must be above 0');
// we stay at the same end block
const nextEnd = currentEnd;
// move our start block closer to the end block, shrinking the range
const nextStart = Math.max(nextEnd - nextRange, startBlock);
return {
...state,
currentStart: nextStart,
currentEnd: nextEnd,
currentRange: nextRange
};
}
// The main interface to wrap the above pure functions up. requires you to pass in a generic function
// which returns the events based on a start/end block query.
export async function pageEvents(
startBlock: number,
endBlock: number,
maxRange: number,
//start and end block range to query
fetchEvents: (query: { start: number; end: number }) => Promise,
concurrency: number = 5
): Promise {
let state = rangeStart({ startBlock, endBlock, maxRange });
const ranges: { start: number; end: number; index: number }[] = [];
let index = 0;
do {
ranges.push({
start: state.currentStart,
end: state.currentEnd,
index: index++
});
state = rangeSuccessDescending(state);
} while (!state.done);
return (await bb.map(ranges, fetchEvents, { concurrency })).flat();
}
export async function getPagedEvents(params: {
contract: Contract;
eventFilter: EventFilter;
startBlock: number;
latestBlock: number;
maxRange: number;
}) {
const { contract, eventFilter, startBlock, latestBlock, maxRange } = params;
const eventPager = ({ start, end }: { start: number; end: number }) => {
return contract.queryFilter(eventFilter, start, end);
};
const pagedEvents = await pageEvents(
startBlock,
latestBlock,
maxRange,
eventPager
);
return pagedEvents as EventType[];
}
================================================
FILE: src/plugins/oSnap/utils/getters.ts
================================================
import { TreasuryWallet } from '@/helpers/interfaces';
import { defaultAbiCoder } from '@ethersproject/abi';
import { BigNumber } from '@ethersproject/bignumber';
import { Contract } from '@ethersproject/contracts';
import { keccak256 } from '@ethersproject/keccak256';
import { StaticJsonRpcProvider } from '@ethersproject/providers';
import { pack } from '@ethersproject/solidity';
import { toUtf8Bytes } from '@ethersproject/strings';
import snapshot from '@snapshot-labs/snapshot.js';
import getProvider from '@snapshot-labs/snapshot.js/src/utils/provider';
import memoize from 'lodash/memoize';
import detectProxyTarget from 'evm-proxy-detection';
import {
ERC20_ABI,
GNOSIS_SAFE_TRANSACTION_API_URLS,
OPTIMISTIC_GOVERNOR_ABI,
OPTIMISTIC_ORACLE_V3_ABI,
SAFE_APP_URLS,
contractData,
safePrefixes,
solidityZeroHexString
} from '../constants';
import {
AssertionGql,
AssertionMadeEvent,
BalanceResponse,
CollateralDetails,
NFT,
Network,
OGModuleDetails,
OGProposalState,
OptimisticGovernorTransaction,
ProposalExecutedEvent,
SafeNetworkPrefix,
SpaceConfigResponse,
Token,
TransactionsProposedEvent
} from '../types';
import { getPagedEvents } from './events';
import { shortenAddress, toChecksumAddress } from '@/helpers/utils';
import { getNativeAsset } from './coins';
import { formatUnits } from '@ethersproject/units';
/**
* Calls the Gnosis Safe Transaction API
*
* Ideal usage is to specify the shape of the response with the generic type parameter, assuming that the shape of the response is known.
*/
async function callGnosisSafeTransactionApi(
network: Network,
url: string
) {
if (!GNOSIS_SAFE_TRANSACTION_API_URLS[network])
throw new Error(`No gnosis safe api defined for network ${network}`);
const apiUrl = GNOSIS_SAFE_TRANSACTION_API_URLS[network];
const response = await fetch(apiUrl + url);
return response.json() as TResult;
}
/**
* Fetches the balances of the tokens owned by a given Safe.
*/
export const getGnosisSafeBalances = memoize(
(network: Network, safeAddress: string) => {
const checksumAddress = toChecksumAddress(safeAddress);
const endpointPath = `/v1/safes/${checksumAddress}/balances?exclude_spam=true`;
return callGnosisSafeTransactionApi[]>(
network,
endpointPath
);
},
(safeAddress, network) => `${safeAddress}_${network}`
);
/**
* Fetches the collectibles owned by a given Safe.
*/
export const getGnosisSafeCollectibles = memoize(
(network: Network, safeAddress: string) => {
const endpointPath = `/v2/safes/${safeAddress}/collectibles/`;
// the endpoint returns the data in this form, most likely to allow you to page the data.
type Result = {
count: number;
next: unknown;
previous: unknown;
results: NFT[];
};
return callGnosisSafeTransactionApi(network, endpointPath);
},
(safeAddress, network) => `${safeAddress}_${network}`
);
/**
* Fetches the block number of a given contract's deployment.
*/
function getDeployBlock(params: { network: Network; name: string }): number {
const results = contractData.filter(
contract =>
contract.network === params.network && contract.name === params.name
);
if (results.length === 1) return results[0].deployBlock ?? 0;
return 0;
}
export class ConfigError extends Error {
constructor(message: string, responsibleVar: string) {
super(message);
this.name = 'CONFIG_ERROR';
}
}
export function logIfErrorMessage(e: unknown, overrideMessage: string) {
console.error(e instanceof Error ? e.message : overrideMessage);
}
/**
* Fetches the subgraph url for a given contract on a given network.
*/
function getContractSubgraph(params: { network: Network; name: string }) {
const results = contractData.filter(
contract =>
contract.network === params.network && contract.name === params.name
);
if (results.length > 1)
throw new ConfigError(
`Too many results finding ${params.name} subgraph on network ${params.network}`,
'subgraph'
);
if (results.length < 1 || !results[0].subgraph)
throw new ConfigError(
`No subgraph url defined for ${params.name} on network ${params.network}`,
'subgraph'
);
return results[0].subgraph;
}
/**
* A helper that wraps the getContractSubgraph function to return the subgraph url for the OptimisticGovernor contract on a given network.
*/
function getOptimisticGovernorSubgraph(network: Network): string {
return getContractSubgraph({ network, name: 'OptimisticGovernor' });
}
/**
* A helper that wraps the getContractSubgraph function to return the subgraph url for the OptimisticOracleV3 contract on a given network.
*/
function getOracleV3Subgraph(network: Network): string {
return getContractSubgraph({ network, name: 'OptimisticOracleV3' });
}
/**
* Executes a graphql query.
*
* Ideal usage is to specify the shape of the response with the generic type parameter, assuming that the shape of the response is known.
*/
export const queryGql = async (url: string, query: string) => {
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json'
},
body: JSON.stringify({ query: query })
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(
`Network Error: ${response.status}, message: ${errorData.message}`
);
}
const data = await response.json();
// Throw an error if there are errors in the GraphQL response
if (data.errors) {
throw new Error(
`GraphQL Error: ${data.errors.map(error => error.message).join(', ')}`
);
}
return data.data as Result;
} catch (error) {
throw new Error(
`Network error: ${error instanceof Error ? error.message : error}`
);
}
};
/**
* Returns the address of the Optimistic Governor contract deployment associated with a given treasury (Safe) from the graph.
*/
export const getModuleAddressForTreasury = async (
network: Network,
treasuryAddress: string
) => {
try {
const subgraph = getOptimisticGovernorSubgraph(network);
const query = `
query getModuleAddressForTreasury {
safe(id: "${treasuryAddress.toLowerCase()}") {
optimisticGovernor {
id
}
}
}
`;
type Result = {
safe: { optimisticGovernor: { id: string } };
};
const result = await queryGql(subgraph, query);
return result?.safe?.optimisticGovernor?.id ?? '';
} catch (error) {
logIfErrorMessage(
error,
`Unable to get module address for treasury ${shortenAddress(
treasuryAddress
)} on network ${network}`
);
throw error;
}
};
/**
* Checks if a given treasury (Safe) has enabled oSnap.
*/
export const getIsOsnapEnabled = async (
network: Network,
safeAddress: string
) => {
try {
const subgraph = getOptimisticGovernorSubgraph(network);
const query = `
query isOSnapEnabled {
safe(id:"${safeAddress.toLowerCase()}"){
isOptimisticGovernorEnabled
}
}
`;
type Result = {
safe: { isOptimisticGovernorEnabled: boolean };
};
const result = await queryGql(subgraph, query);
return result?.safe?.isOptimisticGovernorEnabled ?? false;
} catch (error) {
logIfErrorMessage(
error,
`Unable to check if oSnap is enable for address ${shortenAddress(
safeAddress
)} on network ${network}`
);
throw error;
}
};
/**
* Takes an array of treasuries and checks if any of them have oSnap enabled.
*/
export const getSpaceHasOsnapEnabledTreasury = async (
treasuries: TreasuryWallet[]
) => {
const isOsnapEnabledOnTreasuriesResult = await Promise.all(
treasuries.map(treasury =>
getIsOsnapEnabled(treasury.network as Network, treasury.address)
)
);
return isOsnapEnabledOnTreasuriesResult.some(
isOsnapEnabled => isOsnapEnabled
);
};
/**
* Creates the url for the Safe app to configure oSnap.
*
* The data that the Safe app needs is encoded as URL search params.
*/
export function makeConfigureOsnapUrl(params: {
safeAddress: string;
network: Network;
spaceName: string;
spaceUrl: string;
baseUrl?: string;
appUrl?: string;
}) {
const {
safeAddress,
network,
spaceName,
spaceUrl,
appUrl = 'https://osnap.uma.xyz/'
} = params;
const baseUrl =
params.baseUrl ??
SAFE_APP_URLS[network] ??
'https://app.safe.global/apps/open';
const safeAddressPrefix = getSafeNetworkPrefix(network);
const appUrlSearchParams = new URLSearchParams();
appUrlSearchParams.set('spaceName', spaceName);
appUrlSearchParams.set('spaceUrl', spaceUrl);
const appUrlSearch = appUrlSearchParams.toString();
const safeAppSearchParams = new URLSearchParams();
safeAppSearchParams.set('safe', `${safeAddressPrefix}:${safeAddress}`);
safeAppSearchParams.set('appUrl', `${appUrl}?${appUrlSearch}`);
const safeAppSearch = safeAppSearchParams.toString();
const url = `${baseUrl}?${safeAppSearch}`;
return url;
}
/**
* Fetches the details of a given assertion from the Optimistic Oracle V3 subgraph.
*/
async function getAssertionGql(params: {
network: Network;
assertionId: string;
}) {
const { assertionId, network } = params;
const oracleUrl = getOracleV3Subgraph(network);
const request = `
{
assertion(id:"${assertionId}"){
assertionId
expirationTime
assertionHash
disputeHash
settlementHash
assertionLogIndex
settlementResolution
}
}
`;
type Result = {
assertion: AssertionGql | undefined;
};
const result = await queryGql(oracleUrl, request);
return result?.assertion;
}
/**
* Fetches the details of a given proposal from the Optimistic Governor subgraph.
*
* The subgraph uses the `assertionId` that comes from assertion events as the primary key for proposals.
* However, this `assertionId` will be deleted if the proposal is disputed, so we can't use it to query the subgraph.
* Instead, we use the `proposalHash` and `explanation` to query the subgraph.
* The `explanation` contains the ipfs url of the proposal, which is the only way to distinguish between proposals with the same `proposalHash`.
* This means we must use a `where` clause to filter the results, which is not ideal.
*/
async function getOgProposalGql(params: {
network: Network;
explanation: string;
moduleAddress: string;
proposalHash: string;
}) {
const { network, explanation, moduleAddress, proposalHash } = params;
const encodedExplanation = pack(
['bytes'],
[toUtf8Bytes(explanation.replace(/^0x/, ''))]
);
const subgraph = getOptimisticGovernorSubgraph(network);
const request = `
{
proposals(where:{proposalHash:"${proposalHash}",explanation:"${encodedExplanation}",optimisticGovernor:"${moduleAddress.toLowerCase()}"}){
id
executed
deleted
assertionId
}
}
`;
type Result = {
proposals: {
id: string;
executed: boolean;
assertionId: string;
deleted: boolean;
}[];
};
const result = await queryGql(subgraph, request);
// we can only use the gql `where` clause when querying a list, but we know that there will only be one result.
return result?.proposals[0];
}
/**
* Fetches the details of a Optimistic Governor module's collateral token.
*
* Returns the address, symbol, and decimals of the collateral token, along with the token contract for further querying.
*/
export async function getCollateralDetailsForProposal(
provider: StaticJsonRpcProvider,
moduleAddress: string
): Promise {
const moduleContract = new Contract(
moduleAddress,
OPTIMISTIC_GOVERNOR_ABI,
provider
);
const erc20Contract = new Contract(
await moduleContract.collateral(),
ERC20_ABI,
provider
);
const address = erc20Contract.address;
const symbol: string = await erc20Contract.symbol();
const decimals: BigNumber = await erc20Contract.decimals();
return { erc20Contract, address, symbol, decimals };
}
/**
* Fetches the allowance of a given collateral token for a given user.
*/
export async function getUserCollateralAllowance(
erc20Contract: Contract,
userAddress: string,
moduleAddress: string
) {
return erc20Contract.allowance(userAddress, moduleAddress);
}
/**
* Fetches the balance of a given collateral token for a given user.
*/
export async function getUserCollateralBalance(
erc20Contract: Contract,
userAddress: string
) {
return erc20Contract.balanceOf(userAddress);
}
/**
* Fetches the details of a given Optimistic Governor module from the chain.
*
* Performs a multicall to fetch the oracle address, rules, minimum bond, and challenge period.
*/
export async function getOGModuleDetails(params: {
provider: StaticJsonRpcProvider;
network: Network;
moduleAddress: string;
transactions: OptimisticGovernorTransaction[];
}): Promise {
const { provider, network, moduleAddress } = params;
const moduleDetails: [
[oracleAddress: string],
[rules: string],
[minimumBond: BigNumber],
[challengePeriod: BigNumber]
] = await snapshot.utils.multicall(network, provider, OPTIMISTIC_GOVERNOR_ABI as any, [
[moduleAddress, 'optimisticOracleV3'],
[moduleAddress, 'rules'],
[moduleAddress, 'getProposalBond'],
[moduleAddress, 'liveness']
]);
const oracleAddress = moduleDetails[0][0];
const rules = moduleDetails[1][0];
const minimumBond = moduleDetails[2][0];
const challengePeriod = moduleDetails[3][0];
return {
moduleAddress,
oracleAddress,
rules,
minimumBond,
challengePeriod
};
}
/**
* Fetches the state of an Optimistic Governor proposal from the chain.
*
* This is a fallback function that should only be used if the subgraph is not available, because it is very slow.
*
* The contract is designed in such a way that it deletes the `assertionId` from the proposal if the proposal is disputed, _or_ if the transactions are executed successfully. This means we can't tell the difference between a proposal that has not yet been proposed, has been disputed, or that has been executed by querying the chain.
*
* Instead, we must query the chain for the proposal events, and then query the chain for the execution events, and then compare the two to determine the state of the proposal. This is very slow.
*/
export async function getOgProposalStateFromChain(params: {
moduleDetails: OGModuleDetails;
network: Network;
proposalHash: string;
explanation: string;
}): Promise {
const { network, moduleDetails, explanation, proposalHash } = params;
const { moduleAddress, oracleAddress } = moduleDetails;
const provider = getProvider(network);
const latestBlock = (await provider.getBlock('latest')).number;
const oGstartBlock = getDeployBlock({ network, name: 'OptimisticGovernor' });
const oOStartBlock = getDeployBlock({ network, name: 'OptimisticOracleV3' });
const maxRange = 3000;
const moduleContract = new Contract(
moduleAddress,
OPTIMISTIC_GOVERNOR_ABI,
provider
);
const oracleContract = new Contract(
oracleAddress,
OPTIMISTIC_ORACLE_V3_ABI,
provider
);
const assertionId: string = await moduleContract.assertionIds(proposalHash);
const hasAssertionId = assertionIdIsNotZero(assertionId);
if (hasAssertionId) {
const assertionMadeEventForCurrentAssertionIdFilter =
oracleContract.filters.AssertionMade(assertionId);
const assertionMadeEvents = await getPagedEvents({
contract: oracleContract,
eventFilter: assertionMadeEventForCurrentAssertionIdFilter,
startBlock: oOStartBlock,
latestBlock,
maxRange
});
// assertion ids are unique, so this will have only one result
// we need to get an event instead of getting the `Assertion` struct from the chain because the oracle dapp needs the assertion transaction hash and the log index to link to the oracle dapp.
const assertionMadeEvent = assertionMadeEvents[0];
const expirationTime = assertionMadeEvent.args?.expirationTime.toNumber();
const isInChallengePeriod = expirationTime * 1000 > Date.now();
const assertionHash = assertionMadeEvent.transactionHash;
const assertionLogIndex = assertionMadeEvent.logIndex.toString();
if (isInChallengePeriod) {
return {
status: 'in-oo-challenge-period',
assertionHash,
assertionLogIndex,
expirationTime
};
}
return {
status: 'can-request-tx-execution',
assertionHash,
assertionLogIndex
};
}
const transactionsProposedEventFilter =
moduleContract.filters.TransactionsProposed();
const transactionsProposedEvents =
await getPagedEvents({
contract: moduleContract,
eventFilter: transactionsProposedEventFilter,
startBlock: oGstartBlock,
latestBlock,
maxRange
});
const transactionsProposedEventsThatMatch = transactionsProposedEvents.filter(
event =>
event.args?.proposalHash === proposalHash &&
event.args?.explanation === explanation
);
const hasTransactionProposedEvents =
transactionsProposedEventsThatMatch.length > 0;
// we can return early and skip querying for execution events if there are no proposal events
if (!hasTransactionProposedEvents) {
return {
status: 'can-propose-to-og',
isDisputed: false
};
}
// the proposal hash is not indexed on the transactions proposed event unfortunately, but it is on the proposal executed event.
// so at least we can use it for this one to narrow down the results.
const proposalExecutedEventFilter =
moduleContract.filters.ProposalExecuted(proposalHash);
const proposalExecutedEvents = await getPagedEvents({
contract: moduleContract,
eventFilter: proposalExecutedEventFilter,
startBlock: oGstartBlock,
latestBlock,
maxRange
});
// we know that the transactions have been executed if there is an execution event with an assertion id that matches an assertion id for a proposal event
let proposalExecuted = false;
for (const proposalExecutedEvent of proposalExecutedEvents) {
for (const transactionsProposedEvent of transactionsProposedEventsThatMatch) {
if (
proposalExecutedEvent.args?.assertionId ===
transactionsProposedEvent.args?.assertionId
) {
proposalExecuted = true;
break;
}
}
}
if (proposalExecuted) {
const assertionMadeEventForExecutedAssertionIdFilter =
oracleContract.filters.AssertionMade(assertionId);
const assertionMadeEvents = await getPagedEvents({
contract: oracleContract,
eventFilter: assertionMadeEventForExecutedAssertionIdFilter,
startBlock: oOStartBlock,
latestBlock,
maxRange
});
// assertion ids are unique, so this will have only one result
const assertionMadeEvent = assertionMadeEvents[0];
return {
status: 'transactions-executed',
assertionHash: assertionMadeEvent.transactionHash,
assertionLogIndex: assertionMadeEvent.logIndex.toString()
};
}
return {
status: 'can-propose-to-og',
isDisputed: false
};
}
/**
* Fetches the state of an Optimistic Governor proposal from the subgraph.
*
* This is the preferred method of fetching the state of a proposal, because it is much faster than querying the chain.
*/
export async function getOGProposalStateGql(params: {
network: Network;
moduleAddress: string;
proposalHash: string;
explanation: string;
}): Promise {
const { network } = params;
const oGproposal = await getOgProposalGql(params);
if (!oGproposal) {
return { status: 'can-propose-to-og', isDisputed: false };
}
const { executed, assertionId, deleted } = oGproposal;
const hasAssertionId = assertionIdIsNotZero(assertionId);
// the subgraph records `ProposalDeleted` events, which are fired when a proposal is disputed.
if (!hasAssertionId && deleted) {
return { status: 'can-propose-to-og', isDisputed: true };
}
// no assertion made yet
if (!hasAssertionId) {
return { status: 'can-propose-to-og', isDisputed: false };
}
const assertion = await getAssertionGql({ network, assertionId });
// cannot find assertion for some reason
if (!assertion) {
return { status: 'can-propose-to-og', isDisputed: false };
}
const {
assertionHash,
settlementHash,
assertionLogIndex,
settlementResolution
} = assertion;
// if the assertion is settled and the graph says the transactions have been executed, then we know the assertion passed and we can return early
if (executed && settlementHash) {
return {
status: 'transactions-executed',
assertionHash,
assertionLogIndex
};
}
// if the settlement hash exists and the settlement resolution is true, then we know that the assertion passed.
// we already checked if the transactions were executed, so we know that the assertion passed but the transactions were not executed yet.
if (settlementHash && settlementResolution) {
return {
status: 'can-request-tx-execution',
assertionHash,
assertionLogIndex
};
}
const expirationTime = Number(assertion.expirationTime);
const isExpired = Math.floor(Date.now() / 1000) >= expirationTime;
// from the above checks, we know that the transactions have not been executed, the assertion has not been disputed, and the assertion has not been settled.
// if the assertion challenge period has not yet expired, then we know we are still in the challenge period.
if (!isExpired) {
return {
status: 'in-oo-challenge-period',
assertionHash,
assertionLogIndex,
expirationTime
};
}
// request execution if there is no settlement yet and liveness has expired
return {
status: 'can-request-tx-execution',
assertionHash,
assertionLogIndex
};
}
/**
* Querying for an assertion ID that does not map to a proposal hash will return '0x0000000000000000000000000000000000000000000000000000000000000000'
*/
function assertionIdIsNotZero(assertionId: string) {
return assertionId !== solidityZeroHexString;
}
/**
* Fetches the state of an Optimistic Governor proposal.
*
* This function will attempt to fetch the state of a proposal from the subgraph, and if that fails, it will fall back to querying the chain.
*/
export async function getOGProposalState(params: {
moduleDetails: OGModuleDetails;
network: Network;
explanation: string;
transactions: OptimisticGovernorTransaction[];
}): Promise {
const { network, moduleDetails, explanation, transactions } = params;
const { moduleAddress } = moduleDetails;
const proposalHash = getProposalHashFromTransactions(transactions);
try {
return await getOGProposalStateGql({
network,
moduleAddress,
explanation,
proposalHash
});
} catch (error) {
console.warn(
'Error fetching OG proposal state from subgraph, falling back to chain',
error
);
return getOgProposalStateFromChain({
network,
moduleDetails,
explanation,
proposalHash
});
}
}
/**
* The `proposalHash` as represented in the Optimistic Governor contract is the keccak256 hash of the transactions that make up the proposal.
*/
export function getProposalHashFromTransactions(
transactions: OptimisticGovernorTransaction[]
) {
return keccak256(
defaultAbiCoder.encode(
['(address to, uint8 operation, uint256 value, bytes data)[]'],
[transactions]
)
);
}
/**
* Returns the EIP-3770 prefix for a given network.
*
* @see SafeNetworkPrefix
*/
export function getSafeNetworkPrefix(network: Network): SafeNetworkPrefix {
return safePrefixes[network];
}
/**
* Returns the url for a given Safe app on a given network.
*/
export function getSafeAppLink(
network: Network,
safeAddress: string,
{ appUrl = 'https://app.safe.global', path = '/home' } = {
appUrl: 'https://app.safe.global',
path: '/home'
}
) {
const prefix = getSafeNetworkPrefix(network);
return new URL(`${path}?safe=${prefix}:${safeAddress}`, appUrl).toString();
}
/**
* Returns the url for an Optimistic Governor proposal's assertion on the Optimistic Oracle dapp.
*/
export function getOracleUiLink(
chain: string,
txHash: string,
logIndex: number
) {
if (Number(chain) !== 5 && Number(chain) !== 80001) {
return `https://oracle.uma.xyz?transactionHash=${txHash}&eventIndex=${logIndex}`;
}
return `https://testnet.oracle.uma.xyz?transactionHash=${txHash}&eventIndex=${logIndex}`;
}
export async function fetchImplementationAddress(
proxyAddress: string,
network: string
): Promise {
try {
const provider = getProvider(network);
const requestFunc = ({ method, params }) => provider.send(method, params);
return (await detectProxyTarget(proxyAddress, requestFunc)) ?? undefined;
} catch (error) {
console.error(error);
}
}
/**
* Check if a space's deployed (on-chain) settings are supported by oSnap bots for auto execution
*/
export async function isConfigCompliant(safeAddress: string, chainId: string) {
const res = await fetch(
`https://osnap.uma.xyz/api/space-config?address=${safeAddress}&chainId=${chainId}`
);
if (!res.ok) {
throw new Error('Unable to fetch setting status');
}
const data = await res.json();
return data as unknown as SpaceConfigResponse;
}
export async function fetchBalances(network: Network, safeAddress: string) {
if (!safeAddress) {
return [];
}
try {
const balances = await getGnosisSafeBalances(network, safeAddress);
const balancesWithNative = balances.map(balance => {
if (!balance.tokenAddress || !balance.token) {
return {
...balance,
token: getNativeAsset(network),
tokenAddress: 'main'
};
}
return balance;
});
const tokens = await fetchTokens('https://tokens.uniswap.org');
return enhanceTokensWithBalances(balancesWithNative, tokens, network);
} catch (e) {
console.warn('Error fetching balances', e);
return [];
}
}
export async function fetchTokens(url: string): Promise {
try {
const response = await fetch(url);
const data = await response.json();
return data.verifiedTokens?.tokens || data.tokens || [];
} catch {
return [];
}
}
export function enhanceTokensWithBalances(
balances: Partial[],
tokens: Token[],
network: Network
) {
return balances
.filter(
(balance): balance is BalanceResponse =>
!!balance.token && !!balance.tokenAddress && !!balance.balance
)
.map(balance => enhanceTokenWithBalance(balance, tokens, network))
.sort((a, b) => {
if (a.address === 'main' && b.address !== 'main') return -1;
if (!(a.address === 'main') && b.address === 'main') return 1;
if (a.verified && !b.verified) return -1;
if (!a.verified && b.verified) return +1;
if (!a.balance || !b.balance) return 0;
if (parseFloat(a.balance) > parseFloat(b.balance)) return -1;
return 0;
});
}
// gets token balances and also determines if the token is verified
function enhanceTokenWithBalance(
balance: BalanceResponse,
tokens: Token[],
network: Network
): Token {
const verifiedToken = getVerifiedToken(balance.tokenAddress, tokens);
return {
...balance.token,
address: balance.tokenAddress,
balance: balance.balance
? formatUnits(balance.balance, balance.token.decimals)
: '0',
verified: !!verifiedToken,
chainId: network
};
}
function getVerifiedToken(tokenAddress: string, tokens: Token[]) {
return tokens.find(
token => token.address.toLowerCase() === tokenAddress.toLowerCase()
);
}
export async function fetchCollectibles(
network: Network,
gnosisSafeAddress: string
) {
try {
const response = await getGnosisSafeCollectibles(
network,
gnosisSafeAddress
);
return response.results;
} catch (error) {
console.warn('Error fetching collectibles');
}
return [];
}
================================================
FILE: src/plugins/oSnap/utils/index.ts
================================================
export * from './abi';
export * from './coins';
export * from './events';
export * from './getters';
export * from './proposal';
export * from './transactions';
export * from './validators';
================================================
FILE: src/plugins/oSnap/utils/proposal.ts
================================================
import { BigNumber } from '@ethersproject/bignumber';
import { toUtf8Bytes } from '@ethersproject/strings';
import { sendTransaction } from '@snapshot-labs/snapshot.js/src/utils';
import { ERC20_ABI, OPTIMISTIC_GOVERNOR_ABI } from '../constants';
import { OptimisticGovernorTransaction } from '../types';
/**
* The user must approve the spend of the collateral token before they can submit a proposal.
*
* If the proposal is disputed and fails a vote, the user will lose their bond.
*/
export async function* approveBond(
web3: any,
moduleAddress: string,
collateralAddress: string,
minimumBond: BigNumber
) {
const approveTx = await sendTransaction(
web3,
collateralAddress,
ERC20_ABI as any,
'approve',
[moduleAddress, minimumBond],
{}
);
yield approveTx;
const approvalReceipt = await approveTx.wait();
console.log('[DAO module] token transfer approved:', approvalReceipt);
yield;
}
/**
* Submits a proposal to the Optimistic Governor.
*/
export async function* submitProposal(
web3: any,
moduleAddress: string,
explanation: string,
transactions: OptimisticGovernorTransaction[]
) {
const explanationBytes = toUtf8Bytes(explanation);
const tx = await sendTransaction(
web3,
moduleAddress,
OPTIMISTIC_GOVERNOR_ABI as any,
'proposeTransactions',
[transactions, explanationBytes]
// [[["0xB8034521BB1a343D556e5005680B3F17FFc74BeD", 0, "0", "0x"]], '0x']
);
yield tx;
const receipt = await tx.wait();
console.log('[DAO module] submitted proposal:', receipt);
}
/**
* Executes a proposal on the Optimistic Governor.
*
* This can only be done after the dispute window has ended.
*/
export async function* executeProposal(
web3: any,
moduleAddress: string,
transactions: OptimisticGovernorTransaction[]
) {
const tx = await sendTransaction(
web3,
moduleAddress,
OPTIMISTIC_GOVERNOR_ABI as any,
'executeProposal',
[transactions]
);
yield tx;
const receipt = await tx.wait();
console.log('[DAO module] executed proposal:', receipt);
}
================================================
FILE: src/plugins/oSnap/utils/safeImport.ts
================================================
import { addressEqual } from '@/helpers/utils';
import { GnosisSafe } from '../types';
import { isSafeFile } from './validators';
export async function parseGnosisSafeFile(
file: File,
safe: GnosisSafe | null
): Promise {
return new Promise((res, rej) => {
const reader = new FileReader();
reader.readAsText(file);
reader.onload = async () => {
try {
if (typeof reader.result !== 'string') {
return rej(new Error('Buffer can not be parsed'));
}
const json = JSON.parse(reader.result);
if (!isSafeFile(json)) {
return rej(new Error('Not a valid Safe transaction file.'));
}
if (!isCreatedFromSafe(json, safe)) {
return rej(
new Error(
"Safe file does not match the selected treasury's address or chain ID"
)
);
}
return res(json);
} catch (err) {
return rej(new Error('Safe file corrupted. Please select another.'));
}
};
});
}
function isCreatedFromSafe(
batchFile: GnosisSafe.BatchFile,
safe: GnosisSafe | null
): boolean {
if (safe && batchFile.meta.createdFromSafeAddress) {
return (
safe.network === batchFile.chainId &&
addressEqual(safe.safeAddress, batchFile.meta.createdFromSafeAddress)
);
}
return false;
}
================================================
FILE: src/plugins/oSnap/utils/tenderly.ts
================================================
import {
OsnapPluginData,
Transaction as TTransaction,
GnosisSafe,
Network,
TenderlySimulationResult
} 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 const OSNAP_GAS_SUBSIDY = 500_000;
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 (!tx.isValid) {
throw new Error(`Transaction ${i + 1} in invalid`);
}
});
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 };
}
export function exceedsOsnapGasSubsidy(res: TenderlySimulationResult): boolean {
return res.gasUsed > OSNAP_GAS_SUBSIDY;
}
================================================
FILE: src/plugins/oSnap/utils/transactions.ts
================================================
import { FunctionFragment } from '@ethersproject/abi';
import { BigNumber } from '@ethersproject/bignumber';
import {
ContractInteractionTransaction,
NFT,
OptimisticGovernorTransaction,
RawTransaction,
Token,
TransferFundsTransaction,
TransferNftTransaction,
SafeImportTransaction
} from '../types';
import { encodeMethodAndParams, encodeSafeMethodAndParams } from './abi';
import { isAddress } from '@ethersproject/address';
import { validateTransaction } from './validators';
/**
* Creates a formatted transaction for the Optimistic Governor to execute
*
* note: the value for `operation` is always zero because we do not support delegatecall.
*
* @see OptimisticGovernorTransaction
*/
export function createFormattedOptimisticGovernorTransaction({
to,
value,
data
}: {
to: string;
value: string;
data: string;
}): OptimisticGovernorTransaction {
return [to, 0, value, data];
}
/**
* Creates a raw transaction for the Optimistic Governor to execute
*
* @see RawTransaction
*/
export function createRawTransaction(params: {
to: string;
value: string;
data: string;
}): RawTransaction {
const type = 'raw';
const formatted = createFormattedOptimisticGovernorTransaction(params);
return {
...params,
type,
formatted,
isValid: true
};
}
/**
* Creates a transaction to transfer an NFT
*
* @see TransferNftTransaction
*/
export function createTransferNftTransaction(params: {
recipient: string;
collectable: NFT;
data: string;
}): TransferNftTransaction {
const type = 'transferNFT';
const to = params.collectable.address;
const value = '0';
const data = params.data;
const formatted = createFormattedOptimisticGovernorTransaction({
to,
value,
data
});
return {
...params,
type,
to,
value,
data,
formatted,
isValid: true
};
}
/**
* Creates a transaction to transfer funds
*
* @see TransferFundsTransaction
*/
export function createTransferFundsTransaction(params: {
recipient: string;
amount: string;
token: Token;
data: string;
}): TransferFundsTransaction {
if (!(parseFloat(params.amount) > 0)) {
throw new Error('Amount invalid');
}
const type = 'transferFunds';
const isNativeToken = params.token.address === 'main';
const data = isNativeToken ? '0x' : params.data;
const to = isNativeToken ? params.recipient : params.token.address;
const amount = parseAmount(params.amount);
const value = isNativeToken ? amount : '0';
const formatted = createFormattedOptimisticGovernorTransaction({
to,
value,
data
});
return {
...params,
type,
data,
to,
value,
amount,
formatted,
isValid: true
};
}
/**
* Creates a transaction to interact with a contract.
*
* the `method` is executed with the given `parameters`.
*
* @see ContractInteractionTransaction
*/
export function createContractInteractionTransaction(params: {
to: string;
value: string;
abi: string;
method: FunctionFragment;
parameters: string[];
}): ContractInteractionTransaction {
try {
const type = 'contractInteraction';
const data = encodeMethodAndParams(
params.abi,
params.method,
params.parameters
);
const formatted = createFormattedOptimisticGovernorTransaction({
...params,
data
});
return {
...params,
data,
type,
formatted,
isValid: true
};
} catch (error) {
console.error(error);
throw new Error('Invalid function parameters');
}
}
export function parseAmount(input: string) {
return BigNumber.from(input).toString();
}
export function parseValueInput(input: string) {
if (input === '') {
return parseAmount('0');
}
return parseAmount(input);
}
export function createSafeImportTransaction(
params: SafeImportTransaction
): SafeImportTransaction {
try {
// check "value" & "to"
if (!validateTransaction(params)) {
throw new Error('Invalid transaction');
}
const abi = params.method
? JSON.stringify(Array(params.method))
: undefined;
// is native transfer funds
if (!params.method) {
const data = '0x';
const formatted = createFormattedOptimisticGovernorTransaction({
to: params.to,
value: params.value,
data
});
return {
...params,
isValid: true,
abi,
formatted,
data
};
}
// is contract interaction with NO args
if (!params.parameters) {
const data = params?.data || '0x';
const formatted = createFormattedOptimisticGovernorTransaction({
to: params.to,
value: params.value,
data
});
return {
...params,
isValid: true,
formatted,
data
};
}
// is contract interaction WITH args
// will throw if args are invalid
const encodedData =
params?.data ||
encodeSafeMethodAndParams(params.method, params.parameters) ||
'0x';
const formatted = createFormattedOptimisticGovernorTransaction({
to: params.to,
value: params.value,
data: encodedData
});
return {
...params,
isValid: true,
abi,
formatted,
data: encodedData
};
} catch (error) {
console.error(error);
return {
...params,
isValid: false
};
}
}
================================================
FILE: src/plugins/oSnap/utils/validators.ts
================================================
import { isAddress } from '@ethersproject/address';
import {
JsonRpcProvider,
StaticJsonRpcProvider
} from '@ethersproject/providers';
import memoize from 'lodash/memoize';
import { Contract } from '@ethersproject/contracts';
import {
BigNumber,
isBigNumberish
} from '@ethersproject/bignumber/lib/bignumber';
import { isBytesLike, isHexString } from '@ethersproject/bytes';
import getProvider from '@snapshot-labs/snapshot.js/src/utils/provider';
import { OPTIMISTIC_GOVERNOR_ABI } from '../constants';
import {
BaseTransaction,
NFT,
Token,
Transaction,
GnosisSafe,
InputTypes,
isIntegerType
} from '../types';
import { parseUnits } from '@ethersproject/units';
import { useMemoize } from '@vueuse/core';
import { parseInputArray } from '../components/Input/MethodParameter/utils';
/**
* Validates that the given `address` is a valid Ethereum address
*/
export const mustBeEthereumAddress = memoize((address: string) => {
const startsWith0x = address?.startsWith('0x');
const isValidAddress = isAddress(address);
return startsWith0x && isValidAddress;
});
/**
* Validates that the given `address` is a valid Ethereum contract address
*/
export const mustBeEthereumContractAddress = memoize(
async (network: string, address: string) => {
const provider = getProvider(network) as JsonRpcProvider;
const contractCode = await provider.getCode(address);
return (
contractCode && contractCode.replace(/^0x/, '').replace(/0/g, '') !== ''
);
},
(url, contractAddress) => `${url}_${contractAddress}`
);
/**
* Validates a transaction.
*/
export function validateTransaction(transaction: BaseTransaction) {
const addressNotEmptyOrInvalid =
transaction.to !== '' && isAddress(transaction.to);
const isBN = isBigNumberish(transaction.value);
return (
isBN &&
addressNotEmptyOrInvalid &&
(!transaction.data || isHexString(transaction.data))
);
}
/**
* Validates a module address.
*/
export async function validateModuleAddress(
network: string,
moduleAddress: string
): Promise {
if (!isAddress(moduleAddress)) return false;
const provider: StaticJsonRpcProvider = getProvider(network);
const moduleContract = new Contract(
moduleAddress,
OPTIMISTIC_GOVERNOR_ABI,
provider
);
return moduleContract
.rules()
.then(() => true)
.catch(() => false);
}
export function isTransferFundsValid(params: {
token: Token;
recipient: string;
amount: string;
}): boolean {
if (!amountPositive(params.amount, params.token.decimals)) {
return false;
}
if (!params.recipient || !isAddress(params.recipient)) {
return false;
}
if (!(params.token.address === 'main') && !isAddress(params.token.address)) {
return false;
}
return true;
}
export function isTransferNftValid(params: {
collectable: NFT | undefined;
recipient: string;
}): boolean {
// check NFT transfer variables are correct
if (!params.collectable) {
return false;
}
if (
!isAddress(params.recipient) ||
!isAddress(params.collectable.address) ||
!params.collectable.id
) {
return false;
}
return true;
}
export function amountPositive(amount: string, decimals = 18) {
try {
const isBigNumber = isBigNumberish(parseUnits(amount, decimals)); // checks for underflow
const isPositive = parseFloat(amount) > 0;
return isBigNumber && isPositive;
} catch {
return false;
}
}
export function isBytesLikeSafe(value: string): boolean {
try {
return isBytesLike(value);
} catch {
return false;
}
}
export function allTransactionsValid(transactions: Transaction[]): boolean {
return transactions.every(tx => tx.isValid === true);
}
export async function isContractAddress(
address: string,
network: string
): Promise {
const provider = getProvider(network);
const code = await provider.getCode(address);
return code !== '0x' && code !== '0x0';
}
export const checkIsContract = useMemoize(
async (address: string, network: string) =>
await isContractAddress(address, network)
);
export function isBool(value: string): boolean {
if (value === 'true' || value === 'false') {
return true;
}
return false;
}
export function validateInput(inputValue: string, type: InputTypes): boolean {
if (type === 'address') {
return isAddress(inputValue);
}
if (type === 'bytes') {
return isBytesLike(inputValue);
}
if (type === 'bytes32') {
return isBytesLike(inputValue);
}
if (isIntegerType(type)) {
return isValidInt(inputValue, type);
}
if (type === 'bool') {
return isBool(inputValue);
}
return true;
}
type Integer = `int${number}` | `uint${number}`;
function isValidInt(value: string, type: Integer) {
// check if is number like
if (!isBigNumberish(value)) {
return false;
}
const unsigned = type.startsWith('uint');
const signed = type.startsWith('int');
if (!unsigned && !signed) {
throw new Error(
'Invalid type specified. Type must be either an unsigned integer (uint) or a signed integer (int).'
);
}
const bits = parseInt(type.slice(unsigned ? 4 : 3));
if (isNaN(bits) || bits % 8 !== 0 || bits < 8 || bits > 256) {
throw new Error(
'Invalid integer type specified. Bit size must be a multiple of 8 with a max of 256'
);
}
const number = BigNumber.from(value);
// range checks
if (unsigned) {
const max = BigNumber.from(2).pow(bits).sub(1);
return number.gte(0) && number.lte(max);
} else {
const halfRange = BigNumber.from(2).pow(bits - 1);
const min = halfRange.mul(-1);
const max = halfRange.sub(1);
return number.gte(min) && number.lte(max);
}
}
// recursive type
export type MaybeNestedArrays = T | T[] | MaybeNestedArrays[];
function validateMaybeArray(
valuesMaybeArray: MaybeNestedArrays,
typesMaybeArray: MaybeNestedArrays
): MaybeNestedArrays {
try {
if (Array.isArray(valuesMaybeArray)) {
// handle single type arrays
if (!Array.isArray(typesMaybeArray)) {
return valuesMaybeArray.map(value =>
validateInput(value as string, typesMaybeArray)
);
} else {
if (valuesMaybeArray.length !== typesMaybeArray.length) {
throw new Error("Types and values don't match");
}
// handle array of arrays
return valuesMaybeArray.map(
(value: MaybeNestedArrays, index: number) =>
validateMaybeArray(value, typesMaybeArray[index])
);
}
} else {
if (Array.isArray(typesMaybeArray)) {
throw new Error("Types and values don't match");
}
// handle single item
return validateInput(valuesMaybeArray, typesMaybeArray);
}
} catch {
return false;
}
}
export function validateSingleOrArray(
value: string,
types: MaybeNestedArrays // ['bool', 'uint256'] | ['bool','uint32', ['bool', 'string', 'address'] ]
): boolean {
const parsed = parseInputArray(value);
if (!parsed) {
return false;
}
const res = validateMaybeArray(parsed, types);
if (Array.isArray(res)) {
return res.flat().every(Boolean);
}
return res;
}
// check if json is a safe json type
export const isSafeFile = (input: any): input is GnosisSafe.BatchFile => {
const $io0 = (input: any): boolean =>
'string' === typeof input.version &&
'string' === typeof input.chainId &&
'number' === typeof input.createdAt &&
'object' === typeof input.meta &&
null !== input.meta &&
$io1(input.meta) &&
Array.isArray(input.transactions) &&
input.transactions.every(
(elem: any) => 'object' === typeof elem && null !== elem && $io2(elem)
);
const $io1 = (input: any): boolean =>
(null === input.txBuilderVersion ||
undefined === input.txBuilderVersion ||
'string' === typeof input.txBuilderVersion) &&
(null === input.checksum ||
undefined === input.checksum ||
'string' === typeof input.checksum) &&
(null === input.createdFromSafeAddress ||
undefined === input.createdFromSafeAddress ||
'string' === typeof input.createdFromSafeAddress) &&
(null === input.createdFromOwnerAddress ||
undefined === input.createdFromOwnerAddress ||
'string' === typeof input.createdFromOwnerAddress) &&
'string' === typeof input.name &&
(null === input.description ||
undefined === input.description ||
'string' === typeof input.description);
const $io2 = (input: any): boolean =>
'string' === typeof input.to &&
'string' === typeof input.value &&
(null === input.data ||
undefined === input.data ||
'string' === typeof input.data) &&
(null === input.contractMethod ||
undefined === input.contractMethod ||
('object' === typeof input.contractMethod &&
null !== input.contractMethod &&
$io3(input.contractMethod))) &&
(null === input.contractInputsValues ||
undefined === input.contractInputsValues ||
('object' === typeof input.contractInputsValues &&
null !== input.contractInputsValues &&
false === Array.isArray(input.contractInputsValues) &&
$io5(input.contractInputsValues)));
const $io3 = (input: any): boolean =>
Array.isArray(input.inputs) &&
input.inputs.every(
(elem: any) => 'object' === typeof elem && null !== elem && $io4(elem)
) &&
'string' === typeof input.name &&
'boolean' === typeof input.payable;
const $io4 = (input: any): boolean =>
(undefined === input.internalType ||
'string' === typeof input.internalType) &&
'string' === typeof input.name &&
'string' === typeof input.type &&
(null === input.components ||
undefined === input.components ||
(Array.isArray(input.components) &&
input.components.every(
(elem: any) => 'object' === typeof elem && null !== elem && $io4(elem)
)));
const $io5 = (input: any): boolean =>
Object.keys(input).every((key: any) => {
const value = input[key];
if (undefined === value) return true;
return 'string' === typeof value;
});
return 'object' === typeof input && null !== input && $io0(input);
};
================================================
FILE: src/plugins/poap/ProposalSidebar.vue
================================================
================================================
FILE: src/plugins/poap/components/CustomBlock.vue
================================================
{{ $t(header) }}
{{ $t(buttonText) }}
================================================
FILE: src/plugins/poap/index.ts
================================================
import { getProposalVotes } from '../../helpers/snapshot';
// URLS
const API_BASE_URL = 'https://api.poap.tech';
const APP_BASE_URL = 'https://app.poap.xyz';
export default class Plugin {
public author = 'Poap-xyz';
public version = '1.0.0';
public name = 'POAP Module';
public options: any;
private static readonly POAP_API_API_KEY =
'e68BJLYJ1ns9Mdb9hd21j8Ci1e6dVYsAddQfOVQp3oYHo9bqLCutMewQP8NhbVvguMdHiHefNat3eZWiFReeV9nr7QH4xAl67fYASKHhFfGbVKKzak11PV6NM1wYy2eP';
openScanPage(address) {
window.open(`${APP_BASE_URL}/scan/${address}`, '_blank');
}
async getCurrentState(snapshot, address) {
// Fetch the event
const eventResponse = await fetch(
`${API_BASE_URL}/snapshot/proposal/${snapshot}`,
{ headers: { 'x-api-key': Plugin.POAP_API_API_KEY } }
);
// If the fetch fails: the event doesn't exists for this poap yet
if (!eventResponse.ok) {
return { image_url: '', currentState: 'NO_POAP' };
}
// Get the image from the event
const { image_url } = await eventResponse.json();
// Check that the address is not empty
if (!address) {
return { image_url, currentState: 'NOT_VOTED' };
}
// Fetch the vote
const votes = await getProposalVotes(snapshot, { voter: address });
const voted = votes.length > 0;
if (!voted) {
// Address did not vote proposal
return { image_url, currentState: 'NOT_VOTED' };
}
// Fetch the claim info for the address
const addressResponse = await fetch(
`${API_BASE_URL}/snapshot/proposal/${snapshot}/${address}`,
{ headers: { 'x-api-key': Plugin.POAP_API_API_KEY } }
);
// If the fetch failed return the NOT_VOTED state
if (!addressResponse.ok) {
return { image_url, currentState: 'NOT_VOTED' };
}
const { claimed, status } = await addressResponse.json();
if (claimed) {
// If the address claimed the token but the status is not passed
// it means that the token is being minted
if (claimed && status !== 'passed') {
return { image_url, currentState: 'LOADING' };
}
// If the status is passed: the token was claimed
return { image_url, currentState: 'CLAIMED' };
} else if (voted) {
// The token is not claimed but the address voted
return { image_url, currentState: 'UNCLAIMED' };
}
return { image_url, currentState: 'NOT_VOTED' };
}
async claim(snapshot, address) {
const body = {
snapshotProposalHash: snapshot,
address: address
};
const response = await fetch(`${API_BASE_URL}/claim/snapshot-proposal`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': Plugin.POAP_API_API_KEY
},
body: JSON.stringify(body)
});
if (!response.ok) {
// If the response is not ok: return the UNCLAIMED state
console.log(response.json());
return 'UNCLAIMED';
}
return 'LOADING';
}
}
================================================
FILE: src/plugins/poap/plugin.json
================================================
{
"name": "Poap Module",
"version": "1.0.0",
"author": "Poap-xyz",
"website": "https://github.com/snapshot-labs/snapshot/tree/master/src/plugins/poap",
"icon": "ipfs://QmSH2PsJUSpHUwS9XAoqLKRvF7qibrsqmBTdmM9Mmyv5fb"
}
================================================
FILE: src/plugins/progress/ProposalSidebar.vue
================================================
================================================
FILE: src/plugins/progress/components/CustomBlock.vue
================================================
{{ $t('progress.comeBack') }}
{{ $t('progress.voting') }}
{{ $t('progress.completed') }}
{{ completedDate() }}
{{ $t('progress.completed') }}
{{ $t('progress.inProgress') }}
{{ $t('progress.completed') }}
{{ formatDate(step.completedDate) }}
{{ $t('progress.completed') }}
{{ $t('progress.inProgress') }}
{{
$t('progress.complete')
}}
{{ $t('progress.soon') }}
{{ $t('progress.newStep') }}
{{ $t('progress.add') }}
{{ $t('progress.deleteStep') }}
{{ $t('progress.deleteConfirm') }}
{{ $t('progress.delete') }}
{{ $t('progress.cancel') }}
================================================
FILE: src/plugins/progress/index.ts
================================================
export default class Plugin {
public author = 'nick';
public version = '0.1.0';
public name = 'progess';
}
================================================
FILE: src/plugins/progress/plugin.json
================================================
{
"name": "Progress",
"version": "1.0.0",
"author": "Deadeye07",
"website": "https://github.com/Deadeye07/snapshot-develop/tree/master/src/plugins/progress",
"icon": "ipfs://Qmd1Zpmscf5HBvh5ouu6AUcDTst28CKYyrpfGnPCqd73gZ"
}
================================================
FILE: src/plugins/progress/readme.md
================================================
# Progress
## Keep Track of [Snapshot.org](http://Snapshot.org) Proposal Progress
Progress is a small and simple plugin for [Snapshot.org](http://snapshot.org) that allows Spaces to inform DAO members the progress of a proposal _after_ voting has completed - how a proposal is progressing in being executed. Often, finding this information requires monitoring notifications on Discord, Telegram and Twitter - what if we could just check back right at the source of the proposal on Snapshot? Yes please.
## Features
- Creation of New Steps
- Marking of steps as 'Completed'
- Deletion of Steps (Only allowed for steps yet to be 'Completed')
- Web3 Signature Authentication
- Cool loading spinners
## Who can add, complete, and delete steps?
Only a Proposals Author is able to view the Edit button, which displays options to do the following:
- Create new Steps
- Mark Steps as 'Complete'
- Delete Steps
================================================
FILE: src/plugins/projectGalaxy/ProposalSidebar.vue
================================================
================================================
FILE: src/plugins/projectGalaxy/README.md
================================================
# Galxe Plugin
Created time: July 21, 2022 5:51 PM
The **Galxe Plugin is designed for Snapshot.org. This will incentivize communities to participate in the proposal vote by rewarding them with OATs (On-Chain Achievement Tokens). Space Admin can add this plugin into your space. After users vote, they can claim an OAT directly from the Galxe Plugin section on the proposal page.**
### 1. Preparation
1. Create a Snapshot proposal on [https://snapshot.org/](https://snapshot.org/)
2. Create a Snapshot Credential with the proposal ID created from step1 on [https://galxe.com/](https://galxe.com/)
3. Create an OAT campaign ONLY with the credential created from step2 on [https://galxe.com/](https://galxe.com/)
### 2. Add Galxe Plugin to your space
In Snapshot space setting, you can search and find the Galxe Plugin in the plugins section, and add it to your space.
### 3. Set config JSON
Add campaign and proposal info to your plugin.
First, click the plugin, then you will see an editor section. See example below:
_Specific JSON format like this, and you could use more than 1 OAT in your space:_
```javascript
{
"oats": {
"": "/campaign/",
"": "/campaign/",
"": "/campaign/",
}
}
```
notice: The domain should not included in .
#### Usually you don't need to change api part unless you know which api you are using.
````javascript
{
"api": "https://graphigo.stg.galaxy.eco/query",
"oats": {
"0x554ca2bd7d979e8b72c6ae6415946a7bb470da9f60a9cf931205f083c03632a3": "jokey/campaign/GCixQUUqfE"
}
}
Second, fill it with the proposal ID and the information of the Campaign which you would like to link with the proposal. If you have multiple proposals which all distribute OATs to voters, you can also add multiple proposalID-campaignInfo pairs at one time.
**Notice:** if you delete a proposalID-campaignInfo pair, people won’t see the OAT information on the page of your proposal even if the proposal ends or the OATs have already been distributed.
*Here is a Demo:*
```javascript
{
"oats": {
"0x554ca2bd7d979e8b72c6ae6415946a7bb470da9f60a9cf931205f083c03632a3": "galaxy/campaign/GCcqvUtDaM"
}
}
````
### More
For Production Environment, we are using API Base Url https://graphigo.prd.galaxy.eco/query and only allows domain snapshot.org.
For Local Environment or any other test purpose, you can switch the API Base Url(_file index.ts_) to https://graphigo.stg.galaxy.eco/query and change port to 3000(_file package.json_).
When you see "Access-Control-Allow-Origin" error on console, please check if you have set the correct API Base Url and correct port, then restart the server.
================================================
FILE: src/plugins/projectGalaxy/components/CustomBlock.vue
================================================
================================================
FILE: src/plugins/projectGalaxy/index.ts
================================================
export default class Plugin {
API_BASE_URL = 'https://graphigo.prd.galaxy.eco/query';
constructor(apiBaseUrl) {
if (apiBaseUrl) {
this.API_BASE_URL = apiBaseUrl;
}
}
async fetchGQL({ query, variables }) {
return await fetch(this.API_BASE_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
query,
variables
})
});
}
async getOATImage(campaignId) {
const response = await this.fetchGQL({
query: `query getOATImage($id: ID!) {
campaign(id: $id) {
thumbnail
}
}`,
variables: {
id: campaignId
}
});
if (!response.ok) {
return '';
}
const resultJSON = await response.json();
const { thumbnail } = resultJSON.data.campaign;
return thumbnail;
}
async claim(address, campaignID) {
const eventResponse = await this.fetchGQL({
query: `mutation claimOAT($input: PrepareParticipateInput!) {
prepareParticipate(input: $input) {
allow
disallowReason
}
}`,
variables: {
input: {
signature: '',
campaignID,
address
}
}
});
if (!eventResponse.ok) {
return false;
}
const responseJSON = await eventResponse.json();
return responseJSON.data.prepareParticipate.allow;
}
async getCurrentState(snapshot, address, campaign) {
// Fetch the event
const eventResponse = await this.fetchGQL({
query: `query getCurrentState($input: SnapshotClaimStatusInput!) {
snapshotClaimStatus(input: $input) {
status
}
}`,
variables: {
input: {
address,
campaign,
proposalId: snapshot
}
}
});
// If the fetch fails: the event doesn't exists for this poap yet
if (!eventResponse.ok) {
return { currentState: 'NO_OAT' };
}
// Get the status from the event
const responseJSON = await eventResponse.json();
const responseStatus = responseJSON.data.snapshotClaimStatus.status;
// Check that the address is not empty
if (!address) {
return { currentState: 'VOTE_TO_CLAIM' };
}
return { currentState: responseStatus };
}
}
================================================
FILE: src/plugins/projectGalaxy/plugin.json
================================================
{
"name": "Galxe",
"version": "0.0.1",
"author": "Galxe",
"description": "Galxe OAT",
"website": "https://github.com/snapshot-labs/snapshot/tree/master/src/plugins/projectGalaxy",
"icon": "https://ipfs.io/ipfs/bafkreibjxoeegwucimd4hjfn3xmxrmkes3je5gwa723gaqosirzgewutfq",
"defaults": {
"space": {
"oats": {
"": "/campaign/"
}
}
}
}
================================================
FILE: src/plugins/quorum/examples.json
================================================
[
{
"strategy": "static",
"total": 1234
},
{
"strategy": "balance",
"address": "0x892f481BD6E9d7D26aE365211D9B45175d5D00e4",
"decimals": 18,
"quorumModifier": 0.5,
"methodABI": {
"inputs": [],
"name": "totalVotes",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
}
},
{
"strategy": "multichainBalance",
"network": "1",
"quorumModifier": 0.05,
"strategies": [
{
"network": "1",
"address": "0x0e42acBD23FAee03249DAFF896b78d7e79fBD58E",
"symbol": "veSTG",
"decimals": 18,
"methodABI": {
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
}
},
{
"network": "56",
"address": "0xD4888870C8686c748232719051b677791dBDa26D",
"symbol": "veSTG",
"decimals": 18,
"methodABI": {
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
}
},
{
"network": "43114",
"address": "0xCa0F57D295bbcE554DA2c07b005b7d6565a58fCE",
"symbol": "veSTG",
"decimals": 18,
"methodABI": {
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
}
}
]
}
]
================================================
FILE: src/plugins/quorum/plugin.json
================================================
{
"name": "Quorum",
"version": "0.1.0",
"author": "lbeder",
"website": "https://github.com/snapshot-labs/snapshot/tree/master/src/plugins/quorum",
"icon": "ipfs://Qmbyq2emXpjv1oFFJnhS3jL8aXZAqnya7zmNtKYXEs8jXa",
"defaults": {
"space": {
"strategy": "static",
"total": 1234
}
}
}
================================================
FILE: src/plugins/safeSnap/Create.vue
================================================
================================================
FILE: src/plugins/safeSnap/Proposal.vue
================================================
================================================
FILE: src/plugins/safeSnap/components/Config.vue
================================================
{{ $t('safeSnap.transactions') }}
View Details
================================================
FILE: src/plugins/safeSnap/components/Form/ContractInteraction.vue
================================================
{{ $t('safeSnap.value') }}
ABI
function
{{ method.name }}()
================================================
FILE: src/plugins/safeSnap/components/Form/ImportTransactionsButton.vue
================================================
================================================
FILE: src/plugins/safeSnap/components/Form/RawTransaction.vue
================================================
{{ $t('safeSnap.value') }}
{{ $t('safeSnap.data') }}
================================================
FILE: src/plugins/safeSnap/components/Form/SendAsset.vue
================================================
{{ $t('safeSnap.asset') }}
- {{ $t('safeSnap.noCollectibles') }} -
{{ collectable.name }} #{{ shorten(collectable.id, 10) }}
================================================
FILE: src/plugins/safeSnap/components/Form/TokensModal.vue
================================================
{{ $t('noResultsFound') }}
This token isn't known to us. Please make sure it is the correct address
before proceeding.
{{ $t('learnMore') }}
================================================
FILE: src/plugins/safeSnap/components/Form/TokensModalItem.vue
================================================
{{
formatNumber(
Number(token.balance),
getNumberFormatter({ maximumFractionDigits: 6 }).value
)
}}
{{ shorten(token.address) }}
================================================
FILE: src/plugins/safeSnap/components/Form/Transaction.vue
================================================
{{ $t('safeSnap.type') }}
{{ $t('safeSnap.transferFunds') }}
{{ $t('safeSnap.transferNFT') }}
{{ $t('safeSnap.contractInteraction') }}
{{ $t('safeSnap.rawTransaction') }}
================================================
FILE: src/plugins/safeSnap/components/Form/TransactionBatch.vue
================================================
Error: {{ safesnap.batchError.message }}
{{ $t('safeSnap.addTransaction') }}
================================================
FILE: src/plugins/safeSnap/components/Form/TransferFunds.vue
================================================
{{ $t('safeSnap.asset') }}
{{ selectedToken.symbol }}
{{
selectedToken.address === 'main'
? ''
: `(${shorten(selectedToken.address)})`
}}
================================================
FILE: src/plugins/safeSnap/components/HandleOutcome.vue
================================================
{{ $t('safeSnap.labels.error') }}
{{ $t('safeSnap.labels.connectWallet') }}
{{ $t('safeSnap.labels.request') }}
Reality oracle
{{
questionDetails?.finalizedAt
? $t('safeSnap.finalOutcome')
: $t('safeSnap.currentOutcome')
}}:
{{ approvalData?.decision }}
{{ $t('safeSnap.currentBond') }}:
{{ approvalData?.currentBond }}
{{ approvalData?.timeLeft }}
{{ $t('safeSnap.labels.setOutcome') }}
{{ $t('safeSnap.labels.changeOutcome') }}
{{ $t('safeSnap.claimBond') }}
{{
$t('safeSnap.labels.executeTxs', [
questionDetails.nextTxIndex + 1,
batches.length
])
}}
{{ $t('safeSnap.labels.switchChain', [networkName]) }}
{{ $t('safeSnap.labels.executed') }}
{{ $t('safeSnap.labels.rejected') }}
{{ $t('safeSnap.labels.expired') }}
================================================
FILE: src/plugins/safeSnap/components/HandleOutcomeUma.vue
================================================
{{ $t('safeSnap.labels.error') }}
{{ $t('safeSnap.labels.connectWallet') }}
Waiting on vote to be finalized
{{ $t('safeSnap.labels.confirmVoteResults') }}
{{ $t('safeSnap.labels.noTransactions') }}
{{ $t('safeSnap.labels.approveBond') }}
{{ $t('safeSnap.labels.approveBondToolTip') }}
{{ $t('safeSnap.labels.request') }}
{{ $t('safeSnap.labels.confirmVoteResultsToolTip') }}
{{
$t('safeSnap.labels.requiredBond')
}}
{{
formatUnits(
questionDetails.minimumBond ?? 0,
questionDetails.decimals
) +
' ' +
questionDetails.symbol
}}
{{
$t('safeSnap.labels.challengePeriod')
}}
{{ formatDuration(Number(questionDetails.livenessPeriod)) }}
{{ $t('safeSnap.labels.quorumWarning') }}
{{ $t('safeSnap.labels.bondWarning') }}
{{ $t('safeSnap.labels.request') }}
{{
'Proposal can be executed at ' +
new Date(
questionDetails.assertionEvent.expirationTimestamp.toNumber() *
1000
).toLocaleString()
}}
{{ $t('safeSnap.labels.executeTxsUma', [batches.length]) }}
{{ $t('safeSnap.labels.executeToolTip') }}
{{ $t('safeSnap.labels.switchChain', [networkName]) }}
{{ $t('safeSnap.labels.executed') }}
{{ $t('safeSnap.labels.rejected') }}
================================================
FILE: src/plugins/safeSnap/components/Input/Address.vue
================================================
{{ label }}
================================================
FILE: src/plugins/safeSnap/components/Input/Amount.vue
================================================
{{ label }}
================================================
FILE: src/plugins/safeSnap/components/Input/ArrayType.vue
================================================
{{ label }}
================================================
FILE: src/plugins/safeSnap/components/Input/MethodParameter.vue
================================================
{{ placeholder }}
true
false
{{ placeholder }}
================================================
FILE: src/plugins/safeSnap/components/Modal/OptionApproval.vue
================================================
SafeSnap
{{ $t('safeSnap.labels.question') }}
{{ $t('safeSnap.labels.criteria') }}
{{ $t('safeSnap.labels.proposalPassed') }}
{{ $t('safeSnap.currentOutcome') }}:
{{ answer }}
{{ $t('safeSnap.currentBond') }}:
{{ bondData.current + ' ' + tokenSymbol }}
{{ $t('safeSnap.nextBond') }}:
{{ bondData.toSet + ' ' + bondData.tokenSymbol }}
{{ $t('safeSnap.setOutcomeTo') }}
No
Yes
================================================
FILE: src/plugins/safeSnap/components/SafeTransactions.vue
================================================
{{ $t('safeSnap.addBatch') }}
================================================
FILE: src/plugins/safeSnap/components/Tooltip.vue
================================================
Multisend address
{{ shorten(multiSendAddress) }}
{{ moduleType === 'reality' ? 'Reality' : 'UMA' }} Module address
{{ shorten(moduleAddress) }}
================================================
FILE: src/plugins/safeSnap/constants.ts
================================================
import { MULTI_SEND_VERSION } from './utils/multiSend';
export const EIP712_TYPES = {
Transaction: [
{ name: 'to', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'data', type: 'bytes' },
{ name: 'operation', type: 'uint8' },
{ name: 'nonce', type: 'uint256' }
]
};
export const EIP3770_PREFIXES = {
1: 'eth',
5: 'gor',
56: 'bnb',
100: 'gno',
246: 'ewt',
73799: 'vt',
42161: 'arb1',
137: 'matic',
1116: 'core',
11155111: 'sep'
};
export const EXPLORER_API_URLS = {
'1': 'https://api.etherscan.io/api',
'5': 'https://api-goerli.etherscan.io/api',
'100': 'https://gnosis.blockscout.com/api',
'73799': 'https://volta-explorer.energyweb.org/api',
'246': 'https://explorer.energyweb.org/api',
'137': 'https://api.polygonscan.com/api',
'56': 'https://api.bscscan.com/api',
'42161': 'https://api.arbiscan.io/api',
// '1116': Add 'https://openapi.coredao.org/api' if API key requirement is removed
'11155111': 'https://api-sepolia.etherscan.io/api'
};
export const GNOSIS_SAFE_TRANSACTION_API_URLS = {
'1': 'https://safe-transaction-mainnet.safe.global/api',
'5': 'https://safe-transaction-goerli.safe.global/api',
'100': 'https://safe-transaction-gnosis-chain.safe.global/api',
'73799': 'https://safe-transaction-volta.safe.global/api',
'246': 'https://safe-transaction-ewc.safe.global/api',
'137': 'https://safe-transaction-polygon.safe.global/api',
'56': 'https://safe-transaction-bsc.safe.global/api',
'42161': 'https://safe-transaction-arbitrum.safe.global/api',
'1116': 'https://safetx.coredao.org/api',
'11155111': 'https://safe-transaction-sepolia.safe.global/api'
};
// ABIs
export const REALITY_MODULE_ABI = [
// Events
'event ProposalQuestionCreated(bytes32 indexed questionId, string indexed proposalId)',
// Read functions
'function avatar() view returns (address)', // Reality Module
'function executor() view returns (address)', // Dao Module
'function oracle() view returns (address)',
'function questionCooldown() view returns (uint32)',
'function answerExpiration() view returns (uint32)',
'function buildQuestion(string proposalId, bytes32[] txHashes) view returns (string)',
'function executedProposalTransactions(bytes32 questionHash, bytes32 txHash) view returns (bool)',
'function questionIds(bytes32 questionHash) view returns (bytes32)',
'function minimumBond() view returns (uint256)',
// Write functions
'function addProposal(string proposalId, bytes32[] txHashes)',
'function executeProposalWithIndex(string proposalId, bytes32[] txHashes, address to, uint256 value, bytes data, uint8 operation, uint256 txIndex)'
];
export const ORACLE_ABI = [
// Events
`event LogNewAnswer(
bytes32 answer,
bytes32 indexed question_id,
bytes32 history_hash,
address indexed user,
uint256 bond,
uint256 ts,
bool is_commitment
)`,
// Read functions
'function resultFor(bytes32 question_id) view returns (bytes32)',
'function getFinalizeTS(bytes32 question_id) view returns (uint32)',
'function getBond(bytes32 question_id) view returns (uint256)',
'function getBestAnswer(bytes32 question_id) view returns (uint32)',
'function balanceOf(address) view returns (uint256)',
'function getHistoryHash(bytes32 question_id) view returns (bytes32)',
'function isFinalized(bytes32 question_id) view returns (bool)',
'function token() view returns (address)',
// Write functions
'function submitAnswer(bytes32 question_id, bytes32 answer, uint256 max_previous) external payable',
'function submitAnswerERC20(bytes32 question_id, bytes32 answer, uint256 max_previous, uint256 tokens) external',
`function claimMultipleAndWithdrawBalance(
bytes32[] question_ids,
uint256[] lengths,
bytes32[] hist_hashes,
address[] addrs,
uint256[] bonds,
bytes32[] answers
) public`,
'function withdraw() public'
];
export const UMA_MODULE_ABI = [
'constructor(address _finder, address _owner, address _collateral, uint256 _bondAmount, string _rules, bytes32 _identifier, uint64 _liveness)',
'error NotIERC165Compliant(address guard_)',
'event AvatarSet(address indexed previousAvatar, address indexed newAvatar)',
'event ChangedGuard(address guard)',
'event Initialized(uint8 version)',
'event OptimisticGovernorDeployed(address indexed owner, address indexed avatar, address target)',
'event OptimisticOracleChanged(address indexed newOptimisticOracleV3)',
'event OwnershipTransferred(address indexed previousOwner, address indexed newOwner)',
'event ProposalDeleted(bytes32 indexed proposalHash, bytes32 indexed assertionId)',
'event ProposalExecuted(bytes32 indexed proposalHash, bytes32 indexed assertionId)',
'event SetCollateralAndBond(address indexed collateral, uint256 indexed bondAmount)',
'event SetEscalationManager(address indexed escalationManager)',
'event SetIdentifier(bytes32 indexed identifier)',
'event SetLiveness(uint64 indexed liveness)',
'event SetRules(string rules)',
'event TargetSet(address indexed previousTarget, address indexed newTarget)',
'event TransactionExecuted(bytes32 indexed proposalHash, bytes32 indexed assertionId, uint256 indexed transactionIndex)',
'event TransactionsProposed(address indexed proposer, uint256 indexed proposalTime, bytes32 indexed assertionId, tuple(tuple(address to, uint8 operation, uint256 value, bytes data)[] transactions, uint256 requestTime) proposal, bytes32 proposalHash, bytes explanation, string rules, uint256 challengeWindowEnds)',
'function EXPLANATION_KEY() view returns (bytes)',
'function PROPOSAL_HASH_KEY() view returns (bytes)',
'function RULES_KEY() view returns (bytes)',
'function assertionDisputedCallback(bytes32 assertionId)',
'function assertionIds(bytes32) view returns (bytes32)',
'function assertionResolvedCallback(bytes32 assertionId, bool assertedTruthfully)',
'function avatar() view returns (address)',
'function bondAmount() view returns (uint256)',
'function collateral() view returns (address)',
'function deleteProposalOnUpgrade(bytes32 proposalHash)',
'function escalationManager() view returns (address)',
'function executeProposal(tuple(address to, uint8 operation, uint256 value, bytes data)[] transactions)',
'function finder() view returns (address)',
'function getCurrentTime() view returns (uint256)',
'function getGuard() view returns (address _guard)',
'function getProposalBond() view returns (uint256)',
'function guard() view returns (address)',
'function identifier() view returns (bytes32)',
'function liveness() view returns (uint64)',
'function optimisticOracleV3() view returns (address)',
'function owner() view returns (address)',
'function proposalHashes(bytes32) view returns (bytes32)',
'function proposeTransactions(tuple(address to, uint8 operation, uint256 value, bytes data)[] transactions, bytes explanation)',
'function renounceOwnership()',
'function rules() view returns (string)',
'function setAvatar(address _avatar)',
'function setCollateralAndBond(address _collateral, uint256 _bondAmount)',
'function setEscalationManager(address _escalationManager)',
'function setGuard(address _guard)',
'function setIdentifier(bytes32 _identifier)',
'function setLiveness(uint64 _liveness)',
'function setRules(string _rules)',
'function setTarget(address _target)',
'function setUp(bytes initializeParams)',
'function sync()',
'function target() view returns (address)',
'function transferOwnership(address newOwner)'
];
export const UMA_ORACLE_ABI = [
'constructor(address _finder, address _defaultCurrency, uint64 _defaultLiveness)',
'event AdminPropertiesSet(address defaultCurrency, uint64 defaultLiveness, uint256 burnedBondPercentage)',
'event AssertionDisputed(bytes32 indexed assertionId, address indexed caller, address indexed disputer)',
'event AssertionMade(bytes32 indexed assertionId, bytes32 domainId, bytes claim, address indexed asserter, address callbackRecipient, address escalationManager, address caller, uint64 expirationTime, address currency, uint256 bond, bytes32 indexed identifier)',
'event AssertionSettled(bytes32 indexed assertionId, address indexed bondRecipient, bool disputed, bool settlementResolution, address settleCaller)',
'event OwnershipTransferred(address indexed previousOwner, address indexed newOwner)',
'function assertTruth(bytes claim, address asserter, address callbackRecipient, address escalationManager, uint64 liveness, address currency, uint256 bond, bytes32 identifier, bytes32 domainId) returns (bytes32 assertionId)',
'function assertTruthWithDefaults(bytes claim, address asserter) returns (bytes32)',
'function assertions(bytes32) view returns (tuple(bool arbitrateViaEscalationManager, bool discardOracle, bool validateDisputers, address assertingCaller, address escalationManager) escalationManagerSettings, address asserter, uint64 assertionTime, bool settled, address currency, uint64 expirationTime, bool settlementResolution, bytes32 domainId, bytes32 identifier, uint256 bond, address callbackRecipient, address disputer)',
'function burnedBondPercentage() view returns (uint256)',
'function cachedCurrencies(address) view returns (bool isWhitelisted, uint256 finalFee)',
'function cachedIdentifiers(bytes32) view returns (bool)',
'function cachedOracle() view returns (address)',
'function defaultCurrency() view returns (address)',
'function defaultIdentifier() view returns (bytes32)',
'function defaultLiveness() view returns (uint64)',
'function disputeAssertion(bytes32 assertionId, address disputer)',
'function finder() view returns (address)',
'function getAssertion(bytes32 assertionId) view returns (tuple(tuple(bool arbitrateViaEscalationManager, bool discardOracle, bool validateDisputers, address assertingCaller, address escalationManager) escalationManagerSettings, address asserter, uint64 assertionTime, bool settled, address currency, uint64 expirationTime, bool settlementResolution, bytes32 domainId, bytes32 identifier, uint256 bond, address callbackRecipient, address disputer))',
'function getAssertionResult(bytes32 assertionId) view returns (bool)',
'function getCurrentTime() view returns (uint256)',
'function getMinimumBond(address currency) view returns (uint256)',
'function multicall(bytes[] data) returns (bytes[] results)',
'function numericalTrue() view returns (int256)',
'function owner() view returns (address)',
'function renounceOwnership()',
'function setAdminProperties(address _defaultCurrency, uint64 _defaultLiveness, uint256 _burnedBondPercentage)',
'function settleAndGetAssertionResult(bytes32 assertionId) returns (bool)',
'function settleAssertion(bytes32 assertionId)',
'function stampAssertion(bytes32 assertionId) view returns (bytes)',
'function syncUmaParams(bytes32 identifier, address currency)',
'function transferOwnership(address newOwner)'
];
export const UMA_VOTING_ABI = [
'constructor(uint128 _emissionRate, uint64 _unstakeCoolDown, uint64 _phaseLength, uint32 _maxRolls, uint32 _maxRequestsPerRound, uint128 _gat, uint64 _spat, address _votingToken, address _finder, address _slashingLibrary, address _previousVotingContract)',
'event DelegateSet(address indexed delegator, address indexed delegate)',
'event DelegatorSet(address indexed delegate, address indexed delegator)',
'event EncryptedVote(address indexed caller, uint32 indexed roundId, bytes32 indexed identifier, uint256 time, bytes ancillaryData, bytes encryptedVote)',
'event ExecutedUnstake(address indexed voter, uint128 tokensSent, uint128 voterStake)',
'event GatAndSpatChanged(uint128 newGat, uint64 newSpat)',
'event MaxRequestsPerRoundChanged(uint32 newMaxRequestsPerRound)',
'event MaxRollsChanged(uint32 newMaxRolls)',
'event OwnershipTransferred(address indexed previousOwner, address indexed newOwner)',
'event RequestAdded(address indexed requester, uint32 indexed roundId, bytes32 indexed identifier, uint256 time, bytes ancillaryData, bool isGovernance)',
'event RequestDeleted(bytes32 indexed identifier, uint256 indexed time, bytes ancillaryData, uint32 rollCount)',
'event RequestResolved(uint32 indexed roundId, uint256 indexed resolvedPriceRequestIndex, bytes32 indexed identifier, uint256 time, bytes ancillaryData, int256 price)',
'event RequestRolled(bytes32 indexed identifier, uint256 indexed time, bytes ancillaryData, uint32 rollCount)',
'event RequestedUnstake(address indexed voter, uint128 amount, uint64 unstakeTime, uint128 voterStake)',
'event SetNewEmissionRate(uint128 newEmissionRate)',
'event SetNewUnstakeCoolDown(uint64 newUnstakeCoolDown)',
'event SlashingLibraryChanged(address newAddress)',
'event Staked(address indexed voter, address indexed from, uint128 amount, uint128 voterStake, uint128 voterPendingUnstake, uint128 cumulativeStake)',
'event UpdatedReward(address indexed voter, uint128 newReward, uint64 lastUpdateTime)',
'event VoteCommitted(address indexed voter, address indexed caller, uint32 roundId, bytes32 indexed identifier, uint256 time, bytes ancillaryData)',
'event VoteRevealed(address indexed voter, address indexed caller, uint32 roundId, bytes32 indexed identifier, uint256 time, bytes ancillaryData, int256 price, uint128 numTokens)',
'event VoterSlashApplied(address indexed voter, int128 slashedTokens, uint128 postStake)',
'event VoterSlashed(address indexed voter, uint256 indexed requestIndex, int128 slashedTokens)',
'event VotingContractMigrated(address newAddress)',
'event WithdrawnRewards(address indexed voter, address indexed delegate, uint128 tokensWithdrawn)',
'function ANCILLARY_BYTES_LIMIT() view returns (uint256)',
'function UINT64_MAX() view returns (uint64)',
'function commitAndEmitEncryptedVote(bytes32 identifier, uint256 time, bytes ancillaryData, bytes32 hash, bytes encryptedVote)',
'function commitVote(bytes32 identifier, uint256 time, bytes ancillaryData, bytes32 hash)',
'function cumulativeStake() view returns (uint128)',
'function currentActiveRequests() view returns (bool)',
'function delegateToStaker(address) view returns (address)',
'function emissionRate() view returns (uint128)',
'function executeUnstake()',
'function finder() view returns (address)',
'function gat() view returns (uint128)',
'function getCurrentRoundId() view returns (uint32)',
'function getCurrentTime() view returns (uint256)',
'function getNumberOfPriceRequests() view returns (uint256 numberPendingPriceRequests, uint256 numberResolvedPriceRequests)',
'function getNumberOfPriceRequestsPostUpdate() returns (uint256 numberPendingPriceRequests, uint256 numberResolvedPriceRequests)',
'function getPendingRequests() view returns (tuple(uint32 lastVotingRound, bool isGovernance, uint64 time, uint32 rollCount, bytes32 identifier, bytes ancillaryData)[])',
'function getPrice(bytes32 identifier, uint256 time, bytes ancillaryData) view returns (int256)',
'function getPrice(bytes32 identifier, uint256 time) view returns (int256)',
'function getPriceRequestStatuses(tuple(bytes32 identifier, uint256 time, bytes ancillaryData)[] requests) view returns (tuple(uint8 status, uint32 lastVotingRound)[])',
'function getRoundEndTime(uint256 roundId) view returns (uint256)',
'function getRoundIdToVoteOnRequest(uint32 targetRoundId) view returns (uint32)',
'function getVotePhase() view returns (uint8)',
'function getVoterFromDelegate(address caller) view returns (address)',
'function getVoterParticipation(uint256 requestIndex, uint32 lastVotingRound, address voter) view returns (uint8)',
'function getVoterPendingStake(address voter, uint32 roundId) view returns (uint128)',
'function getVoterStakePostUpdate(address voter) returns (uint128)',
'function hasPrice(bytes32 identifier, uint256 time) view returns (bool)',
'function hasPrice(bytes32 identifier, uint256 time, bytes ancillaryData) view returns (bool)',
'function lastRoundIdProcessed() view returns (uint32)',
'function lastUpdateTime() view returns (uint64)',
'function maxRequestsPerRound() view returns (uint32)',
'function maxRolls() view returns (uint32)',
'function migratedAddress() view returns (address)',
'function multicall(bytes[] data) returns (bytes[] results)',
'function nextPendingIndexToProcess() view returns (uint64)',
'function outstandingRewards(address voter) view returns (uint256)',
'function owner() view returns (address)',
'function pendingPriceRequestsIds(uint256) view returns (bytes32)',
'function previousVotingContract() view returns (address)',
'function priceRequests(bytes32) view returns (uint32 lastVotingRound, bool isGovernance, uint64 time, uint32 rollCount, bytes32 identifier, bytes ancillaryData)',
'function processResolvablePriceRequests()',
'function processResolvablePriceRequestsRange(uint64 maxTraversals)',
'function renounceOwnership()',
'function requestGovernanceAction(bytes32 identifier, uint256 time, bytes ancillaryData)',
'function requestPrice(bytes32 identifier, uint256 time, bytes ancillaryData)',
'function requestPrice(bytes32 identifier, uint256 time)',
'function requestSlashingTrackers(uint256 requestIndex) view returns (tuple(uint256 wrongVoteSlashPerToken, uint256 noVoteSlashPerToken, uint256 totalSlashed, uint256 totalCorrectVotes, uint32 lastVotingRound))',
'function requestUnstake(uint128 amount)',
'function resolvedPriceRequestIds(uint256) view returns (bytes32)',
'function retrieveRewardsOnMigratedVotingContract(address voter, uint256 roundId, tuple(bytes32 identifier, uint256 time, bytes ancillaryData)[] toRetrieve) returns (uint256)',
'function revealVote(bytes32 identifier, uint256 time, int256 price, bytes ancillaryData, int256 salt)',
'function rewardPerToken() view returns (uint256)',
'function rewardPerTokenStored() view returns (uint128)',
'function rounds(uint256) view returns (address slashingLibrary, uint128 minParticipationRequirement, uint128 minAgreementRequirement, uint128 cumulativeStakeAtRound, uint32 numberOfRequestsToVote)',
'function setDelegate(address delegate)',
'function setDelegator(address delegator)',
'function setEmissionRate(uint128 newEmissionRate)',
'function setGatAndSpat(uint128 newGat, uint64 newSpat)',
'function setMaxRequestPerRound(uint32 newMaxRequestsPerRound)',
'function setMaxRolls(uint32 newMaxRolls)',
'function setMigrated(address newVotingAddress)',
'function setSlashingLibrary(address _newSlashingLibrary)',
'function setUnstakeCoolDown(uint64 newUnstakeCoolDown)',
'function slashingLibrary() view returns (address)',
'function spat() view returns (uint64)',
'function stake(uint128 amount)',
'function stakeTo(address recipient, uint128 amount)',
'function transferOwnership(address newOwner)',
'function unstakeCoolDown() view returns (uint64)',
'function updateTrackers(address voter)',
'function updateTrackersRange(address voter, uint64 maxTraversals)',
'function voteTiming() view returns (uint256 phaseLength)',
'function voterStakes(address) view returns (uint128 stake, uint128 pendingUnstake, uint128 rewardsPaidPerToken, uint128 outstandingRewards, int128 unappliedSlash, uint64 nextIndexToProcess, uint64 unstakeTime, address delegate)',
'function votingToken() view returns (address)',
'function withdrawAndRestake() returns (uint128)',
'function withdrawRewards() returns (uint128)'
];
export const UMA_FINDER_ABI = [
'event InterfaceImplementationChanged(bytes32 indexed interfaceName, address indexed newImplementationAddress)',
'event OwnershipTransferred(address indexed previousOwner, address indexed newOwner)',
'function changeImplementationAddress(bytes32 interfaceName, address implementationAddress)',
'function getImplementationAddress(bytes32 interfaceName) view returns (address)',
'function interfacesImplemented(bytes32) view returns (address)',
'function owner() view returns (address)',
'function renounceOwnership()',
'function transferOwnership(address newOwner)'
];
export const ERC20_ABI = [
//Read functions
'function balanceOf(address account) view returns (uint256)',
'function decimals() view returns (uint32)',
'function symbol() view returns (string)',
'function allowance(address owner, address spender) external view returns (uint256)',
// Write functions
'function approve(address spender, uint256 value) external returns (bool)',
'function transfer(address recipient, uint256 amount) public virtual override returns (bool)'
];
export const ERC721_ABI = [
'function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable'
];
export const MULTI_SEND_ABI = [
'function multiSend(bytes transactions) payable'
];
// MULTI SEND CONSTANTS
export const MULTI_SEND_V1_3_0 = {
'1': '0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761',
'3': '0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761',
'10': '0x998739BFdAAdde7C933B942a68053933098f9EDa',
'28': '0x998739BFdAAdde7C933B942a68053933098f9EDa',
'42': '0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761',
'5': '0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761',
'56': '0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761',
'69': '0x998739BFdAAdde7C933B942a68053933098f9EDa',
'100': '0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761',
'122': '0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761',
'123': '0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761',
'137': '0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761',
'246': '0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761',
'288': '0x998739BFdAAdde7C933B942a68053933098f9EDa',
'588': '0x998739BFdAAdde7C933B942a68053933098f9EDa',
'1088': '0x998739BFdAAdde7C933B942a68053933098f9EDa',
'1116': '0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761',
'1285': '0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761',
'1287': '0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761',
'4002': '0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761',
'8453': '0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761',
'42161': '0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761',
'42220': '0x998739BFdAAdde7C933B942a68053933098f9EDa',
'43114': '0x998739BFdAAdde7C933B942a68053933098f9EDa',
'73799': '0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761',
'80001': '0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761',
'333999': '0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761',
'11155111': '0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761',
'1313161554': '0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761',
'1313161555': '0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761'
};
export const MULTI_SEND_V1_2_0 = {
'1': '0x6851D6fDFAfD08c0295C392436245E5bc78B0185',
'42': '0x6851D6fDFAfD08c0295C392436245E5bc78B0185',
'5': '0x6851D6fDFAfD08c0295C392436245E5bc78B0185',
'88': '0x6851D6fDFAfD08c0295C392436245E5bc78B0185',
'100': '0x6851D6fDFAfD08c0295C392436245E5bc78B0185',
'246': '0x6851D6fDFAfD08c0295C392436245E5bc78B0185',
'73799': '0x6851D6fDFAfD08c0295C392436245E5bc78B0185'
};
export const MULTI_SEND_V1_1_1 = {
'1': '0x8D29bE29923b68abfDD21e541b9374737B49cdAD',
'5': '0x8D29bE29923b68abfDD21e541b9374737B49cdAD',
'42': '0x8D29bE29923b68abfDD21e541b9374737B49cdAD',
'88': '0x8D29bE29923b68abfDD21e541b9374737B49cdAD',
'100': '0x8D29bE29923b68abfDD21e541b9374737B49cdAD',
'246': '0x8D29bE29923b68abfDD21e541b9374737B49cdAD',
'73799': '0x8D29bE29923b68abfDD21e541b9374737B49cdAD'
};
export const MULTI_SEND_VERSIONS: Record<
MULTI_SEND_VERSION,
Record
> = {
[MULTI_SEND_VERSION.V1_1_1]: MULTI_SEND_V1_1_1,
[MULTI_SEND_VERSION.V1_2_0]: MULTI_SEND_V1_2_0,
[MULTI_SEND_VERSION.V1_3_0]: MULTI_SEND_V1_3_0
};
// to potentially cut down on event ranges we query, hard code some deploy blocks for contracts
export type ContractData = {
network: string;
name: string;
address?: string;
deployBlock?: number;
subgraph?: string;
};
// contract addresses pulled from https://github.com/UMAprotocol/protocol/tree/master/packages/core/networks
export const contractData: ContractData[] = [
{
// mainnet
network: '1',
name: 'OptimisticOracleV3',
address: '0xfb55F43fB9F48F63f9269DB7Dde3BbBe1ebDC0dE',
subgraph:
'https://subgrapher.snapshot.org/subgraph/arbitrum/Bm3ytsa1YvcyFJahdfQQgscFQVCcMvoXujzkd3Cz6aof',
deployBlock: 16636058
},
{
// goerli
network: '5',
name: 'OptimisticOracleV3',
address: '0x9923D42eF695B5dd9911D05Ac944d4cAca3c4EAB',
subgraph:
'https://api.thegraph.com/subgraphs/name/md0x/goerli-optimistic-oracle-v3',
deployBlock: 8497481
},
{
// optimism
network: '10',
name: 'OptimisticOracleV3',
address: '0x072819Bb43B50E7A251c64411e7aA362ce82803B',
subgraph:
'https://subgrapher.snapshot.org/subgraph/arbitrum/FyJQyV5TLNeowZrL6kLUTB9JNPyWQNCNXJoxJWGEtBcn',
deployBlock: 74537234
},
{
// gnosis
network: '100',
name: 'OptimisticOracleV3',
address: '0x22A9AaAC9c3184f68C7B7C95b1300C4B1D2fB95C',
subgraph:
'https://subgrapher.snapshot.org/subgraph/arbitrum/9K2nctaB2rAh7Cgzx3wKtdHwWoEeEQ9AThGATak6Ngm9',
deployBlock: 27087150
},
{
// polygon
network: '137',
name: 'OptimisticOracleV3',
address: '0x5953f2538F613E05bAED8A5AeFa8e6622467AD3D',
subgraph:
'https://subgrapher.snapshot.org/subgraph/arbitrum/7KWbDhUE5Eqcfn3LXQtLbCfJLkNucnhzJLpi2jKhqNuf',
deployBlock: 39331673
},
{
//arbitrum
network: '42161',
name: 'OptimisticOracleV3',
address: '0xa6147867264374F324524E30C02C331cF28aa879',
subgraph:
'https://subgrapher.snapshot.org/subgraph/arbitrum/9wpkM5tHgJBHYTzKEKk4tK8a7q6MimfS9QnW7Japa8hW',
deployBlock: 61236565
},
{
// avalanche
network: '43114',
name: 'OptimisticOracleV3',
address: '0xa4199d73ae206d49c966cF16c58436851f87d47F',
subgraph:
'https://subgrapher.snapshot.org/subgraph/arbitrum/3k8gzGzTMV2vDZAGBFM2q642SUyVbE31bAUL8SjFQkre',
deployBlock: 27816737
},
{
// core
network: '43114',
name: 'OptimisticOracleV3',
address: '0xD84ACa67d683aF7702705141b3C7E57e4e5e7726',
subgraph:
'https://thegraph.coredao.org/subgraphs/name/umaprotocol/core-optimistic-oracle-v3',
deployBlock: 11341063
},
{
// base
network: '8453',
name: 'OptimisticOracleV3',
address: '0x2aBf1Bd76655de80eDB3086114315Eec75AF500c',
subgraph:
'https://subgrapher.snapshot.org/subgraph/arbitrum/2Q4i8YgVZd6bAmLyDxXgrKPL2B6QwySiEUqbTyQ4vm4C',
deployBlock: 12066343
},
{
// sepolia
network: '11155111',
name: 'OptimisticOracleV3',
address: '0xFd9e2642a170aDD10F53Ee14a93FcF2F31924944',
subgraph:
'https://subgrapher.snapshot.org/subgraph/arbitrum/78JbrMhcC9CVDZHDADvNcyhRrrccTJG4vCVBztyer1Xa',
deployBlock: 5421195
},
{
// mainnet
network: '1',
name: 'OptimisticGovernor',
address: '0x28CeBFE94a03DbCA9d17143e9d2Bd1155DC26D5d',
subgraph:
'https://subgrapher.snapshot.org/subgraph/arbitrum/DQpwhiRSPQJEuc8y6ZBGsFfNpfwFQ8NjmjLmfv8kBkLu',
deployBlock: 16890621
},
// Keep in mind, OG addresses are not the module addresses for each individual space, these addresses typically
// are not used, but are here for reference.
{
//goerli
network: '5',
name: 'OptimisticGovernor',
address: '0x07a7Be7AA4AaD42696A17e974486cb64A4daC47b',
deployBlock: 8700589,
subgraph:
'https://api.thegraph.com/subgraphs/name/md0x/goerli-optimistic-governor'
},
{
// optimism
network: '10',
name: 'OptimisticGovernor',
address: '0x357fe84E438B3150d2F68AB9167bdb8f881f3b9A',
deployBlock: 83168480,
subgraph:
'https://subgrapher.snapshot.org/subgraph/arbitrum/Fd5RvSfkajAJ8Mi9sPxFSMVPFf56SDivDQW3ocqTAW5'
},
{
// gnosis
network: '100',
name: 'OptimisticGovernor',
deployBlock: 27102135,
subgraph:
'https://subgrapher.snapshot.org/subgraph/arbitrum/RrkjZ6wTgLJkcjX68auzrEZHMRYwDx8kR5sFQQy4Phz'
},
{
// polygon
network: '137',
name: 'OptimisticGovernor',
address: '0x3Cc4b597E9c3f51288c6Cd0c087DC14c3FfdD966',
deployBlock: 40677035,
subgraph:
'https://subgrapher.snapshot.org/subgraph/arbitrum/7L2JM14PnZgxGnRX7xaz54zWS6KVK6ZqVRCxEKJrJTDG'
},
{
// arbitrum
network: '42161',
name: 'OptimisticGovernor',
address: '0x30679ca4ea452d3df8a6c255a806e08810321763',
deployBlock: 72850751,
subgraph:
'https://subgrapher.snapshot.org/subgraph/arbitrum/BfK867bnkQhnx1LspA99ypqiqxbAReQ92aZz66Ubv4tz'
},
{
// avalanche
network: '43114',
name: 'OptimisticGovernor',
address: '0xEF8b46765ae805537053C59f826C3aD61924Db45',
deployBlock: 28050250,
subgraph:
'https://subgrapher.snapshot.org/subgraph/arbitrum/5F8875fmvtnv8Vv4aeedUcwNWjuxUg54aTHdapFuMJi3'
},
{
// core
network: '1116',
name: 'OptimisticGovernor',
address: '0x596Fd6A5A185c67aBD1c845b39f593fBA9C233aa',
deployBlock: 11341122,
subgraph:
'https://thegraph.coredao.org/subgraphs/name/umaprotocol/core-optimistic-governor'
},
{
// base
network: '8453',
name: 'OptimisticGovernor',
address: '0x80bCA2E1c272239AdFDCdc87779BC8Af6E12e633',
deployBlock: 13062540,
subgraph:
'https://subgrapher.snapshot.org/subgraph/arbitrum/H1WyWZqh5pHebWRDCXs7GhvGj7XznSP7arPY6pYcCqLn'
},
{
// sepolia
network: '11155111',
name: 'OptimisticGovernor',
address: '0x40153DdFAd90C49dbE3F5c9F96f2a5B25ec67461',
deployBlock: 5421242,
subgraph:
'https://subgrapher.snapshot.org/subgraph/arbitrum/5pwrjCkpcpCd79k9MBS5yVgnsHQiw6afvXUfzqHjdRFw'
}
];
================================================
FILE: src/plugins/safeSnap/index.ts
================================================
import { Result } from '@ethersproject/abi';
import { isAddress } from '@ethersproject/address';
import { isHexString } from '@ethersproject/bytes';
import { toUtf8Bytes } from '@ethersproject/strings';
import { Contract } from '@ethersproject/contracts';
import { BigNumber } from '@ethersproject/bignumber';
import { _TypedDataEncoder } from '@ethersproject/hash';
import { StaticJsonRpcProvider } from '@ethersproject/providers';
import { keccak256 as solidityKeccak256 } from '@ethersproject/solidity';
import { isBigNumberish } from '@ethersproject/bignumber/lib/bignumber';
import snapshot from '@snapshot-labs/snapshot.js';
import getProvider from '@snapshot-labs/snapshot.js/src/utils/provider';
import {
SafeTransaction,
RealityOracleProposal,
UmaOracleProposal
} from '@/helpers/interfaces';
import {
EIP712_TYPES,
REALITY_MODULE_ABI,
UMA_MODULE_ABI,
ORACLE_ABI,
ERC20_ABI
} from './constants';
import {
buildQuestion,
checkPossibleExecution,
getModuleDetailsReality,
getProposalDetails
} from './utils/realityModule';
import { getModuleDetailsUma, getModuleDetailsUmaGql } from './utils/umaModule';
import { retrieveInfoFromOracle } from './utils/realityETH';
import { getNativeAsset } from '@/plugins/safeSnap/utils/coins';
import { Network } from './types';
export * from './constants';
export * from './utils/abi';
export * from './utils/safe';
export * from './utils/coins';
export * from './utils/index';
export * from './utils/decoder';
export * from './utils/multiSend';
export * from './utils/realityETH';
export * from './utils/transactions';
export * from './utils/realityModule';
const broviderUrl = import.meta.env.VITE_BROVIDER_URL;
export default class Plugin {
validateTransaction(transaction: SafeTransaction) {
const addressEmptyOrValidate =
transaction.to === '' || isAddress(transaction.to);
return (
isBigNumberish(transaction.value) &&
addressEmptyOrValidate &&
(!transaction.data || isHexString(transaction.data)) &&
['0', '1'].includes(transaction.operation) &&
isBigNumberish(transaction.nonce)
);
}
calcTransactionHash(
network: string,
moduleAddress: string,
transaction: SafeTransaction
) {
const chainId = parseInt(network);
const domain = {
chainId,
verifyingContract: moduleAddress
};
return _TypedDataEncoder.hash(domain, EIP712_TYPES, transaction);
}
calcTransactionHashes(
chainId: number,
moduleAddress: string,
transactions: SafeTransaction[]
) {
const domain = {
chainId: chainId,
verifyingContract: moduleAddress
};
return transactions.map(tx => {
return _TypedDataEncoder.hash(domain, EIP712_TYPES, {
...tx,
nonce: tx.nonce || '0',
data: tx.data || '0x'
});
});
}
async getExecutionDetailsWithHashes(
network: string,
moduleAddress: string,
proposalId: string,
txHashes: string[]
): Promise> {
const provider: StaticJsonRpcProvider = getProvider(network, {
broviderUrl
});
const question = await buildQuestion(proposalId, txHashes);
const questionHash = solidityKeccak256(['string'], [question]);
const proposalDetails = await getProposalDetails(
provider,
network,
moduleAddress,
questionHash,
txHashes
);
const moduleDetails = await getModuleDetailsReality(
provider,
network,
moduleAddress
);
const questionState = await checkPossibleExecution(
provider,
network,
moduleDetails.oracle,
proposalDetails.questionId
);
const infoFromOracle = await retrieveInfoFromOracle(
provider,
network,
moduleDetails.oracle,
proposalDetails.questionId
);
return {
...moduleDetails,
proposalId,
...questionState,
...proposalDetails,
txHashes,
...infoFromOracle
};
}
async getModuleDetailsReality(network: string, moduleAddress: string) {
const provider: StaticJsonRpcProvider = getProvider(network, {
broviderUrl
});
return getModuleDetailsReality(provider, network, moduleAddress);
}
async validateUmaModule(network: string, umaAddress: string) {
if (!isAddress(umaAddress)) return 'reality';
const provider: StaticJsonRpcProvider = getProvider(network, {
broviderUrl
});
const moduleContract = new Contract(umaAddress, UMA_MODULE_ABI, provider);
return moduleContract
.rules()
.then(() => 'uma')
.catch(() => 'reality');
}
async getExecutionDetailsUma(
network: Network,
moduleAddress: string,
proposalId: string,
explanation: string,
transactions: any
) {
const moduleDetails = await this.getModuleDetailsUma(
network,
moduleAddress,
explanation,
transactions
);
return {
...moduleDetails,
proposalId,
explanation
};
}
async *approveBondUma(
network: Network,
web3: any,
moduleAddress: string,
transactions?: any
) {
const moduleDetails = await this.getModuleDetailsUma(
network,
moduleAddress,
'',
transactions
);
const approveTx = await snapshot.utils.sendTransaction(
web3,
moduleDetails.collateral,
ERC20_ABI,
'approve',
[moduleAddress, moduleDetails.minimumBond],
{}
);
yield approveTx;
const approvalReceipt = await approveTx.wait();
console.log('[DAO module] token transfer approved:', approvalReceipt);
yield;
}
async getModuleDetailsUma(
network: Network,
moduleAddress: string,
explanation: string,
transactions: any
) {
const provider: StaticJsonRpcProvider = getProvider(network, {
broviderUrl
});
try {
// try optimized calls, which use the graph over web3 event queries
return await getModuleDetailsUmaGql(
provider,
network,
moduleAddress,
explanation,
transactions
);
} catch (err) {
console.warn('Error querying module details from the graph:', err);
// fall back to web3 event queries.
return getModuleDetailsUma(
provider,
network,
moduleAddress,
explanation,
transactions
);
}
}
async *submitProposalWithHashes(
web3: any,
moduleAddress: string,
proposalId: string,
txHashes: string[]
) {
const tx = await snapshot.utils.sendTransaction(
web3,
moduleAddress,
REALITY_MODULE_ABI,
'addProposal',
[proposalId, txHashes]
);
yield tx;
const receipt = await tx.wait();
console.log('[DAO module] submitted proposal:', receipt);
}
async *submitProposalUma(
web3: any,
moduleAddress: string,
explanation: string,
transactions: any
) {
const explanationBytes = toUtf8Bytes(explanation);
const tx = await snapshot.utils.sendTransaction(
web3,
moduleAddress,
UMA_MODULE_ABI,
'proposeTransactions',
[transactions, explanationBytes]
// [[["0xB8034521BB1a343D556e5005680B3F17FFc74BeD", 0, "0", "0x"]], '0x']
);
yield tx;
const receipt = await tx.wait();
console.log('[DAO module] submitted proposal:', receipt);
}
async loadClaimBondData(
web3: any,
network: Network,
questionId: string,
oracleAddress: string,
block: string
) {
const contract = new Contract(oracleAddress, ORACLE_ABI, web3);
const provider: StaticJsonRpcProvider = getProvider(network, {
broviderUrl
});
const account = (await web3.listAccounts())[0];
const [[userBalance], [bestAnswer], [historyHash], [isFinalized]] =
await snapshot.utils.multicall(network, provider, ORACLE_ABI, [
[oracleAddress, 'balanceOf', [account]],
[oracleAddress, 'getBestAnswer', [questionId]],
[oracleAddress, 'getHistoryHash', [questionId]],
[oracleAddress, 'isFinalized', [questionId]]
]);
const nativeToken = getNativeAsset(network);
let token = {
symbol: nativeToken.symbol,
decimals: nativeToken.decimals
};
try {
const tokenCall = await snapshot.utils.call(provider, ORACLE_ABI, [
oracleAddress,
'token',
[]
]);
const [[symbol], [decimals]] = await snapshot.utils.multicall(
network,
provider,
ERC20_ABI,
[
[tokenCall, 'symbol', []],
[tokenCall, 'decimals', []]
]
);
token = {
symbol,
decimals
};
} catch (e) {
console.log('[Realitio] Info: Oracle is not ERC20 based.');
}
const answersFilter = contract.filters.LogNewAnswer(null, questionId);
const events = await contract.queryFilter(answersFilter, parseInt(block));
const users: Result[] = [];
const historyHashes: Result[] = [];
const bonds: Result[] = [];
const answers: Result[] = [];
// We need to send the information from last to first
events.reverse();
events.forEach(({ args }) => {
users.push(args?.user.toLowerCase());
historyHashes.push(args?.history_hash);
bonds.push(args?.bond);
answers.push(args?.answer);
});
const alreadyClaimed = BigNumber.from(historyHash).eq(0);
const address = account.toLowerCase();
// Check if current user has submitted an answer
const currentUserAnswers = users.map((user, i) => {
if (user === address) return answers[i];
});
// If the user has answers, check if one of them is the winner
const votedForCorrectQuestion =
currentUserAnswers.some(answer => {
if (answer) {
return BigNumber.from(answer).eq(bestAnswer);
}
}) && isFinalized;
// If user has balance in the contract, he should be able to withdraw
const hasBalance = !userBalance.eq(0) && isFinalized;
// Remove the first history and add an empty one
// More info: https://github.com/realitio/realitio-contracts/blob/master/truffle/contracts/Realitio.sol#L502
historyHashes.shift();
const firstHash =
'0x0000000000000000000000000000000000000000000000000000000000000000' as unknown;
historyHashes.push(firstHash as Result);
return {
tokenSymbol: token.symbol,
tokenDecimals: token.decimals,
canClaim: (!alreadyClaimed && votedForCorrectQuestion) || hasBalance,
data: {
length: [bonds.length.toString()],
historyHashes,
users,
bonds,
answers
}
};
}
async *claimBond(
web3: any,
oracleAddress: string,
questionId: string,
claimParams: [string[], string[], number[], string[]]
) {
const currentHistoryHash = await snapshot.utils.call(web3, ORACLE_ABI, [
oracleAddress,
'getHistoryHash',
[questionId]
]);
if (BigNumber.from(currentHistoryHash).eq(0)) {
const withdrawTx = await snapshot.utils.sendTransaction(
web3,
oracleAddress,
ORACLE_ABI,
'withdraw',
[]
);
yield withdrawTx;
const withdrawReceipt = await withdrawTx.wait();
console.log('[Realitio] executed withdraw:', withdrawReceipt);
return;
}
const tx = await snapshot.utils.sendTransaction(
web3,
oracleAddress,
ORACLE_ABI,
'claimMultipleAndWithdrawBalance',
[[questionId], ...claimParams]
);
yield tx;
const receipt = await tx.wait();
console.log(
'[Realitio] executed claimMultipleAndWithdrawBalance:',
receipt
);
}
async *executeProposalWithHashes(
web3: any,
moduleAddress: string,
proposalId: string,
txHashes: string[],
moduleTx: SafeTransaction,
transactionIndex: number
) {
const tx = await snapshot.utils.sendTransaction(
web3,
moduleAddress,
REALITY_MODULE_ABI,
'executeProposalWithIndex',
[
proposalId,
txHashes,
moduleTx.to,
moduleTx.value,
moduleTx.data || '0x',
moduleTx.operation,
transactionIndex
]
);
yield tx;
const receipt = await tx.wait();
console.log('[DAO module] executed proposal:', receipt);
}
async *executeProposalUma(
web3: any,
moduleAddress: string,
transactions: any
) {
const tx = await snapshot.utils.sendTransaction(
web3,
moduleAddress,
UMA_MODULE_ABI,
'executeProposal',
[transactions]
);
yield tx;
const receipt = await tx.wait();
console.log('[DAO module] executed proposal:', receipt);
}
async *voteForQuestion(
network: string,
web3: any,
oracleAddress: string,
questionId: string,
minimumBondInDaoModule: string,
answer: '1' | '0'
) {
const currentBond = await snapshot.utils.call(web3, ORACLE_ABI, [
oracleAddress,
'getBond',
[questionId]
]);
let bond;
let methodName;
const txOverrides = {};
let parameters = [
questionId,
`0x000000000000000000000000000000000000000000000000000000000000000${answer}`
];
const currentBondIsZero = currentBond.eq(BigNumber.from(0));
if (currentBondIsZero) {
// DaoModules can have 0 minimumBond, if it happens, the initial bond will be 1 token
const daoBondIsZero = BigNumber.from(minimumBondInDaoModule).eq(0);
bond = daoBondIsZero ? BigNumber.from(10) : minimumBondInDaoModule;
} else {
bond = currentBond.mul(2);
}
// fetch token attribute from Realitio contract, if it works, it means it is
// a RealitioERC20, otherwise the catch will handle the currency as ETH
try {
const account = (await web3.listAccounts())[0];
const token = await snapshot.utils.call(web3, ORACLE_ABI, [
oracleAddress,
'token',
[]
]);
const [[tokenDecimals], [allowance]] = await snapshot.utils.multicall(
network,
web3,
ERC20_ABI,
[
[token, 'decimals', []],
[token, 'allowance', [account, oracleAddress]]
]
);
if (bond.eq(10)) {
bond = bond.pow(tokenDecimals);
}
// Check if contract has allowance on user tokens,
// if not, trigger approve method
if (allowance.lt(bond)) {
const approveTx = await snapshot.utils.sendTransaction(
web3,
token,
ERC20_ABI,
'approve',
[oracleAddress, bond],
{}
);
yield 'erc20-approval';
const approvalReceipt = await approveTx.wait();
console.log('[DAO module] token transfer approved:', approvalReceipt);
yield;
}
parameters = [...parameters, bond, bond];
methodName = 'submitAnswerERC20';
} catch (e) {
if (bond.eq(10)) {
bond = bond.pow(18);
}
parameters = [...parameters, bond];
txOverrides['value'] = bond.toString();
methodName = 'submitAnswer';
}
const tx = await snapshot.utils.sendTransaction(
web3,
oracleAddress,
ORACLE_ABI,
methodName,
parameters,
txOverrides
);
yield tx;
const receipt = await tx.wait();
console.log('[DAO module] executed vote on oracle:', receipt);
}
}
================================================
FILE: src/plugins/safeSnap/plugin.json
================================================
{
"name": "Gnosis SafeSnap",
"version": "1.0.0",
"author": "Gnosis",
"website": "https://safe.global",
"icon": "ipfs://QmQjZaheMTLRrk22mrGCkGk2LNoNqqYMR8Bxtwa8mBHyc9",
"defaults": {
"space": {
"safes": [
{
"network": "CHAIN_ID",
"realityAddress": "0xSWITCH_WITH_REALITY_MODULE_ADDRESS",
"umaAddress": "0xSWITCH_WITH_UMA_MODULE_ADDRESS"
}
]
},
"proposal": {
"safes": [
{
"network": "CHAIN_ID",
"realityAddress": "0xSWITCH_WITH_REALITY_MODULE_ADDRESS",
"umaAddress": "0xSWITCH_WITH_UMA_MODULE_ADDRESS"
}
]
}
}
}
================================================
FILE: src/plugins/safeSnap/types/index.ts
================================================
import networks from '@snapshot-labs/snapshot.js/src/networks.json';
type Networks = typeof networks;
export type Network = keyof Networks;
================================================
FILE: src/plugins/safeSnap/utils/abi.ts
================================================
import {
FormatTypes,
Fragment,
FunctionFragment,
Interface,
ParamType
} from '@ethersproject/abi';
import { BigNumberish } from '@ethersproject/bignumber';
import memoize from 'lodash/memoize';
import { ERC20_ABI, ERC721_ABI, EXPLORER_API_URLS } from '../constants';
import { ABI } from '@/helpers/interfaces';
import { mustBeEthereumAddress, mustBeEthereumContractAddress } from './index';
export function isArrayParameter(parameter: string): boolean {
return ['tuple', 'array'].includes(parameter);
}
const fetchContractABI = memoize(
async (url: string, contractAddress: string) => {
const params = new URLSearchParams({
module: 'contract',
action: 'getAbi',
address: contractAddress
});
const response = await fetch(`${url}?${params}`);
if (!response.ok) {
return { status: 0, result: '' };
}
return response.json();
},
(url, contractAddress) => `${url}_${contractAddress}`
);
export function parseMethodToABI(method: FunctionFragment) {
return [method.format(FormatTypes.full)];
}
export async function getContractABI(
network: string,
contractAddress: string
): Promise {
const apiUrl = EXPLORER_API_URLS[network as keyof typeof EXPLORER_API_URLS];
if (!apiUrl) {
return '';
}
const isEthereumAddress = mustBeEthereumAddress(contractAddress);
const isEthereumContractAddress = await mustBeEthereumContractAddress(
network,
contractAddress
);
if (!isEthereumAddress || !isEthereumContractAddress) {
return '';
}
try {
const { result, status } = await fetchContractABI(apiUrl, contractAddress);
if (status === '0') {
return '';
}
return result;
} catch (e) {
console.error('Failed to retrieve ABI', e);
return '';
}
}
export function isWriteFunction(method: FunctionFragment) {
if (!method.stateMutability) return true;
return !['view', 'pure'].includes(method.stateMutability);
}
export function getABIWriteFunctions(abi: Fragment[]) {
const abiInterface = new Interface(abi);
return (
abiInterface.fragments
// Return only contract's functions
.filter(FunctionFragment.isFunctionFragment)
.map(FunctionFragment.fromObject)
// Return only write functions
.filter(isWriteFunction)
// Sort by name
.sort((a, b) => (a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1))
);
}
function extractMethodArgs(values: string[]) {
return (param: ParamType, index: number) => {
const value = values[index];
if (isArrayParameter(param.baseType)) {
return JSON.parse(value);
}
return value;
};
}
export function getContractTransactionData(
abi: string,
method: FunctionFragment,
values: string[]
) {
const contractInterface = new Interface(abi);
const parameterValues = method.inputs.map(extractMethodArgs(values));
return contractInterface.encodeFunctionData(method, parameterValues);
}
export function getAbiFirstFunctionName(abi: ABI): string {
const abiInterface = new Interface(abi);
return abiInterface.fragments[0].name;
}
export function getERC20TokenTransferTransactionData(
recipientAddress: string,
amount: BigNumberish
): string {
const contractInterface = new Interface(ERC20_ABI);
return contractInterface.encodeFunctionData('transfer', [
recipientAddress,
amount
]);
}
export function getERC721TokenTransferTransactionData(
fromAddress: string,
recipientAddress: string,
id: BigNumberish
): string {
const contractInterface = new Interface(ERC721_ABI);
return contractInterface.encodeFunctionData('safeTransferFrom', [
fromAddress,
recipientAddress,
id
]);
}
================================================
FILE: src/plugins/safeSnap/utils/coins.ts
================================================
import { TokenAsset } from '@/helpers/interfaces';
import { Network } from '../types';
export const ETHEREUM_COIN: TokenAsset = {
name: 'Ether',
decimals: 18,
symbol: 'ETH',
logoUri:
'https://safe-transaction-assets.safe.global/chains/1/currency_logo.png',
address: 'main'
};
export const MATIC_COIN: TokenAsset = {
name: 'MATIC',
decimals: 18,
symbol: 'MATIC',
address: 'main',
logoUri:
'https://safe-transaction-assets.safe.global/chains/137/currency_logo.png'
};
const EWC_COIN: TokenAsset = {
name: 'Energy Web Token',
symbol: 'EWT',
address: 'main',
decimals: 18,
logoUri:
'https://safe-transaction-assets.safe.global/chains/246/currency_logo.png'
};
const XDAI_COIN: TokenAsset = {
name: 'XDAI',
symbol: 'XDAI',
address: 'main',
decimals: 18,
logoUri:
'https://safe-transaction-assets.safe.global/chains/100/currency_logo.png'
};
const BNB_COIN: TokenAsset = {
name: 'BNB',
symbol: 'BNB',
address: 'main',
decimals: 18,
logoUri:
'https://safe-transaction-assets.safe.global/chains/56/currency_logo.png'
};
const CORE_COIN: TokenAsset = {
name: 'Core',
symbol: 'CORE',
address: 'main',
decimals: 18,
logoUri:
'https://cloudflare-ipfs.com/ipfs/bafkreigjv5yb7uhlrryzib7j2f73nnwqan2tmfnwjdu26vkk365fyesoiu'
};
export function getNativeAsset(network: Network) {
switch (parseInt(network)) {
case 137:
case 80001:
return MATIC_COIN;
case 100:
return XDAI_COIN;
case 246:
return EWC_COIN;
case 56:
return BNB_COIN;
case 1116:
return CORE_COIN;
}
return ETHEREUM_COIN;
}
================================================
FILE: src/plugins/safeSnap/utils/decoder.ts
================================================
import {
FunctionFragment,
Interface,
Fragment,
JsonFragment
} from '@ethersproject/abi';
import { isArrayParameter } from './abi';
export class InterfaceDecoder extends Interface {
public decodeFunction(
data: string,
fragmentOrName?: string | Fragment | JsonFragment
) {
const fragment = this.getMethodFragment(data, fragmentOrName);
if (!FunctionFragment.isFunctionFragment(fragment)) {
throw new Error(
`could not resolved to a function fragment fragmentOrName: ${fragmentOrName}`
);
}
const functionFragment = FunctionFragment.fromObject(fragment);
const decodedValues = this.decodeFunctionData(functionFragment.name, data);
return functionFragment.inputs.reduce((acc, parameter, index) => {
const value = decodedValues[index];
const formattedValue = this.formatParameter(parameter, value);
acc.push(formattedValue);
if (parameter.name) {
acc[parameter.name] = formattedValue;
}
return acc;
}, [] as string[]);
}
public getMethodFragment(
data: string,
fragmentOrName?: string | Fragment | JsonFragment
): Fragment | JsonFragment {
if (typeof fragmentOrName === 'string') {
return this.getFunction(fragmentOrName);
} else if (!fragmentOrName) {
const signature = data.slice(0, 10);
return this.getFunction(signature);
}
return fragmentOrName;
}
private formatParameter(parameter, value, deep = 0): string {
if (isArrayParameter(parameter.baseType)) {
return this.formatArrayValue(parameter.arrayChildren, value, deep);
}
return value.toString();
}
private formatArrayValue(paramType, value, deep = 0) {
const formattedValues = value.map(paramValue =>
this.formatParameter(paramType, paramValue, deep + 1)
);
if (deep) return formattedValues;
return JSON.stringify(formattedValues);
}
}
================================================
FILE: src/plugins/safeSnap/utils/events.ts
================================================
import bb from 'bluebird';
// This state is meant for adjusting a start/end block when querying events. Some apis will fail if the range
// is too big, so the following functions will adjust range dynamically.
export type RangeState = {
startBlock: number;
endBlock: number;
maxRange: number;
currentRange: number;
currentStart: number; // This is the start value you want for your query.
currentEnd: number; // this is the end value you want for your query.
done: boolean; // Signals we successfully queried the entire range.
multiplier?: number; // Multiplier increases or decreases range by this value, depending on success or failure
};
/**
* rangeStart. This starts a new range query and sets defaults for state. Use this as the first call before starting your queries
*
* @param {Pick} state
* @returns {RangeState}
*/
export function rangeStart(
state: Pick & {
maxRange?: number;
}
): RangeState {
const { startBlock, endBlock, multiplier = 2 } = state;
if (state.maxRange && state.maxRange > 0) {
const range = endBlock - startBlock;
if (!(range >= 0)) {
throw new Error('End block must be higher than start block');
}
const currentRange = Math.min(state.maxRange, range);
const currentStart = endBlock - currentRange;
const currentEnd = endBlock;
return {
done: false,
startBlock,
endBlock,
maxRange: state.maxRange,
currentRange,
currentStart,
currentEnd,
multiplier
};
} else {
// the largest range we can have, since this is the users query for start and end
const maxRange = endBlock - startBlock;
if (!(maxRange > 0)) {
throw new Error('End block must be higher than start block');
}
const currentStart = startBlock;
const currentEnd = endBlock;
const currentRange = maxRange;
return {
done: false,
startBlock,
endBlock,
maxRange,
currentRange,
currentStart,
currentEnd,
multiplier
};
}
}
/**
* rangeSuccessDescending. We have 2 ways of querying events, from oldest to newest, or newest to oldest. Typically we want them in order, from
* oldest to newest, but for this particular case we want them newest to oldest, ie descending ( larger timestamp to smaller timestamp).
* This function will increase the range between start/end block and return a new start/end to use since by calling this you are signalling
* that the last range ended in a successful query.
*
* @param {RangeState} state
* @returns {RangeState}
*/
export function rangeSuccessDescending(state: RangeState): RangeState {
const {
startBlock,
currentStart,
maxRange,
currentRange,
multiplier = 2
} = state;
// we are done if we succeeded querying where the currentStart matches are initial start block
const done = currentStart <= startBlock;
// increase range up to max range for every successful query
const nextRange = Math.min(Math.ceil(currentRange * multiplier), maxRange);
// move our end point to the previously successful start, ie moving from newest to oldest
const nextEnd = currentStart;
// move our start block to the next range down
const nextStart = Math.max(nextEnd - nextRange, startBlock);
return {
...state,
currentStart: nextStart,
currentEnd: nextEnd,
currentRange: nextRange,
done
};
}
/**
* rangeFailureDescending. Like the previous function, this will decrease the range between start/end for your query, because you are signalling
* that the last query failed. It will also keep the end of your range the same, while moving the start range up. This is why
* its considered descending, it will attempt to move from end to start, rather than start to end.
*
* @param {RangeState} state
* @returns {RangeState}
*/
export function rangeFailureDescending(state: RangeState): RangeState {
const { startBlock, currentEnd, currentRange, multiplier = 2 } = state;
const nextRange = Math.floor(currentRange / multiplier);
// this will eventually throw an error if you keep calling this function, which protects us against re-querying a broken api in a loop
if (currentRange <= 0) throw new Error('Current range must be above 0');
if (!(nextRange > 0)) throw new Error('Range must be above 0');
// we stay at the same end block
const nextEnd = currentEnd;
// move our start block closer to the end block, shrinking the range
const nextStart = Math.max(nextEnd - nextRange, startBlock);
return {
...state,
currentStart: nextStart,
currentEnd: nextEnd,
currentRange: nextRange
};
}
// The main interface to wrap the above pure functions up. requires you to pass in a generic function
// which returns the events based on a start/end block query.
export async function pageEvents(
startBlock: number,
endBlock: number,
maxRange: number,
//start and end block range to query
fetchEvents: (query: { start: number; end: number }) => Promise,
concurrency: number = 5
): Promise {
let state = rangeStart({ startBlock, endBlock, maxRange });
const ranges: { start: number; end: number }[] = [];
let index = 0;
do {
ranges.push({
start: state.currentStart,
end: state.currentEnd,
index: index++
});
state = rangeSuccessDescending(state);
} while (!state.done);
return (await bb.map(ranges, fetchEvents, { concurrency })).flat();
}
================================================
FILE: src/plugins/safeSnap/utils/index.ts
================================================
import { isAddress } from '@ethersproject/address';
import { JsonRpcProvider } from '@ethersproject/providers';
import { keccak256 } from '@ethersproject/solidity';
import memoize from 'lodash/memoize';
import { SafeExecutionData, SafeTransaction } from '@/helpers/interfaces';
import getProvider from '@snapshot-labs/snapshot.js/src/utils/provider';
import SafeSnapPlugin, { MULTI_SEND_VERSION } from '../index';
import { createMultiSendTx, getMultiSend } from './multiSend';
export const mustBeEthereumAddress = memoize((address: string) => {
const startsWith0x = address?.startsWith('0x');
const isValidAddress = isAddress(address);
return startsWith0x && isValidAddress;
});
export const mustBeEthereumContractAddress = memoize(
async (network: string, address: string) => {
const broviderUrl = import.meta.env.VITE_BROVIDER_URL;
const provider = getProvider(network, { broviderUrl }) as JsonRpcProvider;
const contractCode = await provider.getCode(address);
return (
contractCode && contractCode.replace(/^0x/, '').replace(/0/g, '') !== ''
);
},
(url, contractAddress) => `${url}_${contractAddress}`
);
export function formatBatchTransaction(
batch: SafeTransaction[],
nonce: number,
multiSendAddress: string
): SafeTransaction | null {
if (!batch.every(x => x)) return null;
if (batch.length === 1) {
return { ...batch[0], nonce: nonce.toString() };
}
return createMultiSendTx(batch, nonce, multiSendAddress);
}
export function createBatch(
module: string,
chainId: number,
nonce: number,
txs: SafeTransaction[],
multiSendAddress: string
) {
const mainTransaction = formatBatchTransaction(txs, nonce, multiSendAddress);
const hash = mainTransaction
? getBatchHash(module, chainId, mainTransaction)
: null;
return {
hash,
nonce,
mainTransaction,
transactions: txs
};
}
export function getBatchHash(
module: string,
chainId: number,
transaction: SafeTransaction
) {
try {
const safeSnap = new SafeSnapPlugin();
const hashes = safeSnap.calcTransactionHashes(chainId, module, [
transaction
]);
return hashes[0];
} catch (err) {
console.warn('invalid batch hash', err);
return null;
}
}
export function getSafeHash(safe: SafeExecutionData) {
const hashes = safe.txs.map(batch => batch.hash);
const valid = hashes.every(hash => hash);
if (!valid || !hashes.length) return null;
return keccak256(['bytes32[]'], [hashes]);
}
export function validateSafeData(safe: SafeExecutionData) {
return (
safe.txs.length === 0 ||
safe.txs
.map(batch => batch.transactions)
.flat()
.every(tx => tx)
);
}
export function isValidInput (
input: Input
) {
return input.safes.every(validateSafeData);
}
export function coerceConfig(config, network) {
if (config.safes) {
return {
...config,
safes: config.safes.map(safe => {
const _network = safe.network || network;
const multiSendAddress =
safe.multiSendAddress || getMultiSend(_network);
const txs = (safe.txs || []).map((batch, nonce) => {
const oldMultiSendAddress =
safe.multiSendAddress ||
getMultiSend(_network, MULTI_SEND_VERSION.V1_1_1) ||
getMultiSend(_network, MULTI_SEND_VERSION.V1_3_0);
if (Array.isArray(batch)) {
// Assume old config
return createBatch(
safe.realityAddress,
_network,
nonce,
batch,
oldMultiSendAddress
);
}
if (!batch.mainTransaction) {
return {
...batch,
mainTransaction: formatBatchTransaction(
batch.transactions,
batch.nonce,
oldMultiSendAddress
)
};
}
return batch;
});
const sanitizedSafe = {
...safe,
txs,
multiSendAddress
};
return {
...sanitizedSafe,
hash: sanitizedSafe.hash ?? getSafeHash(sanitizedSafe)
};
})
};
}
// map legacy config to new format
return {
safes: [
{
network,
realityAddress: config.address,
multiSendAddress:
getMultiSend(network, MULTI_SEND_VERSION.V1_1_1) ||
getMultiSend(network, MULTI_SEND_VERSION.V1_3_0)
}
]
};
}
export async function fetchTextSignatures(
methodSignature: string
): Promise {
const url = new URL('/api/v1/signatures', 'https://www.4byte.directory');
url.searchParams.set('hex_signature', methodSignature);
url.searchParams.set('ordering', 'created_at');
const response = await fetch(url.toString());
const { results } = await response.json();
return results.map(signature => signature.text_signature);
}
================================================
FILE: src/plugins/safeSnap/utils/multiSend.ts
================================================
import { pack } from '@ethersproject/solidity';
import { Interface } from '@ethersproject/abi';
import { hexDataLength } from '@ethersproject/bytes';
import { SafeTransaction } from '@/helpers/interfaces';
import { MULTI_SEND_ABI, MULTI_SEND_VERSIONS } from '../constants';
export enum MULTI_SEND_VERSION {
V1_3_0 = '1.3.0',
V1_2_0 = '1.2.0',
V1_1_1 = '1.1.1'
}
export function getMultiSend(
network: number | string,
version: MULTI_SEND_VERSION = MULTI_SEND_VERSION.V1_3_0
) {
return MULTI_SEND_VERSIONS[version][network.toString()];
}
export function encodeTransactions(transactions: SafeTransaction[]) {
const values = transactions.map(tx => [
tx.operation,
tx.to,
tx.value,
hexDataLength(tx.data || '0x'),
tx.data || '0x'
]);
const types = transactions.map(() => [
'uint8',
'address',
'uint256',
'uint256',
'bytes'
]);
return pack(types.flat(1), values.flat(1));
}
export function createMultiSendTx(
txs: SafeTransaction[],
nonce: number,
multiSendAddress: string
) {
const multiSendContract = new Interface(MULTI_SEND_ABI);
const transactionsEncoded = encodeTransactions(txs);
const data = multiSendContract.encodeFunctionData('multiSend', [
transactionsEncoded
]);
return {
to: multiSendAddress,
operation: '1',
value: '0',
nonce: nonce.toString(),
data
};
}
================================================
FILE: src/plugins/safeSnap/utils/realityETH.ts
================================================
import { StaticJsonRpcProvider } from '@ethersproject/providers';
import { BigNumber } from '@ethersproject/bignumber';
import snapshot from '@snapshot-labs/snapshot.js';
import { ORACLE_ABI } from '../constants';
export const retrieveInfoFromOracle = async (
provider: StaticJsonRpcProvider,
network: string,
oracleAddress: string,
questionId: string | undefined
): Promise<{
currentBond: BigNumber | undefined;
isApproved: boolean;
endTime: number | undefined;
}> => {
if (questionId) {
const result = await snapshot.utils.multicall(network, provider, ORACLE_ABI, [
[oracleAddress, 'getFinalizeTS', [questionId]],
[oracleAddress, 'getBond', [questionId]],
[oracleAddress, 'getBestAnswer', [questionId]]
]);
const currentBond = BigNumber.from(result[1][0]);
const answer = BigNumber.from(result[2][0]);
return {
currentBond,
isApproved: answer.eq(BigNumber.from(1)),
endTime: BigNumber.from(result[0][0]).toNumber()
};
}
return {
currentBond: undefined,
isApproved: false,
endTime: undefined
};
};
================================================
FILE: src/plugins/safeSnap/utils/realityModule.ts
================================================
import { StaticJsonRpcProvider } from '@ethersproject/providers';
import snapshot from '@snapshot-labs/snapshot.js';
import { REALITY_MODULE_ABI, ORACLE_ABI } from '../constants';
import { HashZero } from '@ethersproject/constants';
import { BigNumber } from '@ethersproject/bignumber';
import { keccak256 as solidityKeccak256 } from '@ethersproject/solidity';
export const buildQuestion = async (proposalId: string, txHashes: string[]) => {
const hashesHash = solidityKeccak256(['bytes32[]'], [txHashes]).slice(2);
return `${proposalId}␟${hashesHash}`;
};
export const getProposalDetails = async (
provider: StaticJsonRpcProvider,
network: string,
moduleAddress: string,
questionHash: string,
txHashes: string[]
): Promise<{ questionId: string; nextTxIndex: number | undefined }> => {
const proposalInfo = (
await snapshot.utils.multicall(
network,
provider,
REALITY_MODULE_ABI,
[[moduleAddress, 'questionIds', [questionHash]]].concat(
txHashes.map(txHash => [
moduleAddress,
'executedProposalTransactions',
[questionHash, txHash]
])
)
)
).map(res => res[0]);
const questionId = proposalInfo[0];
// We need to offset the index by -1 the first element is the questionId
const nextIndexToExecute = proposalInfo.indexOf(false, 1) - 1;
return {
questionId: questionId !== HashZero ? questionId : undefined,
nextTxIndex:
nextIndexToExecute < 0 || nextIndexToExecute >= txHashes.length
? undefined
: nextIndexToExecute
};
};
export const getModuleDetailsReality = async (
provider: StaticJsonRpcProvider,
network: string,
moduleAddress: string
): Promise<{
dao: string;
oracle: string;
cooldown: number;
minimumBond: number;
expiration: number;
}> => {
let moduleDetails;
try {
// Assume module is Reality Module
moduleDetails = await snapshot.utils.multicall(network, provider, REALITY_MODULE_ABI, [
[moduleAddress, 'avatar'],
[moduleAddress, 'oracle'],
[moduleAddress, 'questionCooldown'],
[moduleAddress, 'minimumBond'],
[moduleAddress, 'answerExpiration']
]);
} catch (err) {
// The Reality Module doesn't have an avatar field, causing tx to fails.
// Assume module is Dao Module (old version)
moduleDetails = await snapshot.utils.multicall(network, provider, REALITY_MODULE_ABI, [
[moduleAddress, 'executor'],
[moduleAddress, 'oracle'],
[moduleAddress, 'questionCooldown'],
[moduleAddress, 'minimumBond'],
[moduleAddress, 'answerExpiration']
]);
}
return {
dao: moduleDetails[0][0],
oracle: moduleDetails[1][0],
cooldown: moduleDetails[2][0],
minimumBond: moduleDetails[3][0],
expiration: moduleDetails[4][0]
};
};
export const checkPossibleExecution = async (
provider: StaticJsonRpcProvider,
network: string,
oracleAddress: string,
questionId: string | undefined
): Promise<{
executionApproved: boolean;
finalizedAt: number | undefined;
}> => {
if (questionId) {
try {
const result = await snapshot.utils.multicall(network, provider, ORACLE_ABI, [
[oracleAddress, 'resultFor', [questionId]],
[oracleAddress, 'getFinalizeTS', [questionId]]
]);
return {
executionApproved: BigNumber.from(result[0][0]).eq(BigNumber.from(1)),
finalizedAt: BigNumber.from(result[1][0]).toNumber()
};
} catch (e) {
// We expect an error while the question is not answered yet
}
}
return {
executionApproved: false,
finalizedAt: undefined
};
};
================================================
FILE: src/plugins/safeSnap/utils/safe.ts
================================================
import { GNOSIS_SAFE_TRANSACTION_API_URLS } from '../constants';
import { TokenAsset } from '@/helpers/interfaces';
import memoize from 'lodash/memoize';
async function callGnosisSafeTransactionApi(
network: keyof typeof GNOSIS_SAFE_TRANSACTION_API_URLS,
url: string
) {
const apiUrl = GNOSIS_SAFE_TRANSACTION_API_URLS[network];
const response = await fetch(apiUrl + url);
return response.json();
}
export const getGnosisSafeBalances = memoize(
(network, safeAddress) => {
const endpointPath = `/v1/safes/${safeAddress}/balances/`;
return callGnosisSafeTransactionApi(network, endpointPath);
},
(safeAddress, network) => `${safeAddress}_${network}`
);
export const getGnosisSafeCollectibles = memoize(
(network, safeAddress) => {
const endpointPath = `/v2/safes/${safeAddress}/collectibles/`;
return callGnosisSafeTransactionApi(network, endpointPath);
},
(safeAddress, network) => `${safeAddress}_${network}`
);
export const getGnosisSafeToken = memoize(
async (network, tokenAddress): Promise => {
const endpointPath = `/v1/tokens/${tokenAddress}`;
return callGnosisSafeTransactionApi(network, endpointPath);
},
(tokenAddress, network) => `${tokenAddress}_${network}`
);
================================================
FILE: src/plugins/safeSnap/utils/transactions.ts
================================================
import {
CollectableAsset,
CollectableAssetTransaction,
CustomContractTransaction,
SafeTransaction,
TokenAsset,
TokenAssetTransaction
} from '@/helpers/interfaces';
import { Fragment, FunctionFragment, JsonFragment } from '@ethersproject/abi';
import { BigNumber } from '@ethersproject/bignumber';
import { isHexString } from '@ethersproject/bytes';
import { ERC20_ABI, ERC721_ABI } from '../constants';
import { getContractABI, parseMethodToABI } from './abi';
import { getNativeAsset } from './coins';
import { InterfaceDecoder } from './decoder';
import { fetchTextSignatures } from './index';
import { getGnosisSafeToken } from './safe';
import { Network } from '../types';
export function rawToModuleTransaction({
to,
value,
data,
nonce
}: {
to: string;
value: string;
data: string;
nonce: string;
}): SafeTransaction {
return {
to,
value,
data,
nonce,
operation: '0'
};
}
export function sendAssetToModuleTransaction({
recipient,
collectable,
data,
nonce
}: {
recipient: string;
collectable: CollectableAsset;
data: string;
nonce: string;
}): CollectableAssetTransaction {
return {
data,
nonce,
recipient,
value: '0',
operation: '0',
type: 'transferNFT',
to: collectable.address,
collectable
};
}
export function transferFundsToModuleTransaction({
recipient,
amount,
token,
data,
nonce
}: {
recipient: string;
amount: string;
token: TokenAsset;
data: string;
nonce: string;
}): TokenAssetTransaction {
const base = {
operation: '0',
nonce,
token,
recipient
};
if (token.address === 'main') {
return {
...base,
type: 'transferFunds',
data: '0x',
to: recipient,
amount: parseAmount(amount),
value: parseAmount(amount)
};
}
return {
...base,
data,
type: 'transferFunds',
to: token.address,
amount: parseAmount(amount),
value: '0'
};
}
export function contractInteractionToModuleTransaction(
{
to,
value,
data,
nonce,
method
}: {
to: string;
value: string;
data: string;
nonce: string;
method: FunctionFragment;
},
multiSendAddress: string
): CustomContractTransaction {
const operation = to === multiSendAddress ? '1' : '0';
return {
to,
data,
nonce,
operation,
type: 'contractInteraction',
value: parseValueInput(value),
abi: parseMethodToABI(method)
};
}
export async function decodeContractTransaction(
network: string,
transaction: SafeTransaction,
multiSendAddress: string
): Promise {
const decode = (abi: string | FunctionFragment[]) => {
const contractInterface = new InterfaceDecoder(abi);
const method = contractInterface.getMethodFragment(transaction.data);
contractInterface.decodeFunction(transaction.data, method); // Validate data can be decode by method.
return contractInteractionToModuleTransaction(
{
data: transaction.data,
nonce: '0',
to: transaction.to,
value: transaction.value,
method: method as FunctionFragment
},
multiSendAddress
);
};
const contractAbi = await getContractABI(network, transaction.to);
if (contractAbi) return decode(contractAbi);
const methodSignature = getMethodSignature(transaction.data);
if (methodSignature) {
const textSignatures = await fetchTextSignatures(methodSignature);
for (const signature of textSignatures) {
try {
return decode([FunctionFragment.fromString(signature)]);
} catch (e) {
console.warn('invalid abi for transaction');
}
}
}
throw new Error(`we were not able to decode this transaction`);
}
function getMethodSignature(data: string) {
const methodSignature = data.slice(0, 10);
if (isHexString(methodSignature) && methodSignature.length === 10) {
return methodSignature;
}
return null;
}
export function isERC20TransferTransaction(transaction: SafeTransaction) {
// 0xa9059cbb == transfer(address to, uint256 amount)
return getMethodSignature(transaction.data) === '0xa9059cbb';
}
function decodeERC721TransferTransaction(transaction: SafeTransaction) {
const erc721ContractInterface = new InterfaceDecoder(ERC721_ABI);
try {
return erc721ContractInterface.decodeFunction(transaction.data);
} catch (e) {
return null;
}
}
export async function decodeTransactionData(
network: Network,
transaction: SafeTransaction,
multiSendAddress: string
) {
if (!transaction.data || transaction.data === '0x') {
return transferFundsToModuleTransaction({
recipient: transaction.to,
amount: transaction.value,
data: '0x',
token: getNativeAsset(network),
nonce: '0'
});
}
if (isERC20TransferTransaction(transaction)) {
try {
const erc20ContractInterface = new InterfaceDecoder(ERC20_ABI);
const params = erc20ContractInterface.decodeFunction(transaction.data);
const token = await getGnosisSafeToken(network, transaction.to);
return transferFundsToModuleTransaction({
recipient: params[0],
amount: params[1],
data: transaction.data,
nonce: '0',
token
});
} catch (e) {
console.warn('invalid ERC20 transfer transaction');
}
}
const erc721DecodedParams = decodeERC721TransferTransaction(transaction);
if (erc721DecodedParams) {
const collectable: CollectableAsset = {
id: erc721DecodedParams[2],
address: transaction.to,
name: 'Unknown'
};
return sendAssetToModuleTransaction({
collectable,
nonce: '0',
data: transaction.data,
recipient: erc721DecodedParams[1]
});
}
return decodeContractTransaction(network, transaction, multiSendAddress);
}
export function parseAmount(input: string) {
return BigNumber.from(input).toString();
}
export function parseValueInput(input: string) {
try {
return parseAmount(input);
} catch (e) {
return input;
}
}
================================================
FILE: src/plugins/safeSnap/utils/umaModule.ts
================================================
import gql from 'graphql-tag';
import { defaultAbiCoder } from '@ethersproject/abi';
import { BigNumber } from '@ethersproject/bignumber';
import { Contract } from '@ethersproject/contracts';
import { keccak256 } from '@ethersproject/keccak256';
import { StaticJsonRpcProvider } from '@ethersproject/providers';
import { pack } from '@ethersproject/solidity';
import { toUtf8Bytes, toUtf8String } from '@ethersproject/strings';
import snapshot from '@snapshot-labs/snapshot.js';
import {
ERC20_ABI,
UMA_MODULE_ABI,
UMA_ORACLE_ABI,
contractData
} from '../constants';
import { pageEvents } from './events';
import filter from 'lodash/filter';
function getDeployBlock(network: string, name: string): number {
const data = filter(contractData, { network, name });
if (data.length === 1) return data[0].deployBlock ?? 0;
return 0;
}
function getContractSubgraph(search: {
network: string;
name: string;
}): string {
const results = filter(contractData, search);
if (results.length > 1)
throw new Error(
`Too many results finding ${search.name} subgraph on network ${search.network}`
);
if (results.length < 1)
throw new Error(
`No results finding ${search.name} subgraph on network ${search.network}`
);
if (!results[0].subgraph)
throw new Error(
`No subgraph url defined for ${search.name} on network ${search.network}`
);
return results[0].subgraph;
}
function getOptimisticGovernorSubgraph(network: string): string {
return getContractSubgraph({ network, name: 'OptimisticGovernor' });
}
function getOracleV3Subgraph(network: string): string {
return getContractSubgraph({ network, name: 'OptimisticOracleV3' });
}
export const queryGql = async (url: string, query: string) => {
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json'
},
body: JSON.stringify({ query: query })
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(
`Network Error: ${response.status}, message: ${errorData.message}`
);
}
const data = await response.json();
// Throw an error if there are errors in the GraphQL response
if (data.errors) {
throw new Error(
`GraphQL Error: ${data.errors.map(error => error.message).join(', ')}`
);
}
return data.data;
} catch (error) {
throw new Error(`Network error: ${error.message}`);
}
};
const findAssertionGql = async (
network: string,
params: { assertionId: string }
) => {
const oracleUrl = getOracleV3Subgraph(network);
const request = `
{
assertion(id:"${params.assertionId}"){
assertionId
expirationTime
assertionHash
assertionLogIndex
settlementHash
}
}
`;
const result = await queryGql(oracleUrl, request);
return result?.assertion;
};
// Search optimistic governor for individual proposal
const findProposalGql = async (
network: string,
params: { proposalHash; explanation; ogAddress }
) => {
const subgraph = getOptimisticGovernorSubgraph(network);
const request = `
{
proposals(where:{proposalHash:"${params.proposalHash}",explanation:"${
params.explanation
}",optimisticGovernor:"${params.ogAddress.toLowerCase()}"}){
id
executed
assertionId
}
}
`;
const result = await queryGql(subgraph, request);
return result?.proposals;
};
const getBondDetailsUma = async (
provider: StaticJsonRpcProvider,
moduleAddress: string
) => {
const { web3Account } = useWeb3();
const moduleContract = new Contract(moduleAddress, UMA_MODULE_ABI, provider);
const erc20Contract = new Contract(
await moduleContract.collateral(),
ERC20_ABI,
provider
);
const bondInfo = ref({
collateral: erc20Contract.address,
symbol: await erc20Contract.symbol(),
decimals: await erc20Contract.decimals(),
currentUserBondAllowance: BigNumber.from(0),
currentUserBalance: BigNumber.from(0)
});
async function updateCurrentUserBondInfo() {
bondInfo.value.currentUserBondAllowance = BigNumber.from(
web3Account.value
? await erc20Contract.allowance(web3Account.value, moduleAddress)
: 0
);
bondInfo.value.currentUserBalance = BigNumber.from(
web3Account.value ? await erc20Contract.balanceOf(web3Account.value) : 0
);
}
await updateCurrentUserBondInfo();
return bondInfo.value;
};
export const getModuleDetailsUma = async (
provider: StaticJsonRpcProvider,
network: string,
moduleAddress: string,
explanation: string,
transactions: any
) => {
const moduleContract = new Contract(moduleAddress, UMA_MODULE_ABI, provider);
const moduleDetails = await snapshot.utils.multicall(network, provider, UMA_MODULE_ABI, [
[moduleAddress, 'avatar'],
[moduleAddress, 'optimisticOracleV3'],
[moduleAddress, 'rules'],
[moduleAddress, 'bondAmount'],
[moduleAddress, 'liveness']
]);
let needsApproval = false;
const optimisticOracle = moduleDetails[1][0];
const rules = moduleDetails[2][0];
const minimumBond = moduleDetails[3][0];
const livenessPeriod = moduleDetails[4][0];
const bondDetails = await getBondDetailsUma(provider, moduleAddress);
if (
Number(minimumBond) > 0 &&
Number(minimumBond) > Number(bondDetails.currentUserBondAllowance)
) {
needsApproval = true;
}
// Create ancillary data for proposal hash
let ancillaryData = '';
let proposalHash: string;
if (transactions !== undefined) {
proposalHash = keccak256(
defaultAbiCoder.encode(
['(address to, uint8 operation, uint256 value, bytes data)[]'],
[transactions]
)
);
ancillaryData = pack(
['string', 'bytes', 'bytes', 'bytes', 'bytes', 'bytes', 'bytes', 'bytes'],
[
'',
pack(['string', 'string'], ['proposalHash', ':']),
toUtf8Bytes(proposalHash.replace(/^0x/, '')),
pack(
['string', 'string', 'string', 'string'],
[',', 'explanation', ':', '"']
),
toUtf8Bytes(explanation.replace(/^0x/, '')),
pack(
['string', 'string', 'string', 'string', 'string'],
['"', ',', 'rules', ':', '"']
),
toUtf8Bytes(rules.replace(/^0x/, '')),
pack(['string'], ['"'])
]
);
} else {
return {
dao: moduleDetails[0][0],
oracle: moduleDetails[1][0],
rules: moduleDetails[2][0],
minimumBond: minimumBond,
expiration: moduleDetails[4][0],
allowance: bondDetails.currentUserBondAllowance,
collateral: bondDetails.collateral,
decimals: bondDetails.decimals,
symbol: bondDetails.symbol,
userBalance: bondDetails.currentUserBalance,
needsBondApproval: needsApproval,
noTransactions: true,
activeProposal: false,
assertionEvent: undefined,
proposalExecuted: false,
livenessPeriod: livenessPeriod
};
}
// Check for active proposals. proposal hash can be identical across assertions
// but the explanation field should be unique. we will filter this out later.
const assertionId = await moduleContract.assertionIds(proposalHash);
const activeProposal =
assertionId !==
'0x0000000000000000000000000000000000000000000000000000000000000000';
// Search for requests with matching ancillary data
const oracleContract = new Contract(
optimisticOracle,
UMA_ORACLE_ABI,
provider
);
const latestBlock = await provider.getBlock('latest');
// modify this per chain. this should be updated with constants for all chains. start block is og deploy block.
// this needs to be optimized to reduce loading time, currently takes a long time to parse 3k blocks at a time.
const oGstartBlock = getDeployBlock(network, 'OptimisticGovernor');
const oOStartBlock = getDeployBlock(network, 'OptimisticOracleV3');
const maxRange = 3000;
const [assertionEvents, transactionsProposedEvents, executionEvents] =
await Promise.all([
pageEvents(
oOStartBlock,
latestBlock.number,
maxRange,
({ start, end }: { start: number; end: number }) => {
return oracleContract.queryFilter(
oracleContract.filters.AssertionMade(),
start,
end
);
}
),
pageEvents(
oGstartBlock,
latestBlock.number,
maxRange,
({ start, end }: { start: number; end: number }) => {
return moduleContract.queryFilter(
moduleContract.filters.TransactionsProposed(),
start,
end
);
}
),
pageEvents(
oGstartBlock,
latestBlock.number,
maxRange,
({ start, end }: { start: number; end: number }) => {
return moduleContract.queryFilter(
moduleContract.filters.ProposalExecuted(proposalHash),
start,
end
);
}
)
]);
const thisModuleAssertionEvent = assertionEvents.filter(event => {
return (
event.args?.claim === ancillaryData &&
event.args?.callbackRecipient === moduleAddress
);
});
// Get the full proposal events (with state).
const fullAssertionEvent = await Promise.all(
thisModuleAssertionEvent.map(async event => {
const assertion = await oracleContract.getAssertion(
event.args?.assertionId
);
const isExpired =
Math.floor(Date.now() / 1000) >= assertion.expirationTime.toNumber();
return {
assertionId: event?.args?.assertionId,
expirationTimestamp: assertion.expirationTime,
isExpired: isExpired,
isSettled: assertion.settled,
proposalHash: proposalHash,
proposalTxHash: event.transactionHash,
logIndex: event.logIndex
};
})
);
const thisProposalTransactionsProposedEvents =
transactionsProposedEvents.filter(
event => toUtf8String(event.args?.explanation) === explanation
);
const assertion = thisProposalTransactionsProposedEvents.map(
tx => tx.args?.assertionId
);
const assertionIds = executionEvents.map(tx => tx.args?.assertionId);
const proposalExecuted = assertion.some(assertionId =>
assertionIds.includes(assertionId)
);
return {
dao: moduleDetails[0][0],
oracle: moduleDetails[1][0],
rules: moduleDetails[2][0],
minimumBond: minimumBond,
expiration: moduleDetails[4][0],
allowance: bondDetails.currentUserBondAllowance,
collateral: bondDetails.collateral,
decimals: bondDetails.decimals,
symbol: bondDetails.symbol,
userBalance: bondDetails.currentUserBalance,
needsBondApproval: needsApproval,
noTransactions: false,
activeProposal: activeProposal,
assertionEvent: fullAssertionEvent[0],
proposalExecuted: proposalExecuted,
livenessPeriod: livenessPeriod.toString()
};
};
// This is intended to function identically to getModuleDetailsUma but use subgraphs rather than web3 events.
// This has a lot of duplicate code on purpose. Reducing code duplication will require a risky refactor,
// and we also want a fallback function in case the graph is down, so we will leave the original untouched for now.
export const getModuleDetailsUmaGql = async (
provider: StaticJsonRpcProvider,
network: string,
moduleAddress: string,
explanation: string,
transactions: any
) => {
const moduleContract = new Contract(moduleAddress, UMA_MODULE_ABI, provider);
const moduleDetails = await snapshot.utils.multicall(network, provider, UMA_MODULE_ABI, [
[moduleAddress, 'avatar'],
[moduleAddress, 'optimisticOracleV3'],
[moduleAddress, 'rules'],
[moduleAddress, 'bondAmount'],
[moduleAddress, 'liveness']
]);
let needsApproval = false;
const optimisticOracle = moduleDetails[1][0];
const rules = moduleDetails[2][0];
const minimumBond = moduleDetails[3][0];
const livenessPeriod = moduleDetails[4][0];
const bondDetails = await getBondDetailsUma(provider, moduleAddress);
if (
Number(minimumBond) > 0 &&
Number(minimumBond) > Number(bondDetails.currentUserBondAllowance)
) {
needsApproval = true;
}
let proposalHash: string;
let encodedExplanation: string;
if (transactions !== undefined && explanation !== undefined) {
proposalHash = keccak256(
defaultAbiCoder.encode(
['(address to, uint8 operation, uint256 value, bytes data)[]'],
[transactions]
)
);
encodedExplanation = pack(
['bytes'],
[toUtf8Bytes(explanation.replace(/^0x/, ''))]
);
} else {
return {
dao: moduleDetails[0][0],
oracle: moduleDetails[1][0],
rules: moduleDetails[2][0],
minimumBond: minimumBond,
expiration: moduleDetails[4][0],
allowance: bondDetails.currentUserBondAllowance,
collateral: bondDetails.collateral,
decimals: bondDetails.decimals,
symbol: bondDetails.symbol,
userBalance: bondDetails.currentUserBalance,
needsBondApproval: needsApproval,
noTransactions: true,
activeProposal: false,
assertionEvent: undefined,
proposalExecuted: false,
livenessPeriod: livenessPeriod
};
}
// Check for active proposals. proposal hash can be identical across assertions
// but the explanation field should be unique. we will filter this out later.
const assertionId = await moduleContract.assertionIds(proposalHash);
const activeProposal =
assertionId !==
'0x0000000000000000000000000000000000000000000000000000000000000000';
const [proposal] = await findProposalGql(network, {
proposalHash,
explanation: encodedExplanation,
ogAddress: moduleAddress
});
const proposalExecuted = proposal?.executed ? true : false;
const assertion = proposal?.assertionId
? await findAssertionGql(network, { assertionId: proposal.assertionId })
: undefined;
const assertionEvent = assertion
? {
assertionId: assertion.assertionId,
expirationTimestamp: BigNumber.from(assertion.expirationTime),
isExpired:
Math.floor(Date.now() / 1000) >= Number(assertion.expirationTime),
isSettled: assertion.settlementHash ? true : false,
proposalHash,
proposalTxHash: assertion.assertionHash,
logIndex: assertion.assertionLogIndex
}
: undefined;
return {
dao: moduleDetails[0][0],
oracle: moduleDetails[1][0],
rules: moduleDetails[2][0],
minimumBond: minimumBond,
expiration: moduleDetails[4][0],
allowance: bondDetails.currentUserBondAllowance,
collateral: bondDetails.collateral,
decimals: bondDetails.decimals,
symbol: bondDetails.symbol,
userBalance: bondDetails.currentUserBalance,
needsBondApproval: needsApproval,
noTransactions: false,
activeProposal,
assertionEvent,
proposalExecuted,
livenessPeriod: livenessPeriod.toString()
};
};
================================================
FILE: src/plugins/safeSnap/utils/validator.ts
================================================
import { mustBeEthereumAddress, isArrayParameter } from '../index';
export const isAddress = (type: string): boolean =>
type.indexOf('address') === 0;
export const isBoolean = (type: string): boolean => type.indexOf('bool') === 0;
export const isString = (type: string): boolean => type.indexOf('string') === 0;
export const isUint = (type: string): boolean => type.indexOf('uint') === 0;
export const isInt = (type: string): boolean => type.indexOf('int') === 0;
export const isByte = (type: string): boolean => type.indexOf('byte') === 0;
export const isStringArray = (text: string): boolean => {
try {
const values = JSON.parse(text);
return Array.isArray(values);
} catch (e) {
return false;
}
};
export const isParameterValue = (type: string, value: string) => {
if (type === 'address') {
return mustBeEthereumAddress(value);
} else if (isArrayParameter(type)) {
return isStringArray(value);
}
return !!value;
};
================================================
FILE: src/router/index.ts
================================================
import { createRouter, createWebHashHistory, RouteLocation } from 'vue-router';
import DelegateView from '@/views/DelegateView.vue';
import ExploreView from '@/views/ExploreView.vue';
import PlaygroundView from '@/views/PlaygroundView.vue';
import SetupView from '@/views/SetupView.vue';
import StrategyView from '@/views/StrategyView.vue';
import TimelineView from '@/views/TimelineView.vue';
import RankingView from '@/views/RankingView.vue';
import ProfileView from '@/views/ProfileView.vue';
import ProfileAbout from '@/views/ProfileAbout.vue';
import ProfileActivity from '@/views/ProfileActivity.vue';
import SpaceView from '@/views/SpaceView.vue';
import SpaceProposals from '@/views/SpaceProposals.vue';
import SpaceProposal from '@/views/SpaceProposal.vue';
import SpaceCreate from '@/views/SpaceCreate.vue';
import SpaceSettings from '@/views/SpaceSettings.vue';
import SpaceAbout from '@/views/SpaceAbout.vue';
import SpaceTreasury from '@/views/SpaceTreasury.vue';
import SpaceDelegates from '@/views/SpaceDelegates.vue';
import SpaceDelegate from '@/views/SpaceDelegate.vue';
import SpaceBoost from '@/views/SpaceBoost.vue';
import TermsView from '@/views/TermsView.vue';
// The frontend shows all spaces or just a single one, when being accessed
// through that space's custom domain.
const { domain, domainAlias } = useApp();
const routes: any[] = [];
// These routes get prefixed with the respective space's ENS domain (/:key)
// or they get mounted at "/" in the single space scenario.
const spaceRoutes = [
{
path: '',
name: 'spaceProposals',
component: SpaceProposals
},
{
path: 'proposal/:id',
name: 'spaceProposal',
component: SpaceProposal
},
{
path: 'create/:sourceProposal?',
name: 'spaceCreate',
component: SpaceCreate
},
{
path: 'about',
name: 'spaceAbout',
component: SpaceAbout
},
{
path: 'settings',
name: 'spaceSettings',
component: SpaceSettings
},
{
path: 'treasury/:wallet?',
name: 'spaceTreasury',
component: SpaceTreasury
},
{
path: 'delegates',
name: 'spaceDelegates',
component: SpaceDelegates
},
{
path: 'delegate/:address?',
name: 'spaceDelegate',
component: SpaceDelegate
},
{
path: 'boost/:proposalId',
name: 'spaceBoost',
component: SpaceBoost
}
];
const profileRoutes = [
{
path: '',
name: 'profileActivity',
component: ProfileActivity
},
{
path: 'about/',
name: 'profileAbout',
component: ProfileAbout
}
];
// If accessed through custom domain, mount space routes under /.
// Requests starting with /:key will be redirected.
// E.g. /balancer/proposal/:proposalId becomes /proposal/:proposalId
if (domain) {
routes.push(
{ path: '/', name: 'home', component: SpaceView, children: spaceRoutes },
{
path: `/${domain}`,
alias: `/${domainAlias ?? domain}`,
name: 'home-redirect',
redirect: '/'
},
{
path: `/${domain}/:path(.*)`,
alias: `/${domainAlias ?? domain}/:path(.*)`,
name: 'space-redirect',
redirect: (to: RouteLocation) => ({ path: `/${to.params.path}` })
}
);
} else {
// If accessed through localhost or snapshot.org, add all routes and
// prefix space routes with space domain (/:key).
routes.push(
{ path: '/', name: 'home', component: ExploreView },
{
path: '/setup/:ens?',
name: 'setup',
component: SetupView
},
{ path: '/timeline', name: 'timeline', component: TimelineView },
{ path: '/ranking', name: 'ranking', component: RankingView },
{
path: '/playground/:name',
name: 'playground',
component: PlaygroundView
},
{ path: '/strategy/:name', name: 'strategy', component: StrategyView },
{
path: '/profile/:address',
name: 'profile',
component: ProfileView,
children: profileRoutes
},
{ path: '/delegate/:key?/:to?', name: 'delegate', component: DelegateView },
{
path: '/:key',
name: 'space',
component: SpaceView,
children: spaceRoutes,
beforeEnter: to => {
// Make sure key is lowercase
if (to.params.key) {
to.params.key = to.params.key.toLowerCase();
}
}
},
{
path: '/network',
component: {
// To redirect existing links to snapshot.box
beforeRouteEnter() {
window.location.href = 'https://snapshot.box/#/network';
}
}
},
{
path: '/terms-and-conditions',
name: 'terms-and-conditions',
component: TermsView
}
);
}
// catch all
routes.push({
path: '/:pathMatch(.*)*',
name: 'error-404',
redirect: '/'
});
const router = createRouter({
history: createWebHashHistory(),
routes,
scrollBehavior(to, _from, savedPosition) {
if (savedPosition) {
return savedPosition;
}
if (to.params.retainScrollPosition) {
return {};
}
if (to.hash) {
const position = { selector: to.hash };
return { el: position };
}
return { top: 0 };
}
});
export { routes };
export default router;
================================================
FILE: src/sentry.ts
================================================
import * as Sentry from '@sentry/vue';
export const initSentry = (app, router) => {
const dsn = import.meta.env.VITE_SENTRY_DSN;
if (!dsn) {
return;
}
Sentry.init({
app,
tunnel: `${import.meta.env.VITE_SIDEKICK_URL}/sentry`,
dsn,
integrations: [
new Sentry.BrowserTracing({
routingInstrumentation: Sentry.vueRouterInstrumentation(router)
}),
new Sentry.Replay()
],
sampleRate: 0,
maxBreadcrumbs: 50,
tracingOptions: {
trackComponents: true
},
replaysSessionSampleRate: 0,
replaysOnErrorSampleRate: 0,
denyUrls: [/extensions\//i, /^chrome:\/\//i, /^chrome-extension:\/\//i]
});
};
export const setUser = user => {
Sentry.setUser(user);
};
================================================
FILE: src/views/DelegateView.vue
================================================
{{
$t('delegate.delegateNotSupported', {
network:
networks?.[networkKey]?.shortName ?? $t('theCurrentNetwork')
})
}}
{{ $t('learnMore') }}
{{ $t('setDelegationToSpace') }}
{{ $t('delegate.noDelegationsAndDelegates') }}
Error while retrieving the
delegates list
{{ $tc('delegate.noDelegatesFoundFor', [space.id]) }}
{{ $t('confirm') }}
================================================
FILE: src/views/ExploreView.vue
================================================
{{ formatCompactNumber(items.length) }} {{ resultsStr }}
{{ buttonStr }}
================================================
FILE: src/views/PlaygroundView.vue
================================================
(searchInput = value)"
/>
{{ $t('networkErrorPlayground') }}
{{ strategyError }}
{{ t('copyLink') }}
{{ formatCompactNumber(score) }}
{{ form.params.symbol }}
================================================
FILE: src/views/ProfileAbout.vue
================================================
================================================
FILE: src/views/ProfileActivity.vue
================================================
{{ $t('profile.activity.noActivity') }}
================================================
FILE: src/views/ProfileView.vue
================================================
Invalid address
================================================
FILE: src/views/RankingView.vue
================================================
================================================
FILE: src/views/SetupView.vue
================================================
router.push({ query: { step: value } })"
/>
================================================
FILE: src/views/SpaceAbout.vue
================================================
{{ $t('about') }}
{{ $t('settings.terms.label') }}
{{ space.terms }}
admin
moderator
author
================================================
FILE: src/views/SpaceBoost.vue
================================================
Create a new boost
Reward voters on this proposal and drive engagement
{{ item.name }}
{{ item.description }}
{{ formToken?.symbol }}
Winners
%
Reward per winner
{{ amountPerWinner }}
{{ formToken?.symbol }}
Selecting a specific choice is disabled for the
{{ space.name }}
space. Please enable strategic incentivization in the space
settings to enable this feature.
Strategic incentivization is disabled for proposal with
shutter on.
Strategic incentivization is available only for basic and
single choice voting type.
Create boost
Once a boost is created, it can no longer be modified.
The boost contract is not audited.
Create
This proposal is closed
Boost is not enabled in this space
Proposal
{{
getRelativeProposalPeriod(
'active',
proposal.start,
proposal.end
)
}}
================================================
FILE: src/views/SpaceCreate.vue
================================================
{{ preview ? $t('create.edit') : $t('create.preview') }}
{{ $t('back') }}
{{ isEditing ? 'Save changes' : $t('create.publish') }}
{{ web3Account ? $t('create.continue') : $t('connectWallet') }}
================================================
FILE: src/views/SpaceDelegate.vue
================================================
About
{{ statementForm.about }}
No about provided yet
Statement
No statement provided yet
{{ item.label }}
{{ item.value }}
{{ isLoggedUser ? 'Delegate to yourself' : 'Delegate' }}
Un-delegate
================================================
FILE: src/views/SpaceDelegates.vue
================================================
{{ $t('delegates.header') }}
{{
filterItems.find(i => i.action === selectedFilter)
?.text
}}
{{ item.text }}
This space has misconfigured or not setup their delegation portal
An error occurred while loading delegates. Please try again later. If
the problem persists, consider contacting our support team on
Help Center
================================================
FILE: src/views/SpaceProposal.vue
================================================
================================================
FILE: src/views/SpaceProposals.vue
================================================
New proposal
================================================
FILE: src/views/SpaceSettings.vue
================================================
{{ $t('settings.validationErrorsMessage') }}
{{
Object.keys(validationErrors)
.map(key => key)
.join(', ')
}}
{{ $t('settings.connectWithSpaceOwner') }}
{{ $t('reset') }}
{{ $t('save') }}
{{ $t('newsletter.join') }}
{{
$t('setup.confirmToSetAddress', {
address: shorten(spaceControllerInput)
})
}}
{{ $t('setup.controllerHasAuthority') + '.' }}
{{ $t('settings.confirmDeleteSpace', { name: space.id }) }}
{{ $t('settings.noticeSavedText') }}
================================================
FILE: src/views/SpaceTreasury.vue
================================================
================================================
FILE: src/views/SpaceView.vue
================================================
================================================
FILE: src/views/StrategyView.vue
================================================
{{ $t('strategiesPage') }}
{{ strategy.id }}
{{ $t('author') }}
{{ strategy.author }}
{{ $t('version') }}
{{ strategy.version }}
{{
$t('playground')
}}
================================================
FILE: src/views/TermsView.vue
================================================
================================================
FILE: src/views/TimelineView.vue
================================================
{{ $t('noSpacesJoined') }}
{{ $t('joinSpaces') }}
================================================
FILE: tailwind.config.js
================================================
import formsPlugin from '@tailwindcss/forms';
export default {
content: ['./index.html', './src/**/*.{js,ts,vue}'],
theme: {
extend: {
colors: {
snapshot: '#F2994A',
boost: '#F2994A',
'skin-primary': 'var(--primary-color)',
'skin-border': 'var(--border-color)',
'skin-text': 'var(--text-color)',
'skin-link': 'var(--link-color)',
'skin-bg': 'var(--bg-color)',
'skin-block-bg': 'var(--block-bg)',
'skin-header-bg': 'var(--header-bg)',
'skin-heading': 'var(--heading-color)',
green: '#21b66f',
red: '#ff3856',
'skin-success': '#57B375'
},
keyframes: {
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' }
}
},
animation: {
'fade-in': 'fadeIn 1s',
'pulse-fast': 'pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite'
}
},
spacing: {
0: '0px',
1: '4px',
2: '8px',
3: '16px',
4: '24px',
5: '32px',
6: '40px'
},
screens: {
xs: '420px',
sm: '544px',
md: '768px',
lg: '1012px',
xl: '1280px',
'2xl': '1536px'
},
fontFamily: {
sans: [
'Calibre, -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji'
],
mono: ['monospace'],
space: ['SpaceMono']
},
fontSize: {
'2xl': ['36px', '50px'],
xl: ['28px', '44px'],
lg: ['24px', '32px'],
md: ['20px', '28px'],
base: ['18px', '24px'],
sm: ['16px'],
xs: ['14px']
},
boxShadow: {
lg: '2px 4px 9px var(--shadow-color)',
xl: '7px 10.5px 28px 0px var(--shadow-color)'
}
},
plugins: [
'prettier-plugin-tailwindcss',
formsPlugin({
strategy: 'class'
})
]
};
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"moduleResolution": "node",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"lib": ["esnext", "dom"],
"importHelpers": true,
"allowSyntheticDefaultImports": true,
"allowJs": true,
"baseUrl": ".",
"types": ["vite/client", "unplugin-icons/types/vue"],
"paths": {
"@/*": ["src/*"]
},
"noImplicitAny": false,
"skipLibCheck": true,
"outDir": "dist"
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue",
"components.d.ts",
"auto-imports.d.ts"
]
}
================================================
FILE: vercel.json
================================================
{
"rewrites": [
{
"source": "/(.*)",
"destination": "/"
}
]
}
================================================
FILE: vite.config.ts
================================================
///
import path from 'path';
import { defineConfig } from 'vitest/config';
import { getPlugins } from './src/helpers/vitePlugins';
export default defineConfig({
define: {
'process.env': process.env
},
plugins: getPlugins(),
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'readable-stream': 'vite-compatible-readable-stream'
}
},
test: {
environment: 'happy-dom',
deps: {
inline: ['@pusher/push-notifications-web']
}
},
build: {
sourcemap: process.env.VITE_ENV === 'production',
target: 'esnext'
},
optimizeDeps: {
esbuildOptions: {
target: 'esnext'
}
}
});