diff --git a/alm/package.json b/alm/package.json index 8d45f626..65119351 100644 --- a/alm/package.json +++ b/alm/package.json @@ -14,6 +14,7 @@ "@types/react-dom": "^16.9.0", "@types/react-router-dom": "^5.1.5", "@types/styled-components": "^5.1.0", + "@use-it/interval": "^0.1.3", "customize-cra": "^1.0.0", "date-fns": "^2.14.0", "fast-memoize": "^2.5.2", diff --git a/alm/src/components/ExecutionConfirmation.tsx b/alm/src/components/ExecutionConfirmation.tsx index 5bc40ea5..d2f5fee5 100644 --- a/alm/src/components/ExecutionConfirmation.tsx +++ b/alm/src/components/ExecutionConfirmation.tsx @@ -57,7 +57,7 @@ export const ExecutionConfirmation = ({ executionData, isHome }: ExecutionConfir {formattedValidator ? formattedValidator : } {getExecutionStatusElement(executionData.status)} - + {executionData.timestamp > 0 ? formatTimestamp(executionData.timestamp) : ''} diff --git a/alm/src/components/ValidatorsConfirmations.tsx b/alm/src/components/ValidatorsConfirmations.tsx index fa53a2f2..df8d94de 100644 --- a/alm/src/components/ValidatorsConfirmations.tsx +++ b/alm/src/components/ValidatorsConfirmations.tsx @@ -1,11 +1,12 @@ import React from 'react' -import { formatTxHashExtended } from '../utils/networks' +import { formatTimestamp, formatTxHash, getExplorerTxUrl } from '../utils/networks' import { useWindowWidth } from '@react-hook/window-size' import { VALIDATOR_CONFIRMATION_STATUS } from '../config/constants' import { SimpleLoading } from './commons/Loading' import styled from 'styled-components' import { ConfirmationParam } from '../hooks/useMessageConfirmations' import { GreyLabel, RedLabel, SuccessLabel } from './commons/Labels' +import { ExplorerTxLink } from './commons/ExplorerTxLink' const Thead = styled.thead` border-bottom: 2px solid #9e9e9e; @@ -49,7 +50,8 @@ export const ValidatorsConfirmations = ({ Validator - Confirmations + Status + Age @@ -57,10 +59,25 @@ export const ValidatorsConfirmations = ({ const filteredConfirmation = confirmations.filter(c => c.validator === validator) const confirmation = filteredConfirmation.length > 0 ? filteredConfirmation[0] : null const displayedStatus = confirmation && confirmation.status ? confirmation.status : '' + const explorerLink = confirmation && confirmation.txHash ? getExplorerTxUrl(confirmation.txHash, true) : '' + const elementIfNoTimestamp = + displayedStatus !== VALIDATOR_CONFIRMATION_STATUS.WAITING && + displayedStatus !== VALIDATOR_CONFIRMATION_STATUS.NOT_REQUIRED ? ( + + ) : ( + '' + ) return ( - {windowWidth < 850 ? formatTxHashExtended(validator) : validator} + {windowWidth < 850 ? formatTxHash(validator) : validator} {getValidatorStatusElement(displayedStatus)} + + + {confirmation && confirmation.timestamp > 0 + ? formatTimestamp(confirmation.timestamp) + : elementIfNoTimestamp} + + ) })} diff --git a/alm/src/config/constants.ts b/alm/src/config/constants.ts index 5fe96bc9..09fcb5c6 100644 --- a/alm/src/config/constants.ts +++ b/alm/src/config/constants.ts @@ -23,6 +23,7 @@ export const EXECUTE_AFFIRMATION_HASH = 'e7a2c01f' export const SUBMIT_SIGNATURE_HASH = '630cea8e' export const EXECUTE_SIGNATURES_HASH = '3f7658fd' +export const CACHE_KEY_SUCCESS = 'success-confirmation-validator-' export const CACHE_KEY_FAILED = 'failed-confirmation-validator-' export const CACHE_KEY_EXECUTION_FAILED = 'failed-execution-validator-' diff --git a/alm/src/hooks/useMessageConfirmations.ts b/alm/src/hooks/useMessageConfirmations.ts index 6faaef2a..b63457c6 100644 --- a/alm/src/hooks/useMessageConfirmations.ts +++ b/alm/src/hooks/useMessageConfirmations.ts @@ -21,7 +21,8 @@ import { getValidatorFailedTransactionsForMessage, getExecutionFailedTransactionForMessage, getValidatorPendingTransactionsForMessage, - getExecutionPendingTransactionsForMessage + getExecutionPendingTransactionsForMessage, + getValidatorSuccessTransactionsForMessage } from '../utils/explorer' export interface useMessageConfirmationsParams { @@ -33,11 +34,16 @@ export interface useMessageConfirmationsParams { validatorList: string[] } -export interface ConfirmationParam { +export interface BasicConfirmationParam { validator: string status: string } +export interface ConfirmationParam extends BasicConfirmationParam { + txHash: string + timestamp: number +} + export interface ExecutionData { status: string validator: string @@ -221,7 +227,8 @@ export const useMessageConfirmations = ({ getValidatorFailedTransactionsForMessage, setFailedConfirmations, getValidatorPendingTransactionsForMessage, - setPendingConfirmations + setPendingConfirmations, + getValidatorSuccessTransactionsForMessage ) return () => { diff --git a/alm/src/hooks/useTransactionStatus.ts b/alm/src/hooks/useTransactionStatus.ts index c6ad73e2..4c20d5de 100644 --- a/alm/src/hooks/useTransactionStatus.ts +++ b/alm/src/hooks/useTransactionStatus.ts @@ -4,6 +4,7 @@ import { HOME_RPC_POLLING_INTERVAL, TRANSACTION_STATUS } from '../config/constan import { getTransactionStatusDescription } from '../utils/networks' import { useStateProvider } from '../state/StateProvider' import { getHomeMessagesFromReceipt, getForeignMessagesFromReceipt, MessageObject, getBlock } from '../utils/web3' +import useInterval from '@use-it/interval' export const useTransactionStatus = ({ txHash, chainId }: { txHash: string; chainId: number }) => { const { home, foreign } = useStateProvider() @@ -14,6 +15,12 @@ export const useTransactionStatus = ({ txHash, chainId }: { txHash: string; chai const [timestamp, setTimestamp] = useState(0) const [loading, setLoading] = useState(true) + // Update description so the time displayed is accurate + useInterval(() => { + if (!status || !timestamp || !description) return + setDescription(getTransactionStatusDescription(status, timestamp)) + }, 30000) + useEffect( () => { const subscriptions: Array = [] diff --git a/alm/src/services/ValidatorsCache.ts b/alm/src/services/ValidatorsCache.ts index a3406943..9708edf3 100644 --- a/alm/src/services/ValidatorsCache.ts +++ b/alm/src/services/ValidatorsCache.ts @@ -1,8 +1,12 @@ +import { ConfirmationParam } from '../hooks/useMessageConfirmations' + class ValidatorsCache { private readonly store: { [key: string]: boolean } + private readonly dataStore: { [key: string]: ConfirmationParam } constructor() { this.store = {} + this.dataStore = {} } get(key: string) { @@ -12,6 +16,14 @@ class ValidatorsCache { set(key: string, value: boolean) { this.store[key] = value } + + getData(key: string) { + return this.dataStore[key] + } + + setData(key: string, value: ConfirmationParam) { + this.dataStore[key] = value + } } export default new ValidatorsCache() diff --git a/alm/src/utils/explorer.ts b/alm/src/utils/explorer.ts index 8d745c58..8561a6ab 100644 --- a/alm/src/utils/explorer.ts +++ b/alm/src/utils/explorer.ts @@ -154,6 +154,31 @@ export const getFailedTransactions = async ( return transactions.filter(t => t.isError !== '0') } +export const getSuccessTransactions = async ( + account: string, + to: string, + startTimestamp: number, + endTimestamp: number, + api: string, + fetchAccountTransactions: (args: AccountTransactionsParams) => Promise +): Promise => { + const transactions = await fetchAccountTransactions({ account, to, startTimestamp, endTimestamp, api }) + + return transactions.filter(t => t.isError === '0') +} + +export const filterValidatorSignatureTransaction = ( + transactions: APITransaction[], + messageData: string +): APITransaction[] => { + const messageDataValue = messageData.replace('0x', '') + return transactions.filter( + t => + (t.input.includes(SUBMIT_SIGNATURE_HASH) || t.input.includes(EXECUTE_AFFIRMATION_HASH)) && + t.input.includes(messageDataValue) + ) +} + export const getValidatorFailedTransactionsForMessage = async ({ account, to, @@ -170,12 +195,26 @@ export const getValidatorFailedTransactionsForMessage = async ({ fetchAccountTransactionsFromBlockscout ) - const messageDataValue = messageData.replace('0x', '') - return failedTransactions.filter( - t => - (t.input.includes(SUBMIT_SIGNATURE_HASH) || t.input.includes(EXECUTE_AFFIRMATION_HASH)) && - t.input.includes(messageDataValue) + return filterValidatorSignatureTransaction(failedTransactions, messageData) +} + +export const getValidatorSuccessTransactionsForMessage = async ({ + account, + to, + messageData, + startTimestamp, + endTimestamp +}: GetFailedTransactionParams): Promise => { + const transactions = await getSuccessTransactions( + account, + to, + startTimestamp, + endTimestamp, + HOME_EXPLORER_API, + fetchAccountTransactionsFromBlockscout ) + + return filterValidatorSignatureTransaction(transactions, messageData) } export const getExecutionFailedTransactionForMessage = async ({ diff --git a/alm/src/utils/getConfirmationsForTx.ts b/alm/src/utils/getConfirmationsForTx.ts index f570c6b0..1fa2d7c2 100644 --- a/alm/src/utils/getConfirmationsForTx.ts +++ b/alm/src/utils/getConfirmationsForTx.ts @@ -3,6 +3,7 @@ import { Contract } from 'web3-eth-contract' import validatorsCache from '../services/ValidatorsCache' import { CACHE_KEY_FAILED, + CACHE_KEY_SUCCESS, HOME_RPC_POLLING_INTERVAL, ONE_DAY_TIMESTAMP, VALIDATOR_CONFIRMATION_STATUS @@ -13,14 +14,14 @@ import { APIPendingTransaction, GetPendingTransactionParams } from './explorer' -import { ConfirmationParam } from '../hooks/useMessageConfirmations' +import { BasicConfirmationParam, ConfirmationParam } from '../hooks/useMessageConfirmations' export const getValidatorConfirmation = ( web3: Web3, hashMsg: string, bridgeContract: Contract, confirmationContractMethod: Function -) => async (validator: string): Promise => { +) => async (validator: string): Promise => { const hashSenderMsg = web3.utils.soliditySha3Raw(validator, hashMsg) const signatureFromCache = validatorsCache.get(hashSenderMsg) @@ -45,20 +46,65 @@ export const getValidatorConfirmation = ( } } +export const getValidatorSuccessTransaction = ( + bridgeContract: Contract, + messageData: string, + timestamp: number, + getSuccessTransactions: (args: GetFailedTransactionParams) => Promise +) => async (validatorData: BasicConfirmationParam): Promise => { + const { validator } = validatorData + const validatorCacheKey = `${CACHE_KEY_SUCCESS}${validatorData.validator}` + const fromCache = validatorsCache.getData(validatorCacheKey) + + if (fromCache && fromCache.txHash) { + return fromCache + } + + const transactions = await getSuccessTransactions({ + account: validatorData.validator, + to: bridgeContract.options.address, + messageData, + startTimestamp: timestamp, + endTimestamp: timestamp + ONE_DAY_TIMESTAMP + }) + + let txHashTimestamp = 0 + let txHash = '' + const status = VALIDATOR_CONFIRMATION_STATUS.SUCCESS + + if (transactions.length > 0) { + const tx = transactions[0] + txHashTimestamp = parseInt(tx.timeStamp) + txHash = tx.hash + + // cache the result + validatorsCache.setData(validatorCacheKey, { + validator, + status, + txHash, + timestamp: txHashTimestamp + }) + } + + return { + validator, + status, + txHash, + timestamp: txHashTimestamp + } +} + export const getValidatorFailedTransaction = ( bridgeContract: Contract, messageData: string, timestamp: number, getFailedTransactions: (args: GetFailedTransactionParams) => Promise -) => async (validatorData: ConfirmationParam): Promise => { +) => async (validatorData: BasicConfirmationParam): Promise => { const validatorCacheKey = `${CACHE_KEY_FAILED}${validatorData.validator}` - const failedFromCache = validatorsCache.get(validatorCacheKey) + const failedFromCache = validatorsCache.getData(validatorCacheKey) - if (failedFromCache) { - return { - validator: validatorData.validator, - status: VALIDATOR_CONFIRMATION_STATUS.FAILED - } + if (failedFromCache && failedFromCache.txHash) { + return failedFromCache } const failedTransactions = await getFailedTransactions({ @@ -71,14 +117,27 @@ export const getValidatorFailedTransaction = ( const newStatus = failedTransactions.length > 0 ? VALIDATOR_CONFIRMATION_STATUS.FAILED : VALIDATOR_CONFIRMATION_STATUS.UNDEFINED + let txHashTimestamp = 0 + let txHash = '' // If validator signature failed, we cache the result to avoid doing future requests for a result that won't change if (failedTransactions.length > 0) { - validatorsCache.set(validatorCacheKey, true) + const failedTx = failedTransactions[0] + txHashTimestamp = parseInt(failedTx.timeStamp) + txHash = failedTx.hash + + validatorsCache.setData(validatorCacheKey, { + validator: validatorData.validator, + status: newStatus, + txHash, + timestamp: txHashTimestamp + }) } return { validator: validatorData.validator, - status: newStatus + status: newStatus, + txHash, + timestamp: txHashTimestamp } } @@ -86,7 +145,7 @@ export const getValidatorPendingTransaction = ( bridgeContract: Contract, messageData: string, getPendingTransactions: (args: GetPendingTransactionParams) => Promise -) => async (validatorData: ConfirmationParam): Promise => { +) => async (validatorData: BasicConfirmationParam): Promise => { const failedTransactions = await getPendingTransactions({ account: validatorData.validator, to: bridgeContract.options.address, @@ -96,9 +155,20 @@ export const getValidatorPendingTransaction = ( const newStatus = failedTransactions.length > 0 ? VALIDATOR_CONFIRMATION_STATUS.PENDING : VALIDATOR_CONFIRMATION_STATUS.UNDEFINED + let timestamp = 0 + let txHash = '' + + if (failedTransactions.length > 0) { + const failedTx = failedTransactions[0] + timestamp = Math.floor(new Date().getTime() / 1000.0) + txHash = failedTx.hash + } + return { validator: validatorData.validator, - status: newStatus + status: newStatus, + txHash, + timestamp } } @@ -117,9 +187,14 @@ export const getConfirmationsForTx = async ( getFailedTransactions: (args: GetFailedTransactionParams) => Promise, setFailedConfirmations: Function, getPendingTransactions: (args: GetPendingTransactionParams) => Promise, - setPendingConfirmations: Function + setPendingConfirmations: Function, + getSuccessTransactions: (args: GetFailedTransactionParams) => Promise ) => { if (!web3 || !validatorList || !bridgeContract || !waitingBlocksResolved) return + + // If all the information was not collected, then it should retry + let shouldRetry = false + const hashMsg = web3.utils.soliditySha3Raw(messageData) let validatorConfirmations = await Promise.all( validatorList.map(getValidatorConfirmation(web3, hashMsg, bridgeContract, confirmationContractMethod)) @@ -174,28 +249,7 @@ export const getConfirmationsForTx = async ( ) if (missingConfirmations.length > 0) { - const timeoutId = setTimeout( - () => - getConfirmationsForTx( - messageData, - web3, - validatorList, - bridgeContract, - confirmationContractMethod, - setResult, - requiredSignatures, - setSignatureCollected, - waitingBlocksResolved, - subscriptions, - timestamp, - getFailedTransactions, - setFailedConfirmations, - getPendingTransactions, - setPendingConfirmations - ), - HOME_RPC_POLLING_INTERVAL - ) - subscriptions.push(timeoutId) + shouldRetry = true } } else { // If signatures collected, it should set other signatures as not required @@ -207,5 +261,58 @@ export const getConfirmationsForTx = async ( validatorConfirmations = [...successConfirmations, ...notRequiredConfirmations] setSignatureCollected(true) } + + // Set confirmations to update UI and continue requesting the transactions for the signatures setResult(validatorConfirmations) + + // get transactions from success signatures + const successConfirmationWithData = await Promise.all( + validatorConfirmations + .filter(c => c.status === VALIDATOR_CONFIRMATION_STATUS.SUCCESS) + .map(getValidatorSuccessTransaction(bridgeContract, messageData, timestamp, getSuccessTransactions)) + ) + + const successConfirmationWithTxFound = successConfirmationWithData.filter(v => v.txHash !== '') + + const updatedValidatorConfirmations = [...validatorConfirmations] + + if (successConfirmationWithTxFound.length > 0) { + successConfirmationWithTxFound.forEach(validatorData => { + const index = updatedValidatorConfirmations.findIndex(e => e.validator === validatorData.validator) + updatedValidatorConfirmations[index] = validatorData + }) + } + + setResult(updatedValidatorConfirmations) + + // Retry if not all transaction were found for validator confirmations + if (successConfirmationWithTxFound.length < successConfirmationWithData.length) { + shouldRetry = true + } + + if (shouldRetry) { + const timeoutId = setTimeout( + () => + getConfirmationsForTx( + messageData, + web3, + validatorList, + bridgeContract, + confirmationContractMethod, + setResult, + requiredSignatures, + setSignatureCollected, + waitingBlocksResolved, + subscriptions, + timestamp, + getFailedTransactions, + setFailedConfirmations, + getPendingTransactions, + setPendingConfirmations, + getSuccessTransactions + ), + HOME_RPC_POLLING_INTERVAL + ) + subscriptions.push(timeoutId) + } } diff --git a/yarn.lock b/yarn.lock index 20ab8039..4edd5fa5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3220,6 +3220,11 @@ lodash.unescape "4.0.1" semver "5.5.0" +"@use-it/interval@^0.1.3": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@use-it/interval/-/interval-0.1.3.tgz#5d1096b2295d7a5dda8e8022f3abb5f9d9ef27f8" + integrity sha512-chshdtDZTFoWA9aszBz1Cc04Ca9NBD2JTi/GMjdJ+HGm4q7Vy1v71+2mm22r7Kfb2nYW+lTRsPcEHdB/VFVHsQ== + "@web3-js/scrypt-shim@^0.1.0": version "0.1.0" resolved "https://registry.yarnpkg.com/@web3-js/scrypt-shim/-/scrypt-shim-0.1.0.tgz#0bf7529ab6788311d3e07586f7d89107c3bea2cc"