diff --git a/.env b/.env new file mode 100644 index 00000000..0e550378 --- /dev/null +++ b/.env @@ -0,0 +1,5 @@ +NEXT_PUBLIC_CELO_EXPLORER_API_URL=https://explorer.celo.org/mainnet/graphiql +NEXT_PUBLIC_CELO_EXPLORER_API_URL_ALFAJORES=https://explorer.celo.org/alfajores/graphiql +NEXT_PUBLIC_CELO_EXPLORER_API_URL_BAKLAVA=https://explorer.celo.org/baklava/graphiql +NEXT_PUBLIC_WALLET_CONNECT_ID=3f6f578ca4a77fd09bc984689c9d095f +NEXT_PUBLIC_SUBGRAPH_URL=https://api.studio.thegraph.com/query/63311/mento/version/latest diff --git a/.eslintignore b/.eslintignore index b23c34b8..ed13b0e4 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1 @@ -app/graphql/generated \ No newline at end of file +app/graphql/**/generated \ No newline at end of file diff --git a/app/(routes)/proposals/[id]/_components/execution-code.component.tsx b/app/(routes)/proposals/[id]/_components/execution-code.component.tsx index 95237f30..b66c594e 100644 --- a/app/(routes)/proposals/[id]/_components/execution-code.component.tsx +++ b/app/(routes)/proposals/[id]/_components/execution-code.component.tsx @@ -1,27 +1,64 @@ import BlockExplorerLink from "@/app/components/_shared/block-explorer-link/block-explorer-link.component"; -import type { ProposalCall } from "@/app/graphql"; -import { useAccount } from "wagmi"; +import { + GetContractsInfo, + type GetContractsInfoQuery, + type ProposalCall, +} from "@/app/graphql"; +import { useCeloExplorerApi } from "@/app/hooks/useCeloExplorer"; +import { useQuery } from "@apollo/experimental-nextjs-app-support/ssr"; +import { useMemo } from "react"; +import { decodeFunctionData } from "viem"; type Props = { calls: ProposalCall[]; }; +type ContractInfo = { + [address: string]: { + abi?: string; + name?: string; + }; +}; + export default function ExecutionCode({ calls }: Props) { - const { chain } = useAccount(); - const blockExplorerUrl = chain?.blockExplorers?.default.url; + const { name: apiName } = useCeloExplorerApi(); + + const { data, error: apolloError } = useQuery(GetContractsInfo, { + variables: { + addresses: calls.map((call) => call.target.id), + }, + context: { apiName }, + skip: !calls.length, + }); + + if (apolloError) { + // TODO: Sentrify me + console.error( + "Failed to fetch contract metadata from Celo Explorer", + apolloError, + ); + } + + const contractMetadata = getContractMetadata(data); + + const formattedCalls = useMemo( + () => calls.map((call) => formatCall(call, contractMetadata)), + [calls, contractMetadata], + ); + return (

Execution Code

