Skip to content

Commit

Permalink
Merge pull request #767 from clrfund/feat/export-project-images
Browse files Browse the repository at this point in the history
Export project images and load them statically without IPFS gateway
  • Loading branch information
yuetloo authored Aug 1, 2024
2 parents 3171017 + 35fedb0 commit caf4ec9
Show file tree
Hide file tree
Showing 39 changed files with 755 additions and 570 deletions.
1 change: 1 addition & 0 deletions contracts/tasks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
83 changes: 83 additions & 0 deletions contracts/tasks/runners/exportImages.ts
Original file line number Diff line number Diff line change
@@ -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,
})
}
})
6 changes: 6 additions & 0 deletions subgraph/config/clrfund-arbitrum.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"network": "arbitrum-one",
"address": "0xc06349D95C30551Ea510bD5F35CfA2151499D60a",
"factoryStartBlock": 96912420,
"recipientRegistryStartBlock": 96912420
}
127 changes: 120 additions & 7 deletions vue-app/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,20 @@
<div v-if="isSidebarShown" id="sidebar" :class="`${showCartPanel ? 'desktop-l' : 'desktop'}`">
<round-information />
</div>
<active-app v-if="isActiveApp" :is-sidebar-shown="isSidebarShown" :show-bread-crumb="showBreadCrumb" />
<static-app v-else :is-sidebar-shown="isSidebarShown" :show-bread-crumb="showBreadCrumb" />
<div
id="content"
:class="{
padded: isVerifyStep || (isSidebarShown && !isCartPadding),
'mr-cart-open': showCartPanel && isSideCartShown,
'mr-cart-closed': !showCartPanel && isSideCartShown,
}"
>
<breadcrumbs v-if="showBreadCrumb" />
<router-view :key="route.path" />
</div>
<div v-if="isSideCartShown" id="cart" :class="`desktop ${showCartPanel ? 'open-cart' : 'closed-cart'}`">
<cart-widget />
</div>
</div>
<mobile-tabs v-if="isMobileTabsShown" />
</div>
Expand All @@ -20,22 +32,29 @@
<script setup lang="ts">
import NavBar from '@/components/NavBar.vue'
import MobileTabs from '@/components/MobileTabs.vue'
import ActiveApp from './components/ActiveApp.vue'
import StaticApp from './components/StaticApp.vue'
// @ts-ignore
import { ModalsContainer } from 'vue-final-modal'
import { getDefaultColorScheme } from '@/utils/theme'
import { operator, isActiveApp } from '@/api/core'
import { useAppStore } from '@/stores'
import { operator } from '@/api/core'
import { useAppStore, useRecipientStore, useUserStore, useWalletStore } from '@/stores'
import { storeToRefs } from 'pinia'
import { useRoute } from 'vue-router'
import { useMeta } from 'vue-meta'
import { getCurrentRound } from './api/round'
import type { WalletUser } from '@/stores'
import type { BrowserProvider } from 'ethers'
const route = useRoute()
const appStore = useAppStore()
const { theme, showCartPanel } = storeToRefs(appStore)
const { theme, showCartPanel, currentRound } = storeToRefs(appStore)
const userStore = useUserStore()
const { currentUser } = storeToRefs(userStore)
const recipientStore = useRecipientStore()
const wallet = useWalletStore()
const { user: walletUser } = storeToRefs(wallet)
const appReady = ref(false)
// https://stackoverflow.com/questions/71785473/how-to-use-vue-meta-with-vue3
// https://www.npmjs.com/package/vue-meta/v/3.0.0-alpha.7
Expand Down Expand Up @@ -100,6 +119,100 @@ watch(theme, () => {
const savedTheme = theme.value
document.documentElement.setAttribute('data-theme', savedTheme || getDefaultColorScheme())
})
const intervals: { [key: string]: any } = {}
const isUserAndRoundLoaded = computed(() => !!currentUser.value && !!currentRound.value)
const isSideCartShown = computed(() => isUserAndRoundLoaded.value && isSidebarShown.value && routeName.value !== 'cart')
const isVerifyStep = computed(() => routeName.value === 'verify-step')
const isCartPadding = computed(() => {
const routes = ['cart']
return routes.includes(routeName.value)
})
function setupLoadIntervals() {
intervals.round = setInterval(() => {
appStore.loadRoundInfo()
}, 60 * 1000)
intervals.recipient = setInterval(async () => {
recipientStore.loadRecipientRegistryInfo()
}, 60 * 1000)
intervals.user = setInterval(() => {
userStore.loadUserInfo()
}, 60 * 1000)
}
onMounted(async () => {
try {
await wallet.reconnect()
} catch (err) {
/* eslint-disable-next-line no-console */
console.warn('Unable to reconnect wallet', err)
}
try {
const roundAddress = appStore.currentRoundAddress || (await getCurrentRound())
if (roundAddress) {
appStore.selectRound(roundAddress)
/* eslint-disable-next-line no-console */
console.log('roundAddress', roundAddress)
}
} catch (err) {
/* eslint-disable-next-line no-console */
console.warn('Failed to get current round:', err)
}
appReady.value = true
try {
await appStore.loadClrFundInfo()
await appStore.loadMACIFactoryInfo()
await appStore.loadRoundInfo()
await recipientStore.loadRecipientRegistryInfo()
appStore.isAppReady = true
setupLoadIntervals()
} catch (err) {
/* eslint-disable-next-line no-console */
console.warn('Failed to load application data:', err)
}
})
onBeforeUnmount(() => {
for (const interval of Object.keys(intervals)) {
clearInterval(intervals[interval])
}
})
watch(walletUser, async () => {
try {
if (walletUser.value) {
const user: WalletUser = {
chainId: walletUser.value.chainId,
walletAddress: walletUser.value.walletAddress,
web3Provider: walletUser.value.web3Provider as BrowserProvider,
}
// make sure factory is loaded
await appStore.loadClrFundInfo()
userStore.loginUser(user)
await userStore.loadUserInfo()
await userStore.loadBrightID()
} else {
await userStore.logoutUser()
}
} catch (err) {
/* eslint-disable-next-line no-console */
console.log('error', err)
}
})
watch(isUserAndRoundLoaded, async () => {
if (!isUserAndRoundLoaded.value) {
return
}
// load contribution when we get round information
await userStore.loadUserInfo()
})
</script>

