diff --git a/contracts/tasks/index.ts b/contracts/tasks/index.ts
index 19050a901..98c143c27 100644
--- a/contracts/tasks/index.ts
+++ b/contracts/tasks/index.ts
@@ -15,6 +15,7 @@ import './runners/finalize'
import './runners/claim'
import './runners/cancel'
import './runners/exportRound'
+import './runners/exportImages'
import './runners/mergeAllocation'
import './runners/loadSimpleUsers'
import './runners/loadMerkleUsers'
diff --git a/contracts/tasks/runners/exportImages.ts b/contracts/tasks/runners/exportImages.ts
new file mode 100644
index 000000000..7800a85a2
--- /dev/null
+++ b/contracts/tasks/runners/exportImages.ts
@@ -0,0 +1,83 @@
+/**
+ * Export the project logo images in a ClrFund round.
+ *
+ * Sample usage:
+ * yarn hardhat export-images \
+ * --output-dir ../vue-apps/public/ipfs
+ * --gateway https://ipfs.io
+ * --round-file ../vue-app/src/rounds/arbitrum/0x4A2d90844EB9C815eF10dB0371726F0ceb2848B0.json
+ *
+ * Notes:
+ * 1) This script assumes the round has been exported using the `export-round` hardhat task
+ */
+
+import { task } from 'hardhat/config'
+import { isPathExist, makeDirectory } from '../../utils/misc'
+import { getIpfsContent } from '@clrfund/common'
+import fs from 'fs'
+import { dirname } from 'path'
+
+/**
+ * Download the IPFS file with the ipfsHash to the output directory
+ * @param gateway IPFS gateway url
+ * @param ipfsHash IPFS hash of the file to download
+ * @param outputDir The directory to store the downloaded file
+ */
+async function download({
+ gateway,
+ ipfsHash,
+ outputDir,
+}: {
+ gateway: string
+ ipfsHash: string
+ outputDir: string
+}) {
+ if (!ipfsHash) return
+
+ const res = await getIpfsContent(ipfsHash, gateway)
+ if (res.hasBody()) {
+ console.log('Downloaded', ipfsHash)
+ const path = `${outputDir}/${ipfsHash}`
+ const folder = dirname(path)
+ if (!isPathExist(folder)) {
+ makeDirectory(folder)
+ }
+
+ fs.writeFileSync(path, res.body)
+ }
+}
+
+task('export-images', 'Export project logo images')
+ .addParam('outputDir', 'The output directory')
+ .addParam('roundFile', 'The exported funding round file path')
+ .addParam('gateway', 'The IPFS gateway url')
+ .setAction(async ({ outputDir, roundFile, gateway }) => {
+ console.log('Starting to download from ipfs')
+
+ const data = fs.readFileSync(roundFile, { encoding: 'utf-8' })
+ const round = JSON.parse(data)
+ const projects = round.projects
+ const images = projects.map((project: any) => {
+ const { bannerImageHash, thumbnailImageHash, imageHash } =
+ project.metadata
+ return { bannerImageHash, thumbnailImageHash, imageHash }
+ })
+
+ for (let i = 0; i < images.length; i++) {
+ await download({
+ gateway,
+ ipfsHash: images[i].bannerImageHash,
+ outputDir,
+ })
+ await download({
+ gateway,
+ ipfsHash: images[i].thumbnailImageHash,
+ outputDir,
+ })
+ await download({
+ gateway,
+ ipfsHash: images[i].imageHash,
+ outputDir,
+ })
+ }
+ })
diff --git a/subgraph/config/clrfund-arbitrum.json b/subgraph/config/clrfund-arbitrum.json
new file mode 100644
index 000000000..2c58c9603
--- /dev/null
+++ b/subgraph/config/clrfund-arbitrum.json
@@ -0,0 +1,6 @@
+{
+ "network": "arbitrum-one",
+ "address": "0xc06349D95C30551Ea510bD5F35CfA2151499D60a",
+ "factoryStartBlock": 96912420,
+ "recipientRegistryStartBlock": 96912420
+}
diff --git a/vue-app/src/App.vue b/vue-app/src/App.vue
index 3c7ad9b98..01f555ac1 100644
--- a/vue-app/src/App.vue
+++ b/vue-app/src/App.vue
@@ -8,8 +8,20 @@
-
-
+
+
+
+
+
+
+
@@ -20,22 +32,29 @@
diff --git a/vue-app/src/graphql/sdk.ts b/vue-app/src/graphql/sdk.ts
index fc22db618..dcd511bdb 100644
--- a/vue-app/src/graphql/sdk.ts
+++ b/vue-app/src/graphql/sdk.ts
@@ -1,7 +1,15 @@
import { GraphQLClient } from 'graphql-request'
import { SUBGRAPH_ENDPOINT } from '@/api/core'
-import { getSdk } from './API'
+import { getSdk, type SdkFunctionWrapper } from './API'
const client = new GraphQLClient(SUBGRAPH_ENDPOINT)
-export default getSdk(client)
+
+const wrapper: SdkFunctionWrapper = (action, _operationName, _operationType) => {
+ if (!SUBGRAPH_ENDPOINT) {
+ throw new Error('Subgraph not available')
+ }
+ return action()
+}
+
+export default getSdk(client, wrapper)
diff --git a/vue-app/src/router/index.ts b/vue-app/src/router/index.ts
index 86a75d5b9..e6e415a07 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, isActiveApp } from '@/api/core'
+import { isUserRegistrationRequired, isOptimisticRecipientRegistry } from '@/api/core'
const Landing = () => import('@/views/Landing.vue')
const JoinLanding = () => import('@/views/JoinLanding.vue')
@@ -262,7 +262,7 @@ if (isUserRegistrationRequired) {
)
}
-if (isOptimisticRecipientRegistry && isActiveApp) {
+if (isOptimisticRecipientRegistry) {
routes.push({
path: '/recipients',
name: 'recipients',
diff --git a/vue-app/src/stores/app.ts b/vue-app/src/stores/app.ts
index 65b7fc494..ad873972d 100644
--- a/vue-app/src/stores/app.ts
+++ b/vue-app/src/stores/app.ts
@@ -9,8 +9,8 @@ import {
serializeCart,
} from '@/api/contributions'
import { getCommittedCart } from '@/api/cart'
-import { operator, chain, ThemeMode, recipientRegistryType, recipientJoinDeadlineConfig, isActiveApp } from '@/api/core'
-import { type RoundInfo, RoundStatus, getRoundInfo, getLeaderboardRoundInfo } from '@/api/round'
+import { operator, chain, ThemeMode, recipientRegistryType, recipientJoinDeadlineConfig } from '@/api/core'
+import { type RoundInfo, RoundStatus, getRoundInfo, getStaticRoundInfo } from '@/api/round'
import { getTally, type Tally } from '@/api/tally'
import { type ClrFund, getClrFundInfo, getMatchingFunds } from '@/api/clrFund'
import { getMACIFactoryInfo, type MACIFactory } from '@/api/maci-factory'
@@ -70,11 +70,6 @@ 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
@@ -470,38 +465,6 @@ 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
diff --git a/vue-app/src/utils/chains.ts b/vue-app/src/utils/chains.ts
index acab4ecfe..94e752f8a 100644
--- a/vue-app/src/utils/chains.ts
+++ b/vue-app/src/utils/chains.ts
@@ -15,6 +15,7 @@ export enum ChainId {
export type ChainInfo = {
[chainId in ChainId]: {
label: string
+ name: string
currency: string
logo: string
isLayer2: boolean
@@ -29,6 +30,7 @@ export type ChainInfo = {
export const CHAIN_INFO: ChainInfo = {
[ChainId.MAINNET]: {
label: 'Mainnet',
+ name: 'mainnet',
currency: 'ETH',
logo: 'eth.svg',
isLayer2: false,
@@ -38,6 +40,7 @@ export const CHAIN_INFO: ChainInfo = {
},
[ChainId.SEPOLIA]: {
label: 'Sepolia',
+ name: 'sepolia',
currency: 'ETH',
logo: 'eth.svg',
isLayer2: false,
@@ -47,6 +50,7 @@ export const CHAIN_INFO: ChainInfo = {
},
[ChainId.HARDHAT]: {
label: 'Arbitrum Hardhat',
+ name: 'hardhat',
currency: 'AETH',
logo: 'arbitrum.svg',
isLayer2: true,
@@ -58,6 +62,7 @@ export const CHAIN_INFO: ChainInfo = {
},
[ChainId.ARBITRUM_ONE]: {
label: 'Arbitrum',
+ name: 'arbitrum',
currency: 'AETH',
logo: 'arbitrum.svg',
isLayer2: true,
@@ -69,6 +74,7 @@ export const CHAIN_INFO: ChainInfo = {
},
[ChainId.ARBITRUM_RINKEBY]: {
label: 'Arbitrum Rinkeby',
+ name: 'arbitrum-rinkeby',
currency: 'AETH',
logo: 'arbitrum.svg',
isLayer2: true,
@@ -80,6 +86,7 @@ export const CHAIN_INFO: ChainInfo = {
},
[ChainId.ARBITRUM_GOERLI]: {
label: 'Arbitrum Goerli',
+ name: 'arbitrum-goerli',
currency: 'AETH',
logo: 'arbitrum.svg',
isLayer2: true,
@@ -91,6 +98,7 @@ export const CHAIN_INFO: ChainInfo = {
},
[ChainId.ARBITRUM_SEPOLIA]: {
label: 'Arbitrum Sepolia',
+ name: 'arbitrum-sepolia',
currency: 'AETH',
logo: 'arbitrum.svg',
isLayer2: true,
@@ -102,6 +110,7 @@ export const CHAIN_INFO: ChainInfo = {
},
[ChainId.OPTIMISM]: {
label: 'Optimism',
+ name: 'optimism',
currency: 'OETH',
logo: 'optimism.svg',
isLayer2: true,
@@ -113,6 +122,7 @@ export const CHAIN_INFO: ChainInfo = {
},
[ChainId.OPTIMISM_SEPOLIA]: {
label: 'Optimism Sepolia',
+ name: 'optimism-sepolia',
currency: 'OETH',
logo: 'optimism.svg',
isLayer2: true,
@@ -124,6 +134,7 @@ export const CHAIN_INFO: ChainInfo = {
},
[ChainId.XDAI]: {
label: 'xDai',
+ name: 'xdai',
currency: 'xDai',
logo: 'xdai.svg',
isLayer2: false,
@@ -135,6 +146,7 @@ export const CHAIN_INFO: ChainInfo = {
},
[ChainId.POLYGON]: {
label: 'Polygon',
+ name: 'polygon',
currency: 'MATIC',
logo: 'polygon.svg',
isLayer2: false,
diff --git a/vue-app/src/utils/url.ts b/vue-app/src/utils/url.ts
index 8596ed105..0f797e971 100644
--- a/vue-app/src/utils/url.ts
+++ b/vue-app/src/utils/url.ts
@@ -1,3 +1,16 @@
+import { ipfsGatewayUrl } from '@/api/core'
+
export function getAssetsUrl(path) {
return new URL(`/src/assets/${path}`, import.meta.url).href
}
+
+export function getStaticUrlByIpfsHash(hash): string | null {
+ return hash ? `/ipfs/${hash}` : null
+}
+
+export function getIpfsUrl(hash): string | null {
+ if (!hash || !ipfsGatewayUrl) {
+ return null
+ }
+ return `${ipfsGatewayUrl}/ipfs/${hash}`
+}
diff --git a/vue-app/src/views/Leaderboard.vue b/vue-app/src/views/Leaderboard.vue
index b5c08a706..c7d338ffb 100644
--- a/vue-app/src/views/Leaderboard.vue
+++ b/vue-app/src/views/Leaderboard.vue
@@ -46,10 +46,9 @@
import { useAppStore } from '@/stores'
import { useRouter, useRoute } from 'vue-router'
import type { RoundInfo } from '@/api/round'
-import { toRoundInfo } from '@/api/round'
+import { toRoundInfo, findStaticRound } from '@/api/round'
import type { LeaderboardProject } from '@/api/projects'
import { toLeaderboardProject } from '@/api/projects'
-import { getLeaderboardData } from '@/api/leaderboard'
import { getRouteParamValue } from '@/utils/route'
const router = useRouter()
@@ -63,7 +62,7 @@ const appStore = useAppStore()
const { showSimpleLeaderboard } = storeToRefs(appStore)
async function loadLeaderboard(address: string, network: string) {
- const data = await getLeaderboardData(address, network)
+ const data = await findStaticRound(address, network)
return data
}
@@ -77,9 +76,9 @@ onMounted(async () => {
const network = getRouteParamValue(route.params.network)
const data = await loadLeaderboard(address, network)
- // redirect to projects view if not finalized or no static round data for leaderboard
- if (!data?.projects) {
- router.push({ name: 'round' })
+ // redirect to projects view if no tally data or no static round data for leaderboard
+ if (!data?.projects || !data?.tally) {
+ router.push({ name: 'round', params: { address } })
return
}
@@ -88,7 +87,7 @@ onMounted(async () => {
.filter(project => project.state != 'Removed')
.map(project => toLeaderboardProject(project))
.sort((p1: LeaderboardProject, p2: LeaderboardProject) => {
- const diff = p2.allocatedAmount - p1.allocatedAmount
+ const diff = BigInt(p2.allocatedAmount || 0) - BigInt(p1.allocatedAmount || 0)
if (diff === BigInt(0)) return 0
if (diff > BigInt(0)) return 1
return -1
@@ -98,7 +97,7 @@ onMounted(async () => {
}
try {
- round.value = toRoundInfo(data.round, network)
+ round.value = toRoundInfo(data.round)
} catch (e) {
console.log('Error converting to round info', e)
}
diff --git a/vue-app/src/views/LeaderboardProject.vue b/vue-app/src/views/LeaderboardProject.vue
index 3c4c52ad5..c3dfa65ce 100644
--- a/vue-app/src/views/LeaderboardProject.vue
+++ b/vue-app/src/views/LeaderboardProject.vue
@@ -2,7 +2,7 @@
-
+
diff --git a/vue-app/src/views/Profile.vue b/vue-app/src/views/Profile.vue
index b4c6612e9..da632f2d6 100644
--- a/vue-app/src/views/Profile.vue
+++ b/vue-app/src/views/Profile.vue
@@ -69,8 +69,8 @@
{{ $t('profile.h2_3') }}
-
-
+
+
{{ name }}
@@ -103,19 +103,18 @@ import CopyButton from '@/components/CopyButton.vue'
import Loader from '@/components/Loader.vue'
import FundsNeededWarning from '@/components/FundsNeededWarning.vue'
-import { userRegistryType, UserRegistryType, chain, isActiveApp } from '@/api/core'
-import { type Project, getProjects, getProjectsForStaticRound } from '@/api/projects'
+import { userRegistryType, UserRegistryType, chain } from '@/api/core'
+import { type Project, getProjects } 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'
+import { RoundStatus, getRoundInfo } from '@/api/round'
interface Props {
balance: string
@@ -179,20 +178,17 @@ watch(recipientRegistryAddress, () => loadProjects())
async function loadProjects(): Promise
{
isLoading.value = true
- 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)
- }
+ let _projects: Project[] | undefined = undefined
+
+ if (!recipientRegistryAddress.value) return
+
+ _projects = await getProjects({
+ registryAddress: recipientRegistryAddress.value,
+ fundingRoundAddress: currentRound.value?.fundingRoundAddress,
+ network: currentRound.value?.network,
+ startTime: currentRound.value?.startTime.toSeconds(),
+ endTime: currentRound.value?.votingDeadline.toSeconds(),
+ })
const userProjects: Project[] = _projects.filter(
({ address, requester }) =>
diff --git a/vue-app/src/views/Project.vue b/vue-app/src/views/Project.vue
index 08ce13444..4eda5cf04 100644
--- a/vue-app/src/views/Project.vue
+++ b/vue-app/src/views/Project.vue
@@ -2,7 +2,7 @@
-
+
@@ -60,8 +60,14 @@ onMounted(async () => {
roundAddress.value = (route.params.address as string) || currentRoundAddress || ''
+ const recipientId = route.params.id as string
const registryAddress = await getRecipientRegistryAddress(roundAddress.value || null)
- const _project = await getProject(registryAddress, route.params.id as string)
+ const _project = await getProject({
+ registryAddress,
+ fundingRoundAddress: roundAddress.value || undefined,
+ recipientId,
+ filter: false,
+ })
if (_project === null || _project.isHidden) {
// Project not found
router.push({ name: 'projects' })
diff --git a/vue-app/src/views/ProjectList.vue b/vue-app/src/views/ProjectList.vue
index 8bc21b7c5..9b2ef9c96 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, getProjectsForStaticRound } from '@/api/projects'
+import { type Project, getProjects, getRecipientRegistryAddress } from '@/api/projects'
import CallToActionCard from '@/components/CallToActionCard.vue'
import ProjectListItem from '@/components/ProjectListItem.vue'
@@ -70,7 +70,6 @@ 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 = {
@@ -124,10 +123,7 @@ onMounted(async () => {
roundAddress.value =
(route.params.address as string) || currentRoundAddress.value || (await getCurrentRound()) || ''
- const round = isActiveApp
- ? await loadProjectRoundInfo(roundAddress.value)
- : await loadStaticRoundInfo(roundAddress.value)
- await loadProjects(round)
+ await loadProjectRoundInfo(roundAddress.value)
} catch (err) {
/* eslint-disable-next-line no-console */
console.error('Error loading projects', err)
@@ -135,11 +131,13 @@ onMounted(async () => {
isLoading.value = false
})
-async function loadProjectRoundInfo(roundAddress: string): Promise
{
+async function loadProjectRoundInfo(roundAddress: string) {
// defaults when a round has not been created yet
let recipientRegistryAddress = ''
let startTime = 0
let votingDeadline = DateTime.local().toSeconds()
+ let network = ''
+ let fundingRoundAddress = ''
if (roundAddress) {
const round = await getRoundInfo(roundAddress, currentRound.value)
@@ -147,6 +145,8 @@ 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 = isActiveApp
- ? await getProjects(round.recipientRegistryAddress, round.startTime, round.votingDeadline)
- : await getProjectsForStaticRound(roundAddress.value, round.network)
+ const _projects = await getProjects({
+ registryAddress: recipientRegistryAddress,
+ fundingRoundAddress,
+ network,
+ startTime,
+ endTime: votingDeadline,
+ })
const visibleProjects = _projects.filter(project => {
return !project.isHidden && !project.isLocked
})
diff --git a/vue-app/src/views/RecipientProfile.vue b/vue-app/src/views/RecipientProfile.vue
index cb2c548eb..9e3bfecf9 100644
--- a/vue-app/src/views/RecipientProfile.vue
+++ b/vue-app/src/views/RecipientProfile.vue
@@ -4,7 +4,7 @@
{{ $t('recipientProfile.not_found') }}
-
+
{{ recipient.name }}
@@ -22,15 +22,15 @@
{{ $t('projectProfile.h2_1') }}
-
+
{{ $t('projectProfile.h2_2') }}
-
+
{{ $t('projectProfile.h2_3') }}
-
+
-
-
-
+
{{ request.metadata.name }}
|