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"