diff --git a/docs/deployment.md b/docs/deployment.md index 284076f84..4d292238e 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -162,6 +162,8 @@ VITE_GOOGLE_SPREADSHEET_ID= ``` +Note: if VITE_SUBGRAPH_URL is not set, the app will try to get the round information from the vue-app/src/rounds.json file which can be generated using the `hardhat export-round` command. + ##### Setup the netlify functions 1. Set the `functions directory` to `vue-app/dist/lambda`. diff --git a/vue-app/src/App.vue b/vue-app/src/App.vue index 923ff52d0..3c7ad9b98 100644 --- a/vue-app/src/App.vue +++ b/vue-app/src/App.vue @@ -4,25 +4,12 @@
- -
+
-
- - -
-
- -
+ +
@@ -32,33 +19,23 @@ diff --git a/vue-app/src/locales/cn.json b/vue-app/src/locales/cn.json index 63d63539e..ecbc7e298 100644 --- a/vue-app/src/locales/cn.json +++ b/vue-app/src/locales/cn.json @@ -719,6 +719,7 @@ "tooltip2": "{chain} 链上钱包余额", "h2_3": "项目", "div1": "正在审核", + "withdraw_button": "取回 {contribution} {tokenSymbol}", "btn2_1": "预览", "btn2_2": "查看", "div2": "您尚未提交任何项目" @@ -886,7 +887,6 @@ "div8": "匹配池", "div9": "剩余的将会加入匹配池", "div10": "平均分配 {contribution} {tokenSymbol}", - "button1": "取回 {contribution} {tokenSymbol}", "div11": "不可以", "div11_if2": "重新分配", "div11_if3": "捐赠", diff --git a/vue-app/src/locales/en.json b/vue-app/src/locales/en.json index 8462655d5..031def9e7 100644 --- a/vue-app/src/locales/en.json +++ b/vue-app/src/locales/en.json @@ -719,6 +719,7 @@ "tooltip2": "Balance of wallet on {chain} chain", "h2_3": "Projects", "div1": "Under review", + "withdraw_button": "Withdraw {contribution} {tokenSymbol}", "btn2_1": "Preview", "btn2_2": "View", "div2": "You haven't submitted any projects" @@ -886,7 +887,6 @@ "div8": "Matching pool", "div9": "Remaining funds go to matching pool", "div10": "Split {contribution} {tokenSymbol} evenly", - "button1": "Withdraw {contribution} {tokenSymbol}", "div11": "Can't", "div11_if2": "reallocate", "div11_if3": "contribute", diff --git a/vue-app/src/locales/es.json b/vue-app/src/locales/es.json index d76cba03a..ec1371371 100644 --- a/vue-app/src/locales/es.json +++ b/vue-app/src/locales/es.json @@ -719,6 +719,7 @@ "tooltip2": "Saldo de la billetera en la cadena {chain}", "h2_3": "Proyectos", "div1": "En revisión", + "withdraw_button": "Retirar {contribution} {tokenSymbol}", "btn2_1": "Vista previa", "btn2_2": "Ver", "div2": "No has enviado ningún proyecto" @@ -886,7 +887,6 @@ "div8": "Matching pool", "div9": "Los fondos restantes se destinarán al matching pool", "div10": "Distribuir {contribution} {tokenSymbol} de manera uniforme", - "button1": "Retirar {contribution} {tokenSymbol}", "div11": "No puedes", "div11_if2": "reasignar", "div11_if3": "contribuir", diff --git a/vue-app/src/router/index.ts b/vue-app/src/router/index.ts index e6e415a07..86a75d5b9 100644 --- a/vue-app/src/router/index.ts +++ b/vue-app/src/router/index.ts @@ -1,6 +1,6 @@ import { createRouter, createWebHashHistory } from 'vue-router' import type { RouteRecordRaw } from 'vue-router' -import { isUserRegistrationRequired, isOptimisticRecipientRegistry } from '@/api/core' +import { isUserRegistrationRequired, isOptimisticRecipientRegistry, isActiveApp } from '@/api/core' const Landing = () => import('@/views/Landing.vue') const JoinLanding = () => import('@/views/JoinLanding.vue') @@ -262,7 +262,7 @@ if (isUserRegistrationRequired) { ) } -if (isOptimisticRecipientRegistry) { +if (isOptimisticRecipientRegistry && isActiveApp) { routes.push({ path: '/recipients', name: 'recipients', diff --git a/vue-app/src/stores/app.ts b/vue-app/src/stores/app.ts index 9b2009d76..d977e04b7 100644 --- a/vue-app/src/stores/app.ts +++ b/vue-app/src/stores/app.ts @@ -9,10 +9,10 @@ import { serializeCart, } from '@/api/contributions' import { getCommittedCart } from '@/api/cart' -import { operator, chain, ThemeMode, recipientRegistryType, recipientJoinDeadlineConfig } from '@/api/core' -import { type RoundInfo, RoundStatus, getRoundInfo } from '@/api/round' +import { operator, chain, ThemeMode, recipientRegistryType, recipientJoinDeadlineConfig, isActiveApp } from '@/api/core' +import { type RoundInfo, RoundStatus, getRoundInfo, getLeaderboardRoundInfo } from '@/api/round' import { getTally, type Tally } from '@/api/tally' -import { type ClrFund, getClrFundInfo } from '@/api/clrFund' +import { type ClrFund, getClrFundInfo, getMatchingFunds } from '@/api/clrFund' import { getMACIFactoryInfo, type MACIFactory } from '@/api/maci-factory' import { isSameAddress } from '@/utils/accounts' import { storage } from '@/api/storage' @@ -23,6 +23,8 @@ import { getAssetsUrl } from '@/utils/url' import { getTokenLogo } from '@/utils/tokens' import { assert, ASSERT_MISSING_ROUND, ASSERT_MISSING_SIGNATURE, ASSERT_NOT_CONNECTED_WALLET } from '@/utils/assert' import { Keypair } from '@clrfund/common' +import { getRounds } from '@/api/rounds' +import { DateTime } from 'luxon' export type AppState = { isAppReady: boolean @@ -68,6 +70,11 @@ export const useAppStore = defineStore('app', { return recipientJoinDeadlineConfig } + if (!isActiveApp) { + // when running in static mode, do not allow adding recipients + return DateTime.now() + } + const recipientStore = useRecipientStore() if (!state.currentRound || !recipientStore.recipientRegistryInfo) { return null @@ -113,6 +120,9 @@ export const useAppStore = defineStore('app', { isCurrentRound: state => (roundAddress: string): boolean => { + if (state.currentRoundAddress === null) { + return false + } const currentRoundAddress = state.currentRoundAddress || '' return isSameAddress(roundAddress, currentRoundAddress) }, @@ -460,12 +470,44 @@ export const useAppStore = defineStore('app', { stateIndex, } }, + async loadStaticClrFundInfo() { + const rounds = await getRounds() + // rounds are sorted in reverse order, first one is the newest round + const currentRound = rounds[0] + + let maxRecipients = 0 + if (currentRound) { + const network = currentRound.network || '' + const currentRoundInfo = await getLeaderboardRoundInfo(currentRound.address, network) + if (currentRoundInfo) { + const matchingPool = await getMatchingFunds(currentRoundInfo.nativeTokenAddress) + this.clrFund = { + nativeTokenAddress: currentRoundInfo.nativeTokenAddress, + nativeTokenSymbol: currentRoundInfo.nativeTokenSymbol, + nativeTokenDecimals: currentRoundInfo.nativeTokenDecimals, + userRegistryAddress: currentRoundInfo.userRegistryAddress, + recipientRegistryAddress: currentRoundInfo.recipientRegistryAddress, + matchingPool, + } + this.selectRound(currentRound.address) + this.currentRound = currentRoundInfo + if (currentRoundInfo.tally) { + this.tally = currentRoundInfo.tally + } + maxRecipients = currentRoundInfo.maxRecipients + } + } + if (!this.clrFund) { + this.clrFund = await getClrFundInfo() + } + await this.loadMACIFactoryInfo(maxRecipients) + }, async loadClrFundInfo() { const clrFund = await getClrFundInfo() this.clrFund = clrFund }, - async loadMACIFactoryInfo() { - const factory = await getMACIFactoryInfo() + async loadMACIFactoryInfo(maxRecipients?: number) { + const factory = await getMACIFactoryInfo(maxRecipients) this.maciFactory = factory }, async loadTally() { diff --git a/vue-app/src/views/Profile.vue b/vue-app/src/views/Profile.vue index 768352a67..b4c6612e9 100644 --- a/vue-app/src/views/Profile.vue +++ b/vue-app/src/views/Profile.vue @@ -56,6 +56,16 @@
+
+ +

{{ $t('profile.h2_3') }}

@@ -93,13 +103,19 @@ import CopyButton from '@/components/CopyButton.vue' import Loader from '@/components/Loader.vue' import FundsNeededWarning from '@/components/FundsNeededWarning.vue' -import { userRegistryType, UserRegistryType, chain } from '@/api/core' -import { type Project, getProjects } from '@/api/projects' +import { userRegistryType, UserRegistryType, chain, isActiveApp } from '@/api/core' +import { type Project, getProjects, getProjectsForStaticRound } from '@/api/projects' import { isSameAddress } from '@/utils/accounts' import { getTokenLogo } from '@/utils/tokens' import { useAppStore, useUserStore, useRecipientStore, useWalletStore } from '@/stores' import { storeToRefs } from 'pinia' import { useRouter } from 'vue-router' +import { getLeaderboardData } from '@/api/leaderboard' +import { formatAmount } from '@/utils/amounts' + +import WithdrawalModal from '@/components/WithdrawalModal.vue' +import { useModal } from 'vue-final-modal' +import { RoundStatus } from '@/api/round' interface Props { balance: string @@ -111,7 +127,14 @@ const emit = defineEmits(['close']) const router = useRouter() const appStore = useAppStore() -const { hasContributionPhaseEnded, nativeTokenSymbol, currentRound } = storeToRefs(appStore) +const { + hasContributionPhaseEnded, + nativeTokenSymbol, + nativeTokenDecimals, + currentRound, + hasUserContributed, + contribution, +} = storeToRefs(appStore) const userStore = useUserStore() const { currentUser } = storeToRefs(userStore) const recipientStore = useRecipientStore() @@ -139,17 +162,38 @@ const displayAddress = computed(() => { return currentUser.value.ensName ?? currentUser.value.walletAddress }) +const canWithdrawContribution = computed( + () => currentRound.value?.status === RoundStatus.Cancelled && hasUserContributed, +) + +const { open: openWithdrawalModal, close: closeWithdrawalModal } = useModal({ + component: WithdrawalModal, + attrs: { + onClose() { + closeWithdrawalModal() + }, + }, +}) + watch(recipientRegistryAddress, () => loadProjects()) async function loadProjects(): Promise { - if (!recipientRegistryAddress.value) return - isLoading.value = true - const _projects: Project[] = await getProjects( - recipientRegistryAddress.value, - currentRound.value?.startTime.toSeconds(), - currentRound.value?.votingDeadline.toSeconds(), - ) + let _projects: Project[] = [] + + if (isActiveApp) { + if (!recipientRegistryAddress.value) return + _projects = await getProjects( + recipientRegistryAddress.value, + currentRound.value?.startTime.toSeconds(), + currentRound.value?.votingDeadline.toSeconds(), + ) + } else { + const currentRoundAddress = currentRound.value?.fundingRoundAddress || '' + const network = currentRound.value?.network || '' + _projects = await getProjectsForStaticRound(currentRoundAddress, network) + } + const userProjects: Project[] = _projects.filter( ({ address, requester }) => isSameAddress(address, currentUser.value?.walletAddress as string) || diff --git a/vue-app/src/views/ProjectList.vue b/vue-app/src/views/ProjectList.vue index d6345fb3b..8bc21b7c5 100644 --- a/vue-app/src/views/ProjectList.vue +++ b/vue-app/src/views/ProjectList.vue @@ -60,7 +60,7 @@ import { ref, computed, onMounted } from 'vue' import { getCurrentRound, getRoundInfo } from '@/api/round' -import { type Project, getProjects, getRecipientRegistryAddress } from '@/api/projects' +import { type Project, getProjects, getRecipientRegistryAddress, getProjectsForStaticRound } from '@/api/projects' import CallToActionCard from '@/components/CallToActionCard.vue' import ProjectListItem from '@/components/ProjectListItem.vue' @@ -70,11 +70,15 @@ import { useRoute } from 'vue-router' import { useAppStore, useUserStore } from '@/stores' import { storeToRefs } from 'pinia' import { DateTime } from 'luxon' +import { isActiveApp } from '@/api/core' +import { getSecondsFromNow } from '@/utils/dates' type ProjectRoundInfo = { + fundingRoundAddress: string recipientRegistryAddress: string startTime: number votingDeadline: number + network: string } const SHUFFLE_RANDOM_SEED = Math.random() @@ -119,7 +123,10 @@ onMounted(async () => { try { roundAddress.value = (route.params.address as string) || currentRoundAddress.value || (await getCurrentRound()) || '' - const round = await loadProjectRoundInfo(roundAddress.value) + + const round = isActiveApp + ? await loadProjectRoundInfo(roundAddress.value) + : await loadStaticRoundInfo(roundAddress.value) await loadProjects(round) } catch (err) { /* eslint-disable-next-line no-console */ @@ -148,11 +155,27 @@ async function loadProjectRoundInfo(roundAddress: string): Promise { + await appStore.loadClrFundInfo() + const network = currentRound.value?.network || '' + const recipientRegistryAddress = currentRound.value?.recipientRegistryAddress || '' + const startTime = getSecondsFromNow(currentRound.value?.startTime || DateTime.now()) + const votingDeadline = getSecondsFromNow(currentRound.value?.votingDeadline || DateTime.now()) + return { recipientRegistryAddress, startTime, votingDeadline, fundingRoundAddress: roundAddress, network } } async function loadProjects(round: ProjectRoundInfo) { - const _projects = await getProjects(round.recipientRegistryAddress, round.startTime, round.votingDeadline) + const _projects = isActiveApp + ? await getProjects(round.recipientRegistryAddress, round.startTime, round.votingDeadline) + : await getProjectsForStaticRound(roundAddress.value, round.network) const visibleProjects = _projects.filter(project => { return !project.isHidden && !project.isLocked }) diff --git a/vue-app/src/views/RoundList.vue b/vue-app/src/views/RoundList.vue index 501f65678..eb1ac6273 100644 --- a/vue-app/src/views/RoundList.vue +++ b/vue-app/src/views/RoundList.vue @@ -41,12 +41,11 @@ import { onMounted, ref } from 'vue' import { type Round, getRounds } from '@/api/rounds' import Links from '@/components/Links.vue' import { DateTime } from 'luxon' -import { clrfundContractAddress } from '@/api/core' const rounds = ref([]) onMounted(async () => { - rounds.value = (await getRounds(clrfundContractAddress)).reverse() + rounds.value = await getRounds() })