- {calls.map((call, index) => ( -
+ {formattedCalls.map((call, index) => ( +
{index > 0 &&
}
Target {index + 1}
-              
-                {call.target.id}
+              
+                {call.target}
               
             

@@ -36,3 +73,82 @@ export default function ExecutionCode({ calls }: Props) {
); } + +/** + * The Celo Explorer API returns nothing (as in not even undefined) for + * non-existing addresses. So we can't rely on the input amount of + * addresses to equal the output amount of addresses as the output amount + * might be lower than the input amount. Hence this `reduce` to create a map + * of addresses to contract names and ABIs. + */ +function getContractMetadata( + data: GetContractsInfoQuery | undefined, +): ContractInfo | undefined { + return data?.addresses?.reduce((acc, address) => { + if (address && address.hash) { + acc[address.hash] = { + abi: address.smartContract?.abi ?? undefined, + name: address.smartContract?.name ?? undefined, + }; + } + + return acc; + }, {} as ContractInfo); +} + +function formatCall( + call: ProposalCall, + contractMetadata: ContractInfo | undefined, +) { + const address = call.target.id; + const contractName = contractMetadata?.[address]?.name ?? undefined; + const decodedCalldata = decodeCalldata(call, contractMetadata); + + return { + ...call, + target: contractName ? `${contractName} (${address})` : address, + calldata: decodedCalldata || call.calldata, + }; +} + +function decodeCalldata( + call: ProposalCall, + contractMetadata: ContractInfo | undefined, +) { + const address = call.target.id; + const abiRaw = contractMetadata?.[address]?.abi; + + if (typeof abiRaw === "string") { + let abi; + + try { + abi = JSON.parse(abiRaw); + } catch (error) { + // TODO: Sentrify me + console.error( + "Failed to parse ABI. Falling back to returning raw calldata", + error, + ); + + return call.calldata; + } + + let data; + + if (call.calldata.startsWith("0x")) { + try { + data = decodeFunctionData({ abi, data: call.calldata }); + } catch (error) { + // TODO: Sentrify me + console.error("Failed to decode calldata", error); + } + } + + if (data?.functionName) { + // output example: `transfer(0x1234etc, 100)` + return `${data.functionName}(${data.args.length ? data.args.join(", ") : ""})`; + } else { + return call.calldata; + } + } +} diff --git a/app/(routes)/proposals/[id]/page.tsx b/app/(routes)/proposals/[id]/page.tsx index 373fdba1..8e285b9f 100644 --- a/app/(routes)/proposals/[id]/page.tsx +++ b/app/(routes)/proposals/[id]/page.tsx @@ -37,11 +37,8 @@ const Page = ({ params }: { params: { id: string } }) => { useProposalStates(data.proposals); - const [mobileVotingModalActive, setMobileVotingModalActive] = useState(false); - const [mobileParticipantsModalActive, setMobileParticipantsModelActive] = - useState(false); - const proposal = data.proposals[0]; + const { title, description } = proposal.metadata; const currentBlock = useBlockNumber(); const endBlock = useBlock({ blockNumber: BigInt(proposal.endBlock) }); @@ -68,6 +65,10 @@ const Page = ({ params }: { params: { id: string } }) => { } }, [currentBlock, endBlock, proposal.endBlock]); + const [mobileVotingModalActive, setMobileVotingModalActive] = useState(false); + const [mobileParticipantsModalActive, setMobileParticipantsModelActive] = + useState(false); + return (
{!proposal &&
Proposal not found
} diff --git a/app/components/_shared/block-explorer-link/block-explorer-link.component.tsx b/app/components/_shared/block-explorer-link/block-explorer-link.component.tsx index a46e46a3..36859a39 100644 --- a/app/components/_shared/block-explorer-link/block-explorer-link.component.tsx +++ b/app/components/_shared/block-explorer-link/block-explorer-link.component.tsx @@ -1,4 +1,6 @@ +import classNames from "classnames"; import { useAccount } from "wagmi"; +import styles from "./block-explorer-link.module.scss"; type Props = { children: React.ReactNode; @@ -11,7 +13,7 @@ function BlockExplorerLink({ children, type, item }: Props) { const blockExplorerUrl = chain?.blockExplorers?.default.url; return blockExplorerUrl ? ( { + const { apiName } = operation.getContext(); + + if (apiName === "celoExplorer") return CELO_EXPLORER_API_URL; + if (apiName === "celoExplorerAlfajores") + return CELO_EXPLORER_API_URL_ALFAJORES; + if (apiName === "celoExplorerBaklava") + return CELO_EXPLORER_API_URL_BAKLAVA; + + return SUBGRAPH_URL; + }, + // you can disable result caching here if you want to // (this does not work if you are rendering your page with `export const dynamic = "force-static"`) fetchOptions: { cache: "no-store" }, diff --git a/app/graphql/generated/fragment-masking.ts b/app/graphql/celo-explorer/generated/fragment-masking.ts similarity index 99% rename from app/graphql/generated/fragment-masking.ts rename to app/graphql/celo-explorer/generated/fragment-masking.ts index 2ba06f10..fbedede1 100644 --- a/app/graphql/generated/fragment-masking.ts +++ b/app/graphql/celo-explorer/generated/fragment-masking.ts @@ -1,3 +1,4 @@ +/* eslint-disable */ import { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core'; import { FragmentDefinitionNode } from 'graphql'; import { Incremental } from './graphql'; diff --git a/app/graphql/celo-explorer/generated/gql.ts b/app/graphql/celo-explorer/generated/gql.ts new file mode 100644 index 00000000..c3653153 --- /dev/null +++ b/app/graphql/celo-explorer/generated/gql.ts @@ -0,0 +1,42 @@ +/* eslint-disable */ +import * as types from './graphql'; +import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; + +/** + * Map of all GraphQL operations in the project. + * + * This map has several performance disadvantages: + * 1. It is not tree-shakeable, so it will include all operations in the project. + * 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle. + * 3. It does not support dead code elimination, so it will add unused operations. + * + * Therefore it is highly recommended to use the babel or swc plugin for production. + */ +const documents = { + "query getContractsInfo($addresses: [AddressHash!]!) {\n addresses(hashes: $addresses) {\n hash\n smartContract {\n name\n abi\n }\n }\n}": types.GetContractsInfoDocument, +}; + +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + * + * + * @example + * ```ts + * const query = graphql(`query GetUser($id: ID!) { user(id: $id) { name } }`); + * ``` + * + * The query argument is unknown! + * Please regenerate the types. + */ +export function graphql(source: string): unknown; + +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "query getContractsInfo($addresses: [AddressHash!]!) {\n addresses(hashes: $addresses) {\n hash\n smartContract {\n name\n abi\n }\n }\n}"): (typeof documents)["query getContractsInfo($addresses: [AddressHash!]!) {\n addresses(hashes: $addresses) {\n hash\n smartContract {\n name\n abi\n }\n }\n}"]; + +export function graphql(source: string) { + return (documents as any)[source] ?? {}; +} + +export type DocumentType> = TDocumentNode extends DocumentNode< infer TType, any> ? TType : never; \ No newline at end of file diff --git a/app/graphql/celo-explorer/generated/graphql.ts b/app/graphql/celo-explorer/generated/graphql.ts new file mode 100644 index 00000000..f594bac8 --- /dev/null +++ b/app/graphql/celo-explorer/generated/graphql.ts @@ -0,0 +1,787 @@ +/* eslint-disable */ +import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; +export type Maybe = T | null; +export type InputMaybe = Maybe; +export type Exact = { [K in keyof T]: T[K] }; +export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; +export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; +/** All built-in and custom scalars, mapped to their actual values */ +export type Scalars = { + ID: { input: string; output: string; } + String: { input: string; output: string; } + Boolean: { input: boolean; output: boolean; } + Int: { input: number; output: number; } + Float: { input: number; output: number; } + /** + * The address (40 (hex) characters / 160 bits / 20 bytes) is derived from the public key (128 (hex) characters / + * 512 bits / 64 bytes) which is derived from the private key (64 (hex) characters / 256 bits / 32 bytes). + * + * The address is actually the last 40 characters of the keccak-256 hash of the public key with `0x` appended. + */ + AddressHash: { input: any; output: any; } + /** + * An unpadded hexadecimal number with 0 or more digits. Each pair of digits + * maps directly to a byte in the underlying binary representation. When + * interpreted as a number, it should be treated as big-endian. + */ + Data: { input: any; output: any; } + /** + * The `DateTime` scalar type represents a date and time in the UTC + * timezone. The DateTime appears in a JSON response as an ISO8601 formatted + * string, including UTC timezone ("Z"). The parsed date and time string will + * be converted to UTC if there is an offset. + */ + DateTime: { input: any; output: any; } + /** + * The `Decimal` scalar type represents signed double-precision fractional + * values parsed by the `Decimal` library. The Decimal appears in a JSON + * response as a string to preserve precision. + */ + Decimal: { input: any; output: any; } + /** A 32-byte [KECCAK-256](https://en.wikipedia.org/wiki/SHA-3) hash. */ + FullHash: { input: any; output: any; } + /** + * The `JSON` scalar type represents arbitrary JSON string data, represented as UTF-8 + * character sequences. The JSON type is most often used to represent a free-form + * human-readable JSON string. + */ + Json: { input: any; output: any; } + /** The nonce (16 (hex) characters / 128 bits / 8 bytes) is derived from the Proof-of-Work. */ + NonceHash: { input: any; output: any; } + /** + * The smallest fractional unit of Ether. Using wei instead of ether allows code to do integer match instead of using + * floats. + * + * See [Ethereum Homestead Documentation](http://ethdocs.org/en/latest/ether.html) for examples of various denominations of wei. + * + * Etymology of "wei" comes from [Wei Dai (戴維)](https://en.wikipedia.org/wiki/Wei_Dai), a + * [cypherpunk](https://en.wikipedia.org/wiki/Cypherpunk) who came up with b-money, which outlined modern + * cryptocurrencies. + */ + Wei: { input: any; output: any; } +}; + +/** A stored representation of a Web3 address. */ +export type Address = { + __typename?: 'Address'; + celoAccount?: Maybe; + celoValidator?: Maybe; + celoValidatorGroup?: Maybe; + contractCode?: Maybe; + fetchedCoinBalance?: Maybe; + fetchedCoinBalanceBlockNumber?: Maybe; + hash?: Maybe; + online?: Maybe; + smartContract?: Maybe; + transactions?: Maybe; +}; + + +/** A stored representation of a Web3 address. */ +export type AddressTransactionsArgs = { + after?: InputMaybe; + before?: InputMaybe; + count?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; + order?: InputMaybe; +}; + +/** + * A package of data that contains zero or more transactions, the hash of the previous block ("parent"), and optionally + * other data. Because each block (except for the initial "genesis block") points to the previous block, the data + * structure that they form is called a "blockchain". + */ +export type Block = { + __typename?: 'Block'; + consensus?: Maybe; + difficulty?: Maybe; + gasLimit?: Maybe; + gasUsed?: Maybe; + hash?: Maybe; + minerHash?: Maybe; + nonce?: Maybe; + number?: Maybe; + parentHash?: Maybe; + size?: Maybe; + timestamp?: Maybe; + totalDifficulty?: Maybe; +}; + +export enum CallType { + Call = 'CALL', + Callcode = 'CALLCODE', + Delegatecall = 'DELEGATECALL', + Staticcall = 'STATICCALL' +} + +/** Celo account information */ +export type CeloAccount = { + __typename?: 'CeloAccount'; + accountType?: Maybe; + activeGold?: Maybe; + address?: Maybe; + addressInfo?: Maybe
; + attestationsFulfilled?: Maybe; + attestationsRequested?: Maybe; + claims?: Maybe; + group?: Maybe; + lockedGold?: Maybe; + name?: Maybe; + nonvotingLockedGold?: Maybe; + url?: Maybe; + usd?: Maybe; + validator?: Maybe; + voted?: Maybe; + votes?: Maybe; +}; + + +/** Celo account information */ +export type CeloAccountClaimsArgs = { + after?: InputMaybe; + before?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; +}; + + +/** Celo account information */ +export type CeloAccountVotedArgs = { + after?: InputMaybe; + before?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; +}; + +export type CeloAccountConnection = { + __typename?: 'CeloAccountConnection'; + edges?: Maybe>>; + pageInfo: PageInfo; +}; + +export type CeloAccountEdge = { + __typename?: 'CeloAccountEdge'; + cursor?: Maybe; + node?: Maybe; +}; + +/** Celo Claims */ +export type CeloClaims = { + __typename?: 'CeloClaims'; + address?: Maybe; + element?: Maybe; + type?: Maybe; + verified?: Maybe; +}; + +export type CeloClaimsConnection = { + __typename?: 'CeloClaimsConnection'; + edges?: Maybe>>; + pageInfo: PageInfo; +}; + +export type CeloClaimsEdge = { + __typename?: 'CeloClaimsEdge'; + cursor?: Maybe; + node?: Maybe; +}; + +/** Celo network parameters */ +export type CeloParameters = { + __typename?: 'CeloParameters'; + celoToken?: Maybe; + /** @deprecated Use celoToken instead. */ + goldToken?: Maybe; + maxElectableValidators?: Maybe; + minElectableValidators?: Maybe; + numRegisteredValidators?: Maybe; + /** @deprecated Use stableTokens instead. */ + stableToken?: Maybe; + stableTokens?: Maybe; + totalLockedGold?: Maybe; +}; + +/** Celo stable coins */ +export type CeloStableCoins = { + __typename?: 'CeloStableCoins'; + ceur?: Maybe; + creal?: Maybe; + cusd?: Maybe; +}; + +/** Represents a CELO or usd token transfer between addresses. */ +export type CeloTransfer = Node & { + __typename?: 'CeloTransfer'; + blockNumber?: Maybe; + comment?: Maybe; + fromAccountHash?: Maybe; + fromAddressHash?: Maybe; + gasPrice?: Maybe; + gasUsed?: Maybe; + /** The ID of an object */ + id: Scalars['ID']['output']; + input?: Maybe; + logIndex?: Maybe; + timestamp?: Maybe; + toAccountHash?: Maybe; + toAddressHash?: Maybe; + token?: Maybe; + tokenAddress?: Maybe; + tokenId?: Maybe; + tokenType?: Maybe; + transactionHash?: Maybe; + value?: Maybe; +}; + +export type CeloTransferConnection = { + __typename?: 'CeloTransferConnection'; + edges?: Maybe>>; + pageInfo: PageInfo; +}; + +export type CeloTransferEdge = { + __typename?: 'CeloTransferEdge'; + cursor?: Maybe; + node?: Maybe; +}; + +/** Celo validator information */ +export type CeloValidator = { + __typename?: 'CeloValidator'; + account?: Maybe; + activeGold?: Maybe; + address?: Maybe; + addressInfo?: Maybe
; + attestationsFulfilled?: Maybe; + attestationsRequested?: Maybe; + groupAddressHash?: Maybe; + groupInfo?: Maybe; + lastElected?: Maybe; + lastOnline?: Maybe; + lockedGold?: Maybe; + member?: Maybe; + name?: Maybe; + nonvotingLockedGold?: Maybe; + score?: Maybe; + signerAddressHash?: Maybe; + url?: Maybe; + usd?: Maybe; +}; + +export type CeloValidatorConnection = { + __typename?: 'CeloValidatorConnection'; + edges?: Maybe>>; + pageInfo: PageInfo; +}; + +export type CeloValidatorEdge = { + __typename?: 'CeloValidatorEdge'; + cursor?: Maybe; + node?: Maybe; +}; + +/** Celo validator group information */ +export type CeloValidatorGroup = { + __typename?: 'CeloValidatorGroup'; + account?: Maybe; + accumulatedActive?: Maybe; + accumulatedRewards?: Maybe; + activeGold?: Maybe; + address?: Maybe; + addressInfo?: Maybe
; + affiliates?: Maybe; + commission?: Maybe; + lockedGold?: Maybe; + name?: Maybe; + nonvotingLockedGold?: Maybe; + numMembers?: Maybe; + receivableVotes?: Maybe; + rewardsRatio?: Maybe; + url?: Maybe; + usd?: Maybe; + voters?: Maybe; + votes?: Maybe; +}; + + +/** Celo validator group information */ +export type CeloValidatorGroupAffiliatesArgs = { + after?: InputMaybe; + before?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; +}; + + +/** Celo validator group information */ +export type CeloValidatorGroupVotersArgs = { + after?: InputMaybe; + before?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; +}; + +export type CeloValidatorGroupConnection = { + __typename?: 'CeloValidatorGroupConnection'; + edges?: Maybe>>; + pageInfo: PageInfo; +}; + +export type CeloValidatorGroupEdge = { + __typename?: 'CeloValidatorGroupEdge'; + cursor?: Maybe; + node?: Maybe; +}; + +/** Coin balance record */ +export type CoinBalance = Node & { + __typename?: 'CoinBalance'; + blockNumber?: Maybe; + blockTimestamp?: Maybe; + delta?: Maybe; + /** The ID of an object */ + id: Scalars['ID']['output']; + value?: Maybe; +}; + +export type CoinBalanceConnection = { + __typename?: 'CoinBalanceConnection'; + edges?: Maybe>>; + pageInfo: PageInfo; +}; + +export type CoinBalanceEdge = { + __typename?: 'CoinBalanceEdge'; + cursor?: Maybe; + node?: Maybe; +}; + +/** Leaderboard entry */ +export type Competitor = { + __typename?: 'Competitor'; + address?: Maybe; + identity?: Maybe; + points?: Maybe; +}; + +/** Represents a CELO token transfer between addresses. */ +export type GoldTransfer = Node & { + __typename?: 'GoldTransfer'; + blockNumber?: Maybe; + comment?: Maybe; + fromAddressHash?: Maybe; + /** The ID of an object */ + id: Scalars['ID']['output']; + toAddressHash?: Maybe; + transactionHash?: Maybe; + value?: Maybe; +}; + +export type GoldTransferConnection = { + __typename?: 'GoldTransferConnection'; + edges?: Maybe>>; + pageInfo: PageInfo; +}; + +export type GoldTransferEdge = { + __typename?: 'GoldTransferEdge'; + cursor?: Maybe; + node?: Maybe; +}; + +/** Models internal transactions. */ +export type InternalTransaction = Node & { + __typename?: 'InternalTransaction'; + blockNumber?: Maybe; + callType?: Maybe; + createdContractAddressHash?: Maybe; + createdContractCode?: Maybe; + error?: Maybe; + fromAddressHash?: Maybe; + gas?: Maybe; + gasUsed?: Maybe; + /** The ID of an object */ + id: Scalars['ID']['output']; + index?: Maybe; + init?: Maybe; + input?: Maybe; + output?: Maybe; + toAddressHash?: Maybe; + traceAddress?: Maybe; + transactionHash?: Maybe; + transactionIndex?: Maybe; + type?: Maybe; + value?: Maybe; +}; + +export type InternalTransactionConnection = { + __typename?: 'InternalTransactionConnection'; + edges?: Maybe>>; + pageInfo: PageInfo; +}; + +export type InternalTransactionEdge = { + __typename?: 'InternalTransactionEdge'; + cursor?: Maybe; + node?: Maybe; +}; + +export type Node = { + /** The ID of the object. */ + id: Scalars['ID']['output']; +}; + +export type PageInfo = { + __typename?: 'PageInfo'; + /** When paginating forwards, the cursor to continue. */ + endCursor?: Maybe; + /** When paginating forwards, are there more items? */ + hasNextPage: Scalars['Boolean']['output']; + /** When paginating backwards, are there more items? */ + hasPreviousPage: Scalars['Boolean']['output']; + /** When paginating backwards, the cursor to continue. */ + startCursor?: Maybe; +}; + +export type RootQueryType = { + __typename?: 'RootQueryType'; + /** Gets an address by hash. */ + address?: Maybe
; + /** Gets addresses by address hash. */ + addresses?: Maybe>>; + /** Gets a block by number. */ + block?: Maybe; + /** Gets an account by address hash. */ + celoAccount?: Maybe; + /** Gets all the claims given a address hash. */ + celoClaims?: Maybe>>; + /** Gets all elected validator signers. */ + celoElectedValidators?: Maybe>>; + /** Gets Celo network parameters */ + celoParameters?: Maybe; + /** Gets CELO and stable token transfers. */ + celoTransfers?: Maybe; + /** Gets a validator by address hash. */ + celoValidator?: Maybe; + /** Gets a validator group by address hash. */ + celoValidatorGroup?: Maybe; + /** Gets all validator groups. */ + celoValidatorGroups?: Maybe>>; + /** Gets coin balances by address hash */ + coinBalances?: Maybe; + /** Gets CELO token transfers. */ + goldTransfers?: Maybe; + /** Gets latest block number. */ + latestBlock?: Maybe; + /** Gets the leaderboard */ + leaderboard?: Maybe>>; + node?: Maybe; + /** Token transfer transactions. */ + tokenTransferTxs?: Maybe; + /** Gets token transfers by token contract address hash. */ + tokenTransfers?: Maybe; + /** Gets a transaction by hash. */ + transaction?: Maybe; + /** Gets CELO and stable token transfer transactions. */ + transferTxs?: Maybe; +}; + + +export type RootQueryTypeAddressArgs = { + hash: Scalars['AddressHash']['input']; +}; + + +export type RootQueryTypeAddressesArgs = { + hashes: Array; +}; + + +export type RootQueryTypeBlockArgs = { + number: Scalars['Int']['input']; +}; + + +export type RootQueryTypeCeloAccountArgs = { + hash: Scalars['AddressHash']['input']; +}; + + +export type RootQueryTypeCeloClaimsArgs = { + hash: Scalars['AddressHash']['input']; + limit?: InputMaybe; +}; + + +export type RootQueryTypeCeloElectedValidatorsArgs = { + blockNumber: Scalars['Int']['input']; +}; + + +export type RootQueryTypeCeloTransfersArgs = { + addressHash?: InputMaybe; + after?: InputMaybe; + before?: InputMaybe; + count?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; +}; + + +export type RootQueryTypeCeloValidatorArgs = { + hash: Scalars['AddressHash']['input']; +}; + + +export type RootQueryTypeCeloValidatorGroupArgs = { + hash: Scalars['AddressHash']['input']; +}; + + +export type RootQueryTypeCoinBalancesArgs = { + address: Scalars['AddressHash']['input']; + after?: InputMaybe; + before?: InputMaybe; + count?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; +}; + + +export type RootQueryTypeGoldTransfersArgs = { + addressHash?: InputMaybe; + after?: InputMaybe; + before?: InputMaybe; + count?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; +}; + + +export type RootQueryTypeNodeArgs = { + id: Scalars['ID']['input']; +}; + + +export type RootQueryTypeTokenTransferTxsArgs = { + addressHash?: InputMaybe; + after?: InputMaybe; + before?: InputMaybe; + count?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; +}; + + +export type RootQueryTypeTokenTransfersArgs = { + after?: InputMaybe; + before?: InputMaybe; + count?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; + tokenContractAddressHash: Scalars['AddressHash']['input']; +}; + + +export type RootQueryTypeTransactionArgs = { + hash: Scalars['FullHash']['input']; +}; + + +export type RootQueryTypeTransferTxsArgs = { + addressHash?: InputMaybe; + after?: InputMaybe; + before?: InputMaybe; + count?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; +}; + +export type RootSubscriptionType = { + __typename?: 'RootSubscriptionType'; + tokenTransfers?: Maybe>>; +}; + + +export type RootSubscriptionTypeTokenTransfersArgs = { + tokenContractAddressHash: Scalars['AddressHash']['input']; +}; + +/** + * The representation of a verified Smart Contract. + * + * "A contract in the sense of Solidity is a collection of code (its functions) + * and data (its state) that resides at a specific address on the Ethereum + * blockchain." + * http://solidity.readthedocs.io/en/v0.4.24/introduction-to-smart-contracts.html + */ +export type SmartContract = { + __typename?: 'SmartContract'; + abi?: Maybe; + addressHash?: Maybe; + compilerVersion?: Maybe; + contractSourceCode?: Maybe; + name?: Maybe; + optimization?: Maybe; +}; + +export enum SortOrder { + Asc = 'ASC', + Desc = 'DESC' +} + +export enum Status { + Error = 'ERROR', + Ok = 'OK' +} + +/** Represents a token transfer between addresses. */ +export type TokenTransfer = Node & { + __typename?: 'TokenTransfer'; + amount?: Maybe; + amounts?: Maybe>>; + blockHash?: Maybe; + blockNumber?: Maybe; + comment?: Maybe; + fromAddressHash?: Maybe; + /** The ID of an object */ + id: Scalars['ID']['output']; + logIndex?: Maybe; + toAddressHash?: Maybe; + tokenContractAddressHash?: Maybe; + tokenId?: Maybe; + tokenIds?: Maybe>>; + transactionHash?: Maybe; +}; + +export type TokenTransferConnection = { + __typename?: 'TokenTransferConnection'; + edges?: Maybe>>; + pageInfo: PageInfo; +}; + +export type TokenTransferEdge = { + __typename?: 'TokenTransferEdge'; + cursor?: Maybe; + node?: Maybe; +}; + +/** Models a Web3 transaction. */ +export type Transaction = Node & { + __typename?: 'Transaction'; + blockNumber?: Maybe; + createdContractAddressHash?: Maybe; + cumulativeGasUsed?: Maybe; + error?: Maybe; + fromAddressHash?: Maybe; + gas?: Maybe; + gasPrice?: Maybe; + gasUsed?: Maybe; + hash?: Maybe; + /** The ID of an object */ + id: Scalars['ID']['output']; + index?: Maybe; + input?: Maybe; + internalTransactions?: Maybe; + nonce?: Maybe; + r?: Maybe; + s?: Maybe; + status?: Maybe; + toAddressHash?: Maybe; + v?: Maybe; + value?: Maybe; +}; + + +/** Models a Web3 transaction. */ +export type TransactionInternalTransactionsArgs = { + after?: InputMaybe; + before?: InputMaybe; + count?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; +}; + +export type TransactionConnection = { + __typename?: 'TransactionConnection'; + edges?: Maybe>>; + pageInfo: PageInfo; +}; + +export type TransactionEdge = { + __typename?: 'TransactionEdge'; + cursor?: Maybe; + node?: Maybe; +}; + +/** Represents a CELO token transfer between addresses. */ +export type TransferTx = Node & { + __typename?: 'TransferTx'; + addressHash?: Maybe; + blockNumber?: Maybe; + celoTransfer?: Maybe; + feeCurrency?: Maybe; + feeToken?: Maybe; + gasPrice?: Maybe; + gasUsed?: Maybe; + gatewayFee?: Maybe; + gatewayFeeRecipient?: Maybe; + /** The ID of an object */ + id: Scalars['ID']['output']; + input?: Maybe; + timestamp?: Maybe; + tokenTransfer?: Maybe; + transactionHash?: Maybe; +}; + + +/** Represents a CELO token transfer between addresses. */ +export type TransferTxCeloTransferArgs = { + after?: InputMaybe; + before?: InputMaybe; + count?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; +}; + + +/** Represents a CELO token transfer between addresses. */ +export type TransferTxTokenTransferArgs = { + after?: InputMaybe; + before?: InputMaybe; + count?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; +}; + +export type TransferTxConnection = { + __typename?: 'TransferTxConnection'; + edges?: Maybe>>; + pageInfo: PageInfo; +}; + +export type TransferTxEdge = { + __typename?: 'TransferTxEdge'; + cursor?: Maybe; + node?: Maybe; +}; + +export enum Type { + Call = 'CALL', + Create = 'CREATE', + Reward = 'REWARD', + Selfdestruct = 'SELFDESTRUCT' +} + +export type GetContractsInfoQueryVariables = Exact<{ + addresses: Array | Scalars['AddressHash']['input']; +}>; + + +export type GetContractsInfoQuery = { __typename?: 'RootQueryType', addresses?: Array<{ __typename?: 'Address', hash?: any | null, smartContract?: { __typename?: 'SmartContract', name?: string | null, abi?: any | null } | null } | null> | null }; + + +export const GetContractsInfoDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"getContractsInfo"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"addresses"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"AddressHash"}}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"addresses"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"hashes"},"value":{"kind":"Variable","name":{"kind":"Name","value":"addresses"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hash"}},{"kind":"Field","name":{"kind":"Name","value":"smartContract"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"abi"}}]}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/app/graphql/generated/index.ts b/app/graphql/celo-explorer/generated/index.ts similarity index 100% rename from app/graphql/generated/index.ts rename to app/graphql/celo-explorer/generated/index.ts diff --git a/app/graphql/celo-explorer/queries/getContractsInfo.graphql b/app/graphql/celo-explorer/queries/getContractsInfo.graphql new file mode 100644 index 00000000..d5f805e6 --- /dev/null +++ b/app/graphql/celo-explorer/queries/getContractsInfo.graphql @@ -0,0 +1,9 @@ +query getContractsInfo($addresses: [AddressHash!]!) { + addresses(hashes: $addresses) { + hash + smartContract { + name + abi + } + } +} diff --git a/app/graphql/index.ts b/app/graphql/index.ts index e1741ae1..3510747c 100644 --- a/app/graphql/index.ts +++ b/app/graphql/index.ts @@ -1,9 +1,18 @@ import { - GetProposalsDocument as GetProposals, GetProposalDocument as GetProposal, -} from "./generated/graphql"; + GetProposalsDocument as GetProposals, +} from "./subgraph/generated/graphql"; -export * from "./generated"; -export * from "./generated/graphql"; +export * from "./subgraph/generated"; +export * from "./subgraph/generated/graphql"; + +// We can't blindly export ALL generated types from the celo-explorer schema +// because some of them conflict with the subgraph schema. So we need to pick +// only the ones we need +import { + GetContractsInfoDocument as GetContractsInfo, + GetContractsInfoQuery, +} from "./celo-explorer/generated/graphql"; -export { GetProposals, GetProposal }; +export { GetContractsInfo, GetProposal, GetProposals }; +export type { GetContractsInfoQuery }; diff --git a/app/graphql/fragments/proposalFields.graphql b/app/graphql/subgraph/fragments/proposalFields.graphql similarity index 100% rename from app/graphql/fragments/proposalFields.graphql rename to app/graphql/subgraph/fragments/proposalFields.graphql diff --git a/app/graphql/subgraph/generated/fragment-masking.ts b/app/graphql/subgraph/generated/fragment-masking.ts new file mode 100644 index 00000000..fbedede1 --- /dev/null +++ b/app/graphql/subgraph/generated/fragment-masking.ts @@ -0,0 +1,67 @@ +/* eslint-disable */ +import { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core'; +import { FragmentDefinitionNode } from 'graphql'; +import { Incremental } from './graphql'; + + +export type FragmentType> = TDocumentType extends DocumentTypeDecoration< + infer TType, + any +> + ? [TType] extends [{ ' $fragmentName'?: infer TKey }] + ? TKey extends string + ? { ' $fragmentRefs'?: { [key in TKey]: TType } } + : never + : never + : never; + +// return non-nullable if `fragmentType` is non-nullable +export function useFragment( + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> +): TType; +// return nullable if `fragmentType` is nullable +export function useFragment( + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> | null | undefined +): TType | null | undefined; +// return array of non-nullable if `fragmentType` is array of non-nullable +export function useFragment( + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> +): ReadonlyArray; +// return array of nullable if `fragmentType` is array of nullable +export function useFragment( + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> | null | undefined +): ReadonlyArray | null | undefined; +export function useFragment( + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> | ReadonlyArray>> | null | undefined +): TType | ReadonlyArray | null | undefined { + return fragmentType as any; +} + + +export function makeFragmentData< + F extends DocumentTypeDecoration, + FT extends ResultOf +>(data: FT, _fragment: F): FragmentType { + return data as FragmentType; +} +export function isFragmentReady( + queryNode: DocumentTypeDecoration, + fragmentNode: TypedDocumentNode, + data: FragmentType, any>> | null | undefined +): data is FragmentType { + const deferredFields = (queryNode as { __meta__?: { deferredFields: Record } }).__meta__ + ?.deferredFields; + + if (!deferredFields) return true; + + const fragDef = fragmentNode.definitions[0] as FragmentDefinitionNode | undefined; + const fragName = fragDef?.name?.value; + + const fields = (fragName && deferredFields[fragName]) || []; + return fields.length > 0 && fields.every(field => data && field in data); +} diff --git a/app/graphql/generated/gql.ts b/app/graphql/subgraph/generated/gql.ts similarity index 100% rename from app/graphql/generated/gql.ts rename to app/graphql/subgraph/generated/gql.ts diff --git a/app/graphql/generated/graphql.ts b/app/graphql/subgraph/generated/graphql.ts similarity index 100% rename from app/graphql/generated/graphql.ts rename to app/graphql/subgraph/generated/graphql.ts diff --git a/app/graphql/subgraph/generated/index.ts b/app/graphql/subgraph/generated/index.ts new file mode 100644 index 00000000..f5159916 --- /dev/null +++ b/app/graphql/subgraph/generated/index.ts @@ -0,0 +1,2 @@ +export * from "./fragment-masking"; +export * from "./gql"; \ No newline at end of file diff --git a/app/graphql/policies/Proposal.ts b/app/graphql/subgraph/policies/Proposal.ts similarity index 100% rename from app/graphql/policies/Proposal.ts rename to app/graphql/subgraph/policies/Proposal.ts diff --git a/app/graphql/queries/getLocks.graphql b/app/graphql/subgraph/queries/getLocks.graphql similarity index 100% rename from app/graphql/queries/getLocks.graphql rename to app/graphql/subgraph/queries/getLocks.graphql diff --git a/app/graphql/queries/getProposal.graphql b/app/graphql/subgraph/queries/getProposal.graphql similarity index 100% rename from app/graphql/queries/getProposal.graphql rename to app/graphql/subgraph/queries/getProposal.graphql diff --git a/app/graphql/queries/getProposals.graphql b/app/graphql/subgraph/queries/getProposals.graphql similarity index 100% rename from app/graphql/queries/getProposals.graphql rename to app/graphql/subgraph/queries/getProposals.graphql diff --git a/app/helpers/load-env-var.ts b/app/helpers/load-env-var.ts new file mode 100644 index 00000000..626ad73d --- /dev/null +++ b/app/helpers/load-env-var.ts @@ -0,0 +1,19 @@ +/** + * Load an environment variable in a typesafe way and throw an error if it is not set + * This way, consuming code can rely on the variable being a string and not having to + * implement its own `undefined` checks. + * + * NOTE: The reason we need to pass `process.env.ACTUAL_VAR_NAME` instead of just a string + * 'ACTUAL_VAR_NAME' is because of how Next.js handles environment variables. It will only + * inline the value of the environment variable if it is used directly in the code, but not + * if being resolved dynamically in a helper function like this. + * + * Source: https://nextjs.org/docs/app/building-your-application/configuring/environment-variables#bundling-environment-variables-for-the-browser + */ +export default function loadEnvVar(envVar: string | undefined): string { + if (envVar == null) { + throw new Error(`Environment variable ${name} is not set`); + } + + return envVar; +} diff --git a/app/hooks/useCeloExplorer.ts b/app/hooks/useCeloExplorer.ts new file mode 100644 index 00000000..f39db91b --- /dev/null +++ b/app/hooks/useCeloExplorer.ts @@ -0,0 +1,41 @@ +import { useChainId } from "wagmi"; +import loadEnvVar from "../helpers/load-env-var"; + +const CELO_EXPLORER_API_URL = loadEnvVar( + process.env.NEXT_PUBLIC_CELO_EXPLORER_API_URL, +); +const CELO_EXPLORER_API_URL_ALFAJORES = loadEnvVar( + process.env.NEXT_PUBLIC_CELO_EXPLORER_API_URL_ALFAJORES, +); +const CELO_EXPLORER_API_URL_BAKLAVA = loadEnvVar( + process.env.NEXT_PUBLIC_CELO_EXPLORER_API_URL_BAKLAVA, +); + +type CeloExplorerApi = { + name: "celoExplorer" | "celoExplorerAlfajores" | "celoExplorerBaklava"; + url: string; +}; + +export const useCeloExplorerApi = (): CeloExplorerApi => { + const chainId = useChainId(); + let result: CeloExplorerApi = { + name: "celoExplorer", + url: CELO_EXPLORER_API_URL, + }; + + if (chainId === 44787) { + result = { + name: "celoExplorerAlfajores", + url: CELO_EXPLORER_API_URL_ALFAJORES, + }; + } + + if (chainId === 62320) { + result = { + name: "celoExplorerBaklava", + url: CELO_EXPLORER_API_URL_BAKLAVA, + }; + } + + return result; +}; diff --git a/codegen.ts b/codegen.ts new file mode 100644 index 00000000..71092b0e --- /dev/null +++ b/codegen.ts @@ -0,0 +1,29 @@ +import { CodegenConfig } from "@graphql-codegen/cli"; +import "dotenv/config"; +import loadEnvVar from "./app/helpers/load-env-var"; + +const CELO_EXPLORER_API_URL = loadEnvVar( + process.env.NEXT_PUBLIC_CELO_EXPLORER_API_URL, +); +const SUBGRAPH_URL = loadEnvVar(process.env.NEXT_PUBLIC_SUBGRAPH_URL); + +const config: CodegenConfig = { + generates: { + "./app/graphql/subgraph/generated/": { + schema: [SUBGRAPH_URL, "./schema.client.graphql"], + documents: ["app/graphql/subgraph/**/*.graphql"], + preset: "client", + presetConfig: { + gqlTagName: "gql", + }, + }, + "./app/graphql/celo-explorer/generated/": { + schema: CELO_EXPLORER_API_URL, + documents: ["app/graphql/celo-explorer/**/*.graphql"], + preset: "client", + }, + }, + ignoreNoDocuments: true, +}; + +export default config; diff --git a/codegen.yaml b/codegen.yaml deleted file mode 100644 index e63b2cd1..00000000 --- a/codegen.yaml +++ /dev/null @@ -1,11 +0,0 @@ -schema: - - https://api.studio.thegraph.com/query/63311/mento/version/latest - - ./schema.client.graphql -documents: - - "app/graphql/**/*.graphql" -generates: - ./app/graphql/generated/: - preset: client - presetConfig: - gqlTagName: gql -ignoreNoDocuments: true diff --git a/package-lock.json b/package-lock.json index 07334a44..fbc2882f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,6 +52,7 @@ "@types/react-copy-to-clipboard": "^5.0.7", "@types/react-dom": "^18", "autoprefixer": "^10", + "dotenv": "^16.4.5", "eslint": "^8", "eslint-config-next": "13.5.4", "eslint-config-prettier": "^9.1.0", @@ -12150,14 +12151,14 @@ "dev": true }, "node_modules/dotenv": { - "version": "16.3.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", - "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", "engines": { "node": ">=12" }, "funding": { - "url": "https://github.com/motdotla/dotenv?sponsor=1" + "url": "https://dotenvx.com" } }, "node_modules/downshift": { diff --git a/package.json b/package.json index 9cbfd846..c82fea0c 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "start": "next start", "lint": "next lint", "lint:fix": "next lint --fix", - "codegen": "graphql-codegen --config codegen.yaml", + "codegen": "graphql-codegen", "todo": "npx leasot '**/*.{ts,tsx,scss,gql}' --ignore 'node_modules/**/*','.git/**/*','app/graphql/generated' || true", "prepare": "husky" }, @@ -57,6 +57,7 @@ "@types/react-copy-to-clipboard": "^5.0.7", "@types/react-dom": "^18", "autoprefixer": "^10", + "dotenv": "^16.4.5", "eslint": "^8", "eslint-config-next": "13.5.4", "eslint-config-prettier": "^9.1.0", diff --git a/tailwind.config.ts b/tailwind.config.ts index 4863940f..cc1084e0 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,22 +1,20 @@ -import type { Config } from 'tailwindcss' +import type { Config } from "tailwindcss"; const config: Config = { content: [ - './pages/**/*.{js,ts,jsx,tsx,mdx}', - './components/**/*.{js,ts,jsx,tsx,mdx}', - './app/**/*.{js,ts,jsx,tsx,mdx}', + "./pages/**/*.{js,ts,jsx,tsx,mdx}", + "./components/**/*.{js,ts,jsx,tsx,mdx}", + "./app/**/*.{js,ts,jsx,tsx,mdx}", ], theme: { extend: { backgroundImage: { - 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', - 'gradient-conic': - 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', + "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", + "gradient-conic": + "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", }, }, }, - plugins: [ - require('@tailwindcss/typography') - ], -} -export default config + plugins: [require("@tailwindcss/typography")], +}; +export default config;