diff --git a/next.config.js b/next.config.js index 8b44a17..7fba1bc 100644 --- a/next.config.js +++ b/next.config.js @@ -13,6 +13,10 @@ const nextConfig = { env: { REACT_APP_IPFS_GATEWAY: process.env.REACT_APP_IPFS_GATEWAY, DEPLOYED_APP: process.env.DEPLOYED_APP, + CHIADO_RPC: process.env.CHIADO_RPC, + SEPOLIA_RPC: process.env.SEPOLIA_RPC, + GNOSIS_RPC: process.env.GNOSIS_RPC, + MAINNET_RPC: process.env.MAINNET_RPC }, images: { remotePatterns: [ diff --git a/src/app/[pohid]/CrossChain.tsx b/src/app/[pohid]/CrossChain.tsx index 5132a17..0fd8429 100644 --- a/src/app/[pohid]/CrossChain.tsx +++ b/src/app/[pohid]/CrossChain.tsx @@ -1,25 +1,32 @@ "use client"; -import { useMemo } from "react"; +import { useEffect, useMemo, useState } from "react"; import Modal from "components/Modal"; import TimeAgo from "components/TimeAgo"; import withClientConnected from "components/HighOrder/withClientConnected"; import { SupportedChain, SupportedChainId, + getChainRpc, + getForeignChain, + idToChain, supportedChains, } from "config/chains"; -import { Contract } from "contracts"; +import { Contract, CreationBlockNumber } from "contracts"; import useCCPoHWrite from "contracts/hooks/useCCPoHWrite"; import { HumanityQuery } from "generated/graphql"; import { timeAgo } from "utils/time"; -import { Address, Hash } from "viem"; -import { useAccount, useChainId } from "wagmi"; -import { useObservable } from "@legendapp/state/react"; +import { Address, Hash, createPublicClient, http } from "viem"; +import { mainnet, sepolia, useAccount, useChainId } from "wagmi"; import ChainLogo from "components/ChainLogo"; import { useLoading } from "hooks/useLoading"; import useWeb3Loaded from "hooks/useWeb3Loaded"; import { ContractData } from "data/contract"; +import useRelayWrite from "contracts/hooks/useRelayWrite"; +import gnosisAmbHelper from "contracts/abis/gnosis-amb-helper"; +import { gnosisChiado } from "viem/chains"; +import { toast } from "react-toastify"; +import abis from "contracts/abis"; interface CrossChainProps extends JSX.IntrinsicAttributes { contractData: Record; @@ -32,15 +39,6 @@ interface CrossChainProps extends JSX.IntrinsicAttributes { winningStatus?: string; } -type TransferType = { - transferHash: string, - foreignProxy: Address, - transferTimestamp: string, - senderChain: SupportedChain | undefined, - receivingChain: SupportedChain, - received: boolean, -} - export default withClientConnected(function CrossChain({ pohId, contractData, @@ -55,7 +53,7 @@ export default withClientConnected(function CrossChain({ const { address } = useAccount(); const loading = useLoading(); const web3Loaded = useWeb3Loaded(); - const chainId = useChainId(); + const chainId = useChainId() as SupportedChainId; const [prepareTransfer, doTransfer] = useCCPoHWrite( "transferHumanity", @@ -71,6 +69,7 @@ export default withClientConnected(function CrossChain({ [loading] ) ); + const [prepareUpdate] = useCCPoHWrite( "updateHumanity", useMemo( @@ -86,23 +85,172 @@ export default withClientConnected(function CrossChain({ ) ); - const transfer$ = useObservable({ - transferHash: lastTransfer?.transferHash, - foreignProxy: lastTransfer?.foreignProxy, - transferTimestamp: lastTransfer?.transferTimestamp, - senderChain: lastTransferChain, - receivingChain: supportedChains.find( - (chain) => - Contract.CrossChainProofOfHumanity[chain.id]?.toLowerCase() === - lastTransfer?.foreignProxy - )!, - received: !!supportedChains.find( - (c) => - Contract.CrossChainProofOfHumanity[c.id]?.toLowerCase() === - lastTransfer?.foreignProxy - ), + const [prepareRelayWrite] = useRelayWrite( + "executeSignatures", + useMemo( + () => ({ + onLoading() { + loading.start(); + }, + onReady(fire) { + fire(); + }, + }), + [loading] + ) + ); + + type RelayUpdateParams = { + sideChainId: SupportedChainId, + publicClientSide: any, + encodedData: `0x${string}` + }; + + const [pendingRelayUpdate, setPendingRelayUpdate] = useState({} as RelayUpdateParams); + + const publicClient = lastTransferChain && createPublicClient({ + chain: supportedChains[lastTransferChain.id], + transport: http(getChainRpc(lastTransferChain.id)), }); - const transferState = transfer$.use(); + + const pendingRelayUpdateEthereum = async () => { + if (web3Loaded && + (winningStatus !== "transferring" && winningStatus !== "transferred") + ) { + const sendingChain = idToChain(getForeignChain(chainId)); + const sendingChainId = sendingChain!.id as SupportedChainId; + const publicClientSending = createPublicClient({ + chain: supportedChains[sendingChainId], + transport: http(getChainRpc(sendingChainId)), + }); + const sendingCCPoHAddress = Contract.CrossChainProofOfHumanity[sendingChainId] as Address; + const allSendingTxs = await publicClientSending.getContractEvents({ + address: sendingCCPoHAddress, + abi: abis.CrossChainProofOfHumanity, + eventName: 'UpdateInitiated', + fromBlock: CreationBlockNumber.CrossChainProofOfHumanity[sendingChainId] as bigint, + strict: true, + args: {humanityId: pohId}, + }); + + if (allSendingTxs.length == 0) { + setPendingRelayUpdate({} as RelayUpdateParams); + return ; + } + + const txHashSending = allSendingTxs && allSendingTxs[allSendingTxs.length-1].transactionHash; + + const txSending = await publicClientSending.getTransactionReceipt({hash: txHashSending}); + + const messageIdSending = txSending.logs.at(0)?.topics.at(1); + const expirationTime = txSending.logs.at(1)?.data.substring(0,66); + const expired = Number(expirationTime) < Date.now() / 1000; + if (expired) { + setPendingRelayUpdate({} as RelayUpdateParams); + return ; + } + + // Looking for the received update with same messageId, if there is no such tx, then it is pending + const publicClientReceiving = createPublicClient({ + chain: supportedChains[chainId], + transport: http(getChainRpc(chainId)), + }); + const receivingCCPoHAddress = Contract.CrossChainProofOfHumanity[chainId] as Address; + const allReceivingTxs = await publicClientReceiving.getContractEvents({ + address: receivingCCPoHAddress, + abi: abis.CrossChainProofOfHumanity, + eventName: 'UpdateReceived', + fromBlock: CreationBlockNumber.CrossChainProofOfHumanity[chainId] as bigint, + strict: true, + args: {humanityId: pohId}, + }); + + var messageIdReceiving; + if (allReceivingTxs.length > 0) { + const txHashReceiving = allReceivingTxs[allReceivingTxs.length-1].transactionHash; + + const txReceiving = await publicClientReceiving.getTransactionReceipt({hash: txHashReceiving}); + + // On main and sepolia we look into the first event, on gnosis side its the second one + const eventIndex = (chainId === 1 || chainId === 11155111)? 1 : 2; + messageIdReceiving = txReceiving.logs.at(eventIndex)?.topics.at(3); + } + + if (allReceivingTxs.length == 0 || messageIdSending !== messageIdReceiving) { + const data = (txSending.logs.at(0)?.data); + + // Encoded data has a different length in Gnosis compared to Chiado + const subEnd = sendingChainId === gnosisChiado.id? 754 : 748; + const encodedData = `0x${data?.substring(130, subEnd)}` as `0x${string}`; + + setPendingRelayUpdate({sideChainId: sendingChainId, publicClientSide: publicClientSending, encodedData: encodedData}); + return ; + } + } + setPendingRelayUpdate({} as RelayUpdateParams); + return ; + } + + const showPendingUpdate = () => { + const sendingChainName = idToChain(pendingRelayUpdate.sideChainId)?.name; + const receivingChainName = idToChain(getForeignChain(pendingRelayUpdate.sideChainId))?.name; + return ( + + ⏳ Pending relay + + } + header="Last update" + > +
+ + {sendingChainName} ▶{" "} + {receivingChainName} + + + There is a pending state update that needs to be relayed on {receivingChainName}. + + {(pendingRelayUpdate.sideChainId === 100 || pendingRelayUpdate.sideChainId === 10200)? ( + + ) : ( +
+ + Relaying a state update in this chain can take around 30 minutes + +
+ )} +
+
+ ) + } + + useEffect(()=>{ + if (web3Loaded && + (winningStatus !== "transferring" && winningStatus !== "transferred") + ) + pendingRelayUpdateEthereum() + }, [web3Loaded, chainId]); return (
@@ -116,7 +264,7 @@ export default withClientConnected(function CrossChain({ {web3Loaded && address?.toLowerCase() === claimer && - homeChain.id === chainId && winningStatus !== "transferring" && ( + homeChain.id === chainId && (winningStatus !== "transferring" && winningStatus !== "transferred") && ( (function CrossChain({ )} {web3Loaded && - homeChain.id === chainId && winningStatus === "transferring"? + homeChain.id === chainId && + (winningStatus !== "transferring" && winningStatus !== "transferred") && + (!pendingRelayUpdate || !pendingRelayUpdate.encodedData) && ( (function CrossChain({
- : web3Loaded && - transferState.senderChain?.id === chainId && winningStatus === "transferring"? - // TODO !!!!!!!!!!!!!!!! - // User will cancel this transfer if updates from sending chain - null - : null - } - {transferState.receivingChain && !(transferState.received) && ( + )} + {web3Loaded && + //address?.toLowerCase() === claimer && + //homeChain?.id === chainId && + homeChain && + winningStatus === 'transferred' && publicClient && ( - ⏳ Pending transfer + ⏳ Pending relay } header="Last transfer" >
- {transferState.senderChain?.name} ▶{" "} - {transferState.receivingChain?.name} - - - Received: {String(transferState.received)} - - Transfer hash: {transferState.transferHash?.substring(0, 12)}... + {lastTransferChain?.name} ▶{" "} + {homeChain?.name} + + {homeChain?.id === chainId && + (chainId === mainnet.id || chainId === sepolia.id) ? ( + + ) : (homeChain.id === mainnet.id || homeChain.id === sepolia.id)? ( +
+ + Connect to home chain for relaying the transferring profile + +
+ ) : ( +
+ + Relaying the transferring profile in this chain can take around 30 minutes + +
+ ) + }
)} + {!!pendingRelayUpdate && pendingRelayUpdate.encodedData? + showPendingUpdate() + : null + } ); }); diff --git a/src/app/[pohid]/[chain]/[request]/ActionBar.tsx b/src/app/[pohid]/[chain]/[request]/ActionBar.tsx index 4bb8df7..d720218 100644 --- a/src/app/[pohid]/[chain]/[request]/ActionBar.tsx +++ b/src/app/[pohid]/[chain]/[request]/ActionBar.tsx @@ -22,6 +22,8 @@ import Challenge from "./Challenge"; import FundButton from "./Funding"; import RemoveVouch from "./RemoveVouch"; import Vouch from "./Vouch"; +import { getMyData } from "data/user"; +import useSWR from "swr"; enableReactUse(); @@ -74,6 +76,8 @@ export default withClientConnected(function ActionBar({ const [pending] = loading.use(); const web3Loaded = useWeb3Loaded(); const userChainId = useChainId(); + const { data: me } = useSWR(address, getMyData); + // const [prepareMulticallAdvance, multicallAdvanceFire] = useWagmiWrite( // "Multicall3", // "aggregate", @@ -332,9 +336,9 @@ export default withClientConnected(function ActionBar({ Withdraw ) : !isVouchGranted.didIVouchFor ? ( - + ) : isVouchGranted.isVouchOnchain ? ( - + ) : null} @@ -354,9 +358,9 @@ export default withClientConnected(function ActionBar({ Withdraw ) : !isVouchGranted.didIVouchFor ? ( - + ) : isVouchGranted.isVouchOnchain ? ( - + ) : null}