<style lang="scss">
Expand Down
45 changes: 21 additions & 24 deletions vue-app/src/api/clrFund.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { clrfundContractAddress, clrFundContract, isActiveApp, provider } from './core'
import { clrfundContractAddress, clrFundContract, provider } from './core'
import sdk from '@/graphql/sdk'
import { ERC20 } from './abi'
import { Contract } from 'ethers'
Expand All @@ -21,34 +21,31 @@ export async function getClrFundInfo() {
let recipientRegistryAddress = ''

try {
if (isActiveApp) {
const data = await sdk.GetClrFundInfo({
clrFundAddress: clrfundContractAddress.toLowerCase(),
})
const nativeTokenInfo = data.clrFund?.nativeTokenInfo
if (nativeTokenInfo) {
nativeTokenAddress = nativeTokenInfo.tokenAddress || ''
nativeTokenSymbol = nativeTokenInfo.symbol || ''
nativeTokenDecimals = Number(nativeTokenInfo.decimals) || 0
}

userRegistryAddress = data.clrFund?.contributorRegistryAddress || ''
recipientRegistryAddress = data.clrFund?.recipientRegistryAddress || ''
} else {
nativeTokenAddress = await clrFundContract.nativeToken()
const nativeTokenContract = new Contract(nativeTokenAddress, ERC20, provider)
nativeTokenSymbol = await nativeTokenContract.symbol()
nativeTokenDecimals = await nativeTokenContract.decimals()
userRegistryAddress = await clrFundContract.userRegistry()
recipientRegistryAddress = await clrFundContract.recipientRegistry()
const data = await sdk.GetClrFundInfo({
clrFundAddress: clrfundContractAddress.toLowerCase(),
})
const nativeTokenInfo = data.clrFund?.nativeTokenInfo
if (nativeTokenInfo) {
nativeTokenAddress = nativeTokenInfo.tokenAddress || ''
nativeTokenSymbol = nativeTokenInfo.symbol || ''
nativeTokenDecimals = Number(nativeTokenInfo.decimals) || 0
}

userRegistryAddress = data.clrFund?.contributorRegistryAddress || ''
recipientRegistryAddress = data.clrFund?.recipientRegistryAddress || ''
} catch (err) {
/* eslint-disable-next-line no-console */
console.error('Failed GetClrFundInfo', err)
nativeTokenAddress = await clrFundContract.nativeToken().catch(() => '')
const nativeTokenContract = new Contract(nativeTokenAddress, ERC20, provider)
nativeTokenSymbol = await nativeTokenContract.symbol().catch(() => '')
nativeTokenDecimals = await nativeTokenContract.decimals().catch(() => nativeTokenDecimals)
userRegistryAddress = await clrFundContract.userRegistry().catch(() => '')
recipientRegistryAddress = await clrFundContract.recipientRegistry().catch(() => '')
}

try {
matchingPool = await getMatchingFunds(nativeTokenAddress)
if (nativeTokenAddress) {
matchingPool = await getMatchingFunds(nativeTokenAddress)
}
} catch (err) {
/* eslint-disable-next-line no-console */
console.error('Failed to get matching pool', err)
Expand Down
41 changes: 29 additions & 12 deletions vue-app/src/api/contributions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { clrFundContract, provider } from './core'
import type { Project } from './projects'
import sdk from '@/graphql/sdk'
import { Transaction } from '@/utils/transaction'
import type { GetContributorIndexQuery, GetContributorMessagesQuery, GetTotalContributedQuery } from '@/graphql/API'

export const DEFAULT_CONTRIBUTION_AMOUNT = 5
export const MAX_CONTRIBUTION_AMOUNT = 10000 // See FundingRound.sol
Expand Down Expand Up @@ -99,9 +100,14 @@ export async function getTotalContributed(fundingRoundAddress: string): Promise<
const nativeToken = new Contract(nativeTokenAddress, ERC20, provider)
const balance = await nativeToken.balanceOf(fundingRoundAddress)

const data = await sdk.GetTotalContributed({
fundingRoundAddress: fundingRoundAddress.toLowerCase(),
})
let data: GetTotalContributedQuery
try {
data = await sdk.GetTotalContributed({
fundingRoundAddress: fundingRoundAddress.toLowerCase(),
})
} catch {
return { count: 0, amount: 0n }
}

if (!data.fundingRound?.contributorCount) {
return { count: 0, amount: 0n }
Expand Down Expand Up @@ -149,10 +155,16 @@ export async function getContributorIndex(maciAddress: string, pubKey: PubKey):
if (!maciAddress) {
return null
}
const id = getPubKeyId(maciAddress, pubKey)
const data = await sdk.GetContributorIndex({
publicKeyId: id,
})

let data: GetContributorIndexQuery
try {
const id = getPubKeyId(maciAddress, pubKey)
data = await sdk.GetContributorIndex({
publicKeyId: id,
})
} catch {
return null
}

if (data.publicKeys.length === 0) {
return null
Expand Down Expand Up @@ -197,11 +209,16 @@ export async function getContributorMessages({
return []
}

const key = getPubKeyId(maciAddress, contributorKey.pubKey)
const result = await sdk.GetContributorMessages({
pubKey: key,
contributorAddress: contributorAddress.toLowerCase(),
})
let result: GetContributorMessagesQuery
try {
const key = getPubKeyId(maciAddress, contributorKey.pubKey)
result = await sdk.GetContributorMessages({
pubKey: key,
contributorAddress: contributorAddress.toLowerCase(),
})
} catch {
return []
}

if (!(result.messages && result.messages.length)) {
return []
Expand Down
5 changes: 1 addition & 4 deletions vue-app/src/api/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,7 @@ if (!['simple', 'optimistic', 'kleros'].includes(recipientRegistryType as string
export const recipientRegistryPolicy = import.meta.env.VITE_RECIPIENT_REGISTRY_POLICY
export const operator: string = import.meta.env.VITE_OPERATOR || 'Clr.fund'

export const SUBGRAPH_ENDPOINT =
import.meta.env.VITE_SUBGRAPH_URL || 'https://api.thegraph.com/subgraphs/name/clrfund/clrfund'

export const isActiveApp = Boolean(import.meta.env.VITE_SUBGRAPH_URL)
export const SUBGRAPH_ENDPOINT = import.meta.env.VITE_SUBGRAPH_URL || ''

// application theme
export enum ThemeMode {
Expand Down
Loading

0 comments on commit caf4ec9

Please sign in to comment.