diff --git a/.env.example b/.env.example index adc9b5d..3ced884 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,5 @@ PINATA_API_KEY= PINATA_SECRET_API_KEY= + +MAINNET_SUBGRAPH= +MAINNET_RPC= \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..eb0ca37 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,46 @@ +# Dockefile from https://notes.ethereum.org/@GW1ZUbNKR5iRjjKYx6_dJQ/Bk8zsJ9xj +# FROM node:22.17.0-bullseye-slim +FROM node@sha256:98663a445a21da13827b841d8df7b4d8743d5133e0d7a4e28ec0852140aa1abe + +# install wget, git and necessary certificates so we can install IPFS below +RUN apt update && apt install --yes --no-install-recommends wget git apt-transport-https ca-certificates && rm -rf /var/lib/apt/lists/* + +# install IPFS +WORKDIR /home/root +RUN wget -qO - https://dist.ipfs.tech/kubo/v0.35.0/kubo_v0.35.0_linux-amd64.tar.gz | tar -xvzf - \ + && cd kubo \ + && ./install.sh \ + && cd .. \ + && rm -rf kubo +RUN ipfs init + +ENV GIT_REPOSITORY=https://codeberg.org/tornadocash/relayers-network-ui.git +# From development branch, double check with tornado.ws +ENV GIT_COMMIT_HASH=e5f1b6f91c372fc58a7a6eb463e21a62a059db3b + +# clone the repository +RUN mkdir /app/ + +WORKDIR /app + +# Simple hack to fetch only commit and nothing more (no need to download 1GB sized repo, only 100MB would be enough) +RUN git init && \ + git remote add origin $GIT_REPOSITORY && \ + git fetch --depth 1 origin $GIT_COMMIT_HASH && \ + git checkout $GIT_COMMIT_HASH + +# install, build and prep for deployment +RUN yarn install --frozen-lockfile --ignore-scripts +RUN yarn build +RUN yarn generate + +# add the build output to IPFS and write the hash to a file +RUN ipfs add --cid-version 1 --quieter --only-hash --recursive ./dist > ipfs_hash.txt +# print the hash for good measure in case someone is looking at the build logs +RUN cat ipfs_hash.txt + +# this entrypoint file will execute `ipfs add` of the build output to the docker host's IPFS API endpoint, so we can easily extract the IPFS build out of the docker image +RUN printf '#!/bin/sh\nipfs --api /ip4/`getent ahostsv4 host.docker.internal | grep STREAM | head -n 1 | cut -d \ -f 1`/tcp/5001 add --cid-version 1 -r ./dist' >> entrypoint.sh +RUN chmod u+x entrypoint.sh + +ENTRYPOINT [ "./entrypoint.sh" ] diff --git a/constants/graph.ts b/constants/graph.ts new file mode 100644 index 0000000..53d09ee --- /dev/null +++ b/constants/graph.ts @@ -0,0 +1,10 @@ +import { ChainId } from '@/types' + +export const GRAPHQL_LIMIT = 1000 + +/** + * todo: add support for subgraph on thegraph & API keys + */ +export const RELAYER_SUBGRAPH_LIST: Record = { + [ChainId.MAINNET]: process.env.MAINNET_SUBGRAPH ?? 'https://tornadocash-rpc.com/subgraphs/name/tornadocash/tornado-governance', +} diff --git a/constants/index.ts b/constants/index.ts index 503fb0e..44a5896 100644 --- a/constants/index.ts +++ b/constants/index.ts @@ -3,6 +3,7 @@ export * from './link' export * from './enums' export * from './steps' export * from './errors' +export * from './graph' export * from './relayer' export * from './variables' export * from './contracts' diff --git a/constants/rpc.ts b/constants/rpc.ts index 90cfec3..e4fdeba 100644 --- a/constants/rpc.ts +++ b/constants/rpc.ts @@ -1,5 +1,5 @@ import { ChainId } from '@/types' -export const RPC_LIST: { [chainId in ChainId]: string } = { - [ChainId.MAINNET]: 'https://tornadocash-rpc.com', +export const RPC_LIST: Record = { + [ChainId.MAINNET]: process.env.MAINNET_RPC ?? 'https://rpc.mevblocker.io', } diff --git a/package.json b/package.json index e8ce1ed..b8d0d94 100644 --- a/package.json +++ b/package.json @@ -4,14 +4,17 @@ "private": true, "type": "module", "scripts": { - "dev": "nuxt", - "build": "nuxt build", - "start": "nuxt start", + "nuxt": "cross-env NODE_OPTIONS=\"--max_old_space_size=8192 --openssl-legacy-provider\" nuxt", + "dev": "yarn nuxt", + "build": "yarn nuxt build", + "start": "yarn nuxt start", "lint": "eslint --ext .js,.ts", "lint:fix": "eslint --ext .js,.ts --quiet --fix", "compile": "typechain --target ethers-v5 --out-dir ./_contracts './abi/*.json'", - "generate": "nuxt generate && cp dist/404.html dist/ipfs-404.html", + "generate": "yarn nuxt generate && cp dist/404.html dist/ipfs-404.html", "prepare": "husky install", + "docker:build": "docker build -t relayers-network-ui .", + "docker:hash": "docker container run --rm -it --entrypoint cat relayers-network-ui /app/ipfs_hash.txt", "ipfs:upload": "node --loader ts-node/esm ipfsUpload.ts" }, "dependencies": { @@ -56,6 +59,7 @@ "@types/node": "^16.10.9", "@typescript-eslint/eslint-plugin": "^4.28.0", "@typescript-eslint/parser": "^4.28.0", + "cross-env": "^7.0.3", "dotenv": "^10.0.0", "eslint": "^7.29.0", "eslint-config-prettier": "^8.3.0", diff --git a/services/ENS/ens.ts b/services/ENS/ens.ts index ad90181..1c194c0 100644 --- a/services/ENS/ens.ts +++ b/services/ENS/ens.ts @@ -7,15 +7,16 @@ async function getEnsOwner(ensName: string, chainId: ChainId) { const { provider } = getProvider(chainId) const ownerAddress = await provider.resolveName(ensName) - return ownerAddress || undefined + return ownerAddress ?? undefined } catch (err) { return undefined } } +/** async function getNameFromHash(ensHash: string) { try { - const response = await fetch('https://tornadocash-rpc.com/subgraphs/name/graphprotocol/ens', { + const response = await fetch('https://api.thegraph.com/subgraphs/name/ensdomains/ens', { body: JSON.stringify({ query: `{ domain(id: "${ensHash}") { @@ -33,4 +34,6 @@ async function getNameFromHash(ensHash: string) { throw new Error(err.message) } } -export { getEnsOwner, getNameFromHash } +**/ + +export { getEnsOwner } diff --git a/services/ENS/index.ts b/services/ENS/index.ts index c2b5e2d..7e05b30 100644 --- a/services/ENS/index.ts +++ b/services/ENS/index.ts @@ -1,9 +1,8 @@ import { checkSubdomains, subdomains } from './ensSubdomains' -import { getEnsOwner, getNameFromHash } from './ens' +import { getEnsOwner } from './ens' export const ensService = { subdomains, getEnsOwner, checkSubdomains, - getNameFromHash, } diff --git a/store/relayer.ts b/store/relayer.ts index a47278c..939e604 100644 --- a/store/relayer.ts +++ b/store/relayer.ts @@ -5,9 +5,26 @@ import { AddStakeParams, AddStakePermitParams, ChainId, RootState } from '@/type import { RelayerMutation, RelayerState } from '@/types/store/relayer' import { getRelayerRegistry } from '@/contracts' -import { DEPLOYED_BLOCK, errors, numbers } from '@/constants' +import { DEPLOYED_BLOCK, GRAPHQL_LIMIT, RELAYER_SUBGRAPH_LIST, errors, numbers } from '@/constants' import { ensService, tornadoRelayerService } from '@/services' import { errorParser, fromWei, toDecimalsPlaces } from '@/utilities' +import { getAddress, parseEther } from 'ethers/lib/utils' +import { BigNumber } from 'ethers' + +interface GraphRelayer { + address: string + ensName: string + ensHash: string + + workers: string[] + stakeBalance: string + blockRegistration: string +} + +interface GraphRelayerFormatted extends Omit { + stakeBalance: BigNumber + registerBlock: number +} export const actions: ActionTree = { async checkIsRelayerRegistered({ getters, commit }) { @@ -24,6 +41,7 @@ export const actions: ActionTree = { } }, + // Unused because block range is too broad async geRelayerWorkers({ getters, commit }) { try { const { walletAddress, chainId } = getters.dependencies @@ -51,11 +69,81 @@ export const actions: ActionTree = { } }, - async getRelayerENSData({ getters, commit, dispatch }, ensHash) { + async getRelayersFromGraph({ getters }) { + try { + const { chainId } = getters.dependencies + + const graphUrl = RELAYER_SUBGRAPH_LIST[chainId as ChainId] + + const res = await fetch(graphUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query: ` + query getRelayers($first: Int) { + relayers(first: $first, orderBy: blockRegistration, orderDirection: asc) { + address + ensName + ensHash + + workers + stakeBalance + blockRegistration + } + _meta { + block { + number + } + hasIndexingErrors + } + } + `, + variables: { + first: GRAPHQL_LIMIT, + }, + }), + }) + + if (!res.ok) { + throw new Error(`Invalid response from ${graphUrl} ${res.statusText}`) + } + + const { data, errors } = await res.json() + + if (errors) { + throw new Error(`Error from graph: ${JSON.stringify(errors)}`) + } + + if (data?._meta?.hasIndexingErrors) { + throw new Error('Subgraph has indexing errors') + } + + return (data.relayers as GraphRelayer[]).map(({ address, ensName, ensHash, workers, stakeBalance, blockRegistration }) => { + if (!workers.includes(address)) { + workers.push(getAddress(address)) + } + + return { + address: getAddress(address), + ensName, + ensHash, + workers, + stakeBalance: parseEther(stakeBalance), + registerBlock: Number(blockRegistration), + } + }) + } catch (err) { + console.log(err) + throw err + } + }, + + async getRelayerENSData({ getters, commit }, ensName) { try { const { chainId } = getters.dependencies - const ensName = await ensService.getNameFromHash(ensHash) const subdomains = await ensService.checkSubdomains(ensName, chainId) const mainnetSubdomain = subdomains.find((el) => el.chainId === ChainId.MAINNET) @@ -74,17 +162,22 @@ export const actions: ActionTree = { } }, - async getRelayers({ getters, commit }) { + async getRelayers({ getters, commit, dispatch }) { try { - const { walletAddress, chainId } = getters.dependencies + const { walletAddress } = getters.dependencies - const registryContract = getRelayerRegistry(chainId) + const relayers = (await dispatch('getRelayersFromGraph')) as GraphRelayerFormatted[] - const { balance, ensHash } = await registryContract.callStatic.relayers(walletAddress) + const relayer = relayers.find((r) => r.address === walletAddress) - commit(RelayerMutation.SET_BALANCE, balance) + if (!relayer) { + throw new Error(`No relayer found for ${walletAddress}`) + } - return ensHash + commit(RelayerMutation.SET_WORKERS, relayer.workers) + commit(RelayerMutation.SET_BALANCE, relayer.stakeBalance) + + await dispatch('getRelayerENSData', relayer.ensName) } catch (err) { throw new Error(err.message) } @@ -98,9 +191,7 @@ export const actions: ActionTree = { throw new Error(errors.relayer.NOT_REGISTERED) } - await dispatch('geRelayerWorkers') - const ensHash = await dispatch('getRelayers') - await dispatch('getRelayerENSData', ensHash) + await dispatch('getRelayers') } catch (err) { const errorText = errorParser(err.message, errors.validation.NO_RESPONSE) diff --git a/yarn.lock b/yarn.lock index 02fd44a..3660e1d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3923,6 +3923,13 @@ create-require@^1.1.0, create-require@^1.1.1: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== +cross-env@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" + integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== + dependencies: + cross-spawn "^7.0.1" + cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -3932,6 +3939,15 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" +cross-spawn@^7.0.1: + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + crypto-browserify@^3.11.0: version "3.12.0" resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec"