Compare commits
3 Commits
master
...
refactor/a
Author | SHA1 | Date | |
---|---|---|---|
|
a973e23a9e | ||
|
a1e258f424 | ||
|
e976dac616 |
4
.github/workflows/main.yml
vendored
4
.github/workflows/main.yml
vendored
@ -77,7 +77,7 @@ jobs:
|
||||
- name: Rebuild and push updated images
|
||||
run: |
|
||||
function check_if_image_exists() {
|
||||
curl -fsSlL "https://${{ github.actor }}:${{ github.token }}@${DOCKER_REGISTRY}/v2/${DOCKER_REPO}/tokenbridge-e2e-$1/manifests/$2" > /dev/null
|
||||
curl -fsSlL -H 'Authorization: bearer ${{ github.token }}' "https://${DOCKER_REGISTRY}/v2/${DOCKER_REPO}/tokenbridge-e2e-$1/manifests/$2" > /dev/null
|
||||
}
|
||||
updated=()
|
||||
if ! check_if_image_exists e2e ${E2E_TAG}; then updated+=("e2e"); fi
|
||||
@ -104,7 +104,7 @@ jobs:
|
||||
- name: Rebuild and push molecule runner e2e image
|
||||
run: |
|
||||
function check_if_image_exists() {
|
||||
curl -fsSlL "https://${{ github.actor }}:${{ github.token }}@${DOCKER_REGISTRY}/v2/${DOCKER_REPO}/tokenbridge-e2e-$1/manifests/$2" > /dev/null
|
||||
curl -fsSlL -H 'Authorization: bearer ${{ github.token }}' "https://${DOCKER_REGISTRY}/v2/${DOCKER_REPO}/tokenbridge-e2e-$1/manifests/$2" > /dev/null
|
||||
}
|
||||
if check_if_image_exists molecule_runner ${MOLECULE_RUNNER_TAG}; then
|
||||
echo "Image already exists"
|
||||
|
@ -8,11 +8,11 @@ COMMON_HOME_RPC_URL | The HTTPS URL(s) used to communicate to the RPC nodes in t
|
||||
COMMON_FOREIGN_RPC_URL | The HTTPS URL(s) used to communicate to the RPC nodes in the Foreign network. Several URLs can be specified, delimited by spaces. If the connection to one of these nodes is lost the next URL is used for connection. | URL(s)
|
||||
COMMON_HOME_BRIDGE_ADDRESS | The address of the bridge contract address in the Home network. It is used to listen to events from and send validators' transactions to the Home network. | hexidecimal beginning with "0x"
|
||||
COMMON_FOREIGN_BRIDGE_ADDRESS | The address of the bridge contract address in the Foreign network. It is used to listen to events from and send validators' transactions to the Foreign network. | hexidecimal beginning with "0x"
|
||||
COMMON_HOME_GAS_PRICE_SUPPLIER_URL | The URL used to get a JSON response from the gas price prediction oracle for the Home network. The gas price provided by the oracle is used to send the validator's transactions to the RPC node. Since it is assumed that the Home network has a predefined gas price (e.g. the gas price in the Core of POA.Network is `1 GWei`), the gas price oracle parameter can be omitted for such networks. Set to `eip1559-gas-estimation` if you want to use EIP1559 RPC-based gas estimation. | URL
|
||||
COMMON_HOME_GAS_PRICE_SUPPLIER_URL | The URL used to get a JSON response from the gas price prediction oracle for the Home network. The gas price provided by the oracle is used to send the validator's transactions to the RPC node. Since it is assumed that the Home network has a predefined gas price (e.g. the gas price in the Core of POA.Network is `1 GWei`), the gas price oracle parameter can be omitted for such networks. | URL
|
||||
COMMON_HOME_GAS_PRICE_SPEED_TYPE | Assuming the gas price oracle responds with the following JSON structure: `{"fast": 20.0, "block_time": 12.834, "health": true, "standard": 6.0, "block_number": 6470469, "instant": 71.0, "slow": 1.889}`, this parameter specifies the desirable transaction speed. The speed type can be omitted when `COMMON_HOME_GAS_PRICE_SUPPLIER_URL` is not used. | `instant` / `fast` / `standard` / `slow`
|
||||
COMMON_HOME_GAS_PRICE_FALLBACK | The gas price (in Wei) that is used if both the oracle and the fall back gas price specified in the Home Bridge contract are not available. | integer
|
||||
COMMON_HOME_GAS_PRICE_FACTOR | A value that will multiply the gas price of the oracle to convert it to gwei. If the oracle API returns gas prices in gwei then this can be set to `1`. Also, it could be used to intentionally pay more gas than suggested by the oracle to guarantee the transaction verification. E.g. `1.25` or `1.5`. | integer
|
||||
COMMON_FOREIGN_GAS_PRICE_SUPPLIER_URL | The URL used to get a JSON response from the gas price prediction oracle for the Foreign network. The provided gas price is used to send the validator's transactions to the RPC node. If the Foreign network is Ethereum Foundation mainnet, the oracle URL can be: https://gasprice.poa.network. Otherwise this parameter can be omitted. Set to `gas-price-oracle` if you want to use npm `gas-price-oracle` package for retrieving gas price from multiple sources. Set to `eip1559-gas-estimation` if you want to use EIP1559 RPC-based gas estimation. | URL
|
||||
COMMON_FOREIGN_GAS_PRICE_SUPPLIER_URL | The URL used to get a JSON response from the gas price prediction oracle for the Foreign network. The provided gas price is used to send the validator's transactions to the RPC node. If the Foreign network is Ethereum Foundation mainnet, the oracle URL can be: https://gasprice.poa.network. Otherwise this parameter can be omitted. Set to `gas-price-oracle` if you want to use npm `gas-price-oracle` package for retrieving gas price from multiple sources. | URL
|
||||
COMMON_FOREIGN_GAS_PRICE_SPEED_TYPE | Assuming the gas price oracle responds with the following JSON structure: `{"fast": 20.0, "block_time": 12.834, "health": true, "standard": 6.0, "block_number": 6470469, "instant": 71.0, "slow": 1.889}`, this parameter specifies the desirable transaction speed. The speed type can be omitted when `COMMON_FOREIGN_GAS_PRICE_SUPPLIER_URL`is not used. | `instant` / `fast` / `standard` / `slow`
|
||||
COMMON_FOREIGN_GAS_PRICE_FALLBACK | The gas price (in Wei) used if both the oracle and fall back gas price specified in the Foreign Bridge contract are not available. | integer
|
||||
COMMON_FOREIGN_GAS_PRICE_FACTOR | A value that will multiply the gas price of the oracle to convert it to gwei. If the oracle API returns gas prices in gwei then this can be set to `1`. Also, it could be used to intentionally pay more gas than suggested by the oracle to guarantee the transaction verification. E.g. `1.25` or `1.5`. | integer
|
||||
@ -52,15 +52,6 @@ ORACLE_SHUTDOWN_CONTRACT_ADDRESS | Optional contract address in the side chain a
|
||||
ORACLE_SHUTDOWN_CONTRACT_METHOD | Method signature to be used in the side chain to identify the current shutdown status. Method should return boolean. Default value is `isShutdown()`. | `function signature`
|
||||
ORACLE_FOREIGN_RPC_BLOCK_POLLING_LIMIT | Max length for the block range used in `eth_getLogs` requests for polling contract events for the Foreign chain. Infinite, if not provided. | `integer`
|
||||
ORACLE_HOME_RPC_BLOCK_POLLING_LIMIT | Max length for the block range used in `eth_getLogs` requests for polling contract events for the Home chain. Infinite, if not provided. | `integer`
|
||||
ORACLE_JSONRPC_ERROR_CODES | Override default JSON rpc error codes that can trigger RPC fallback to the next URL from the list (or a retry in case of a single RPC URL). Default is `-32603,-32002,-32005`. Should be a comma-separated list of negative integers. | `string`
|
||||
ORACLE_HOME_EVENTS_REPROCESSING | If set to `true`, home events happened in the past will be refetched and processed once again, to ensure that nothing was missed on the first pass. | `bool`
|
||||
ORACLE_HOME_EVENTS_REPROCESSING_BATCH_SIZE | Batch size for one `eth_getLogs` request when reprocessing old logs in the home chain. Defaults to `1000` | `integer`
|
||||
ORACLE_HOME_EVENTS_REPROCESSING_BLOCK_DELAY | Block confirmations number, after which old logs are being reprocessed in the home chain. Defaults to `500` | `integer`
|
||||
ORACLE_HOME_RPC_SYNC_STATE_CHECK_INTERVAL | Interval for checking JSON RPC sync state, by requesting the latest block number. Oracle will switch to the fallback JSON RPC in case sync process is stuck | `integer`
|
||||
ORACLE_FOREIGN_EVENTS_REPROCESSING | If set to `true`, foreign events happened in the past will be refetched and processed once again, to ensure that nothing was missed on the first pass. | `bool`
|
||||
ORACLE_FOREIGN_EVENTS_REPROCESSING_BATCH_SIZE | Batch size for one `eth_getLogs` request when reprocessing old logs in the foreign chain. Defaults to `500` | `integer`
|
||||
ORACLE_FOREIGN_EVENTS_REPROCESSING_BLOCK_DELAY | Block confirmations number, after which old logs are being reprocessed in the foreign chain. Defaults to `250` | `integer`
|
||||
ORACLE_FOREIGN_RPC_SYNC_STATE_CHECK_INTERVAL | Interval for checking JSON RPC sync state, by requesting the latest block number. Oracle will switch to the fallback JSON RPC in case sync process is stuck | `integer`
|
||||
|
||||
|
||||
## Monitor configuration
|
||||
|
@ -19,7 +19,6 @@ COPY --from=contracts /mono/contracts/build ./contracts/build
|
||||
COPY commons/package.json ./commons/
|
||||
COPY oracle-e2e/package.json ./oracle-e2e/
|
||||
COPY monitor-e2e/package.json ./monitor-e2e/
|
||||
COPY oracle/src/utils/constants.js ./oracle/src/utils/constants.js
|
||||
|
||||
COPY yarn.lock .
|
||||
RUN NOYARNPOSTINSTALL=1 yarn install --frozen-lockfile --production
|
||||
|
@ -19,7 +19,7 @@ Sub-repositories maintained within this monorepo are listed below.
|
||||
|
||||
| Sub-repository | Description |
|
||||
| --- | --- |
|
||||
| [Oracle](oracle/README.md) | Responsible for listening to bridge related events and authorizing asset transfers. |
|
||||
| [Oracle](oracle/README.md) | Oracle responsible for listening to bridge related events and authorizing asset transfers. |
|
||||
| [Monitor](monitor/README.md) | Tool for checking balances and unprocessed events in bridged networks. |
|
||||
| [Deployment](deployment/README.md) | Ansible playbooks for deploying cross-chain bridges. |
|
||||
| [Oracle-E2E](oracle-e2e/README.md) | End to end tests for the Oracle |
|
||||
|
@ -1,9 +1,9 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import React from 'react'
|
||||
import { TransactionReceipt } from 'web3-eth'
|
||||
import { useMessageConfirmations } from '../hooks/useMessageConfirmations'
|
||||
import { MessageObject } from '../utils/web3'
|
||||
import styled from 'styled-components'
|
||||
import { CONFIRMATIONS_STATUS, VALIDATOR_CONFIRMATION_STATUS } from '../config/constants'
|
||||
import { CONFIRMATIONS_STATUS } from '../config/constants'
|
||||
import { CONFIRMATIONS_STATUS_LABEL, CONFIRMATIONS_STATUS_LABEL_HOME } from '../config/descriptions'
|
||||
import { SimpleLoading } from './commons/Loading'
|
||||
import { ValidatorsConfirmations } from './ValidatorsConfirmations'
|
||||
@ -54,9 +54,7 @@ export const ConfirmationsContainer = ({
|
||||
home: { name: homeName },
|
||||
foreign: { name: foreignName }
|
||||
} = useStateProvider()
|
||||
const src = useValidatorContract(fromHome, receipt ? receipt.blockNumber : 0)
|
||||
const [executionBlockNumber, setExecutionBlockNumber] = useState(0)
|
||||
const dst = useValidatorContract(!fromHome, executionBlockNumber || 'latest')
|
||||
const { requiredSignatures, validatorList } = useValidatorContract({ fromHome, receipt })
|
||||
const { blockConfirmations } = useBlockConfirmations({ fromHome, receipt })
|
||||
const {
|
||||
confirmations,
|
||||
@ -73,21 +71,11 @@ export const ConfirmationsContainer = ({
|
||||
fromHome,
|
||||
homeStartBlock,
|
||||
foreignStartBlock,
|
||||
requiredSignatures: src.requiredSignatures,
|
||||
validatorList: src.validatorList,
|
||||
targetValidatorList: dst.validatorList,
|
||||
requiredSignatures,
|
||||
validatorList,
|
||||
blockConfirmations
|
||||
})
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (executionBlockNumber || executionData.status !== VALIDATOR_CONFIRMATION_STATUS.EXECUTION_SUCCESS) return
|
||||
|
||||
setExecutionBlockNumber(executionData.blockNumber)
|
||||
},
|
||||
[executionData.status, executionBlockNumber, executionData.blockNumber]
|
||||
)
|
||||
|
||||
const statusLabel = fromHome ? CONFIRMATIONS_STATUS_LABEL_HOME : CONFIRMATIONS_STATUS_LABEL
|
||||
|
||||
const parseDescription = () => {
|
||||
@ -126,22 +114,20 @@ export const ConfirmationsContainer = ({
|
||||
</MultiLine>
|
||||
</StatusDescription>
|
||||
<ValidatorsConfirmations
|
||||
confirmations={fromHome ? confirmations.filter(c => dst.validatorList.includes(c.validator)) : confirmations}
|
||||
requiredSignatures={dst.requiredSignatures}
|
||||
validatorList={dst.validatorList}
|
||||
confirmations={confirmations}
|
||||
requiredSignatures={requiredSignatures}
|
||||
validatorList={validatorList}
|
||||
waitingBlocksResolved={waitingBlocksResolved}
|
||||
/>
|
||||
{signatureCollected && (
|
||||
<ExecutionConfirmation
|
||||
message={message}
|
||||
messageData={message.data}
|
||||
executionData={executionData}
|
||||
isHome={!fromHome}
|
||||
confirmations={confirmations}
|
||||
signatureCollected={signatureCollected}
|
||||
setExecutionData={setExecutionData}
|
||||
executionEventsFetched={executionEventsFetched}
|
||||
setPendingExecution={setPendingExecution}
|
||||
dstRequiredSignatures={dst.requiredSignatures}
|
||||
dstValidatorList={dst.validatorList}
|
||||
/>
|
||||
)}
|
||||
</StyledConfirmationContainer>
|
||||
|
@ -4,47 +4,38 @@ import { useWindowWidth } from '@react-hook/window-size'
|
||||
import { SEARCHING_TX, VALIDATOR_CONFIRMATION_STATUS, ALM_HOME_TO_FOREIGN_MANUAL_EXECUTION } from '../config/constants'
|
||||
import { SimpleLoading } from './commons/Loading'
|
||||
import styled from 'styled-components'
|
||||
import { ConfirmationParam, ExecutionData } from '../hooks/useMessageConfirmations'
|
||||
import { ExecutionData } from '../hooks/useMessageConfirmations'
|
||||
import { GreyLabel, RedLabel, SuccessLabel } from './commons/Labels'
|
||||
import { ExplorerTxLink } from './commons/ExplorerTxLink'
|
||||
import { Thead, AgeTd, StatusTd } from './commons/Table'
|
||||
import { ManualExecutionButton } from './ManualExecutionButton'
|
||||
import { useStateProvider } from '../state/StateProvider'
|
||||
import { matchesRule, MessageObject, WarnRule } from '../utils/web3'
|
||||
import { WarningAlert } from './commons/WarningAlert'
|
||||
import { ErrorAlert } from './commons/ErrorAlert'
|
||||
|
||||
const StyledExecutionConfirmation = styled.div`
|
||||
margin-top: 30px;
|
||||
`
|
||||
|
||||
export interface ExecutionConfirmationParams {
|
||||
message: MessageObject
|
||||
messageData: string
|
||||
executionData: ExecutionData
|
||||
setExecutionData: Function
|
||||
confirmations: ConfirmationParam[]
|
||||
signatureCollected: boolean | string[]
|
||||
isHome: boolean
|
||||
executionEventsFetched: boolean
|
||||
setPendingExecution: Function
|
||||
dstRequiredSignatures: number
|
||||
dstValidatorList: string[]
|
||||
}
|
||||
|
||||
export const ExecutionConfirmation = ({
|
||||
message,
|
||||
messageData,
|
||||
executionData,
|
||||
setExecutionData,
|
||||
confirmations,
|
||||
signatureCollected,
|
||||
isHome,
|
||||
executionEventsFetched,
|
||||
setPendingExecution,
|
||||
dstRequiredSignatures,
|
||||
dstValidatorList
|
||||
setPendingExecution
|
||||
}: ExecutionConfirmationParams) => {
|
||||
const { foreign } = useStateProvider()
|
||||
const [safeExecutionAvailable, setSafeExecutionAvailable] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [warning, setWarning] = useState('')
|
||||
const availableManualExecution =
|
||||
!isHome &&
|
||||
(executionData.status === VALIDATOR_CONFIRMATION_STATUS.WAITING ||
|
||||
@ -76,27 +67,9 @@ export const ExecutionConfirmation = ({
|
||||
[availableManualExecution, foreign.bridgeContract]
|
||||
)
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (!message.data || !executionData || !availableManualExecution) return
|
||||
|
||||
try {
|
||||
const fileName = 'warnRules'
|
||||
const rules: WarnRule[] = require(`../snapshots/${fileName}.json`)
|
||||
for (let rule of rules) {
|
||||
if (matchesRule(rule, message)) {
|
||||
setWarning(rule.message)
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
},
|
||||
[availableManualExecution, executionData, message, message.data, setWarning]
|
||||
)
|
||||
|
||||
const getExecutionStatusElement = (validatorStatus = '') => {
|
||||
switch (validatorStatus) {
|
||||
case VALIDATOR_CONFIRMATION_STATUS.EXECUTION_SUCCESS:
|
||||
case VALIDATOR_CONFIRMATION_STATUS.SUCCESS:
|
||||
return <SuccessLabel>{validatorStatus}</SuccessLabel>
|
||||
case VALIDATOR_CONFIRMATION_STATUS.FAILED:
|
||||
return <RedLabel>{validatorStatus}</RedLabel>
|
||||
@ -114,8 +87,6 @@ export const ExecutionConfirmation = ({
|
||||
|
||||
return (
|
||||
<StyledExecutionConfirmation>
|
||||
{error && <ErrorAlert onClick={() => setError('')} error={error} />}
|
||||
{warning && <WarningAlert onClick={() => setWarning('')} error={warning} />}
|
||||
<table>
|
||||
<Thead>
|
||||
<tr>
|
||||
@ -154,13 +125,10 @@ export const ExecutionConfirmation = ({
|
||||
<td>
|
||||
<ManualExecutionButton
|
||||
safeExecutionAvailable={safeExecutionAvailable}
|
||||
messageData={message.data}
|
||||
messageData={messageData}
|
||||
setExecutionData={setExecutionData}
|
||||
confirmations={confirmations}
|
||||
signatureCollected={signatureCollected as string[]}
|
||||
setPendingExecution={setPendingExecution}
|
||||
setError={setError}
|
||||
requiredSignatures={dstRequiredSignatures}
|
||||
validatorList={dstValidatorList}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
|
@ -8,6 +8,7 @@ import { TransactionReceipt } from 'web3-eth'
|
||||
import { InfoAlert } from './commons/InfoAlert'
|
||||
import { ExplorerTxLink } from './commons/ExplorerTxLink'
|
||||
import { FOREIGN_NETWORK_NAME, HOME_NETWORK_NAME } from '../config/constants'
|
||||
import { ErrorAlert } from './commons/ErrorAlert'
|
||||
|
||||
const StyledMainPage = styled.div`
|
||||
text-align: center;
|
||||
@ -51,7 +52,7 @@ export interface FormSubmitParams {
|
||||
|
||||
export const MainPage = () => {
|
||||
const history = useHistory()
|
||||
const { home, foreign } = useStateProvider()
|
||||
const { home, foreign, error, setError } = useStateProvider()
|
||||
const [networkName, setNetworkName] = useState('')
|
||||
const [receipt, setReceipt] = useState<Maybe<TransactionReceipt>>(null)
|
||||
const [showInfoAlert, setShowInfoAlert] = useState(false)
|
||||
@ -131,6 +132,7 @@ export const MainPage = () => {
|
||||
</AlertP>
|
||||
</InfoAlert>
|
||||
)}
|
||||
{error && <ErrorAlert onClick={() => setError('')} error={error} />}
|
||||
<Route exact path={['/']} children={<Form onSubmit={onFormSubmit} />} />
|
||||
<Route
|
||||
path={['/:chainId/:txHash/:messageIdParam', '/:chainId/:txHash']}
|
||||
|
@ -14,7 +14,6 @@ import { useStateProvider } from '../state/StateProvider'
|
||||
import { signatureToVRS, packSignatures } from '../utils/signatures'
|
||||
import { getSuccessExecutionData } from '../utils/getFinalizationEvent'
|
||||
import { TransactionReceipt } from 'web3-eth'
|
||||
import { ConfirmationParam } from '../hooks/useMessageConfirmations'
|
||||
|
||||
const ActionButton = styled.button`
|
||||
color: var(--button-color);
|
||||
@ -31,75 +30,21 @@ interface ManualExecutionButtonParams {
|
||||
safeExecutionAvailable: boolean
|
||||
messageData: string
|
||||
setExecutionData: Function
|
||||
confirmations: ConfirmationParam[]
|
||||
signatureCollected: string[]
|
||||
setPendingExecution: Function
|
||||
setError: Function
|
||||
requiredSignatures: number
|
||||
validatorList: string[]
|
||||
}
|
||||
|
||||
export const ManualExecutionButton = ({
|
||||
safeExecutionAvailable,
|
||||
messageData,
|
||||
setExecutionData,
|
||||
confirmations,
|
||||
setPendingExecution,
|
||||
setError,
|
||||
requiredSignatures,
|
||||
validatorList
|
||||
signatureCollected,
|
||||
setPendingExecution
|
||||
}: ManualExecutionButtonParams) => {
|
||||
const { foreign } = useStateProvider()
|
||||
const { foreign, setError } = useStateProvider()
|
||||
const { library, activate, account, active } = useWeb3React()
|
||||
const [manualExecution, setManualExecution] = useState(false)
|
||||
const [allowFailures, setAllowFailures] = useState(false)
|
||||
const [ready, setReady] = useState(false)
|
||||
const [title, setTitle] = useState('Loading')
|
||||
const [validSignatures, setValidSignatures] = useState<string[]>([])
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (
|
||||
!foreign.bridgeContract ||
|
||||
!foreign.web3 ||
|
||||
!confirmations ||
|
||||
!confirmations.length ||
|
||||
!requiredSignatures ||
|
||||
!validatorList ||
|
||||
!validatorList.length
|
||||
)
|
||||
return
|
||||
|
||||
const signatures = []
|
||||
for (let i = 0; i < confirmations.length && signatures.length < requiredSignatures; i++) {
|
||||
const sig = confirmations[i].signature
|
||||
if (!sig) {
|
||||
continue
|
||||
}
|
||||
const { v, r, s } = signatureToVRS(sig)
|
||||
const signer = foreign.web3.eth.accounts.recover(messageData, `0x${v}`, `0x${r}`, `0x${s}`)
|
||||
if (validatorList.includes(signer)) {
|
||||
signatures.push(sig)
|
||||
}
|
||||
}
|
||||
|
||||
if (signatures.length >= requiredSignatures) {
|
||||
setValidSignatures(signatures.slice(0, requiredSignatures))
|
||||
setTitle('Execute')
|
||||
setReady(true)
|
||||
} else {
|
||||
setTitle('Unavailable')
|
||||
}
|
||||
},
|
||||
[
|
||||
foreign.bridgeContract,
|
||||
foreign.web3,
|
||||
validatorList,
|
||||
requiredSignatures,
|
||||
messageData,
|
||||
setValidSignatures,
|
||||
confirmations
|
||||
]
|
||||
)
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
@ -127,9 +72,9 @@ export const ManualExecutionButton = ({
|
||||
return
|
||||
}
|
||||
|
||||
if (!library || !foreign.bridgeContract || !foreign.web3 || !validSignatures || !validSignatures.length) return
|
||||
if (!library || !foreign.bridgeContract || !signatureCollected || !signatureCollected.length) return
|
||||
|
||||
const signatures = packSignatures(validSignatures.map(signatureToVRS))
|
||||
const signatures = packSignatures(signatureCollected.map(signatureToVRS))
|
||||
const messageId = messageData.slice(0, 66)
|
||||
const bridge = foreign.bridgeContract
|
||||
const executeMethod =
|
||||
@ -194,20 +139,19 @@ export const ManualExecutionButton = ({
|
||||
foreign.bridgeContract,
|
||||
setError,
|
||||
messageData,
|
||||
signatureCollected,
|
||||
setExecutionData,
|
||||
setPendingExecution,
|
||||
safeExecutionAvailable,
|
||||
allowFailures,
|
||||
foreign.web3,
|
||||
validSignatures
|
||||
allowFailures
|
||||
]
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="is-center">
|
||||
<ActionButton disabled={!ready} className="button outline" onClick={() => setManualExecution(true)}>
|
||||
{title}
|
||||
<ActionButton className="button outline" onClick={() => setManualExecution(true)}>
|
||||
Execute
|
||||
</ActionButton>
|
||||
</div>
|
||||
{safeExecutionAvailable && (
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import { formatTimestamp, formatTxHash, getExplorerTxUrl } from '../utils/networks'
|
||||
import { useWindowWidth } from '@react-hook/window-size'
|
||||
import { RECENT_AGE, SEARCHING_TX, VALIDATOR_CONFIRMATION_STATUS } from '../config/constants'
|
||||
import { SEARCHING_TX, VALIDATOR_CONFIRMATION_STATUS } from '../config/constants'
|
||||
import { SimpleLoading } from './commons/Loading'
|
||||
import styled from 'styled-components'
|
||||
import { ConfirmationParam } from '../hooks/useMessageConfirmations'
|
||||
@ -31,9 +31,7 @@ export const ValidatorsConfirmations = ({
|
||||
const getValidatorStatusElement = (validatorStatus = '') => {
|
||||
switch (validatorStatus) {
|
||||
case VALIDATOR_CONFIRMATION_STATUS.SUCCESS:
|
||||
case VALIDATOR_CONFIRMATION_STATUS.MANUAL:
|
||||
case VALIDATOR_CONFIRMATION_STATUS.FAILED_VALID:
|
||||
return <SuccessLabel>{VALIDATOR_CONFIRMATION_STATUS.SUCCESS}</SuccessLabel>
|
||||
return <SuccessLabel>{validatorStatus}</SuccessLabel>
|
||||
case VALIDATOR_CONFIRMATION_STATUS.FAILED:
|
||||
return <RedLabel>{validatorStatus}</RedLabel>
|
||||
case VALIDATOR_CONFIRMATION_STATUS.PENDING:
|
||||
@ -60,28 +58,26 @@ export const ValidatorsConfirmations = ({
|
||||
</tr>
|
||||
</Thead>
|
||||
<tbody>
|
||||
{confirmations.map((confirmation, i) => {
|
||||
const displayedStatus = confirmation.status
|
||||
const explorerLink = getExplorerTxUrl(confirmation.txHash, true)
|
||||
let elementIfNoTimestamp: any = <SimpleLoading />
|
||||
switch (displayedStatus) {
|
||||
case '':
|
||||
case VALIDATOR_CONFIRMATION_STATUS.UNDEFINED:
|
||||
if (waitingBlocksResolved) {
|
||||
elementIfNoTimestamp = SEARCHING_TX
|
||||
}
|
||||
break
|
||||
case VALIDATOR_CONFIRMATION_STATUS.WAITING:
|
||||
case VALIDATOR_CONFIRMATION_STATUS.NOT_REQUIRED:
|
||||
elementIfNoTimestamp = ''
|
||||
break
|
||||
case VALIDATOR_CONFIRMATION_STATUS.MANUAL:
|
||||
elementIfNoTimestamp = RECENT_AGE
|
||||
break
|
||||
}
|
||||
{validatorList.map((validator, i) => {
|
||||
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 ? (
|
||||
(displayedStatus === VALIDATOR_CONFIRMATION_STATUS.UNDEFINED || displayedStatus === '') &&
|
||||
waitingBlocksResolved ? (
|
||||
SEARCHING_TX
|
||||
) : (
|
||||
<SimpleLoading />
|
||||
)
|
||||
) : (
|
||||
''
|
||||
)
|
||||
return (
|
||||
<tr key={i}>
|
||||
<td>{windowWidth < 850 ? formatTxHash(confirmation.validator) : confirmation.validator}</td>
|
||||
<td>{windowWidth < 850 ? formatTxHash(validator) : validator}</td>
|
||||
<StatusTd className="text-center">{getValidatorStatusElement(displayedStatus)}</StatusTd>
|
||||
<AgeTd className="text-center">
|
||||
{confirmation && confirmation.timestamp > 0 ? (
|
||||
@ -98,7 +94,7 @@ export const ValidatorsConfirmations = ({
|
||||
</tbody>
|
||||
</table>
|
||||
<RequiredConfirmations>
|
||||
At least <strong>{requiredSignatures}</strong> of <strong>{validatorList.length}</strong> confirmations required
|
||||
{requiredSignatures} of {validatorList.length} confirmations required
|
||||
</RequiredConfirmations>
|
||||
</div>
|
||||
)
|
||||
|
@ -33,7 +33,7 @@ export const ErrorAlert = ({ onClick, error }: { onClick: () => void; error: str
|
||||
}
|
||||
return (
|
||||
<div className="row is-center">
|
||||
<StyledErrorAlert className="col-12 is-vertical-align row">
|
||||
<StyledErrorAlert className="col-10 is-vertical-align row">
|
||||
<InfoIcon color="var(--failed-color)" />
|
||||
<TextContainer className="col-10">
|
||||
{text}
|
||||
|
@ -1,34 +0,0 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { InfoIcon } from './InfoIcon'
|
||||
import { CloseIcon } from './CloseIcon'
|
||||
|
||||
const StyledErrorAlert = styled.div`
|
||||
border: 1px solid var(--warning-color);
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
padding-top: 10px;
|
||||
`
|
||||
|
||||
const CloseIconContainer = styled.div`
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
const TextContainer = styled.div`
|
||||
white-space: pre-wrap;
|
||||
flex-direction: column;
|
||||
`
|
||||
|
||||
export const WarningAlert = ({ onClick, error }: { onClick: () => void; error: string }) => {
|
||||
return (
|
||||
<div className="row is-center">
|
||||
<StyledErrorAlert className="col-12 is-vertical-align row">
|
||||
<InfoIcon color="var(--warning-color)" />
|
||||
<TextContainer className="col-10">{error}</TextContainer>
|
||||
<CloseIconContainer className="col-1 is-vertical-align is-center" onClick={onClick}>
|
||||
<CloseIcon color="var(--warning-color)" />
|
||||
</CloseIconContainer>
|
||||
</StyledErrorAlert>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -54,19 +54,14 @@ export const CONFIRMATIONS_STATUS = {
|
||||
}
|
||||
|
||||
export const VALIDATOR_CONFIRMATION_STATUS = {
|
||||
SUCCESS: 'Confirmed',
|
||||
MANUAL: 'Manual',
|
||||
EXECUTION_SUCCESS: 'Executed',
|
||||
SUCCESS: 'Success',
|
||||
FAILED: 'Failed',
|
||||
FAILED_VALID: 'Failed valid',
|
||||
PENDING: 'Pending',
|
||||
WAITING: 'Waiting',
|
||||
NOT_REQUIRED: 'Not required',
|
||||
UNDEFINED: 'UNDEFINED'
|
||||
}
|
||||
|
||||
export const RECENT_AGE = 'Recent'
|
||||
|
||||
export const SEARCHING_TX = 'Searching Transaction...'
|
||||
|
||||
export const INCORRECT_CHAIN_ERROR = `Incorrect chain chosen. Switch to ${FOREIGN_NETWORK_NAME} in the wallet.`
|
||||
|
@ -29,16 +29,17 @@ export interface useMessageConfirmationsParams {
|
||||
foreignStartBlock: Maybe<number>
|
||||
requiredSignatures: number
|
||||
validatorList: string[]
|
||||
targetValidatorList: string[]
|
||||
blockConfirmations: number
|
||||
}
|
||||
|
||||
export interface ConfirmationParam {
|
||||
export interface BasicConfirmationParam {
|
||||
validator: string
|
||||
status: string
|
||||
}
|
||||
|
||||
export interface ConfirmationParam extends BasicConfirmationParam {
|
||||
txHash: string
|
||||
timestamp: number
|
||||
signature?: string
|
||||
}
|
||||
|
||||
export interface ExecutionData {
|
||||
@ -47,7 +48,6 @@ export interface ExecutionData {
|
||||
txHash: string
|
||||
timestamp: number
|
||||
executionResult: boolean
|
||||
blockNumber: number
|
||||
}
|
||||
|
||||
export const useMessageConfirmations = ({
|
||||
@ -58,7 +58,6 @@ export const useMessageConfirmations = ({
|
||||
foreignStartBlock,
|
||||
requiredSignatures,
|
||||
validatorList,
|
||||
targetValidatorList,
|
||||
blockConfirmations
|
||||
}: useMessageConfirmationsParams) => {
|
||||
const { home, foreign } = useStateProvider()
|
||||
@ -66,7 +65,7 @@ export const useMessageConfirmations = ({
|
||||
const [status, setStatus] = useState(CONFIRMATIONS_STATUS.UNDEFINED)
|
||||
const [waitingBlocks, setWaitingBlocks] = useState(false)
|
||||
const [waitingBlocksResolved, setWaitingBlocksResolved] = useState(false)
|
||||
const [signatureCollected, setSignatureCollected] = useState(false)
|
||||
const [signatureCollected, setSignatureCollected] = useState<boolean | string[]>(false)
|
||||
const [executionEventsFetched, setExecutionEventsFetched] = useState(false)
|
||||
const [collectedSignaturesEvent, setCollectedSignaturesEvent] = useState<Maybe<EventData>>(null)
|
||||
const [executionData, setExecutionData] = useState<ExecutionData>({
|
||||
@ -74,8 +73,7 @@ export const useMessageConfirmations = ({
|
||||
validator: '',
|
||||
txHash: '',
|
||||
timestamp: 0,
|
||||
executionResult: false,
|
||||
blockNumber: 0
|
||||
executionResult: false
|
||||
})
|
||||
const [waitingBlocksForExecution, setWaitingBlocksForExecution] = useState(false)
|
||||
const [waitingBlocksForExecutionResolved, setWaitingBlocksForExecutionResolved] = useState(false)
|
||||
@ -142,9 +140,10 @@ export const useMessageConfirmations = ({
|
||||
// The collected signature event is only fetched once the signatures are collected on tx from home to foreign, to calculate if
|
||||
// the execution tx on the foreign network is waiting for block confirmations
|
||||
// This is executed if the message is in Home to Foreign direction only
|
||||
const hasCollectedSignatures = !!signatureCollected // true or string[]
|
||||
useEffect(
|
||||
() => {
|
||||
if (!fromHome || !receipt || !home.web3 || !home.bridgeContract || !signatureCollected) return
|
||||
if (!fromHome || !receipt || !home.web3 || !home.bridgeContract || !hasCollectedSignatures) return
|
||||
|
||||
let timeoutId: number
|
||||
let isCancelled = false
|
||||
@ -180,7 +179,7 @@ export const useMessageConfirmations = ({
|
||||
isCancelled = true
|
||||
}
|
||||
},
|
||||
[fromHome, home.bridgeContract, home.web3, message.data, receipt, signatureCollected]
|
||||
[fromHome, home.bridgeContract, home.web3, message.data, receipt, hasCollectedSignatures]
|
||||
)
|
||||
|
||||
// Check if the responsible validator is waiting for block confirmations to execute the message on foreign network
|
||||
@ -253,35 +252,6 @@ export const useMessageConfirmations = ({
|
||||
let timeoutId: number
|
||||
let isCancelled = false
|
||||
|
||||
if (fromHome) {
|
||||
if (!targetValidatorList || !targetValidatorList.length) return
|
||||
const msgHash = home.web3.utils.sha3(message.data)!
|
||||
const allValidators = [...validatorList, ...targetValidatorList].filter((v, i, s) => s.indexOf(v) === i)
|
||||
const manualConfirmations = []
|
||||
for (let i = 0; i < allValidators.length; i++) {
|
||||
try {
|
||||
const overrideSignatures: {
|
||||
[key: string]: string
|
||||
} = require(`../snapshots/signatures_${allValidators[i]}.json`)
|
||||
if (overrideSignatures[msgHash]) {
|
||||
console.log(`Adding manual signature from ${allValidators[i]}`)
|
||||
manualConfirmations.push({
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.MANUAL,
|
||||
validator: allValidators[i],
|
||||
timestamp: 0,
|
||||
txHash: '',
|
||||
signature: overrideSignatures[msgHash]
|
||||
})
|
||||
} else {
|
||||
console.log(`No manual signature from ${allValidators[i]} was found`)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`Signatures overrides are not present for ${allValidators[i]}`)
|
||||
}
|
||||
}
|
||||
setConfirmations(manualConfirmations)
|
||||
}
|
||||
|
||||
getConfirmationsForTx(
|
||||
message.data,
|
||||
home.web3,
|
||||
@ -314,8 +284,7 @@ export const useMessageConfirmations = ({
|
||||
home.bridgeContract,
|
||||
requiredSignatures,
|
||||
waitingBlocksResolved,
|
||||
homeStartBlock,
|
||||
targetValidatorList
|
||||
homeStartBlock
|
||||
]
|
||||
)
|
||||
|
||||
@ -374,10 +343,7 @@ export const useMessageConfirmations = ({
|
||||
// Sets the message status based in the collected information
|
||||
useEffect(
|
||||
() => {
|
||||
if (
|
||||
executionData.status === VALIDATOR_CONFIRMATION_STATUS.EXECUTION_SUCCESS &&
|
||||
existsConfirmation(confirmations)
|
||||
) {
|
||||
if (executionData.status === VALIDATOR_CONFIRMATION_STATUS.SUCCESS && existsConfirmation(confirmations)) {
|
||||
const newStatus = executionData.executionResult
|
||||
? CONFIRMATIONS_STATUS.SUCCESS
|
||||
: CONFIRMATIONS_STATUS.SUCCESS_MESSAGE_FAILED
|
||||
|
@ -4,13 +4,19 @@ import Web3 from 'web3'
|
||||
import { getRequiredSignatures, getValidatorAddress, getValidatorList } from '../utils/contract'
|
||||
import { BRIDGE_VALIDATORS_ABI } from '../abis'
|
||||
import { useStateProvider } from '../state/StateProvider'
|
||||
import { TransactionReceipt } from 'web3-eth'
|
||||
import { foreignSnapshotProvider, homeSnapshotProvider, SnapshotProvider } from '../services/SnapshotProvider'
|
||||
import { FOREIGN_EXPLORER_API, HOME_EXPLORER_API } from '../config/constants'
|
||||
|
||||
export const useValidatorContract = (isHome: boolean, blockNumber: number | 'latest') => {
|
||||
export interface useValidatorContractParams {
|
||||
fromHome: boolean
|
||||
receipt: Maybe<TransactionReceipt>
|
||||
}
|
||||
|
||||
export const useValidatorContract = ({ receipt, fromHome }: useValidatorContractParams) => {
|
||||
const [validatorContract, setValidatorContract] = useState<Maybe<Contract>>(null)
|
||||
const [requiredSignatures, setRequiredSignatures] = useState(0)
|
||||
const [validatorList, setValidatorList] = useState<string[]>([])
|
||||
const [validatorList, setValidatorList] = useState([])
|
||||
|
||||
const { home, foreign } = useStateProvider()
|
||||
|
||||
@ -23,34 +29,34 @@ export const useValidatorContract = (isHome: boolean, blockNumber: number | 'lat
|
||||
|
||||
const callRequiredSignatures = async (
|
||||
contract: Maybe<Contract>,
|
||||
blockNumber: number | 'latest',
|
||||
receipt: TransactionReceipt,
|
||||
setResult: Function,
|
||||
snapshotProvider: SnapshotProvider,
|
||||
web3: Web3,
|
||||
api: string
|
||||
) => {
|
||||
if (!contract) return
|
||||
const result = await getRequiredSignatures(contract, blockNumber, snapshotProvider, web3, api)
|
||||
const result = await getRequiredSignatures(contract, receipt.blockNumber, snapshotProvider, web3, api)
|
||||
setResult(result)
|
||||
}
|
||||
|
||||
const callValidatorList = async (
|
||||
contract: Maybe<Contract>,
|
||||
blockNumber: number | 'latest',
|
||||
receipt: TransactionReceipt,
|
||||
setResult: Function,
|
||||
snapshotProvider: SnapshotProvider,
|
||||
web3: Web3,
|
||||
api: string
|
||||
) => {
|
||||
if (!contract) return
|
||||
const result = await getValidatorList(contract, blockNumber, snapshotProvider, web3, api)
|
||||
const result = await getValidatorList(contract, receipt.blockNumber, snapshotProvider, web3, api)
|
||||
setResult(result)
|
||||
}
|
||||
|
||||
const web3 = isHome ? home.web3 : foreign.web3
|
||||
const api = isHome ? HOME_EXPLORER_API : FOREIGN_EXPLORER_API
|
||||
const bridgeContract = isHome ? home.bridgeContract : foreign.bridgeContract
|
||||
const snapshotProvider = isHome ? homeSnapshotProvider : foreignSnapshotProvider
|
||||
const web3 = fromHome ? home.web3 : foreign.web3
|
||||
const api = fromHome ? HOME_EXPLORER_API : FOREIGN_EXPLORER_API
|
||||
const bridgeContract = fromHome ? home.bridgeContract : foreign.bridgeContract
|
||||
const snapshotProvider = fromHome ? homeSnapshotProvider : foreignSnapshotProvider
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
@ -62,11 +68,11 @@ export const useValidatorContract = (isHome: boolean, blockNumber: number | 'lat
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (!web3 || !blockNumber) return
|
||||
callRequiredSignatures(validatorContract, blockNumber, setRequiredSignatures, snapshotProvider, web3, api)
|
||||
callValidatorList(validatorContract, blockNumber, setValidatorList, snapshotProvider, web3, api)
|
||||
if (!web3 || !receipt) return
|
||||
callRequiredSignatures(validatorContract, receipt, setRequiredSignatures, snapshotProvider, web3, api)
|
||||
callValidatorList(validatorContract, receipt, setValidatorList, snapshotProvider, web3, api)
|
||||
},
|
||||
[validatorContract, blockNumber, web3, snapshotProvider, api]
|
||||
[validatorContract, receipt, web3, snapshotProvider, api]
|
||||
)
|
||||
|
||||
return {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { createContext, ReactNode } from 'react'
|
||||
import React, { createContext, ReactNode, useState } from 'react'
|
||||
import { useNetwork } from '../hooks/useNetwork'
|
||||
import {
|
||||
HOME_RPC_URL,
|
||||
@ -25,6 +25,8 @@ export interface StateContext {
|
||||
home: BaseNetworkParams
|
||||
foreign: BaseNetworkParams
|
||||
loading: boolean
|
||||
error: string
|
||||
setError: Function
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
@ -42,7 +44,9 @@ const initialState = {
|
||||
bridgeAddress: FOREIGN_BRIDGE_ADDRESS,
|
||||
bridgeContract: null
|
||||
},
|
||||
loading: true
|
||||
loading: true,
|
||||
error: '',
|
||||
setError: () => {}
|
||||
}
|
||||
|
||||
const StateContext = createContext<StateContext>(initialState)
|
||||
@ -54,6 +58,7 @@ export const StateProvider = ({ children }: { children: ReactNode }) => {
|
||||
homeWeb3: homeNetwork.web3,
|
||||
foreignWeb3: foreignNetwork.web3
|
||||
})
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const value = {
|
||||
home: {
|
||||
@ -68,7 +73,9 @@ export const StateProvider = ({ children }: { children: ReactNode }) => {
|
||||
bridgeContract: foreignBridge,
|
||||
...foreignNetwork
|
||||
},
|
||||
loading: homeNetwork.loading || foreignNetwork.loading
|
||||
loading: homeNetwork.loading || foreignNetwork.loading,
|
||||
error,
|
||||
setError
|
||||
}
|
||||
|
||||
return <StateContext.Provider value={value}>{children}</StateContext.Provider>
|
||||
|
@ -28,7 +28,5 @@ export const GlobalStyle = createGlobalStyle<{ theme: ThemeType }>`
|
||||
--not-required-bg-color: ${props => props.theme.notRequired.backgroundColor};
|
||||
--failed-color: ${props => props.theme.failed.textColor};
|
||||
--failed-bg-color: ${props => props.theme.failed.backgroundColor};
|
||||
--warning-color: ${props => props.theme.warning.textColor};
|
||||
--warning-bg-color: ${props => props.theme.warning.backgroundColor};
|
||||
}
|
||||
`
|
||||
|
@ -17,10 +17,6 @@ const theme = {
|
||||
failed: {
|
||||
textColor: '#de4437',
|
||||
backgroundColor: 'rgba(222,68,55,.1)'
|
||||
},
|
||||
warning: {
|
||||
textColor: '#ffa758',
|
||||
backgroundColor: 'rgba(222,68,55,.1)'
|
||||
}
|
||||
}
|
||||
export default theme
|
||||
|
@ -14,40 +14,37 @@ const messageData = '0x123456'
|
||||
const OTHER_HASH = 'aabbccdd'
|
||||
const bridgeAddress = '0xFe446bEF1DbF7AFE24E81e05BC8B271C1BA9a560'
|
||||
const otherAddress = '0xD4075FB57fCf038bFc702c915Ef9592534bED5c1'
|
||||
const validator1 = '0x45b96809336A8b714BFbdAB3E4B5e0fe5d839908'
|
||||
const validator2 = '0xAe8bFfc8BBc6AAa9E21ED1E4e4957fe798BEA25f'
|
||||
const validator3 = '0x285A6eB779be4db94dA65e2F3518B1c5F0f71244'
|
||||
|
||||
describe('getFailedTransactions', () => {
|
||||
test('should only return failed transactions', async () => {
|
||||
const to = otherAddress
|
||||
const transactions = [
|
||||
{ isError: '0', to, from: validator1 },
|
||||
{ isError: '1', to, from: validator1 },
|
||||
{ isError: '0', to, from: validator2 },
|
||||
{ isError: '1', to, from: validator2 },
|
||||
{ isError: '1', to, from: validator3 }
|
||||
{ isError: '0', to },
|
||||
{ isError: '1', to },
|
||||
{ isError: '0', to },
|
||||
{ isError: '1', to },
|
||||
{ isError: '1', to }
|
||||
]
|
||||
|
||||
const fetchAccountTransactions = jest.fn().mockImplementation(() => transactions)
|
||||
const result = await getFailedTransactions(validator1, to, 0, 1, '', fetchAccountTransactions)
|
||||
expect(result.length).toEqual(1)
|
||||
const result = await getFailedTransactions('', to, 0, 1, '', fetchAccountTransactions)
|
||||
expect(result.length).toEqual(3)
|
||||
})
|
||||
})
|
||||
describe('getSuccessTransactions', () => {
|
||||
test('should only return success transactions', async () => {
|
||||
const to = otherAddress
|
||||
const transactions = [
|
||||
{ isError: '0', to, from: validator1 },
|
||||
{ isError: '1', to, from: validator1 },
|
||||
{ isError: '0', to, from: validator2 },
|
||||
{ isError: '1', to, from: validator2 },
|
||||
{ isError: '1', to, from: validator3 }
|
||||
{ isError: '0', to },
|
||||
{ isError: '1', to },
|
||||
{ isError: '0', to },
|
||||
{ isError: '1', to },
|
||||
{ isError: '1', to }
|
||||
]
|
||||
|
||||
const fetchAccountTransactions = jest.fn().mockImplementation(() => transactions)
|
||||
const result = await getSuccessTransactions(validator1, to, 0, 1, '', fetchAccountTransactions)
|
||||
expect(result.length).toEqual(1)
|
||||
const result = await getSuccessTransactions('', to, 0, 1, '', fetchAccountTransactions)
|
||||
expect(result.length).toEqual(2)
|
||||
})
|
||||
})
|
||||
describe('filterValidatorSignatureTransaction', () => {
|
||||
|
@ -5,7 +5,7 @@ import Web3 from 'web3'
|
||||
import { Contract } from 'web3-eth-contract'
|
||||
import { APIPendingTransaction, APITransaction } from '../explorer'
|
||||
import { VALIDATOR_CONFIRMATION_STATUS } from '../../config/constants'
|
||||
import { ConfirmationParam } from '../../hooks/useMessageConfirmations'
|
||||
import { BasicConfirmationParam } from '../../hooks/useMessageConfirmations'
|
||||
|
||||
jest.mock('../validatorConfirmationHelpers')
|
||||
|
||||
@ -18,9 +18,6 @@ const messageData = '0x111111111'
|
||||
const web3 = {
|
||||
utils: {
|
||||
soliditySha3Raw: (data: string) => `0xaaaa${data.replace('0x', '')}`
|
||||
},
|
||||
eth: {
|
||||
accounts: new Web3().eth.accounts
|
||||
}
|
||||
} as Web3
|
||||
const validator1 = '0x45b96809336A8b714BFbdAB3E4B5e0fe5d839908'
|
||||
@ -28,7 +25,7 @@ const validator2 = '0xAe8bFfc8BBc6AAa9E21ED1E4e4957fe798BEA25f'
|
||||
const validator3 = '0x285A6eB779be4db94dA65e2F3518B1c5F0f71244'
|
||||
const validatorList = [validator1, validator2, validator3]
|
||||
const signature =
|
||||
'0x6f5b74905669999f1abdb52e1e215506907e1849aac7b31854da458b33a5954e15b165007c3703cfd16e61ca46a96a56727ed11fa47be359d3834515accd016e1b'
|
||||
'0x519d704bceed17423daa79c20531cc34fc27a4be6e53fc5069a8023019188ca4519d704bceed17423daa79c20531cc34fc27a4be6e53fc5069a8023019188ca4'
|
||||
const bridgeContract = {
|
||||
methods: {
|
||||
signature: () => ({
|
||||
@ -64,19 +61,19 @@ describe('getConfirmationsForTx', () => {
|
||||
validator,
|
||||
status: validator !== validator3 ? VALIDATOR_CONFIRMATION_STATUS.SUCCESS : VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
|
||||
}))
|
||||
getSuccessExecutionTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
|
||||
getSuccessExecutionTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS,
|
||||
txHash: '',
|
||||
timestamp: 0
|
||||
}))
|
||||
getValidatorFailedTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
|
||||
getValidatorFailedTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
|
||||
txHash: '',
|
||||
timestamp: 0
|
||||
}))
|
||||
getValidatorPendingTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
|
||||
getValidatorPendingTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
|
||||
txHash: '',
|
||||
@ -113,8 +110,9 @@ describe('getConfirmationsForTx', () => {
|
||||
expect(setResult).toBeCalledTimes(2)
|
||||
expect(getValidatorConfirmation).toBeCalledTimes(1)
|
||||
expect(getSuccessExecutionTransaction).toBeCalledTimes(1)
|
||||
expect(setSignatureCollected).toBeCalledTimes(1)
|
||||
expect(setSignatureCollected).toBeCalledTimes(2)
|
||||
expect(setSignatureCollected.mock.calls[0][0]).toEqual(true)
|
||||
expect(setSignatureCollected.mock.calls[1][0]).toEqual([signature, signature])
|
||||
|
||||
expect(getValidatorFailedTransaction).toBeCalledTimes(1)
|
||||
expect(setFailedConfirmations).toBeCalledTimes(1)
|
||||
@ -137,7 +135,7 @@ describe('getConfirmationsForTx', () => {
|
||||
expect.arrayContaining([
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.NOT_REQUIRED, txHash: '', timestamp: 0 }
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.NOT_REQUIRED }
|
||||
])
|
||||
)
|
||||
})
|
||||
@ -146,19 +144,19 @@ describe('getConfirmationsForTx', () => {
|
||||
validator,
|
||||
status: validator === validator3 ? VALIDATOR_CONFIRMATION_STATUS.SUCCESS : VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
|
||||
}))
|
||||
getSuccessExecutionTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
|
||||
getSuccessExecutionTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS,
|
||||
txHash: '',
|
||||
timestamp: 0
|
||||
}))
|
||||
getValidatorFailedTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
|
||||
getValidatorFailedTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
|
||||
txHash: '',
|
||||
timestamp: 0
|
||||
}))
|
||||
getValidatorPendingTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
|
||||
getValidatorPendingTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
|
||||
txHash: '',
|
||||
@ -210,19 +208,19 @@ describe('getConfirmationsForTx', () => {
|
||||
validator,
|
||||
status: validator !== validator3 ? VALIDATOR_CONFIRMATION_STATUS.SUCCESS : VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
|
||||
}))
|
||||
getSuccessExecutionTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
|
||||
getSuccessExecutionTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS,
|
||||
txHash: validatorData.validator !== validator3 ? '0x123' : '',
|
||||
timestamp: validatorData.validator !== validator3 ? 123 : 0
|
||||
}))
|
||||
getValidatorFailedTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
|
||||
getValidatorFailedTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
|
||||
txHash: '',
|
||||
timestamp: 0
|
||||
}))
|
||||
getValidatorPendingTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
|
||||
getValidatorPendingTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
|
||||
txHash: '',
|
||||
@ -259,8 +257,9 @@ describe('getConfirmationsForTx', () => {
|
||||
expect(setResult).toBeCalledTimes(3)
|
||||
expect(getValidatorConfirmation).toBeCalledTimes(1)
|
||||
expect(getSuccessExecutionTransaction).toBeCalledTimes(1)
|
||||
expect(setSignatureCollected).toBeCalledTimes(1)
|
||||
expect(setSignatureCollected).toBeCalledTimes(2)
|
||||
expect(setSignatureCollected.mock.calls[0][0]).toEqual(true)
|
||||
expect(setSignatureCollected.mock.calls[1][0]).toEqual([signature, signature])
|
||||
|
||||
expect(getValidatorFailedTransaction).toBeCalledTimes(1)
|
||||
expect(setFailedConfirmations).toBeCalledTimes(1)
|
||||
@ -282,16 +281,16 @@ describe('getConfirmationsForTx', () => {
|
||||
)
|
||||
expect(res2).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED }
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.NOT_REQUIRED }
|
||||
])
|
||||
)
|
||||
expect(res3).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.NOT_REQUIRED, txHash: '', timestamp: 0 }
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.NOT_REQUIRED }
|
||||
])
|
||||
)
|
||||
})
|
||||
@ -305,22 +304,22 @@ describe('getConfirmationsForTx', () => {
|
||||
? VALIDATOR_CONFIRMATION_STATUS.SUCCESS
|
||||
: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
|
||||
}))
|
||||
getSuccessExecutionTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
|
||||
getSuccessExecutionTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS,
|
||||
txHash: validatorData.validator !== validator3 && validatorData.validator !== validator4 ? '0x123' : '',
|
||||
timestamp: validatorData.validator !== validator3 && validatorData.validator !== validator4 ? 123 : 0
|
||||
}))
|
||||
getValidatorFailedTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
|
||||
getValidatorFailedTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status:
|
||||
validatorData.validator === validator3
|
||||
? VALIDATOR_CONFIRMATION_STATUS.FAILED_VALID
|
||||
? VALIDATOR_CONFIRMATION_STATUS.FAILED
|
||||
: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
|
||||
txHash: validatorData.validator === validator3 ? '0x123' : '',
|
||||
timestamp: validatorData.validator === validator3 ? 123 : 0
|
||||
}))
|
||||
getValidatorPendingTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
|
||||
getValidatorPendingTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
|
||||
txHash: '',
|
||||
@ -357,8 +356,9 @@ describe('getConfirmationsForTx', () => {
|
||||
expect(setResult).toBeCalledTimes(4)
|
||||
expect(getValidatorConfirmation).toBeCalledTimes(1)
|
||||
expect(getSuccessExecutionTransaction).toBeCalledTimes(1)
|
||||
expect(setSignatureCollected).toBeCalledTimes(1)
|
||||
expect(setSignatureCollected).toBeCalledTimes(2)
|
||||
expect(setSignatureCollected.mock.calls[0][0]).toEqual(true)
|
||||
expect(setSignatureCollected.mock.calls[1][0]).toEqual([signature, signature])
|
||||
|
||||
expect(getValidatorFailedTransaction).toBeCalledTimes(1)
|
||||
expect(setFailedConfirmations).toBeCalledTimes(1)
|
||||
@ -382,26 +382,26 @@ describe('getConfirmationsForTx', () => {
|
||||
)
|
||||
expect(res2).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED },
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.FAILED, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator4, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED }
|
||||
])
|
||||
)
|
||||
expect(res3).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.FAILED_VALID, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator4, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED }
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.FAILED, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator4, status: VALIDATOR_CONFIRMATION_STATUS.NOT_REQUIRED }
|
||||
])
|
||||
)
|
||||
expect(res4).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.FAILED_VALID, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator4, status: VALIDATOR_CONFIRMATION_STATUS.NOT_REQUIRED, txHash: '', timestamp: 0 }
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.FAILED, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator4, status: VALIDATOR_CONFIRMATION_STATUS.NOT_REQUIRED }
|
||||
])
|
||||
)
|
||||
})
|
||||
@ -414,22 +414,22 @@ describe('getConfirmationsForTx', () => {
|
||||
validator,
|
||||
status: validator === validator1 ? VALIDATOR_CONFIRMATION_STATUS.SUCCESS : VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
|
||||
}))
|
||||
getSuccessExecutionTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
|
||||
getSuccessExecutionTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS,
|
||||
txHash: validatorData.validator === validator1 ? '0x123' : '',
|
||||
timestamp: validatorData.validator === validator1 ? 123 : 0
|
||||
}))
|
||||
getValidatorFailedTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
|
||||
getValidatorFailedTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status:
|
||||
validatorData.validator === validator2
|
||||
? VALIDATOR_CONFIRMATION_STATUS.FAILED_VALID
|
||||
? VALIDATOR_CONFIRMATION_STATUS.FAILED
|
||||
: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
|
||||
txHash: validatorData.validator === validator2 ? '0x123' : '',
|
||||
timestamp: validatorData.validator === validator2 ? 123 : 0
|
||||
}))
|
||||
getValidatorPendingTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
|
||||
getValidatorPendingTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status:
|
||||
validatorData.validator === validator3
|
||||
@ -492,22 +492,22 @@ describe('getConfirmationsForTx', () => {
|
||||
)
|
||||
expect(res2).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED }
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.PENDING, txHash: '0x123', timestamp: 123 }
|
||||
])
|
||||
)
|
||||
expect(res3).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED },
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.FAILED, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.PENDING, txHash: '0x123', timestamp: 123 }
|
||||
])
|
||||
)
|
||||
expect(res4).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.FAILED_VALID, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.FAILED, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.PENDING, txHash: '0x123', timestamp: 123 }
|
||||
])
|
||||
)
|
||||
@ -521,13 +521,13 @@ describe('getConfirmationsForTx', () => {
|
||||
validator,
|
||||
status: validator === validator1 ? VALIDATOR_CONFIRMATION_STATUS.SUCCESS : VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
|
||||
}))
|
||||
getSuccessExecutionTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
|
||||
getSuccessExecutionTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS,
|
||||
txHash: validatorData.validator === validator1 ? '0x123' : '',
|
||||
timestamp: validatorData.validator === validator1 ? 123 : 0
|
||||
}))
|
||||
getValidatorFailedTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
|
||||
getValidatorFailedTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status:
|
||||
validatorData.validator !== validator1
|
||||
@ -536,7 +536,7 @@ describe('getConfirmationsForTx', () => {
|
||||
txHash: validatorData.validator !== validator1 ? '0x123' : '',
|
||||
timestamp: validatorData.validator !== validator1 ? 123 : 0
|
||||
}))
|
||||
getValidatorPendingTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
|
||||
getValidatorPendingTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
|
||||
txHash: '',
|
||||
@ -596,9 +596,9 @@ describe('getConfirmationsForTx', () => {
|
||||
)
|
||||
expect(res2).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED }
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.FAILED, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.FAILED, txHash: '0x123', timestamp: 123 }
|
||||
])
|
||||
)
|
||||
expect(res3).toEqual(
|
||||
@ -610,13 +610,9 @@ describe('getConfirmationsForTx', () => {
|
||||
)
|
||||
})
|
||||
test('should remove pending state after transaction mined', async () => {
|
||||
const validator4 = '0x9d2dC11C342F4eF3C5491A048D0f0eBCd2D8f7C3'
|
||||
const validatorList = [validator1, validator2, validator3, validator4]
|
||||
|
||||
// Validator1 success (ts=100)
|
||||
// Validator2 failed (ts=200)
|
||||
// Validator3 Pending (ts=300)
|
||||
// Validator4 Excess confirmation (Failed) (ts=400)
|
||||
// Validator1 success
|
||||
// Validator2 failed
|
||||
// Validator3 Pending
|
||||
|
||||
getValidatorConfirmation
|
||||
.mockImplementationOnce(() => async (validator: string) => ({
|
||||
@ -627,57 +623,41 @@ describe('getConfirmationsForTx', () => {
|
||||
.mockImplementation(() => async (validator: string) => ({
|
||||
validator,
|
||||
status:
|
||||
validator === validator1 || validator === validator3
|
||||
? VALIDATOR_CONFIRMATION_STATUS.SUCCESS
|
||||
: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
|
||||
validator !== validator2 ? VALIDATOR_CONFIRMATION_STATUS.SUCCESS : VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
|
||||
}))
|
||||
getSuccessExecutionTransaction
|
||||
.mockImplementationOnce(() => async (validatorData: ConfirmationParam) => ({
|
||||
.mockImplementationOnce(() => async (validatorData: BasicConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS,
|
||||
txHash: validatorData.validator === validator1 ? '0x100' : '',
|
||||
timestamp: validatorData.validator === validator1 ? 100 : 0
|
||||
txHash: validatorData.validator === validator1 ? '0x123' : '',
|
||||
timestamp: validatorData.validator === validator1 ? 123 : 0
|
||||
}))
|
||||
.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
|
||||
.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS,
|
||||
txHash:
|
||||
validatorData.validator === validator1 ? '0x100' : validatorData.validator === validator3 ? '0x300' : '',
|
||||
timestamp: validatorData.validator === validator1 ? 100 : validatorData.validator === validator3 ? 300 : ''
|
||||
}))
|
||||
getValidatorFailedTransaction
|
||||
.mockImplementationOnce(() => async (validatorData: ConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status:
|
||||
validatorData.validator === validator2
|
||||
? VALIDATOR_CONFIRMATION_STATUS.FAILED
|
||||
: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
|
||||
txHash: validatorData.validator === validator2 ? '0x200' : '',
|
||||
timestamp: validatorData.validator === validator2 ? 200 : 0
|
||||
}))
|
||||
.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status:
|
||||
validatorData.validator === validator2 || validatorData.validator === validator4
|
||||
? validatorData.validator === validator2
|
||||
? VALIDATOR_CONFIRMATION_STATUS.FAILED
|
||||
: VALIDATOR_CONFIRMATION_STATUS.FAILED_VALID
|
||||
: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
|
||||
txHash:
|
||||
validatorData.validator === validator2 ? '0x200' : validatorData.validator === validator4 ? '0x400' : '',
|
||||
timestamp: validatorData.validator === validator2 ? 200 : validatorData.validator === validator4 ? 400 : ''
|
||||
txHash: validatorData.validator !== validator2 ? '0x123' : '',
|
||||
timestamp: validatorData.validator !== validator2 ? 123 : 0
|
||||
}))
|
||||
getValidatorFailedTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status:
|
||||
validatorData.validator === validator2
|
||||
? VALIDATOR_CONFIRMATION_STATUS.FAILED
|
||||
: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
|
||||
txHash: validatorData.validator === validator2 ? '0x123' : '',
|
||||
timestamp: validatorData.validator === validator2 ? 123 : 0
|
||||
}))
|
||||
getValidatorPendingTransaction
|
||||
.mockImplementationOnce(() => async (validatorData: ConfirmationParam) => ({
|
||||
.mockImplementationOnce(() => async (validatorData: BasicConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status:
|
||||
validatorData.validator === validator3
|
||||
? VALIDATOR_CONFIRMATION_STATUS.PENDING
|
||||
: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
|
||||
txHash: validatorData.validator === validator3 ? '0x300' : '',
|
||||
timestamp: validatorData.validator === validator3 ? 300 : 0
|
||||
txHash: validatorData.validator === validator3 ? '0x123' : '',
|
||||
timestamp: validatorData.validator === validator3 ? 123 : 0
|
||||
}))
|
||||
.mockImplementationOnce(() => async (validatorData: ConfirmationParam) => ({
|
||||
.mockImplementationOnce(() => async (validatorData: BasicConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
|
||||
txHash: '',
|
||||
@ -718,7 +698,7 @@ describe('getConfirmationsForTx', () => {
|
||||
|
||||
expect(getValidatorFailedTransaction).toBeCalledTimes(1)
|
||||
expect(setFailedConfirmations).toBeCalledTimes(1)
|
||||
expect(setFailedConfirmations.mock.calls[0][0]).toEqual(true)
|
||||
expect(setFailedConfirmations.mock.calls[0][0]).toEqual(false)
|
||||
|
||||
expect(getValidatorPendingTransaction).toBeCalledTimes(1)
|
||||
expect(setPendingConfirmations).toBeCalledTimes(1)
|
||||
@ -732,32 +712,28 @@ describe('getConfirmationsForTx', () => {
|
||||
expect.arrayContaining([
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED },
|
||||
{ validator: validator4, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED }
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED }
|
||||
])
|
||||
)
|
||||
expect(res2).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x100', timestamp: 100 },
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED },
|
||||
{ validator: validator4, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED }
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.PENDING, txHash: '0x123', timestamp: 123 }
|
||||
])
|
||||
)
|
||||
expect(res3).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x100', timestamp: 100 },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.PENDING, txHash: '0x300', timestamp: 300 },
|
||||
{ validator: validator4, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED }
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.FAILED, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.PENDING, txHash: '0x123', timestamp: 123 }
|
||||
])
|
||||
)
|
||||
expect(res4).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x100', timestamp: 100 },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.FAILED, txHash: '0x200', timestamp: 200 },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.PENDING, txHash: '0x300', timestamp: 300 },
|
||||
{ validator: validator4, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED }
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.FAILED, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.PENDING, txHash: '0x123', timestamp: 123 }
|
||||
])
|
||||
)
|
||||
|
||||
@ -785,13 +761,14 @@ describe('getConfirmationsForTx', () => {
|
||||
expect(setResult).toBeCalledTimes(7)
|
||||
expect(getValidatorConfirmation).toBeCalledTimes(2)
|
||||
expect(getSuccessExecutionTransaction).toBeCalledTimes(2)
|
||||
expect(setSignatureCollected).toBeCalledTimes(2)
|
||||
expect(setSignatureCollected).toBeCalledTimes(3)
|
||||
expect(setSignatureCollected.mock.calls[0][0]).toEqual(false)
|
||||
expect(setSignatureCollected.mock.calls[1][0]).toEqual(true)
|
||||
expect(setSignatureCollected.mock.calls[2][0]).toEqual([signature, signature])
|
||||
|
||||
expect(getValidatorFailedTransaction).toBeCalledTimes(2)
|
||||
expect(setFailedConfirmations).toBeCalledTimes(2)
|
||||
expect(setFailedConfirmations.mock.calls[0][0]).toEqual(true)
|
||||
expect(setFailedConfirmations.mock.calls[0][0]).toEqual(false)
|
||||
expect(setFailedConfirmations.mock.calls[1][0]).toEqual(false)
|
||||
|
||||
expect(getValidatorPendingTransaction).toBeCalledTimes(1)
|
||||
@ -804,26 +781,23 @@ describe('getConfirmationsForTx', () => {
|
||||
const res7 = setResult.mock.calls[6][0](res6)
|
||||
expect(res5).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x100', timestamp: 100 },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.FAILED, txHash: '0x200', timestamp: 200 },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x300', timestamp: 300 },
|
||||
{ validator: validator4, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED }
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.FAILED, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS }
|
||||
])
|
||||
)
|
||||
expect(res6).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x100', timestamp: 100 },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.FAILED, txHash: '0x200', timestamp: 200 },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x300', timestamp: 300 },
|
||||
{ validator: validator4, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED }
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.FAILED, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS }
|
||||
])
|
||||
)
|
||||
expect(res7).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x100', timestamp: 100 },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.FAILED, txHash: '0x200', timestamp: 200 },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x300', timestamp: 300 },
|
||||
{ validator: validator4, status: VALIDATOR_CONFIRMATION_STATUS.FAILED_VALID, txHash: '0x400', timestamp: 400 }
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.FAILED, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 }
|
||||
])
|
||||
)
|
||||
})
|
||||
|
@ -84,11 +84,10 @@ describe('getFinalizationEvent', () => {
|
||||
expect(setResult).toBeCalledTimes(1)
|
||||
expect(setResult.mock.calls[0][0]).toEqual({
|
||||
validator: validator1,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.EXECUTION_SUCCESS,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS,
|
||||
txHash,
|
||||
timestamp,
|
||||
executionResult: true,
|
||||
blockNumber: 5523145
|
||||
executionResult: true
|
||||
})
|
||||
|
||||
expect(getFailedExecution).toBeCalledTimes(0)
|
||||
@ -238,8 +237,7 @@ describe('getFinalizationEvent', () => {
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.PENDING,
|
||||
txHash,
|
||||
timestamp: expect.any(Number),
|
||||
executionResult: false,
|
||||
blockNumber: 0
|
||||
executionResult: false
|
||||
})
|
||||
|
||||
expect(getFailedExecution).toBeCalledTimes(0)
|
||||
@ -296,8 +294,7 @@ describe('getFinalizationEvent', () => {
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.FAILED,
|
||||
txHash,
|
||||
timestamp: expect.any(Number),
|
||||
executionResult: false,
|
||||
blockNumber: expect.any(Number)
|
||||
executionResult: false
|
||||
})
|
||||
|
||||
expect(getFailedExecution).toBeCalledTimes(1)
|
||||
|
@ -22,16 +22,6 @@ export const getRequiredBlockConfirmations = async (
|
||||
web3: Web3 | null = null,
|
||||
api: string = ''
|
||||
) => {
|
||||
let blockConfirmations
|
||||
|
||||
try {
|
||||
blockConfirmations = await contract.methods.requiredBlockConfirmations().call()
|
||||
} catch {}
|
||||
|
||||
if (blockConfirmations) {
|
||||
return parseInt(blockConfirmations)
|
||||
}
|
||||
|
||||
const eventsFromSnapshot = snapshotProvider.requiredBlockConfirmationEvents(blockNumber)
|
||||
const snapshotBlockNumber = snapshotProvider.snapshotBlockNumber()
|
||||
|
||||
@ -45,10 +35,16 @@ export const getRequiredBlockConfirmations = async (
|
||||
|
||||
const events = [...eventsFromSnapshot, ...contractEvents]
|
||||
|
||||
// Use the value from last event before the transaction
|
||||
const event = events[events.length - 1]
|
||||
blockConfirmations = event.returnValues.requiredBlockConfirmations
|
||||
|
||||
let blockConfirmations
|
||||
if (events.length > 0) {
|
||||
// Use the value from last event before the transaction
|
||||
const event = events[events.length - 1]
|
||||
blockConfirmations = event.returnValues.requiredBlockConfirmations
|
||||
} else {
|
||||
// This is a special case where RequiredBlockConfirmationChanged was not emitted during initialization in early versions of AMB
|
||||
// of Sokol - Kovan. In this case the current value is used.
|
||||
blockConfirmations = await contract.methods.requiredBlockConfirmations().call()
|
||||
}
|
||||
return parseInt(blockConfirmations)
|
||||
}
|
||||
|
||||
@ -56,25 +52,11 @@ export const getValidatorAddress = (contract: Contract) => contract.methods.vali
|
||||
|
||||
export const getRequiredSignatures = async (
|
||||
contract: Contract,
|
||||
blockNumber: number | 'latest',
|
||||
blockNumber: number,
|
||||
snapshotProvider: SnapshotProvider,
|
||||
web3: Web3 | null = null,
|
||||
api: string = ''
|
||||
) => {
|
||||
let requiredSignatures
|
||||
|
||||
try {
|
||||
requiredSignatures = await contract.methods.requiredSignatures().call()
|
||||
} catch {}
|
||||
|
||||
if (requiredSignatures) {
|
||||
return parseInt(requiredSignatures)
|
||||
}
|
||||
|
||||
if (blockNumber === 'latest') {
|
||||
return contract.methods.requiredSignatures().call()
|
||||
}
|
||||
|
||||
const eventsFromSnapshot = snapshotProvider.requiredSignaturesEvents(blockNumber)
|
||||
const snapshotBlockNumber = snapshotProvider.snapshotBlockNumber()
|
||||
|
||||
@ -90,25 +72,17 @@ export const getRequiredSignatures = async (
|
||||
|
||||
// Use the value form last event before the transaction
|
||||
const event = events[events.length - 1]
|
||||
;({ requiredSignatures } = event.returnValues)
|
||||
const { requiredSignatures } = event.returnValues
|
||||
return parseInt(requiredSignatures)
|
||||
}
|
||||
|
||||
export const getValidatorList = async (
|
||||
contract: Contract,
|
||||
blockNumber: number | 'latest',
|
||||
blockNumber: number,
|
||||
snapshotProvider: SnapshotProvider,
|
||||
web3: Web3 | null = null,
|
||||
api: string = ''
|
||||
) => {
|
||||
try {
|
||||
const currentList = await contract.methods.validatorList().call()
|
||||
|
||||
if (currentList) {
|
||||
return currentList
|
||||
}
|
||||
} catch {}
|
||||
|
||||
const addedEventsFromSnapshot = snapshotProvider.validatorAddedEvents(blockNumber)
|
||||
const removedEventsFromSnapshot = snapshotProvider.validatorRemovedEvents(blockNumber)
|
||||
const snapshotBlockNumber = snapshotProvider.snapshotBlockNumber()
|
||||
|
@ -12,7 +12,6 @@ import Web3 from 'web3'
|
||||
import { Contract } from 'web3-eth-contract'
|
||||
|
||||
export interface APITransaction {
|
||||
from: string
|
||||
timeStamp: string
|
||||
isError: string
|
||||
input: string
|
||||
@ -55,7 +54,7 @@ export const fetchAccountTransactions = async ({ account, startBlock, endBlock,
|
||||
url.searchParams.append('module', 'account')
|
||||
url.searchParams.append('action', 'txlist')
|
||||
url.searchParams.append('address', account)
|
||||
url.searchParams.append('filterby', 'to')
|
||||
url.searchParams.append('filterby', 'from')
|
||||
url.searchParams.append('startblock', startBlock.toString())
|
||||
url.searchParams.append('endblock', endBlock.toString())
|
||||
|
||||
@ -65,7 +64,7 @@ export const fetchAccountTransactions = async ({ account, startBlock, endBlock,
|
||||
return []
|
||||
}
|
||||
|
||||
return result.result || []
|
||||
return result.result
|
||||
}
|
||||
|
||||
export const fetchPendingTransactions = async ({
|
||||
@ -181,9 +180,7 @@ export const getLogs = async (
|
||||
if (topics[i] !== null) {
|
||||
url.searchParams.append(`topic${i}`, topics[i] as string)
|
||||
for (let j = 0; j < i; j++) {
|
||||
if (topics[j] !== null) {
|
||||
url.searchParams.append(`topic${j}_${i}_opr`, 'and')
|
||||
}
|
||||
url.searchParams.append(`topic${j}_${i}_opr`, 'and')
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -197,7 +194,7 @@ export const getLogs = async (
|
||||
}))
|
||||
}
|
||||
|
||||
const filterSender = (from: string) => (tx: APITransaction) => tx.from.toLowerCase() === from.toLowerCase()
|
||||
const filterReceiver = (to: string) => (tx: APITransaction) => tx.to.toLowerCase() === to.toLowerCase()
|
||||
|
||||
export const getFailedTransactions = async (
|
||||
account: string,
|
||||
@ -207,9 +204,9 @@ export const getFailedTransactions = async (
|
||||
api: string,
|
||||
getAccountTransactionsMethod = getAccountTransactions
|
||||
): Promise<APITransaction[]> => {
|
||||
const transactions = await getAccountTransactionsMethod({ account: to, startBlock, endBlock, api })
|
||||
const transactions = await getAccountTransactionsMethod({ account, startBlock, endBlock, api })
|
||||
|
||||
return transactions.filter(t => t.isError !== '0').filter(filterSender(account))
|
||||
return transactions.filter(t => t.isError !== '0').filter(filterReceiver(to))
|
||||
}
|
||||
|
||||
export const getSuccessTransactions = async (
|
||||
@ -220,9 +217,9 @@ export const getSuccessTransactions = async (
|
||||
api: string,
|
||||
getAccountTransactionsMethod = getAccountTransactions
|
||||
): Promise<APITransaction[]> => {
|
||||
const transactions = await getAccountTransactionsMethod({ account: to, startBlock, endBlock, api })
|
||||
const transactions = await getAccountTransactionsMethod({ account, startBlock, endBlock, api })
|
||||
|
||||
return transactions.filter(t => t.isError === '0').filter(filterSender(account))
|
||||
return transactions.filter(t => t.isError === '0').filter(filterReceiver(to))
|
||||
}
|
||||
|
||||
export const filterValidatorSignatureTransaction = (
|
||||
|
@ -2,37 +2,26 @@ import Web3 from 'web3'
|
||||
import { Contract } from 'web3-eth-contract'
|
||||
import { HOME_RPC_POLLING_INTERVAL, VALIDATOR_CONFIRMATION_STATUS } from '../config/constants'
|
||||
import { GetTransactionParams, APITransaction, APIPendingTransaction, GetPendingTransactionParams } from './explorer'
|
||||
import { getAffirmationsSigned, getMessagesSigned } from './contract'
|
||||
import {
|
||||
getValidatorConfirmation,
|
||||
getValidatorFailedTransaction,
|
||||
getValidatorPendingTransaction,
|
||||
getSuccessExecutionTransaction
|
||||
} from './validatorConfirmationHelpers'
|
||||
import { ConfirmationParam } from '../hooks/useMessageConfirmations'
|
||||
import { signatureToVRS } from './signatures'
|
||||
import { BasicConfirmationParam, ConfirmationParam } from '../hooks/useMessageConfirmations'
|
||||
|
||||
const mergeConfirmations = (oldConfirmations: ConfirmationParam[], newConfirmations: ConfirmationParam[]) => {
|
||||
const mergeConfirmations = (oldConfirmations: BasicConfirmationParam[], newConfirmations: BasicConfirmationParam[]) => {
|
||||
const confirmations = [...oldConfirmations]
|
||||
newConfirmations.forEach(validatorData => {
|
||||
const index = confirmations.findIndex(e => e.validator === validatorData.validator)
|
||||
if (index === -1) {
|
||||
confirmations.push(validatorData)
|
||||
return
|
||||
}
|
||||
const currentStatus = confirmations[index].status
|
||||
const newStatus = validatorData.status
|
||||
if (
|
||||
validatorData.txHash ||
|
||||
!!validatorData.signature ||
|
||||
(validatorData as ConfirmationParam).txHash ||
|
||||
(newStatus !== currentStatus && newStatus !== VALIDATOR_CONFIRMATION_STATUS.UNDEFINED)
|
||||
) {
|
||||
confirmations[index] = {
|
||||
status: validatorData.status,
|
||||
validator: validatorData.validator,
|
||||
timestamp: confirmations[index].timestamp || validatorData.timestamp,
|
||||
txHash: confirmations[index].txHash || validatorData.txHash,
|
||||
signature: confirmations[index].signature || validatorData.signature
|
||||
}
|
||||
confirmations[index] = validatorData
|
||||
}
|
||||
})
|
||||
return confirmations
|
||||
@ -56,17 +45,19 @@ export const getConfirmationsForTx = async (
|
||||
setPendingConfirmations: Function,
|
||||
getSuccessTransactions: (args: GetTransactionParams) => Promise<APITransaction[]>
|
||||
) => {
|
||||
const confirmationContractMethod = fromHome ? getMessagesSigned : getAffirmationsSigned
|
||||
|
||||
const hashMsg = web3.utils.soliditySha3Raw(messageData)
|
||||
let validatorConfirmations = await Promise.all(
|
||||
validatorList.map(getValidatorConfirmation(web3, hashMsg, bridgeContract, fromHome))
|
||||
validatorList.map(getValidatorConfirmation(web3, hashMsg, bridgeContract, confirmationContractMethod))
|
||||
)
|
||||
|
||||
const updateConfirmations = (confirmations: ConfirmationParam[]) => {
|
||||
const updateConfirmations = (confirmations: BasicConfirmationParam[]) => {
|
||||
if (confirmations.length === 0) {
|
||||
return
|
||||
}
|
||||
validatorConfirmations = mergeConfirmations(validatorConfirmations, confirmations)
|
||||
setResult((currentConfirmations: ConfirmationParam[]) => {
|
||||
setResult((currentConfirmations: BasicConfirmationParam[]) => {
|
||||
if (currentConfirmations && currentConfirmations.length) {
|
||||
return mergeConfirmations(currentConfirmations, confirmations)
|
||||
}
|
||||
@ -76,37 +67,11 @@ export const getConfirmationsForTx = async (
|
||||
|
||||
const successConfirmations = validatorConfirmations.filter(c => c.status === VALIDATOR_CONFIRMATION_STATUS.SUCCESS)
|
||||
const notSuccessConfirmations = validatorConfirmations.filter(c => c.status !== VALIDATOR_CONFIRMATION_STATUS.SUCCESS)
|
||||
const hasEnoughSignatures = successConfirmations.length >= requiredSignatures
|
||||
const hasEnoughSignatures = successConfirmations.length === requiredSignatures
|
||||
|
||||
updateConfirmations(validatorConfirmations)
|
||||
setSignatureCollected(hasEnoughSignatures)
|
||||
|
||||
if (hasEnoughSignatures) {
|
||||
setPendingConfirmations(false)
|
||||
if (fromHome) {
|
||||
// fetch collected signatures for possible manual processing
|
||||
const signatures = await Promise.all(
|
||||
Array.from(Array(requiredSignatures).keys()).map(i => bridgeContract.methods.signature(hashMsg, i).call())
|
||||
)
|
||||
const confirmations = signatures.flatMap(sig => {
|
||||
const { v, r, s } = signatureToVRS(sig)
|
||||
const address = web3.eth.accounts.recover(messageData, `0x${v}`, `0x${r}`, `0x${s}`)
|
||||
return successConfirmations.filter(c => c.validator === address).map(c => ({ ...c, signature: sig }))
|
||||
})
|
||||
updateConfirmations(confirmations)
|
||||
}
|
||||
}
|
||||
|
||||
// get transactions from success signatures
|
||||
const successConfirmationWithData = await Promise.all(
|
||||
successConfirmations.map(
|
||||
getSuccessExecutionTransaction(web3, bridgeContract, fromHome, messageData, startBlock, getSuccessTransactions)
|
||||
)
|
||||
)
|
||||
|
||||
const successConfirmationWithTxFound = successConfirmationWithData.filter(v => v.txHash !== '')
|
||||
updateConfirmations(successConfirmationWithTxFound)
|
||||
|
||||
// If signatures not collected, look for pending transactions
|
||||
if (!hasEnoughSignatures) {
|
||||
// Check if confirmation is pending
|
||||
@ -119,6 +84,8 @@ export const getConfirmationsForTx = async (
|
||||
)
|
||||
updateConfirmations(validatorPendingConfirmations)
|
||||
setPendingConfirmations(validatorPendingConfirmations.length > 0)
|
||||
} else {
|
||||
setPendingConfirmations(false)
|
||||
}
|
||||
|
||||
const undefinedConfirmations = validatorConfirmations.filter(
|
||||
@ -128,27 +95,13 @@ export const getConfirmationsForTx = async (
|
||||
// Check if confirmation failed
|
||||
const validatorFailedConfirmationsChecks = await Promise.all(
|
||||
undefinedConfirmations.map(
|
||||
getValidatorFailedTransaction(web3, bridgeContract, messageData, startBlock, getFailedTransactions)
|
||||
getValidatorFailedTransaction(bridgeContract, messageData, startBlock, getFailedTransactions)
|
||||
)
|
||||
)
|
||||
let validatorFailedConfirmations = validatorFailedConfirmationsChecks.filter(
|
||||
c => c.status === VALIDATOR_CONFIRMATION_STATUS.FAILED || c.status === VALIDATOR_CONFIRMATION_STATUS.FAILED_VALID
|
||||
)
|
||||
if (hasEnoughSignatures && !fromHome) {
|
||||
const lastTS = Math.max(...successConfirmationWithTxFound.map(c => c.timestamp || 0))
|
||||
validatorFailedConfirmations = validatorFailedConfirmations.map(
|
||||
c =>
|
||||
c.timestamp < lastTS
|
||||
? c
|
||||
: {
|
||||
...c,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.FAILED_VALID
|
||||
}
|
||||
)
|
||||
}
|
||||
setFailedConfirmations(
|
||||
!hasEnoughSignatures && validatorFailedConfirmations.some(c => c.status === VALIDATOR_CONFIRMATION_STATUS.FAILED)
|
||||
const validatorFailedConfirmations = validatorFailedConfirmationsChecks.filter(
|
||||
c => c.status === VALIDATOR_CONFIRMATION_STATUS.FAILED
|
||||
)
|
||||
setFailedConfirmations(validatorFailedConfirmations.length > validatorList.length - requiredSignatures)
|
||||
updateConfirmations(validatorFailedConfirmations)
|
||||
|
||||
const missingConfirmations = validatorConfirmations.filter(
|
||||
@ -159,13 +112,30 @@ export const getConfirmationsForTx = async (
|
||||
// If signatures collected, it should set other signatures not found as not required
|
||||
const notRequiredConfirmations = missingConfirmations.map(c => ({
|
||||
validator: c.validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.NOT_REQUIRED,
|
||||
timestamp: 0,
|
||||
txHash: ''
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.NOT_REQUIRED
|
||||
}))
|
||||
updateConfirmations(notRequiredConfirmations)
|
||||
|
||||
if (fromHome) {
|
||||
// fetch collected signatures for possible manual processing
|
||||
setSignatureCollected(
|
||||
await Promise.all(
|
||||
Array.from(Array(requiredSignatures).keys()).map(i => bridgeContract.methods.signature(hashMsg, i).call())
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// get transactions from success signatures
|
||||
const successConfirmationWithData = await Promise.all(
|
||||
successConfirmations.map(
|
||||
getSuccessExecutionTransaction(web3, bridgeContract, fromHome, messageData, startBlock, getSuccessTransactions)
|
||||
)
|
||||
)
|
||||
|
||||
const successConfirmationWithTxFound = successConfirmationWithData.filter(v => v.txHash !== '')
|
||||
updateConfirmations(successConfirmationWithTxFound)
|
||||
|
||||
// retry if not all signatures are collected and some confirmations are still missing
|
||||
// or some success transactions were not fetched successfully
|
||||
if (
|
||||
|
@ -59,12 +59,11 @@ export const getSuccessExecutionData = async (
|
||||
const validatorAddress = web3.utils.toChecksumAddress(txReceipt.from)
|
||||
|
||||
return {
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.EXECUTION_SUCCESS,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS,
|
||||
validator: validatorAddress,
|
||||
txHash: event.transactionHash,
|
||||
timestamp: blockTimestamp,
|
||||
executionResult: event.returnValues.status,
|
||||
blockNumber: event.blockNumber
|
||||
executionResult: event.returnValues.status
|
||||
}
|
||||
}
|
||||
return null
|
||||
@ -116,8 +115,7 @@ export const getFinalizationEvent = async (
|
||||
validator: validator,
|
||||
txHash: pendingTx.hash,
|
||||
timestamp: nowTimestamp,
|
||||
executionResult: false,
|
||||
blockNumber: 0
|
||||
executionResult: false
|
||||
})
|
||||
setPendingExecution(true)
|
||||
} else {
|
||||
@ -146,8 +144,7 @@ export const getFinalizationEvent = async (
|
||||
validator: validator,
|
||||
txHash: failedTx.hash,
|
||||
timestamp,
|
||||
executionResult: false,
|
||||
blockNumber: parseInt(failedTx.blockNumber)
|
||||
executionResult: false
|
||||
})
|
||||
setFailedExecution(true)
|
||||
}
|
||||
|
@ -1,45 +1,38 @@
|
||||
import Web3 from 'web3'
|
||||
import { Contract } from 'web3-eth-contract'
|
||||
import { ConfirmationParam } from '../hooks/useMessageConfirmations'
|
||||
import { BasicConfirmationParam, ConfirmationParam } from '../hooks/useMessageConfirmations'
|
||||
import validatorsCache from '../services/ValidatorsCache'
|
||||
import { CACHE_KEY_FAILED, CACHE_KEY_SUCCESS, VALIDATOR_CONFIRMATION_STATUS } from '../config/constants'
|
||||
import { APIPendingTransaction, APITransaction, GetTransactionParams, GetPendingTransactionParams } from './explorer'
|
||||
import { homeBlockNumberProvider } from '../services/BlockNumberProvider'
|
||||
import { getAffirmationsSigned, getMessagesSigned } from './contract'
|
||||
|
||||
export const getValidatorConfirmation = (
|
||||
web3: Web3,
|
||||
hashMsg: string,
|
||||
bridgeContract: Contract,
|
||||
fromHome: boolean
|
||||
) => async (validator: string): Promise<ConfirmationParam> => {
|
||||
confirmationContractMethod: Function
|
||||
) => async (validator: string): Promise<BasicConfirmationParam> => {
|
||||
const hashSenderMsg = web3.utils.soliditySha3Raw(validator, hashMsg)
|
||||
|
||||
const fromCache = validatorsCache.getData(hashSenderMsg)
|
||||
if (fromCache) {
|
||||
return fromCache
|
||||
const signatureFromCache = validatorsCache.get(hashSenderMsg)
|
||||
if (signatureFromCache) {
|
||||
return {
|
||||
validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS
|
||||
}
|
||||
}
|
||||
|
||||
const confirmationContractMethod = fromHome ? getMessagesSigned : getAffirmationsSigned
|
||||
const confirmed = await confirmationContractMethod(bridgeContract, hashSenderMsg)
|
||||
const status = confirmed ? VALIDATOR_CONFIRMATION_STATUS.SUCCESS : VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
|
||||
|
||||
// If validator confirmed signature, we cache the result to avoid doing future requests for a result that won't change
|
||||
if (confirmed) {
|
||||
const confirmation: ConfirmationParam = {
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS,
|
||||
validator,
|
||||
timestamp: 0,
|
||||
txHash: ''
|
||||
}
|
||||
validatorsCache.setData(hashSenderMsg, confirmation)
|
||||
return confirmation
|
||||
validatorsCache.set(hashSenderMsg, confirmed)
|
||||
}
|
||||
|
||||
return {
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
|
||||
validator,
|
||||
timestamp: 0,
|
||||
txHash: ''
|
||||
status
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,7 +43,7 @@ export const getSuccessExecutionTransaction = (
|
||||
messageData: string,
|
||||
startBlock: number,
|
||||
getSuccessTransactions: (args: GetTransactionParams) => Promise<APITransaction[]>
|
||||
) => async (validatorData: ConfirmationParam): Promise<ConfirmationParam> => {
|
||||
) => async (validatorData: BasicConfirmationParam): Promise<ConfirmationParam> => {
|
||||
const { validator } = validatorData
|
||||
const validatorCacheKey = `${CACHE_KEY_SUCCESS}${validatorData.validator}-${messageData}`
|
||||
const fromCache = validatorsCache.getData(validatorCacheKey)
|
||||
@ -94,12 +87,11 @@ export const getSuccessExecutionTransaction = (
|
||||
}
|
||||
|
||||
export const getValidatorFailedTransaction = (
|
||||
web3: Web3,
|
||||
bridgeContract: Contract,
|
||||
messageData: string,
|
||||
startBlock: number,
|
||||
getFailedTransactions: (args: GetTransactionParams) => Promise<APITransaction[]>
|
||||
) => async (validatorData: ConfirmationParam): Promise<ConfirmationParam> => {
|
||||
) => async (validatorData: BasicConfirmationParam): Promise<ConfirmationParam> => {
|
||||
const validatorCacheKey = `${CACHE_KEY_FAILED}${validatorData.validator}-${messageData}`
|
||||
const failedFromCache = validatorsCache.getData(validatorCacheKey)
|
||||
|
||||
@ -114,33 +106,30 @@ export const getValidatorFailedTransaction = (
|
||||
startBlock,
|
||||
endBlock: homeBlockNumberProvider.get() || 0
|
||||
})
|
||||
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) {
|
||||
const failedTx = failedTransactions[0]
|
||||
const confirmation: ConfirmationParam = {
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.FAILED,
|
||||
validator: validatorData.validator,
|
||||
txHash: failedTx.hash,
|
||||
timestamp: parseInt(failedTx.timeStamp)
|
||||
}
|
||||
txHashTimestamp = parseInt(failedTx.timeStamp)
|
||||
txHash = failedTx.hash
|
||||
|
||||
if (failedTx.input && failedTx.input.length > 10) {
|
||||
try {
|
||||
const res = web3.eth.abi.decodeParameters(['bytes', 'bytes'], `0x${failedTx.input.slice(10)}`)
|
||||
confirmation.signature = res[0]
|
||||
confirmation.status = VALIDATOR_CONFIRMATION_STATUS.FAILED_VALID
|
||||
console.log(`Adding manual signature from failed message from ${validatorData.validator}`)
|
||||
} catch {}
|
||||
}
|
||||
validatorsCache.setData(validatorCacheKey, confirmation)
|
||||
return confirmation
|
||||
validatorsCache.setData(validatorCacheKey, {
|
||||
validator: validatorData.validator,
|
||||
status: newStatus,
|
||||
txHash,
|
||||
timestamp: txHashTimestamp
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
|
||||
validator: validatorData.validator,
|
||||
txHash: '',
|
||||
timestamp: 0
|
||||
status: newStatus,
|
||||
txHash,
|
||||
timestamp: txHashTimestamp
|
||||
}
|
||||
}
|
||||
|
||||
@ -148,7 +137,7 @@ export const getValidatorPendingTransaction = (
|
||||
bridgeContract: Contract,
|
||||
messageData: string,
|
||||
getPendingTransactions: (args: GetPendingTransactionParams) => Promise<APIPendingTransaction[]>
|
||||
) => async (validatorData: ConfirmationParam): Promise<ConfirmationParam> => {
|
||||
) => async (validatorData: BasicConfirmationParam): Promise<ConfirmationParam> => {
|
||||
const failedTransactions = await getPendingTransactions({
|
||||
account: validatorData.validator,
|
||||
to: bridgeContract.options.address,
|
||||
|
@ -10,37 +10,6 @@ import { SnapshotProvider } from '../services/SnapshotProvider'
|
||||
export interface MessageObject {
|
||||
id: string
|
||||
data: string
|
||||
sender?: string
|
||||
executor?: string
|
||||
obToken?: string
|
||||
obReceiver?: string
|
||||
}
|
||||
|
||||
export interface WarnRule {
|
||||
message: string
|
||||
sender?: string
|
||||
executor?: string
|
||||
obToken?: string
|
||||
obReceiver?: string
|
||||
}
|
||||
|
||||
export const matchesRule = (rule: WarnRule, msg: MessageObject) => {
|
||||
if (!msg.executor || !msg.sender) {
|
||||
return false
|
||||
}
|
||||
if (!!rule.executor && rule.executor.toLowerCase() !== msg.executor.toLowerCase()) {
|
||||
return false
|
||||
}
|
||||
if (!!rule.sender && rule.sender.toLowerCase() !== msg.sender.toLowerCase()) {
|
||||
return false
|
||||
}
|
||||
if (!!rule.obToken && (!msg.obToken || rule.obToken.toLowerCase() !== msg.obToken.toLowerCase())) {
|
||||
return false
|
||||
}
|
||||
if (!!rule.obReceiver && (!msg.obReceiver || rule.obReceiver.toLowerCase() !== msg.obReceiver.toLowerCase())) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const rawGetWeb3 = (url: string) => new Web3(new Web3.providers.HttpProvider(url))
|
||||
@ -57,33 +26,15 @@ export const filterEventsByAbi = (
|
||||
const eventHash = web3.eth.abi.encodeEventSignature(eventAbi)
|
||||
const events = txReceipt.logs.filter(e => e.address === bridgeAddress && e.topics[0] === eventHash)
|
||||
|
||||
if (!eventAbi || !eventAbi.inputs || !eventAbi.inputs.length) {
|
||||
return []
|
||||
}
|
||||
const inputs = eventAbi.inputs
|
||||
return events.map(e => {
|
||||
const { messageId, encodedData } = web3.eth.abi.decodeLog(inputs, e.data, [e.topics[1]])
|
||||
let sender, executor, obToken, obReceiver
|
||||
if (encodedData.length >= 160) {
|
||||
sender = `0x${encodedData.slice(66, 106)}`
|
||||
executor = `0x${encodedData.slice(106, 146)}`
|
||||
const dataOffset =
|
||||
160 + (parseInt(encodedData.slice(154, 156), 16) + parseInt(encodedData.slice(156, 158), 16)) * 2 + 8
|
||||
if (encodedData.length >= dataOffset + 64) {
|
||||
obToken = `0x${encodedData.slice(dataOffset + 24, dataOffset + 64)}`
|
||||
}
|
||||
if (encodedData.length >= dataOffset + 128) {
|
||||
obReceiver = `0x${encodedData.slice(dataOffset + 88, dataOffset + 128)}`
|
||||
}
|
||||
let decodedLogs: { [p: string]: string } = {
|
||||
messageId: '',
|
||||
encodedData: ''
|
||||
}
|
||||
return {
|
||||
id: messageId || '',
|
||||
data: encodedData || '',
|
||||
sender,
|
||||
executor,
|
||||
obToken,
|
||||
obReceiver
|
||||
if (eventAbi && eventAbi.inputs && eventAbi.inputs.length) {
|
||||
decodedLogs = web3.eth.abi.decodeLog(eventAbi.inputs, e.data, [e.topics[1]])
|
||||
}
|
||||
return { id: decodedLogs.messageId, data: decodedLogs.encodedData }
|
||||
})
|
||||
}
|
||||
|
||||
|
Binary file not shown.
@ -1,5 +1,5 @@
|
||||
const HOME_ERC_TO_NATIVE_ABI = require('../contracts/build/contracts/HomeBridgeErcToNative').abi
|
||||
const FOREIGN_ERC_TO_NATIVE_ABI = require('../contracts/build/contracts/XDaiForeignBridge.json').abi
|
||||
const FOREIGN_ERC_TO_NATIVE_ABI = require('../contracts/build/contracts/ForeignBridgeErcToNative').abi
|
||||
const ERC20_ABI = require('../contracts/build/contracts/ERC20').abi
|
||||
const BLOCK_REWARD_ABI = require('../contracts/build/contracts/BlockRewardMock').abi
|
||||
const BRIDGE_VALIDATORS_ABI = require('../contracts/build/contracts/BridgeValidators').abi
|
||||
|
@ -8,7 +8,6 @@
|
||||
"test": "NODE_ENV=test mocha"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mycrypto/gas-estimation": "^1.1.0",
|
||||
"gas-price-oracle": "^0.1.5",
|
||||
"web3-utils": "^1.3.0",
|
||||
"node-fetch": "^2.1.2"
|
||||
|
@ -1,6 +1,5 @@
|
||||
const { toWei, toBN, BN } = require('web3-utils')
|
||||
const { GasPriceOracle } = require('gas-price-oracle')
|
||||
const { estimateFees } = require('@mycrypto/gas-estimation')
|
||||
const fetch = require('node-fetch')
|
||||
const { BRIDGE_MODES } = require('./constants')
|
||||
const { REWARDABLE_VALIDATORS_ABI } = require('./abis')
|
||||
@ -177,20 +176,12 @@ const gasPriceWithinLimits = (gasPrice, limits) => {
|
||||
const normalizeGasPrice = (oracleGasPrice, factor, limits = null) => {
|
||||
let gasPrice = oracleGasPrice * factor
|
||||
gasPrice = gasPriceWithinLimits(gasPrice, limits)
|
||||
return toWei(gasPrice.toFixed(2).toString(), 'gwei')
|
||||
return toBN(toWei(gasPrice.toFixed(2).toString(), 'gwei'))
|
||||
}
|
||||
|
||||
const gasPriceFromSupplier = async (web3, url, options = {}) => {
|
||||
const gasPriceFromSupplier = async (url, options = {}) => {
|
||||
try {
|
||||
let json
|
||||
if (url === 'eip1559-gas-estimation') {
|
||||
const { maxFeePerGas, maxPriorityFeePerGas } = await estimateFees(web3)
|
||||
const res = { maxFeePerGas: maxFeePerGas.toString(10), maxPriorityFeePerGas: maxPriorityFeePerGas.toString(10) }
|
||||
options.logger &&
|
||||
options.logger.debug &&
|
||||
options.logger.debug(res, 'Gas price updated using eip1559-gas-estimation')
|
||||
return res
|
||||
}
|
||||
if (url === 'gas-price-oracle') {
|
||||
json = await gasPriceOracle.fetchGasPricesOffChain()
|
||||
} else if (url) {
|
||||
@ -214,7 +205,7 @@ const gasPriceFromSupplier = async (web3, url, options = {}) => {
|
||||
options.logger.debug &&
|
||||
options.logger.debug({ oracleGasPrice, normalizedGasPrice }, 'Gas price updated using the API')
|
||||
|
||||
return { gasPrice: normalizedGasPrice }
|
||||
return normalizedGasPrice
|
||||
} catch (e) {
|
||||
options.logger && options.logger.error && options.logger.error(`Gas Price API is not available. ${e.message}`)
|
||||
}
|
||||
@ -223,11 +214,11 @@ const gasPriceFromSupplier = async (web3, url, options = {}) => {
|
||||
|
||||
const gasPriceFromContract = async (bridgeContract, options = {}) => {
|
||||
try {
|
||||
const gasPrice = (await bridgeContract.methods.gasPrice().call()).toString()
|
||||
const gasPrice = await bridgeContract.methods.gasPrice().call()
|
||||
options.logger &&
|
||||
options.logger.debug &&
|
||||
options.logger.debug({ gasPrice }, 'Gas price updated using the contracts')
|
||||
return { gasPrice }
|
||||
return gasPrice
|
||||
} catch (e) {
|
||||
options.logger &&
|
||||
options.logger.error &&
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit 908a48107919d4ab127f9af07d44d47eac91547e
|
||||
Subproject commit 004d466a3d593e6304e52c74e6c3e801b6a33b32
|
@ -1,6 +1,6 @@
|
||||
/var/log/docker/*/docker.log {
|
||||
rotate 5
|
||||
size 100M
|
||||
size 1G
|
||||
compress
|
||||
missingok
|
||||
delaycompress
|
||||
@ -8,7 +8,7 @@
|
||||
}
|
||||
/var/log/docker/*.log {
|
||||
rotate 5
|
||||
size 100M
|
||||
size 1G
|
||||
compress
|
||||
missingok
|
||||
delaycompress
|
||||
|
@ -7,7 +7,7 @@
|
||||
loop_control:
|
||||
loop_var: file
|
||||
|
||||
- name: Set the oracle's containers local logs configuration file
|
||||
- name: Set the local container logs configuration file
|
||||
template:
|
||||
src: 31-oracle-docker.conf.j2
|
||||
dest: /etc/rsyslog.d/31-oracle-docker.conf
|
||||
@ -15,22 +15,6 @@
|
||||
group: root
|
||||
mode: 0644
|
||||
|
||||
- name: Set the redis container local logs configuration file
|
||||
template:
|
||||
src: 32-redis-docker.conf.j2
|
||||
dest: /etc/rsyslog.d/32-redis-docker.conf
|
||||
owner: root
|
||||
group: root
|
||||
mode: 0644
|
||||
|
||||
- name: Set the rabbit MQ container local logs configuration file
|
||||
template:
|
||||
src: 33-rabbit-docker.conf.j2
|
||||
dest: /etc/rsyslog.d/33-rabbit-docker.conf
|
||||
owner: root
|
||||
group: root
|
||||
mode: 0644
|
||||
|
||||
- name: Set the log configuration file to send container logs to remote server
|
||||
template:
|
||||
src: 36-oracle-remote-logging.conf.j2
|
||||
|
@ -11,16 +11,9 @@ ORACLE_HOME_RPC_POLLING_INTERVAL={{ ORACLE_HOME_RPC_POLLING_INTERVAL }}
|
||||
|
||||
## Foreign contract
|
||||
COMMON_FOREIGN_RPC_URL={{ COMMON_FOREIGN_RPC_URL }}
|
||||
{% if ORACLE_FOREIGN_ARCHIVE_RPC_URL | default('') != '' %}
|
||||
ORACLE_FOREIGN_ARCHIVE_RPC_URL={{ ORACLE_FOREIGN_ARCHIVE_RPC_URL }}
|
||||
{% endif %}
|
||||
COMMON_FOREIGN_BRIDGE_ADDRESS={{ COMMON_FOREIGN_BRIDGE_ADDRESS }}
|
||||
ORACLE_FOREIGN_RPC_POLLING_INTERVAL={{ ORACLE_FOREIGN_RPC_POLLING_INTERVAL }}
|
||||
|
||||
{% if ORACLE_TX_REDUNDANCY | default('') != '' %}
|
||||
ORACLE_TX_REDUNDANCY={{ ORACLE_TX_REDUNDANCY }}
|
||||
{% endif %}
|
||||
|
||||
## Gasprice
|
||||
{% if COMMON_HOME_GAS_PRICE_SUPPLIER_URL | default('') != '' %}
|
||||
COMMON_HOME_GAS_PRICE_SUPPLIER_URL={{ COMMON_HOME_GAS_PRICE_SUPPLIER_URL }}
|
||||
@ -54,28 +47,8 @@ COMMON_FOREIGN_GAS_PRICE_FACTOR={{ COMMON_FOREIGN_GAS_PRICE_FACTOR }}
|
||||
ORACLE_ALLOW_HTTP_FOR_RPC={{ "yes" if ORACLE_ALLOW_HTTP_FOR_RPC else "no" }}
|
||||
ORACLE_QUEUE_URL={{ ORACLE_QUEUE_URL }}
|
||||
ORACLE_REDIS_URL={{ ORACLE_REDIS_URL }}
|
||||
{% if ORACLE_FOREIGN_TX_RESEND_INTERVAL | default('') != '' %}
|
||||
ORACLE_FOREIGN_TX_RESEND_INTERVAL={{ ORACLE_FOREIGN_TX_RESEND_INTERVAL }}
|
||||
{% endif %}
|
||||
{% if ORACLE_HOME_TX_RESEND_INTERVAL | default('') != '' %}
|
||||
ORACLE_HOME_TX_RESEND_INTERVAL={{ ORACLE_HOME_TX_RESEND_INTERVAL }}
|
||||
{% endif %}
|
||||
|
||||
## Emergency shutdown configuration
|
||||
{% if ORACLE_SHUTDOWN_SERVICE_URL | default('') != '' %}
|
||||
ORACLE_SHUTDOWN_SERVICE_URL={{ ORACLE_SHUTDOWN_SERVICE_URL }}
|
||||
{% endif %}
|
||||
{% if ORACLE_SHUTDOWN_SERVICE_POLLING_INTERVAL | default('') != '' %}
|
||||
ORACLE_SHUTDOWN_SERVICE_POLLING_INTERVAL={{ ORACLE_SHUTDOWN_SERVICE_POLLING_INTERVAL }}
|
||||
{% endif %}
|
||||
{% if ORACLE_SIDE_RPC_URL | default('') != '' %}
|
||||
ORACLE_SIDE_RPC_URL={{ ORACLE_SIDE_RPC_URL }}
|
||||
{% endif %}
|
||||
{% if ORACLE_SHUTDOWN_CONTRACT_ADDRESS | default('') != '' %}
|
||||
ORACLE_SHUTDOWN_CONTRACT_ADDRESS={{ ORACLE_SHUTDOWN_CONTRACT_ADDRESS }}
|
||||
{% endif %}
|
||||
{% if ORACLE_SHUTDOWN_CONTRACT_METHOD | default('') != '' %}
|
||||
ORACLE_SHUTDOWN_CONTRACT_METHOD={{ ORACLE_SHUTDOWN_CONTRACT_METHOD }}
|
||||
{% if ORACLE_TX_REDUNDANCY | default('') != '' %}
|
||||
ORACLE_TX_REDUNDANCY={{ ORACLE_TX_REDUNDANCY }}
|
||||
{% endif %}
|
||||
|
||||
{% if ORACLE_HOME_START_BLOCK | default('') != '' %}
|
||||
|
@ -1,11 +0,0 @@
|
||||
$FileCreateMode 0644
|
||||
template(name="DockerLogFileName_Redis" type="list") {
|
||||
constant(value="/var/log/docker/")
|
||||
property(name="syslogtag" securepath="replace" regex.type="ERE" regex.submatch="1" regex.expression="oracle_(.*redis.*)\\/[a-zA-Z0-9]+\\[")
|
||||
constant(value="/docker.log")
|
||||
}
|
||||
|
||||
if $programname contains 'oracle' and $programname contains 'redis' then \
|
||||
?DockerLogFileName_Redis
|
||||
|
||||
$FileCreateMode 0600
|
@ -1,11 +0,0 @@
|
||||
$FileCreateMode 0644
|
||||
template(name="DockerLogFileName_Rabbit" type="list") {
|
||||
constant(value="/var/log/docker/")
|
||||
property(name="syslogtag" securepath="replace" regex.type="ERE" regex.submatch="1" regex.expression="oracle_(.*rabbit.*)\\/[a-zA-Z0-9]+\\[")
|
||||
constant(value="/docker.log")
|
||||
}
|
||||
|
||||
if $programname contains 'oracle' and $programname contains 'rabbit' then \
|
||||
?DockerLogFileName_Rabbit
|
||||
|
||||
$FileCreateMode 0600
|
@ -23,9 +23,3 @@ ORACLE_HOME_START_BLOCK=1
|
||||
ORACLE_FOREIGN_START_BLOCK=1
|
||||
ORACLE_HOME_TO_FOREIGN_BLOCK_LIST=/mono/oracle/access-lists/block_list.txt
|
||||
ORACLE_FOREIGN_ARCHIVE_RPC_URL=http://parity2:8545
|
||||
ORACLE_HOME_EVENTS_REPROCESSING=false
|
||||
ORACLE_HOME_EVENTS_REPROCESSING_BATCH_SIZE=10
|
||||
ORACLE_HOME_EVENTS_REPROCESSING_BLOCK_DELAY=10
|
||||
ORACLE_FOREIGN_EVENTS_REPROCESSING=true
|
||||
ORACLE_FOREIGN_EVENTS_REPROCESSING_BATCH_SIZE=10
|
||||
ORACLE_FOREIGN_EVENTS_REPROCESSING_BLOCK_DELAY=10
|
||||
|
@ -22,9 +22,3 @@ ORACLE_ALLOW_HTTP_FOR_RPC=yes
|
||||
ORACLE_HOME_START_BLOCK=1
|
||||
ORACLE_FOREIGN_START_BLOCK=1
|
||||
ORACLE_HOME_TO_FOREIGN_BLOCK_LIST=/mono/oracle/access-lists/block_list.txt
|
||||
ORACLE_HOME_EVENTS_REPROCESSING=true
|
||||
ORACLE_HOME_EVENTS_REPROCESSING_BATCH_SIZE=10
|
||||
ORACLE_HOME_EVENTS_REPROCESSING_BLOCK_DELAY=10
|
||||
ORACLE_FOREIGN_EVENTS_REPROCESSING=true
|
||||
ORACLE_FOREIGN_EVENTS_REPROCESSING_BATCH_SIZE=10
|
||||
ORACLE_FOREIGN_EVENTS_REPROCESSING_BLOCK_DELAY=10
|
||||
|
@ -38,7 +38,7 @@
|
||||
"ercToNativeBridge": {
|
||||
"home": "0x5118AC62AE912Dd5B51EEfF7338c4fcb0248Ba8c",
|
||||
"foreign": "0x32198D570fffC7033641F8A9094FFDCaAEF42624",
|
||||
"foreignToken": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"foreignToken": "0x7cc4b1851c35959d34e635a470f6b5c43ba3c9c9",
|
||||
"monitor": "http://monitor-erc20-native:3012/bridge"
|
||||
},
|
||||
"amb": {
|
||||
|
@ -32,7 +32,7 @@ FOREIGN_GAS_PRICE=10000000000
|
||||
FOREIGN_REWARDABLE=false
|
||||
|
||||
BLOCK_REWARD_ADDRESS=0xdbeE25CbE97e4A5CC6c499875774dc7067E9426B
|
||||
ERC20_TOKEN_ADDRESS=0x6B175474E89094C44Da98b954EedeAC495271d0F
|
||||
ERC20_TOKEN_ADDRESS=0x7cc4b1851c35959d34e635a470f6b5c43ba3c9c9
|
||||
|
||||
REQUIRED_NUMBER_OF_VALIDATORS=1
|
||||
VALIDATORS="0xaaB52d66283F7A1D5978bcFcB55721ACB467384b 0xdCC784657C78054aa61FbcFFd2605F32374816A4 0xDcef88209a20D52165230104B245803C3269454d"
|
||||
|
@ -46,19 +46,29 @@ async function main(bridgeMode, eventsInfo) {
|
||||
const foreignBridge = new web3Foreign.eth.Contract(FOREIGN_ERC_TO_NATIVE_ABI, COMMON_FOREIGN_BRIDGE_ADDRESS)
|
||||
const erc20Address = await foreignBridge.methods.erc20token().call()
|
||||
const erc20Contract = new web3Foreign.eth.Contract(ERC20_ABI, erc20Address)
|
||||
let investedAmountInDai = 0
|
||||
let bridgeDsrBalance = 0
|
||||
let displayChaiToken = false
|
||||
|
||||
try {
|
||||
logger.debug('calling foreignBridge.methods.isChaiTokenEnabled')
|
||||
if (await foreignBridge.methods.isChaiTokenEnabled().call()) {
|
||||
displayChaiToken = true
|
||||
logger.debug('calling foreignBridge.methods.investedAmountInDai')
|
||||
investedAmountInDai = await foreignBridge.methods.investedAmountInDai().call()
|
||||
logger.debug('calling foreignBridge.methods.dsrBalance')
|
||||
bridgeDsrBalance = await foreignBridge.methods.dsrBalance().call()
|
||||
} else {
|
||||
logger.debug('Chai token is currently disabled')
|
||||
}
|
||||
} catch (e) {
|
||||
logger.debug('Methods for chai token are not present')
|
||||
}
|
||||
|
||||
logger.debug('calling erc20Contract.methods.balanceOf')
|
||||
const foreignErc20Balance = await erc20Contract.methods
|
||||
.balanceOf(COMMON_FOREIGN_BRIDGE_ADDRESS)
|
||||
.call({}, foreignDelayedBlockNumber)
|
||||
let foreignErc20BalanceBN = new BN(foreignErc20Balance).plus(lateForeignConfirmationsTotalValue)
|
||||
try {
|
||||
logger.debug('calling foreignBridge.methods.investedAmount')
|
||||
const invested = await foreignBridge.methods.investedAmount(erc20Address).call({}, foreignDelayedBlockNumber)
|
||||
foreignErc20BalanceBN = foreignErc20BalanceBN.plus(invested)
|
||||
} catch (_) {
|
||||
logger.debug('compounding related methods are not present in the foreign bridge')
|
||||
}
|
||||
|
||||
const homeBridge = new web3Home.eth.Contract(HOME_ERC_TO_NATIVE_ABI, COMMON_HOME_BRIDGE_ADDRESS)
|
||||
logger.debug('calling homeBridge.methods.blockRewardContract')
|
||||
@ -74,16 +84,30 @@ async function main(bridgeMode, eventsInfo) {
|
||||
const mintedCoinsBN = new BN(mintedCoins)
|
||||
const burntCoinsBN = new BN(burntCoins)
|
||||
const totalSupplyBN = mintedCoinsBN.minus(burntCoinsBN)
|
||||
const foreignErc20BalanceBN = new BN(foreignErc20Balance).plus(lateForeignConfirmationsTotalValue)
|
||||
const investedAmountInDaiBN = new BN(investedAmountInDai)
|
||||
const bridgeDsrBalanceBN = new BN(bridgeDsrBalance)
|
||||
|
||||
const diff = foreignErc20BalanceBN
|
||||
.plus(investedAmountInDaiBN)
|
||||
.minus(totalSupplyBN)
|
||||
.toFixed()
|
||||
|
||||
const foreign = {
|
||||
erc20Balance: Web3Utils.fromWei(foreignErc20Balance)
|
||||
}
|
||||
|
||||
if (displayChaiToken) {
|
||||
foreign.investedErc20Balance = Web3Utils.fromWei(investedAmountInDai)
|
||||
foreign.accumulatedInterest = Web3Utils.fromWei(bridgeDsrBalanceBN.minus(investedAmountInDaiBN).toString(10))
|
||||
}
|
||||
|
||||
const diff = foreignErc20BalanceBN.minus(totalSupplyBN).toFixed()
|
||||
logger.debug('Done')
|
||||
return {
|
||||
home: {
|
||||
totalSupply: Web3Utils.fromWei(totalSupplyBN.toFixed())
|
||||
},
|
||||
foreign: {
|
||||
erc20Balance: Web3Utils.fromWei(foreignErc20Balance)
|
||||
},
|
||||
foreign,
|
||||
balanceDiff: Number(Web3Utils.fromWei(diff)),
|
||||
...blockRanges,
|
||||
lastChecked: Math.floor(Date.now() / 1000)
|
||||
|
@ -11,6 +11,7 @@ const {
|
||||
OLD_AMB_USER_REQUEST_FOR_AFFIRMATION_ABI
|
||||
} = require('../../commons')
|
||||
const { normalizeEventInformation } = require('./message')
|
||||
const { filterTransferBeforeES } = require('./tokenUtils')
|
||||
const { writeFile, readCacheFile } = require('./file')
|
||||
const { web3Home, web3Foreign, getHomeBlockNumber, getForeignBlockNumber } = require('./web3')
|
||||
const { getPastEvents } = require('./web3Cache')
|
||||
@ -159,32 +160,80 @@ async function main(mode) {
|
||||
filter: { to: COMMON_FOREIGN_BRIDGE_ADDRESS }
|
||||
},
|
||||
chain: 'foreign'
|
||||
}))
|
||||
.map(normalizeEvent)
|
||||
.filter(e => e.recipient !== ZERO_ADDRESS) // filter mint operation during SCD-to-MCD swaps
|
||||
.filter(e => e.recipient.toLowerCase() !== '0x5d3a536e4d6dbd6114cc1ead35777bab948e3643') // filter cDai withdraws during compounding
|
||||
|
||||
// Get transfer events for each previously used Sai token
|
||||
const saiTokenAddress = '0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359'
|
||||
const halfDuplexTokenContract = new web3Foreign.eth.Contract(ERC20_ABI, saiTokenAddress)
|
||||
logger.debug('Half duplex token:', saiTokenAddress)
|
||||
logger.debug("calling halfDuplexTokenContract.getPastEvents('Transfer')")
|
||||
// https://etherscan.io/tx/0xd0c3c92c94e05bc71256055ce8c4c993e047f04e04f3283a04e4cb077b71f6c6
|
||||
const blockNumberHalfDuplexDisabled = 9884448
|
||||
const halfDuplexTransferEvents = (await getPastEvents(halfDuplexTokenContract, {
|
||||
event: 'Transfer',
|
||||
fromBlock: MONITOR_FOREIGN_START_BLOCK,
|
||||
toBlock: Math.min(blockNumberHalfDuplexDisabled, foreignDelayedBlockNumber),
|
||||
options: {
|
||||
filter: { to: COMMON_FOREIGN_BRIDGE_ADDRESS }
|
||||
},
|
||||
chain: 'foreign'
|
||||
})).map(normalizeEvent)
|
||||
|
||||
transferEvents = [...halfDuplexTransferEvents, ...transferEvents]
|
||||
let directTransfers = transferEvents
|
||||
const tokensSwappedAbiExists = FOREIGN_ABI.filter(e => e.type === 'event' && e.name === 'TokensSwapped')[0]
|
||||
if (tokensSwappedAbiExists) {
|
||||
logger.debug('collecting half duplex tokens participated in the bridge balance')
|
||||
logger.debug("calling foreignBridge.getPastEvents('TokensSwapped')")
|
||||
const tokensSwappedEvents = await getPastEvents(foreignBridge, {
|
||||
event: 'TokensSwapped',
|
||||
fromBlock: MONITOR_FOREIGN_START_BLOCK,
|
||||
toBlock: foreignBlockNumber,
|
||||
chain: 'foreign',
|
||||
safeToBlock: foreignDelayedBlockNumber
|
||||
})
|
||||
|
||||
// Get token swap events emitted by foreign bridge
|
||||
const bridgeTokensSwappedEvents = tokensSwappedEvents.filter(e => e.address === COMMON_FOREIGN_BRIDGE_ADDRESS)
|
||||
|
||||
// Get transfer events for each previous erc20
|
||||
const uniqueTokenAddressesSet = new Set(bridgeTokensSwappedEvents.map(e => e.returnValues.from))
|
||||
|
||||
// Exclude chai token from previous erc20
|
||||
try {
|
||||
logger.debug('calling foreignBridge.chaiToken() to remove it from half duplex tokens list')
|
||||
const chaiToken = await foreignBridge.methods.chaiToken().call()
|
||||
uniqueTokenAddressesSet.delete(chaiToken)
|
||||
} catch (e) {
|
||||
logger.debug('call to foreignBridge.chaiToken() failed')
|
||||
}
|
||||
// Exclude dai token from previous erc20
|
||||
try {
|
||||
logger.debug('calling foreignBridge.erc20token() to remove it from half duplex tokens list')
|
||||
const daiToken = await foreignBridge.methods.erc20token().call()
|
||||
uniqueTokenAddressesSet.delete(daiToken)
|
||||
} catch (e) {
|
||||
logger.debug('call to foreignBridge.erc20token() failed')
|
||||
}
|
||||
|
||||
const uniqueTokenAddresses = [...uniqueTokenAddressesSet]
|
||||
await Promise.all(
|
||||
uniqueTokenAddresses.map(async tokenAddress => {
|
||||
const halfDuplexTokenContract = new web3Foreign.eth.Contract(ERC20_ABI, tokenAddress)
|
||||
|
||||
logger.debug('Half duplex token:', tokenAddress)
|
||||
logger.debug("calling halfDuplexTokenContract.getPastEvents('Transfer')")
|
||||
const halfDuplexTransferEvents = (await getPastEvents(halfDuplexTokenContract, {
|
||||
event: 'Transfer',
|
||||
fromBlock: MONITOR_FOREIGN_START_BLOCK,
|
||||
toBlock: foreignDelayedBlockNumber,
|
||||
options: {
|
||||
filter: { to: COMMON_FOREIGN_BRIDGE_ADDRESS }
|
||||
},
|
||||
chain: 'foreign'
|
||||
})).map(normalizeEvent)
|
||||
|
||||
// Remove events after the ES
|
||||
logger.debug('filtering half duplex transfers happened before ES')
|
||||
const validHalfDuplexTransfers = await filterTransferBeforeES(halfDuplexTransferEvents)
|
||||
|
||||
transferEvents = [...validHalfDuplexTransfers, ...transferEvents]
|
||||
})
|
||||
)
|
||||
|
||||
// filter transfer that is part of a token swap
|
||||
directTransfers = transferEvents.filter(
|
||||
e =>
|
||||
bridgeTokensSwappedEvents.findIndex(
|
||||
t => t.transactionHash === e.referenceTx && e.recipient === ZERO_ADDRESS
|
||||
) === -1
|
||||
)
|
||||
}
|
||||
|
||||
// Get transfer events that didn't have a UserRequestForAffirmation event in the same transaction
|
||||
const directTransfers = transferEvents.filter(
|
||||
directTransfers = directTransfers.filter(
|
||||
e => foreignToHomeRequests.findIndex(t => t.referenceTx === e.referenceTx) === -1
|
||||
)
|
||||
|
||||
|
27
monitor/utils/tokenUtils.js
Normal file
27
monitor/utils/tokenUtils.js
Normal file
@ -0,0 +1,27 @@
|
||||
// https://etherscan.io/tx/0xd0c3c92c94e05bc71256055ce8c4c993e047f04e04f3283a04e4cb077b71f6c6
|
||||
const blockNumberHalfDuplexDisabled = 9884448
|
||||
|
||||
/**
|
||||
* Returns true if the event was before the bridge stopped supporting half duplex transfers.
|
||||
*/
|
||||
async function transferBeforeES(event) {
|
||||
return event.blockNumber < blockNumberHalfDuplexDisabled
|
||||
}
|
||||
|
||||
async function filterTransferBeforeES(array) {
|
||||
const newArray = []
|
||||
// Iterate events from newer to older
|
||||
for (let i = array.length - 1; i >= 0; i--) {
|
||||
const beforeES = await transferBeforeES(array[i])
|
||||
if (beforeES) {
|
||||
// add element to first position so the new array will have the same order
|
||||
newArray.unshift(array[i])
|
||||
}
|
||||
}
|
||||
return newArray
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
filterTransferBeforeES,
|
||||
blockNumberHalfDuplexDisabled
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
const Web3 = require('web3')
|
||||
const assert = require('assert')
|
||||
const { ASYNC_CALL_ERRORS } = require('../../oracle/src/utils/constants')
|
||||
const { user, homeRPC, foreignRPC, amb, validator } = require('../../e2e-commons/constants.json')
|
||||
const { uniformRetry } = require('../../e2e-commons/utils')
|
||||
const { BOX_ABI, HOME_AMB_ABI, FOREIGN_AMB_ABI, ambInformationSignatures } = require('../../commons')
|
||||
@ -304,7 +303,7 @@ describe('arbitrary message bridging', () => {
|
||||
await makeAsyncCall(selector, data2)
|
||||
|
||||
assert(!(await homeBox.methods.status().call()), 'status is true')
|
||||
assert.strictEqual(await homeBox.methods.data().call(), ASYNC_CALL_ERRORS.REVERT, 'returned data is incorrect')
|
||||
assert.strictEqual(await homeBox.methods.data().call(), null, 'returned data is incorrect')
|
||||
|
||||
const data3 = homeWeb3.eth.abi.encodeParameters(
|
||||
['address', 'address', 'uint256', 'bytes'],
|
||||
@ -314,7 +313,7 @@ describe('arbitrary message bridging', () => {
|
||||
await makeAsyncCall(selector, data3)
|
||||
|
||||
assert(!(await homeBox.methods.status().call()), 'status is true')
|
||||
assert.strictEqual(await homeBox.methods.data().call(), ASYNC_CALL_ERRORS.REVERT, 'returned data is incorrect')
|
||||
assert.strictEqual(await homeBox.methods.data().call(), null, 'returned data is incorrect')
|
||||
})
|
||||
|
||||
it('should make async eth_call for specific block', async () => {
|
||||
@ -355,11 +354,6 @@ describe('arbitrary message bridging', () => {
|
||||
await makeAsyncCall(selector, data3)
|
||||
|
||||
assert(!(await homeBox.methods.status().call()), 'status is true')
|
||||
assert.strictEqual(
|
||||
await homeBox.methods.data().call(),
|
||||
ASYNC_CALL_ERRORS.BLOCK_IS_IN_THE_FUTURE,
|
||||
'returned data is incorrect'
|
||||
)
|
||||
})
|
||||
|
||||
it('should make async eth_blockNumber', async () => {
|
||||
|
@ -33,10 +33,6 @@ const homeBridge = new homeWeb3.eth.Contract(HOME_ERC_TO_NATIVE_ABI, COMMON_HOME
|
||||
|
||||
describe('erc to native', () => {
|
||||
before(async () => {
|
||||
console.log('Initializing interest')
|
||||
await foreignBridge.methods
|
||||
.initializeInterest(ercToNativeBridge.foreignToken, 1, 1, validator.address)
|
||||
.send({ from: validator.address, gas: '4000000' })
|
||||
if (process.env.ULTIMATE === 'true') {
|
||||
return
|
||||
}
|
||||
@ -116,7 +112,6 @@ describe('erc to native', () => {
|
||||
.catch(e => {
|
||||
console.error(e)
|
||||
})
|
||||
await foreignBridge.methods.investDai().send({ from: validator.address, gas: '4000000' })
|
||||
|
||||
// check that balance increases
|
||||
await uniformRetry(async retry => {
|
||||
|
@ -1,10 +1,10 @@
|
||||
# POA TokenBridge / Oracle
|
||||
An oracle responsible for listening to bridge related events and authorizing asset transfers.
|
||||
Oracle responsible for listening to bridge related events and authorizing asset transfers.
|
||||
|
||||
## Overview
|
||||
Please refer to the [POA TokenBridge](../README.md) overview first of all.
|
||||
|
||||
The oracle is deployed on specified validator nodes (only nodes whose private keys correspond to addresses specified in the smart contracts) in the network. It connects to two chains via a Remote Procedure Call (RPC) and is responsible for:
|
||||
The Oracle is deployed on specified validator nodes (only nodes whose private keys correspond to addresses specified in the smart contracts) in the network. It connects to two chains via a Remote Procedure Call (RPC) and is responsible for:
|
||||
- listening to events related to bridge contracts
|
||||
- sending transactions to authorize asset transfers
|
||||
|
||||
@ -69,7 +69,7 @@ For more information on the Redis/RabbitMQ requirements, see [#90](/../../issues
|
||||
}
|
||||
```
|
||||
|
||||
## Install and configure the oracle
|
||||
## Install and configure the Oracle
|
||||
|
||||
1. [Initialize](../README.md#initializing-the-monorepository) the monorepository.
|
||||
|
||||
|
@ -7,15 +7,7 @@ const {
|
||||
HOME_AMB_ABI,
|
||||
FOREIGN_AMB_ABI
|
||||
} = require('../../commons')
|
||||
const {
|
||||
web3Home,
|
||||
web3Foreign,
|
||||
web3HomeRedundant,
|
||||
web3HomeFallback,
|
||||
web3ForeignRedundant,
|
||||
web3ForeignFallback,
|
||||
web3ForeignArchive
|
||||
} = require('../src/services/web3')
|
||||
const { web3Home, web3Foreign } = require('../src/services/web3')
|
||||
const { add0xPrefix, privateKeyToAddress } = require('../src/utils/utils')
|
||||
const { EXIT_CODES } = require('../src/utils/constants')
|
||||
|
||||
@ -31,15 +23,7 @@ const {
|
||||
ORACLE_HOME_START_BLOCK,
|
||||
ORACLE_FOREIGN_START_BLOCK,
|
||||
ORACLE_HOME_RPC_BLOCK_POLLING_LIMIT,
|
||||
ORACLE_FOREIGN_RPC_BLOCK_POLLING_LIMIT,
|
||||
ORACLE_HOME_EVENTS_REPROCESSING,
|
||||
ORACLE_HOME_EVENTS_REPROCESSING_BATCH_SIZE,
|
||||
ORACLE_HOME_EVENTS_REPROCESSING_BLOCK_DELAY,
|
||||
ORACLE_HOME_RPC_SYNC_STATE_CHECK_INTERVAL,
|
||||
ORACLE_FOREIGN_EVENTS_REPROCESSING,
|
||||
ORACLE_FOREIGN_EVENTS_REPROCESSING_BATCH_SIZE,
|
||||
ORACLE_FOREIGN_EVENTS_REPROCESSING_BLOCK_DELAY,
|
||||
ORACLE_FOREIGN_RPC_SYNC_STATE_CHECK_INTERVAL
|
||||
ORACLE_FOREIGN_RPC_BLOCK_POLLING_LIMIT
|
||||
} = process.env
|
||||
|
||||
let homeAbi
|
||||
@ -73,19 +57,11 @@ const homeConfig = {
|
||||
bridgeAddress: COMMON_HOME_BRIDGE_ADDRESS,
|
||||
bridgeABI: homeAbi,
|
||||
pollingInterval: parseInt(ORACLE_HOME_RPC_POLLING_INTERVAL, 10),
|
||||
syncCheckInterval: parseInt(ORACLE_HOME_RPC_SYNC_STATE_CHECK_INTERVAL, 10) || 60000,
|
||||
startBlock: parseInt(ORACLE_HOME_START_BLOCK, 10) || 0,
|
||||
blockPollingLimit: parseInt(ORACLE_HOME_RPC_BLOCK_POLLING_LIMIT, 10),
|
||||
web3: web3Home,
|
||||
web3Redundant: web3HomeRedundant,
|
||||
web3Fallback: web3HomeFallback,
|
||||
bridgeContract: homeContract,
|
||||
eventContract: homeContract,
|
||||
reprocessingOptions: {
|
||||
enabled: ORACLE_HOME_EVENTS_REPROCESSING === 'true',
|
||||
batchSize: parseInt(ORACLE_HOME_EVENTS_REPROCESSING_BATCH_SIZE, 10) || 1000,
|
||||
blockDelay: parseInt(ORACLE_HOME_EVENTS_REPROCESSING_BLOCK_DELAY, 10) || 500
|
||||
}
|
||||
eventContract: homeContract
|
||||
}
|
||||
|
||||
const foreignContract = new web3Foreign.eth.Contract(foreignAbi, COMMON_FOREIGN_BRIDGE_ADDRESS)
|
||||
@ -94,20 +70,11 @@ const foreignConfig = {
|
||||
bridgeAddress: COMMON_FOREIGN_BRIDGE_ADDRESS,
|
||||
bridgeABI: foreignAbi,
|
||||
pollingInterval: parseInt(ORACLE_FOREIGN_RPC_POLLING_INTERVAL, 10),
|
||||
syncCheckInterval: parseInt(ORACLE_FOREIGN_RPC_SYNC_STATE_CHECK_INTERVAL, 10) || 60000,
|
||||
startBlock: parseInt(ORACLE_FOREIGN_START_BLOCK, 10) || 0,
|
||||
blockPollingLimit: parseInt(ORACLE_FOREIGN_RPC_BLOCK_POLLING_LIMIT, 10),
|
||||
web3: web3Foreign,
|
||||
web3Redundant: web3ForeignRedundant,
|
||||
web3Fallback: web3ForeignFallback,
|
||||
web3Archive: web3ForeignArchive || web3Foreign,
|
||||
bridgeContract: foreignContract,
|
||||
eventContract: foreignContract,
|
||||
reprocessingOptions: {
|
||||
enabled: ORACLE_FOREIGN_EVENTS_REPROCESSING === 'true',
|
||||
batchSize: parseInt(ORACLE_FOREIGN_EVENTS_REPROCESSING_BATCH_SIZE, 10) || 500,
|
||||
blockDelay: parseInt(ORACLE_FOREIGN_EVENTS_REPROCESSING_BLOCK_DELAY, 10) || 250
|
||||
}
|
||||
eventContract: foreignContract
|
||||
}
|
||||
|
||||
const maxProcessingTime =
|
||||
|
@ -1,37 +0,0 @@
|
||||
const baseConfig = require('./base.config')
|
||||
|
||||
const { DEFAULT_TRANSACTION_RESEND_INTERVAL } = require('../src/utils/constants')
|
||||
const { MEV_HELPER_ABI } = require('../src/utils/mev')
|
||||
const { web3Foreign, getFlashbotsProvider } = require('../src/services/web3')
|
||||
|
||||
const {
|
||||
ORACLE_FOREIGN_TX_RESEND_INTERVAL,
|
||||
ORACLE_MEV_FOREIGN_HELPER_CONTRACT_ADDRESS,
|
||||
ORACLE_MEV_FOREIGN_MIN_GAS_PRICE,
|
||||
ORACLE_MEV_FOREIGN_FLAT_MINER_FEE,
|
||||
ORACLE_MEV_FOREIGN_MAX_PRIORITY_FEE_PER_GAS,
|
||||
ORACLE_MEV_FOREIGN_MAX_FEE_PER_GAS,
|
||||
ORACLE_MEV_FOREIGN_BUNDLES_BLOCK_RANGE
|
||||
} = process.env
|
||||
|
||||
const contract = new baseConfig.foreign.web3.eth.Contract(MEV_HELPER_ABI, ORACLE_MEV_FOREIGN_HELPER_CONTRACT_ADDRESS)
|
||||
|
||||
module.exports = {
|
||||
...baseConfig,
|
||||
pollingInterval: baseConfig.foreign.pollingInterval,
|
||||
mevForeign: {
|
||||
contractAddress: ORACLE_MEV_FOREIGN_HELPER_CONTRACT_ADDRESS,
|
||||
contract,
|
||||
minGasPrice: ORACLE_MEV_FOREIGN_MIN_GAS_PRICE,
|
||||
flatMinerFee: ORACLE_MEV_FOREIGN_FLAT_MINER_FEE,
|
||||
maxPriorityFeePerGas: ORACLE_MEV_FOREIGN_MAX_PRIORITY_FEE_PER_GAS,
|
||||
maxFeePerGas: ORACLE_MEV_FOREIGN_MAX_FEE_PER_GAS,
|
||||
bundlesPerIteration: Math.max(parseInt(ORACLE_MEV_FOREIGN_BUNDLES_BLOCK_RANGE, 10) || 5, 1),
|
||||
getFlashbotsProvider
|
||||
},
|
||||
mevJobsRedisKey: `${baseConfig.id}-collected-signatures-mev:mevJobs`,
|
||||
id: 'mev-sender-foreign',
|
||||
name: 'mev-sender-foreign',
|
||||
web3: web3Foreign,
|
||||
resendInterval: parseInt(ORACLE_FOREIGN_TX_RESEND_INTERVAL, 10) || DEFAULT_TRANSACTION_RESEND_INTERVAL
|
||||
}
|
@ -1,14 +1,18 @@
|
||||
const baseConfig = require('./base.config')
|
||||
|
||||
const { DEFAULT_TRANSACTION_RESEND_INTERVAL } = require('../src/utils/constants')
|
||||
const { web3Foreign, web3ForeignRedundant, web3ForeignFallback } = require('../src/services/web3')
|
||||
|
||||
const { ORACLE_FOREIGN_TX_RESEND_INTERVAL } = process.env
|
||||
|
||||
module.exports = {
|
||||
...baseConfig,
|
||||
main: baseConfig.foreign,
|
||||
queue: 'foreign-prioritized',
|
||||
oldQueue: 'foreign',
|
||||
id: 'foreign',
|
||||
name: 'sender-foreign',
|
||||
web3: web3Foreign,
|
||||
web3Redundant: web3ForeignRedundant,
|
||||
web3Fallback: web3ForeignFallback,
|
||||
resendInterval: parseInt(ORACLE_FOREIGN_TX_RESEND_INTERVAL, 10) || DEFAULT_TRANSACTION_RESEND_INTERVAL
|
||||
}
|
||||
|
@ -1,14 +1,18 @@
|
||||
const baseConfig = require('./base.config')
|
||||
|
||||
const { DEFAULT_TRANSACTION_RESEND_INTERVAL } = require('../src/utils/constants')
|
||||
const { web3Home, web3HomeRedundant, web3HomeFallback } = require('../src/services/web3')
|
||||
|
||||
const { ORACLE_HOME_TX_RESEND_INTERVAL } = process.env
|
||||
|
||||
module.exports = {
|
||||
...baseConfig,
|
||||
main: baseConfig.home,
|
||||
queue: 'home-prioritized',
|
||||
oldQueue: 'home',
|
||||
id: 'home',
|
||||
name: 'sender-home',
|
||||
web3: web3Home,
|
||||
web3Redundant: web3HomeRedundant,
|
||||
web3Fallback: web3HomeFallback,
|
||||
resendInterval: parseInt(ORACLE_HOME_TX_RESEND_INTERVAL, 10) || DEFAULT_TRANSACTION_RESEND_INTERVAL
|
||||
}
|
||||
|
@ -1,9 +1,11 @@
|
||||
const baseConfig = require('./base.config')
|
||||
const { web3ForeignArchive } = require('../src/services/web3')
|
||||
|
||||
const id = `${baseConfig.id}-information-request`
|
||||
|
||||
module.exports = {
|
||||
...baseConfig,
|
||||
web3ForeignArchive: web3ForeignArchive || baseConfig.foreign.web3,
|
||||
main: baseConfig.home,
|
||||
event: 'UserRequestForInformation',
|
||||
sender: 'home',
|
||||
|
@ -1,30 +0,0 @@
|
||||
const baseConfig = require('./base.config')
|
||||
const { MEV_HELPER_ABI } = require('../src/utils/mev')
|
||||
|
||||
const {
|
||||
ORACLE_MEV_FOREIGN_HELPER_CONTRACT_ADDRESS,
|
||||
ORACLE_MEV_FOREIGN_MIN_GAS_PRICE,
|
||||
ORACLE_MEV_FOREIGN_FLAT_MINER_FEE,
|
||||
ORACLE_MEV_FOREIGN_MAX_PRIORITY_FEE_PER_GAS,
|
||||
ORACLE_MEV_FOREIGN_MAX_FEE_PER_GAS
|
||||
} = process.env
|
||||
|
||||
const id = `${baseConfig.id}-collected-signatures-mev`
|
||||
|
||||
const contract = new baseConfig.foreign.web3.eth.Contract(MEV_HELPER_ABI, ORACLE_MEV_FOREIGN_HELPER_CONTRACT_ADDRESS)
|
||||
|
||||
module.exports = {
|
||||
...baseConfig,
|
||||
mevForeign: {
|
||||
contractAddress: ORACLE_MEV_FOREIGN_HELPER_CONTRACT_ADDRESS,
|
||||
contract,
|
||||
minGasPrice: ORACLE_MEV_FOREIGN_MIN_GAS_PRICE,
|
||||
flatMinerFee: ORACLE_MEV_FOREIGN_FLAT_MINER_FEE,
|
||||
maxPriorityFeePerGas: ORACLE_MEV_FOREIGN_MAX_PRIORITY_FEE_PER_GAS,
|
||||
maxFeePerGas: ORACLE_MEV_FOREIGN_MAX_FEE_PER_GAS
|
||||
},
|
||||
main: baseConfig.home,
|
||||
event: 'CollectedSignatures',
|
||||
name: `watcher-${id}`,
|
||||
id
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
---
|
||||
version: '2.4'
|
||||
services:
|
||||
redis:
|
||||
cpus: 0.1
|
||||
mem_limit: 500m
|
||||
command: [ redis-server, --appendonly, 'yes' ]
|
||||
hostname: redis
|
||||
image: redis:4
|
||||
restart: unless-stopped
|
||||
volumes: [ '~/bridge_data/helpers/redis:/data' ]
|
||||
interestFetcher:
|
||||
cpus: 0.1
|
||||
mem_limit: 500m
|
||||
image: poanetwork/tokenbridge-oracle:latest
|
||||
env_file: ./.env
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
INTEREST_FETCHER_PRIVATE_KEY: ${INTEREST_FETCHER_PRIVATE_KEY}
|
||||
INTEREST_FETCH_CONTRACT_ADDRESS: '0xCd152c7Bd4189Ddee97EaBb783FC5cD93CF2D230'
|
||||
INTERVAL: 300000
|
||||
restart: unless-stopped
|
||||
entrypoint: yarn helper:interestFether
|
||||
mevWatcher:
|
||||
cpus: 0.1
|
||||
mem_limit: 500m
|
||||
image: poanetwork/tokenbridge-oracle:latest
|
||||
env_file: ./.env
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
ORACLE_VALIDATOR_ADDRESS: ${ORACLE_VALIDATOR_ADDRESS}
|
||||
ORACLE_MEV_FOREIGN_HELPER_CONTRACT_ADDRESS: 'TBD'
|
||||
ORACLE_MEV_FOREIGN_MIN_GAS_PRICE: '50000000000' # 50 gwei
|
||||
ORACLE_MEV_FOREIGN_FLAT_MINER_FEE: '1500000000000000' # 0.0015 eth = 300k gas * 5 gwei
|
||||
ORACLE_MEV_FOREIGN_MAX_PRIORITY_FEE_PER_GAS: '0' # 0 gwei
|
||||
ORACLE_MEV_FOREIGN_MAX_FEE_PER_GAS: '1000000000000' # 1000 gwei
|
||||
ORACLE_FOREIGN_RPC_POLLING_INTERVAL: '15000' # CollectedSignatures event polling interval
|
||||
ORACLE_HOME_START_BLOCK: 'TBD'
|
||||
ORACLE_HOME_SKIP_MANUAL_LANE: 'true'
|
||||
restart: unless-stopped
|
||||
entrypoint: yarn mev:watcher:collected-signatures
|
||||
mevSender:
|
||||
cpus: 0.1
|
||||
mem_limit: 500m
|
||||
image: poanetwork/tokenbridge-oracle:latest
|
||||
env_file: ./.env
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
ORACLE_VALIDATOR_ADDRESS: ${ORACLE_VALIDATOR_ADDRESS}
|
||||
ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY: ${ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY}
|
||||
ORACLE_MEV_FOREIGN_HELPER_CONTRACT_ADDRESS: 'TBD'
|
||||
ORACLE_MEV_FOREIGN_MIN_GAS_PRICE: '50000000000' # 50 gwei
|
||||
ORACLE_MEV_FOREIGN_FLAT_MINER_FEE: '1500000000000000' # 0.0015 eth = 300k gas * 5 gwei
|
||||
ORACLE_MEV_FOREIGN_MAX_PRIORITY_FEE_PER_GAS: '0' # 0 gwei
|
||||
ORACLE_MEV_FOREIGN_MAX_FEE_PER_GAS: '1000000000000' # 1000 gwei
|
||||
ORACLE_MEV_FOREIGN_FLASHBOTS_RPC_URL: 'https://relay-goerli.flashbots.net'
|
||||
ORACLE_MEV_FOREIGN_FLASHBOTS_AUTH_SIGNING_KEY: 82db7175932f4e6c8e45283b78b54fd5f195149378ec90d95b8fd0ec8bdadf1d
|
||||
ORACLE_MEV_FOREIGN_BUNDLES_BLOCK_RANGE: '5'
|
||||
ORACLE_FOREIGN_RPC_POLLING_INTERVAL: '70000' # time between sending different batches of MEV bundles (~= 5 blocks * 14 seconds)
|
||||
restart: unless-stopped
|
||||
entrypoint: yarn mev:sender:foreign
|
@ -18,10 +18,6 @@
|
||||
"confirm:collected-signatures": "./scripts/start-worker.sh confirmRelay collected-signatures-watcher",
|
||||
"confirm:information-request": "./scripts/start-worker.sh confirmRelay information-request-watcher",
|
||||
"manager:shutdown": "./scripts/start-worker.sh shutdownManager shutdown-manager",
|
||||
"helper:interestFether": "node ./scripts/interestFetcher.js",
|
||||
"helper:signPendingMessages": "node ./scripts/signPendingMessages.js",
|
||||
"mev:watcher:collected-signatures": "./scripts/start-worker.sh mevWatcher mev-collected-signatures-watcher",
|
||||
"mev:sender:foreign": "./scripts/start-worker.sh mevSender foreign-mev-sender",
|
||||
"dev": "concurrently -n 'watcher:signature-request,watcher:collected-signatures,watcher:affirmation-request,watcher:transfer, sender:home,sender:foreign' -c 'red,green,yellow,blue,magenta,cyan' 'yarn watcher:signature-request' 'yarn watcher:collected-signatures' 'yarn watcher:affirmation-request' 'yarn watcher:transfer' 'yarn sender:home' 'yarn sender:foreign'",
|
||||
"test": "NODE_ENV=test mocha",
|
||||
"test:watch": "NODE_ENV=test mocha --watch --reporter=min",
|
||||
@ -31,19 +27,17 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@flashbots/ethers-provider-bundle": "^0.4.3",
|
||||
"amqp-connection-manager": "^2.0.0",
|
||||
"amqplib": "^0.5.2",
|
||||
"bignumber.js": "^7.2.1",
|
||||
"dotenv": "^5.0.1",
|
||||
"ethers": "^5.5.3",
|
||||
"ioredis": "^3.2.2",
|
||||
"node-fetch": "^2.1.2",
|
||||
"pino": "^4.17.3",
|
||||
"pino-pretty": "^2.0.1",
|
||||
"promise-limit": "^2.7.0",
|
||||
"promise-retry": "^1.1.1",
|
||||
"web3": "^1.6.0"
|
||||
"web3": "^1.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"bn-chai": "^1.0.1",
|
||||
|
@ -36,7 +36,7 @@ async function main() {
|
||||
data,
|
||||
nonce,
|
||||
gasPrice: FOREIGN_TEST_TX_GAS_PRICE,
|
||||
value: '0',
|
||||
amount: '0',
|
||||
gasLimit,
|
||||
to: bridgeableTokenAddress,
|
||||
web3: web3Foreign,
|
||||
|
@ -29,7 +29,7 @@ async function main() {
|
||||
data: '0x',
|
||||
nonce,
|
||||
gasPrice: HOME_TEST_TX_GAS_PRICE,
|
||||
value: web3Home.utils.toWei(HOME_MIN_AMOUNT_PER_TX),
|
||||
amount: HOME_MIN_AMOUNT_PER_TX,
|
||||
gasLimit: 100000,
|
||||
to: COMMON_HOME_BRIDGE_ADDRESS,
|
||||
web3: web3Home,
|
||||
|
@ -1,65 +0,0 @@
|
||||
require('../env')
|
||||
const { isAddress } = require('web3').utils
|
||||
const { privateKeyToAddress, setIntervalAndRun } = require('../src/utils/utils')
|
||||
const { EXIT_CODES } = require('../src/utils/constants')
|
||||
const { web3Home } = require('../src/services/web3')
|
||||
const { sendTx } = require('../src/tx/sendTx')
|
||||
|
||||
const privateKey = process.env.INTEREST_FETCHER_PRIVATE_KEY
|
||||
const interval = process.env.INTERVAL ? parseInt(process.env.INTERVAL, 10) : 3600 * 1000
|
||||
const contractAddress = process.env.INTEREST_FETCH_CONTRACT_ADDRESS
|
||||
|
||||
if (!privateKey) {
|
||||
console.error('Environment variable INTEREST_FETCHER_PRIVATE_KEY is not set')
|
||||
process.exit(EXIT_CODES.GENERAL_ERROR)
|
||||
}
|
||||
|
||||
if (interval < 300 * 1000) {
|
||||
console.error('Interval is to small, should be at least 5 minutes')
|
||||
process.exit(EXIT_CODES.GENERAL_ERROR)
|
||||
}
|
||||
|
||||
if (!isAddress(contractAddress)) {
|
||||
console.error('Invalid contract address provided', contractAddress)
|
||||
process.exit(EXIT_CODES.GENERAL_ERROR)
|
||||
}
|
||||
|
||||
const gasPrice = process.env.COMMON_HOME_GAS_PRICE_FALLBACK || '1000000000'
|
||||
|
||||
async function main() {
|
||||
// assuming that we are using this contract - https://github.com/omni/interest-fetcher-contract
|
||||
const contract = new web3Home.eth.Contract([{ name: 'fetchInterest', type: 'function', inputs: [] }], contractAddress)
|
||||
const chainId = await web3Home.eth.getChainId()
|
||||
const data = contract.methods.fetchInterest().encodeABI()
|
||||
const senderAddress = privateKeyToAddress(privateKey)
|
||||
console.log(
|
||||
`Initialized, chainId=${chainId}, data=${data}, contract=${contractAddress}, interval=${interval / 1000}s`
|
||||
)
|
||||
|
||||
await setIntervalAndRun(async () => {
|
||||
let gasLimit
|
||||
try {
|
||||
gasLimit = await contract.methods.fetchInterest().estimateGas()
|
||||
} catch (e) {
|
||||
console.log('Gas limit estimation failed, will retry later', new Date())
|
||||
return
|
||||
}
|
||||
|
||||
const nonce = await web3Home.eth.getTransactionCount(senderAddress)
|
||||
|
||||
const txHash = await sendTx({
|
||||
privateKey,
|
||||
to: contractAddress,
|
||||
data,
|
||||
nonce,
|
||||
gasPrice,
|
||||
gasLimit: Math.round(gasLimit * 1.5),
|
||||
value: '0',
|
||||
chainId,
|
||||
web3: web3Home
|
||||
})
|
||||
console.log('Sent transaction with fetch interest', txHash, new Date())
|
||||
}, interval)
|
||||
}
|
||||
|
||||
main()
|
@ -1,83 +0,0 @@
|
||||
require('dotenv').config()
|
||||
|
||||
const {
|
||||
COMMON_HOME_BRIDGE_ADDRESS,
|
||||
COMMON_FOREIGN_BRIDGE_ADDRESS,
|
||||
ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY,
|
||||
ORACLE_HOME_START_BLOCK,
|
||||
ORACLE_HOME_END_BLOCK,
|
||||
ORACLE_BRIDGE_MODE
|
||||
} = process.env
|
||||
|
||||
const fs = require('fs')
|
||||
const promiseLimit = require('promise-limit')
|
||||
|
||||
const { web3Home, web3Foreign } = require('../src/services/web3')
|
||||
const { getBridgeABIs, getPastEvents, parseAMBMessage, BRIDGE_MODES } = require('../../commons')
|
||||
const { setLogger } = require('../src/services/injectedLogger')
|
||||
|
||||
const mockLogger = { debug: () => {}, info: () => {}, error: () => {}, child: () => mockLogger }
|
||||
setLogger(mockLogger)
|
||||
|
||||
const limit = promiseLimit(50)
|
||||
|
||||
const output = process.argv[2]
|
||||
|
||||
async function main() {
|
||||
const { HOME_ABI, FOREIGN_ABI } = getBridgeABIs(ORACLE_BRIDGE_MODE)
|
||||
const wallet = web3Home.eth.accounts.wallet.add(ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY)
|
||||
const homeBridge = new web3Home.eth.Contract(HOME_ABI, COMMON_HOME_BRIDGE_ADDRESS)
|
||||
const foreignBridge = new web3Foreign.eth.Contract(FOREIGN_ABI, COMMON_FOREIGN_BRIDGE_ADDRESS)
|
||||
const fromBlock = parseInt(ORACLE_HOME_START_BLOCK, 10) || 0
|
||||
let toBlock = parseInt(ORACLE_HOME_END_BLOCK, 10)
|
||||
if (!toBlock) {
|
||||
toBlock = await web3Home.eth.getBlockNumber()
|
||||
}
|
||||
console.log(`Getting CollectedSignatures events from block ${fromBlock} to block ${toBlock}`)
|
||||
const events = await getPastEvents(homeBridge, { event: 'CollectedSignatures', fromBlock, toBlock })
|
||||
console.log(`Found ${events.length} CollectedSignatures events`)
|
||||
console.log('Getting messages')
|
||||
let messages = await Promise.all(
|
||||
events.map((event, i) => () => getMessage(ORACLE_BRIDGE_MODE, homeBridge, foreignBridge, event, i)).map(limit)
|
||||
)
|
||||
messages = messages.filter(x => x)
|
||||
console.log(`Filtered ${messages.length} pending messages`)
|
||||
const result = {}
|
||||
messages.forEach(msg => {
|
||||
result[msg.msgHash] = wallet.sign(msg.message).signature
|
||||
})
|
||||
|
||||
console.log('Writing results')
|
||||
if (output === '-') {
|
||||
console.log(JSON.stringify(result))
|
||||
} else {
|
||||
fs.writeFileSync(output, JSON.stringify(result))
|
||||
}
|
||||
}
|
||||
|
||||
async function getMessage(bridgeMode, homeBridge, foreignBridge, event, i) {
|
||||
if (i % 50 === 0) {
|
||||
console.log(`Processing event #${i}`)
|
||||
}
|
||||
const msgHash = event.returnValues.messageHash
|
||||
const message = await homeBridge.methods.message(msgHash).call()
|
||||
|
||||
let msgId
|
||||
if (bridgeMode === BRIDGE_MODES.ARBITRARY_MESSAGE) {
|
||||
msgId = parseAMBMessage(message).messageId
|
||||
} else {
|
||||
msgId = `0x${message.slice(106, 170)}`
|
||||
}
|
||||
const alreadyProcessed = await foreignBridge.methods.relayedMessages(msgId).call()
|
||||
|
||||
if (alreadyProcessed) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
msgHash,
|
||||
message
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
@ -6,7 +6,6 @@ const logger = require('./services/logger')
|
||||
const GasPrice = require('./services/gasPrice')
|
||||
const { getNonce, getChainId, getEventsFromTx } = require('./tx/web3')
|
||||
const { sendTx } = require('./tx/sendTx')
|
||||
const { getTokensState } = require('./utils/tokenState')
|
||||
const { checkHTTPS, watchdog, syncForEach, addExtraGas } = require('./utils/utils')
|
||||
const { EXIT_CODES, EXTRA_GAS_PERCENTAGE, MAX_GAS_LIMIT } = require('./utils/constants')
|
||||
|
||||
@ -18,7 +17,7 @@ if (process.argv.length < 4) {
|
||||
}
|
||||
|
||||
const config = require(path.join('../config/', process.argv[2]))
|
||||
const { web3, eventContract, chain, bridgeContract } = config.main
|
||||
const { web3, eventContract, chain } = config.main
|
||||
|
||||
const isTxHash = txHash => txHash.length === 66 && web3.utils.isHexStrict(txHash)
|
||||
function readTxHashes(filePath) {
|
||||
@ -50,7 +49,7 @@ async function initialize() {
|
||||
try {
|
||||
const checkHttps = checkHTTPS(ORACLE_ALLOW_HTTP_FOR_RPC, logger)
|
||||
|
||||
web3.currentProvider.urls.forEach(checkHttps(chain))
|
||||
web3.currentProvider.subProvider.urls.forEach(checkHttps(chain))
|
||||
|
||||
attached = await isAttached()
|
||||
if (attached) {
|
||||
@ -115,12 +114,6 @@ function processEvents(events) {
|
||||
}
|
||||
|
||||
async function main({ sendJob, txHashes }) {
|
||||
if (config.id === 'erc-native-transfer') {
|
||||
logger.debug('Getting token address to listen Transfer events')
|
||||
const state = await getTokensState(bridgeContract, logger)
|
||||
eventContract.options.address = state.bridgeableTokenAddress
|
||||
}
|
||||
|
||||
logger.info(`Processing ${txHashes.length} input transactions`)
|
||||
for (const txHash of txHashes) {
|
||||
try {
|
||||
@ -153,10 +146,10 @@ async function main({ sendJob, txHashes }) {
|
||||
}
|
||||
|
||||
async function sendJobTx(jobs) {
|
||||
const { web3 } = config.sender === 'foreign' ? config.foreign : config.home
|
||||
await GasPrice.start(chain, true)
|
||||
const gasPrice = GasPrice.getPrice().toString(10)
|
||||
|
||||
await GasPrice.start(chain, web3, true)
|
||||
const gasPriceOptions = GasPrice.gasPriceOptions()
|
||||
const { web3 } = config.sender === 'foreign' ? config.foreign : config.home
|
||||
|
||||
const chainId = await getChainId(web3)
|
||||
let nonce = await getNonce(web3, config.validatorAddress)
|
||||
@ -174,13 +167,13 @@ async function sendJobTx(jobs) {
|
||||
const txHash = await sendTx({
|
||||
data: job.data,
|
||||
nonce,
|
||||
value: '0',
|
||||
gasPrice,
|
||||
amount: '0',
|
||||
gasLimit,
|
||||
privateKey: config.validatorPrivateKey,
|
||||
to: job.to,
|
||||
chainId,
|
||||
web3,
|
||||
gasPriceOptions
|
||||
web3
|
||||
})
|
||||
|
||||
nonce++
|
||||
@ -197,7 +190,7 @@ async function sendJobTx(jobs) {
|
||||
|
||||
if (e.message.toLowerCase().includes('insufficient funds')) {
|
||||
const currentBalance = await web3.eth.getBalance(config.validatorAddress)
|
||||
const minimumBalance = gasLimit.multipliedBy(gasPriceOptions.gasPrice || gasPriceOptions.maxFeePerGas)
|
||||
const minimumBalance = gasLimit.multipliedBy(gasPrice)
|
||||
logger.error(
|
||||
`Insufficient funds: ${currentBalance}. Stop processing messages until the balance is at least ${minimumBalance}.`
|
||||
)
|
||||
|
@ -15,7 +15,7 @@ async function estimateGas({ web3, homeBridge, validatorContract, message, addre
|
||||
const gasEstimate = await homeBridge.methods.executeAffirmation(message).estimateGas({
|
||||
from: address
|
||||
})
|
||||
const msgGasLimit = Math.ceil((parseAMBHeader(message).gasLimit * 64) / 63)
|
||||
const msgGasLimit = parseAMBHeader(message).gasLimit
|
||||
// message length in bytes
|
||||
const len = strip0x(message).length / 2 - MIN_AMB_HEADER_LENGTH
|
||||
|
||||
|
@ -4,6 +4,7 @@ const { AlreadyProcessedError, IncompatibleContractError, InvalidValidatorError
|
||||
const logger = require('../../services/logger').child({
|
||||
module: 'processCollectedSignatures:estimateGas'
|
||||
})
|
||||
const { parseAMBHeader } = require('../../utils/message')
|
||||
|
||||
const web3 = new Web3()
|
||||
const { toBN } = Web3.utils
|
||||
@ -21,9 +22,15 @@ async function estimateGas({
|
||||
address
|
||||
}) {
|
||||
try {
|
||||
return await foreignBridge.methods.executeSignatures(message, signatures).estimateGas({
|
||||
const gasEstimate = await foreignBridge.methods.executeSignatures(message, signatures).estimateGas({
|
||||
from: address
|
||||
})
|
||||
const msgGasLimit = parseAMBHeader(message).gasLimit
|
||||
|
||||
// + estimateExtraGas(len)
|
||||
// is not needed here, since estimateGas will already take into account gas
|
||||
// needed for memory expansion, message processing, etc.
|
||||
return gasEstimate + msgGasLimit
|
||||
} catch (e) {
|
||||
if (e instanceof HttpListProviderError) {
|
||||
throw e
|
||||
|
@ -1,183 +0,0 @@
|
||||
require('dotenv').config()
|
||||
const promiseLimit = require('promise-limit')
|
||||
const { HttpListProviderError } = require('../../services/HttpListProvider')
|
||||
const { getValidatorContract } = require('../../tx/web3')
|
||||
const rootLogger = require('../../services/logger')
|
||||
const { signatureToVRS, packSignatures } = require('../../utils/message')
|
||||
const { readAccessListFile, isRevertError } = require('../../utils/utils')
|
||||
const { parseAMBMessage } = require('../../../../commons')
|
||||
const estimateGas = require('../processAMBCollectedSignatures/estimateGas')
|
||||
const { AlreadyProcessedError, IncompatibleContractError, InvalidValidatorError } = require('../../utils/errors')
|
||||
const { MAX_CONCURRENT_EVENTS, EXTRA_GAS_ABSOLUTE } = require('../../utils/constants')
|
||||
|
||||
const limit = promiseLimit(MAX_CONCURRENT_EVENTS)
|
||||
|
||||
const { ORACLE_HOME_TO_FOREIGN_ALLOWANCE_LIST, ORACLE_HOME_TO_FOREIGN_BLOCK_LIST } = process.env
|
||||
const ORACLE_HOME_SKIP_MANUAL_LANE = process.env.ORACLE_HOME_SKIP_MANUAL_LANE === 'true'
|
||||
|
||||
function processCollectedSignaturesBuilder(config) {
|
||||
const { home, foreign, mevForeign } = config
|
||||
|
||||
let validatorContract = null
|
||||
|
||||
return async function processCollectedSignatures(signatures) {
|
||||
const txToSend = []
|
||||
|
||||
if (validatorContract === null) {
|
||||
validatorContract = await getValidatorContract(foreign.bridgeContract, foreign.web3)
|
||||
}
|
||||
|
||||
rootLogger.debug(`Processing ${signatures.length} CollectedSignatures events`)
|
||||
const callbacks = signatures
|
||||
.map(colSignature => async () => {
|
||||
const { messageHash, NumberOfCollectedSignatures } = colSignature.returnValues
|
||||
|
||||
const logger = rootLogger.child({
|
||||
eventTransactionHash: colSignature.transactionHash
|
||||
})
|
||||
|
||||
logger.info(`Processing CollectedSignatures ${colSignature.transactionHash}`)
|
||||
const message = await home.bridgeContract.methods.message(messageHash).call()
|
||||
const parsedMessage = parseAMBMessage(message)
|
||||
|
||||
if (ORACLE_HOME_TO_FOREIGN_ALLOWANCE_LIST || ORACLE_HOME_TO_FOREIGN_BLOCK_LIST) {
|
||||
const sender = parsedMessage.sender.toLowerCase()
|
||||
const executor = parsedMessage.executor.toLowerCase()
|
||||
|
||||
if (ORACLE_HOME_TO_FOREIGN_ALLOWANCE_LIST) {
|
||||
const allowanceList = await readAccessListFile(ORACLE_HOME_TO_FOREIGN_ALLOWANCE_LIST, logger)
|
||||
if (!allowanceList.includes(executor) && !allowanceList.includes(sender)) {
|
||||
logger.info(
|
||||
{ sender, executor },
|
||||
'Validator skips a message. Neither sender nor executor addresses are in the allowance list.'
|
||||
)
|
||||
return
|
||||
}
|
||||
} else if (ORACLE_HOME_TO_FOREIGN_BLOCK_LIST) {
|
||||
const blockList = await readAccessListFile(ORACLE_HOME_TO_FOREIGN_BLOCK_LIST, logger)
|
||||
if (blockList.includes(executor)) {
|
||||
logger.info({ executor }, 'Validator skips a message. Executor address is in the block list.')
|
||||
return
|
||||
}
|
||||
if (blockList.includes(sender)) {
|
||||
logger.info({ sender }, 'Validator skips a message. Sender address is in the block list.')
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ORACLE_HOME_SKIP_MANUAL_LANE && parsedMessage.decodedDataType.manualLane) {
|
||||
logger.info(
|
||||
{ dataType: parsedMessage.dataType },
|
||||
'Validator skips a message. Message was forwarded to the manual lane by the extension'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
logger.debug({ NumberOfCollectedSignatures }, 'Number of signatures to get')
|
||||
|
||||
const requiredSignatures = []
|
||||
requiredSignatures.length = NumberOfCollectedSignatures
|
||||
requiredSignatures.fill(0)
|
||||
|
||||
const signaturesArray = []
|
||||
const [v, r, s] = [[], [], []]
|
||||
logger.debug('Getting message signatures')
|
||||
const signaturePromises = requiredSignatures.map(async (el, index) => {
|
||||
logger.debug({ index }, 'Getting message signature')
|
||||
const signature = await home.bridgeContract.methods.signature(messageHash, index).call()
|
||||
const vrs = signatureToVRS(signature)
|
||||
v.push(vrs.v)
|
||||
r.push(vrs.r)
|
||||
s.push(vrs.s)
|
||||
signaturesArray.push(vrs)
|
||||
})
|
||||
|
||||
await Promise.all(signaturePromises)
|
||||
const signatures = packSignatures(signaturesArray)
|
||||
logger.info(`Processing messageId: ${parsedMessage.messageId}`)
|
||||
|
||||
let gasEstimate
|
||||
try {
|
||||
logger.debug('Estimate gas')
|
||||
gasEstimate = await estimateGas({
|
||||
foreignBridge: foreign.bridgeContract,
|
||||
validatorContract,
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
signatures,
|
||||
message,
|
||||
numberOfCollectedSignatures: NumberOfCollectedSignatures,
|
||||
messageId: parsedMessage.messageId,
|
||||
address: config.validatorAddress
|
||||
})
|
||||
logger.debug({ gasEstimate }, 'Gas estimated')
|
||||
} catch (e) {
|
||||
if (e instanceof HttpListProviderError) {
|
||||
throw new Error('RPC Connection Error: submitSignature Gas Estimate cannot be obtained.')
|
||||
} else if (e instanceof AlreadyProcessedError) {
|
||||
logger.info(`Already processed CollectedSignatures ${colSignature.transactionHash}`)
|
||||
return
|
||||
} else if (e instanceof IncompatibleContractError || e instanceof InvalidValidatorError) {
|
||||
logger.error(`The message couldn't be processed; skipping: ${e.message}`)
|
||||
return
|
||||
} else {
|
||||
logger.error(e, 'Unknown error while processing transaction')
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
const executeData = foreign.bridgeContract.methods.executeSignatures(message, signatures).encodeABI()
|
||||
const profit = await estimateProfit(
|
||||
mevForeign.contract,
|
||||
mevForeign.minGasPrice,
|
||||
executeData,
|
||||
mevForeign.flatMinerFee
|
||||
)
|
||||
if (profit === '0') {
|
||||
logger.error('No MEV opportunity found when testing with min gas price, skipping job')
|
||||
return
|
||||
}
|
||||
logger.info(`Estimated profit of ${profit} when simulating with ${mevForeign.minGasPrice} gas price`)
|
||||
|
||||
txToSend.push({
|
||||
profit,
|
||||
executeData,
|
||||
data: mevForeign.contract.methods.execute(executeData).encodeABI(),
|
||||
gasEstimate,
|
||||
extraGas: EXTRA_GAS_ABSOLUTE,
|
||||
maxFeePerGas: mevForeign.maxFeePerGas,
|
||||
maxPriorityFeePerGas: mevForeign.maxPriorityFeePerGas,
|
||||
transactionReference: colSignature.transactionHash,
|
||||
to: mevForeign.contractAddress,
|
||||
value: mevForeign.flatMinerFee
|
||||
})
|
||||
})
|
||||
.map(promise => limit(promise))
|
||||
|
||||
await Promise.all(callbacks)
|
||||
|
||||
return txToSend
|
||||
}
|
||||
}
|
||||
|
||||
async function estimateProfit(contract, gasPrice, data, minerFee) {
|
||||
return contract.methods
|
||||
.estimateProfit(gasPrice, data)
|
||||
.call({ value: minerFee })
|
||||
.then(
|
||||
res => res.toString(),
|
||||
e => {
|
||||
if (isRevertError(e)) {
|
||||
return '0'
|
||||
}
|
||||
throw e
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
processCollectedSignaturesBuilder,
|
||||
estimateProfit
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
const { toBN } = require('web3').utils
|
||||
|
||||
const { ASYNC_CALL_ERRORS, ASYNC_ETH_CALL_MAX_GAS_LIMIT } = require('../../../utils/constants')
|
||||
const { zipToObject, isRevertError } = require('../../../utils/utils')
|
||||
const { zipToObject } = require('../../../utils/utils')
|
||||
|
||||
const argTypes = {
|
||||
to: 'address',
|
||||
@ -18,23 +17,14 @@ function makeCall(argNames) {
|
||||
const { blockNumber, ...opts } = zipToObject(argNames, args)
|
||||
|
||||
if (blockNumber && toBN(blockNumber).gt(toBN(foreignBlock.number))) {
|
||||
return [false, ASYNC_CALL_ERRORS.BLOCK_IS_IN_THE_FUTURE]
|
||||
return [false, '0x']
|
||||
}
|
||||
|
||||
// different clients might use different default gas limits, so it makes sense to limit it by some large number
|
||||
if (!opts.gas || toBN(opts.gas).gt(toBN(ASYNC_ETH_CALL_MAX_GAS_LIMIT))) {
|
||||
opts.gas = ASYNC_ETH_CALL_MAX_GAS_LIMIT
|
||||
}
|
||||
|
||||
return web3.eth
|
||||
const [status, result] = await web3.eth
|
||||
.call(opts, blockNumber || foreignBlock.number)
|
||||
.then(result => [true, web3.eth.abi.encodeParameter('bytes', result)])
|
||||
.catch(e => {
|
||||
if (isRevertError(e)) {
|
||||
return [false, ASYNC_CALL_ERRORS.REVERT]
|
||||
}
|
||||
throw e
|
||||
})
|
||||
.then(result => [true, result], err => [false, err.data])
|
||||
|
||||
return [status, web3.eth.abi.encodeParameter('bytes', result)]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,5 @@
|
||||
const { toBN } = require('web3').utils
|
||||
|
||||
const { ASYNC_CALL_ERRORS } = require('../../../utils/constants')
|
||||
|
||||
async function call(web3, data, foreignBlock) {
|
||||
const address = web3.eth.abi.decodeParameter('address', data)
|
||||
|
||||
@ -14,7 +12,7 @@ async function callArchive(web3, data, foreignBlock) {
|
||||
const { 0: address, 1: blockNumber } = web3.eth.abi.decodeParameters(['address', 'uint256'], data)
|
||||
|
||||
if (toBN(blockNumber).gt(toBN(foreignBlock.number))) {
|
||||
return [false, ASYNC_CALL_ERRORS.BLOCK_IS_IN_THE_FUTURE]
|
||||
return [false, '0x']
|
||||
}
|
||||
|
||||
const balance = await web3.eth.getBalance(address, blockNumber)
|
||||
|
@ -1,4 +1,3 @@
|
||||
const { ASYNC_CALL_ERRORS } = require('../../../utils/constants')
|
||||
const { serializeBlock } = require('./serializers')
|
||||
|
||||
async function call(web3, data, foreignBlock) {
|
||||
@ -7,7 +6,7 @@ async function call(web3, data, foreignBlock) {
|
||||
const block = await web3.eth.getBlock(blockHash)
|
||||
|
||||
if (block === null || block.number > foreignBlock.number) {
|
||||
return [false, ASYNC_CALL_ERRORS.NOT_FOUND]
|
||||
return [false, '0x']
|
||||
}
|
||||
|
||||
return [true, serializeBlock(web3, block)]
|
||||
|
@ -1,13 +1,12 @@
|
||||
const { toBN } = require('web3').utils
|
||||
|
||||
const { ASYNC_CALL_ERRORS } = require('../../../utils/constants')
|
||||
const { serializeBlock } = require('./serializers')
|
||||
|
||||
async function call(web3, data, foreignBlock) {
|
||||
const blockNumber = web3.eth.abi.decodeParameter('uint256', data)
|
||||
|
||||
if (toBN(blockNumber).gt(toBN(foreignBlock.number))) {
|
||||
return [false, ASYNC_CALL_ERRORS.BLOCK_IS_IN_THE_FUTURE]
|
||||
return [false, '0x']
|
||||
}
|
||||
|
||||
const block = await web3.eth.getBlock(blockNumber)
|
||||
|
@ -1,7 +1,5 @@
|
||||
const { toBN } = require('web3').utils
|
||||
|
||||
const { ASYNC_CALL_ERRORS } = require('../../../utils/constants')
|
||||
|
||||
async function call(web3, data, foreignBlock) {
|
||||
const { 0: address, 1: slot } = web3.eth.abi.decodeParameters(['address', 'bytes32'], data)
|
||||
|
||||
@ -14,7 +12,7 @@ async function callArchive(web3, data, foreignBlock) {
|
||||
const { 0: address, 1: slot, 2: blockNumber } = web3.eth.abi.decodeParameters(['address', 'bytes32', 'uint256'], data)
|
||||
|
||||
if (toBN(blockNumber).gt(toBN(foreignBlock.number))) {
|
||||
return [false, ASYNC_CALL_ERRORS.BLOCK_IS_IN_THE_FUTURE]
|
||||
return [false, '0x']
|
||||
}
|
||||
|
||||
const value = await web3.eth.getStorageAt(address, slot, blockNumber)
|
||||
|
@ -1,4 +1,3 @@
|
||||
const { ASYNC_CALL_ERRORS } = require('../../../utils/constants')
|
||||
const { serializeTx } = require('./serializers')
|
||||
|
||||
async function call(web3, data, foreignBlock) {
|
||||
@ -7,7 +6,7 @@ async function call(web3, data, foreignBlock) {
|
||||
const tx = await web3.eth.getTransaction(hash)
|
||||
|
||||
if (tx === null || tx.blockNumber > foreignBlock.number) {
|
||||
return [false, ASYNC_CALL_ERRORS.NOT_FOUND]
|
||||
return [false, '0x']
|
||||
}
|
||||
|
||||
return [true, serializeTx(web3, tx)]
|
||||
|
@ -1,7 +1,5 @@
|
||||
const { toBN } = require('web3').utils
|
||||
|
||||
const { ASYNC_CALL_ERRORS } = require('../../../utils/constants')
|
||||
|
||||
async function call(web3, data, foreignBlock) {
|
||||
const address = web3.eth.abi.decodeParameter('address', data)
|
||||
|
||||
@ -14,7 +12,7 @@ async function callArchive(web3, data, foreignBlock) {
|
||||
const { 0: address, 1: blockNumber } = web3.eth.abi.decodeParameters(['address', 'uint256'], data)
|
||||
|
||||
if (toBN(blockNumber).gt(toBN(foreignBlock.number))) {
|
||||
return [false, ASYNC_CALL_ERRORS.BLOCK_IS_IN_THE_FUTURE]
|
||||
return [false, '0x']
|
||||
}
|
||||
|
||||
const nonce = await web3.eth.getTransactionCount(address, blockNumber)
|
||||
|
@ -1,4 +1,3 @@
|
||||
const { ASYNC_CALL_ERRORS } = require('../../../utils/constants')
|
||||
const { serializeReceipt } = require('./serializers')
|
||||
|
||||
async function call(web3, data, foreignBlock) {
|
||||
@ -7,7 +6,7 @@ async function call(web3, data, foreignBlock) {
|
||||
const receipt = await web3.eth.getTransactionReceipt(hash)
|
||||
|
||||
if (receipt === null || receipt.blockNumber > foreignBlock.number) {
|
||||
return [false, ASYNC_CALL_ERRORS.NOT_FOUND]
|
||||
return [false, '0x']
|
||||
}
|
||||
|
||||
return [true, serializeReceipt(web3, receipt)]
|
||||
|
@ -24,8 +24,7 @@ async function estimateGas({
|
||||
// message length in bytes
|
||||
const len = strip0x(result).length / 2
|
||||
|
||||
let callbackGasLimit = parseInt(await homeBridge.methods.maxGasPerTx().call(), 10)
|
||||
callbackGasLimit = Math.ceil((callbackGasLimit * 64) / 63)
|
||||
const callbackGasLimit = parseInt(await homeBridge.methods.maxGasPerTx().call(), 10)
|
||||
|
||||
return gasEstimate + callbackGasLimit + estimateExtraGas(len)
|
||||
} catch (e) {
|
||||
|
@ -4,13 +4,7 @@ const { soliditySha3 } = require('web3').utils
|
||||
const { HttpListProviderError } = require('../../services/HttpListProvider')
|
||||
const rootLogger = require('../../services/logger')
|
||||
const makeBlockFinder = require('../../services/blockFinder')
|
||||
const {
|
||||
EXIT_CODES,
|
||||
MAX_CONCURRENT_EVENTS,
|
||||
EXTRA_GAS_ABSOLUTE,
|
||||
ASYNC_CALL_ERRORS,
|
||||
MAX_ASYNC_CALL_RESULT_LENGTH
|
||||
} = require('../../utils/constants')
|
||||
const { EXIT_CODES, MAX_CONCURRENT_EVENTS, EXTRA_GAS_ABSOLUTE } = require('../../utils/constants')
|
||||
const estimateGas = require('./estimateGas')
|
||||
const { getValidatorContract, getBlock, getBlockNumber, getRequiredBlockConfirmations } = require('../../tx/web3')
|
||||
const { AlreadyProcessedError, AlreadySignedError, InvalidValidatorError } = require('../../utils/errors')
|
||||
@ -35,13 +29,11 @@ Object.keys(asyncCalls).forEach(method => {
|
||||
})
|
||||
|
||||
function processInformationRequestsBuilder(config) {
|
||||
const { home, foreign } = config
|
||||
const { home, foreign, web3ForeignArchive } = config
|
||||
|
||||
let validatorContract = null
|
||||
let blockFinder = null
|
||||
|
||||
foreign.web3Archive.currentProvider.startSyncStateChecker(foreign.syncCheckInterval)
|
||||
|
||||
return async function processInformationRequests(informationRequests) {
|
||||
const txToSend = []
|
||||
|
||||
@ -51,15 +43,13 @@ function processInformationRequestsBuilder(config) {
|
||||
|
||||
if (blockFinder === null) {
|
||||
rootLogger.debug('Initializing block finder')
|
||||
blockFinder = await makeBlockFinder('foreign', foreign.web3Archive)
|
||||
blockFinder = await makeBlockFinder('foreign', foreign.web3)
|
||||
}
|
||||
|
||||
// latest foreign block is requested from an archive RPC, to ensure that it is synced with the network
|
||||
// block confirmations can be requested from the regular JSON RPC
|
||||
const foreignBlockNumber =
|
||||
(await getBlockNumber(foreign.web3Archive)) - (await getRequiredBlockConfirmations(foreign.bridgeContract))
|
||||
(await getBlockNumber(foreign.web3)) - (await getRequiredBlockConfirmations(foreign.bridgeContract))
|
||||
const homeBlock = await getBlock(home.web3, informationRequests[0].blockNumber)
|
||||
const lastForeignBlock = await getBlock(foreign.web3Archive, foreignBlockNumber)
|
||||
const lastForeignBlock = await getBlock(foreign.web3, foreignBlockNumber)
|
||||
|
||||
if (homeBlock.timestamp > lastForeignBlock.timestamp) {
|
||||
rootLogger.debug(
|
||||
@ -89,17 +79,12 @@ function processInformationRequestsBuilder(config) {
|
||||
logger.info({ requestSelector, method: asyncCallMethod, data }, 'Processing async request')
|
||||
|
||||
const call = asyncCalls[asyncCallMethod]
|
||||
let [status, result] = await call(foreign.web3Archive, data, foreignClosestBlock).catch(e => {
|
||||
const [status, result] = await call(web3ForeignArchive, data, foreignClosestBlock).catch(e => {
|
||||
if (e instanceof HttpListProviderError) {
|
||||
throw e
|
||||
}
|
||||
logger.error({ error: e.message }, 'Unknown error during async call execution')
|
||||
throw e
|
||||
return [false, '0x']
|
||||
})
|
||||
if (result.length > 2 + MAX_ASYNC_CALL_RESULT_LENGTH * 2) {
|
||||
status = false
|
||||
result = ASYNC_CALL_ERRORS.RESULT_IS_TOO_LONG
|
||||
}
|
||||
logger.info({ requestSelector, method: asyncCallMethod, status, result }, 'Request result obtained')
|
||||
|
||||
let gasEstimate
|
||||
|
@ -6,9 +6,11 @@ const logger = require('../../services/logger').child({
|
||||
|
||||
async function estimateGas({ web3, homeBridge, validatorContract, recipient, value, txHash, address }) {
|
||||
try {
|
||||
return await homeBridge.methods.executeAffirmation(recipient, value, txHash).estimateGas({
|
||||
const gasEstimate = await homeBridge.methods.executeAffirmation(recipient, value, txHash).estimateGas({
|
||||
from: address
|
||||
})
|
||||
|
||||
return gasEstimate
|
||||
} catch (e) {
|
||||
if (e instanceof HttpListProviderError) {
|
||||
throw e
|
||||
|
@ -20,7 +20,8 @@ async function estimateGas({
|
||||
signatures
|
||||
}) {
|
||||
try {
|
||||
return await foreignBridge.methods.executeSignatures(message, signatures).estimateGas()
|
||||
const gasEstimate = await foreignBridge.methods.executeSignatures(message, signatures).estimateGas()
|
||||
return gasEstimate
|
||||
} catch (e) {
|
||||
if (e instanceof HttpListProviderError) {
|
||||
throw e
|
||||
|
@ -6,9 +6,10 @@ const logger = require('../../services/logger').child({
|
||||
|
||||
async function estimateGas({ web3, homeBridge, validatorContract, signature, message, address }) {
|
||||
try {
|
||||
return await homeBridge.methods.submitSignature(signature, message).estimateGas({
|
||||
const gasEstimate = await homeBridge.methods.submitSignature(signature, message).estimateGas({
|
||||
from: address
|
||||
})
|
||||
return gasEstimate
|
||||
} catch (e) {
|
||||
if (e instanceof HttpListProviderError) {
|
||||
throw e
|
||||
|
@ -13,25 +13,12 @@ const limit = promiseLimit(MAX_CONCURRENT_EVENTS)
|
||||
function processTransfersBuilder(config) {
|
||||
const { bridgeContract, web3 } = config.home
|
||||
|
||||
const userRequestForAffirmationHash = web3.eth.abi.encodeEventSignature('UserRequestForAffirmation(address,uint256)')
|
||||
const redeemHash = web3.eth.abi.encodeEventSignature('Redeem(address,uint256,uint256)')
|
||||
const transferHash = web3.eth.abi.encodeEventSignature('Transfer(address,address,uint256)')
|
||||
|
||||
const foreignBridgeAddress = config.foreign.bridgeAddress
|
||||
|
||||
const decodeAddress = data => web3.eth.abi.decodeParameter('address', data)
|
||||
|
||||
const isUserRequestForAffirmation = e =>
|
||||
e.address.toLowerCase() === foreignBridgeAddress.toLowerCase() && e.topics[0] === userRequestForAffirmationHash
|
||||
const isRedeem = cTokenAddress => e =>
|
||||
e.address.toLowerCase() === cTokenAddress.toLowerCase() &&
|
||||
e.topics[0] === redeemHash &&
|
||||
decodeAddress(e.data.slice(0, 66)).toLowerCase() === foreignBridgeAddress.toLowerCase()
|
||||
const isCTokenTransfer = cTokenAddress => e =>
|
||||
e.address.toLowerCase() === cTokenAddress.toLowerCase() &&
|
||||
e.topics[0] === transferHash &&
|
||||
decodeAddress(e.topics[1]).toLowerCase() === foreignBridgeAddress.toLowerCase() &&
|
||||
decodeAddress(e.topics[2]).toLowerCase() === cTokenAddress.toLowerCase()
|
||||
const userRequestForAffirmationAbi = config.foreign.bridgeABI.find(
|
||||
e => e.type === 'event' && e.name === 'UserRequestForAffirmation'
|
||||
)
|
||||
const tokensSwappedAbi = config.foreign.bridgeABI.find(e => e.type === 'event' && e.name === 'TokensSwapped')
|
||||
const userRequestForAffirmationHash = web3.eth.abi.encodeEventSignature(userRequestForAffirmationAbi)
|
||||
const tokensSwappedHash = tokensSwappedAbi ? web3.eth.abi.encodeEventSignature(tokensSwappedAbi) : '0x'
|
||||
|
||||
let validatorContract = null
|
||||
|
||||
@ -45,35 +32,37 @@ function processTransfersBuilder(config) {
|
||||
rootLogger.debug(`Processing ${transfers.length} Transfer events`)
|
||||
const callbacks = transfers
|
||||
.map(transfer => async () => {
|
||||
const { from, to, value } = transfer.returnValues
|
||||
const { from, value } = transfer.returnValues
|
||||
|
||||
const logger = rootLogger.child({
|
||||
eventTransactionHash: transfer.transactionHash,
|
||||
from,
|
||||
to,
|
||||
value
|
||||
eventTransactionHash: transfer.transactionHash
|
||||
})
|
||||
|
||||
logger.info('Processing transfer')
|
||||
logger.info({ from, value }, `Processing transfer ${transfer.transactionHash}`)
|
||||
|
||||
const receipt = await config.foreign.web3.eth.getTransactionReceipt(transfer.transactionHash)
|
||||
|
||||
if (receipt.logs.some(isUserRequestForAffirmation)) {
|
||||
logger.info('Transfer event discarded because affirmation is detected in the same transaction')
|
||||
const existsAffirmationEvent = receipt.logs.some(
|
||||
e => e.address === config.foreign.bridgeAddress && e.topics[0] === userRequestForAffirmationHash
|
||||
)
|
||||
|
||||
if (existsAffirmationEvent) {
|
||||
logger.info(
|
||||
`Transfer event discarded because a transaction with alternative receiver detected in transaction ${
|
||||
transfer.transactionHash
|
||||
}`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (from === ZERO_ADDRESS) {
|
||||
logger.info('Mint-like transfers from zero address are not processed')
|
||||
return
|
||||
}
|
||||
const existsTokensSwappedEvent = tokensSwappedAbi
|
||||
? receipt.logs.some(e => e.address === config.foreign.bridgeAddress && e.topics[0] === tokensSwappedHash)
|
||||
: false
|
||||
|
||||
// when bridge performs a withdrawal from Compound, the following three events occur
|
||||
// * token.Transfer(from=cToken, to=bridge, amount=X)
|
||||
// * cToken.Redeem(redeemer=bridge, amount=X, tokens=Y)
|
||||
// * cToken.Transfer(from=bridge, to=cToken, amount=Y)
|
||||
if (receipt.logs.some(isRedeem(from)) && receipt.logs.some(isCTokenTransfer(from))) {
|
||||
logger.info('Transfer event discarded because cToken redeem is detected in the same transaction')
|
||||
if (from === ZERO_ADDRESS && existsTokensSwappedEvent) {
|
||||
logger.info(
|
||||
`Transfer event discarded because token swap is detected in transaction ${transfer.transactionHash}`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -1,159 +0,0 @@
|
||||
require('../env')
|
||||
const path = require('path')
|
||||
const BigNumber = require('bignumber.js')
|
||||
const { redis } = require('./services/redisClient')
|
||||
const logger = require('./services/logger')
|
||||
const { sendTx } = require('./tx/sendTx')
|
||||
const { getNonce, getChainId, getBlock } = require('./tx/web3')
|
||||
const { addExtraGas, checkHTTPS, watchdog } = require('./utils/utils')
|
||||
const { EXIT_CODES, EXTRA_GAS_PERCENTAGE, MAX_GAS_LIMIT } = require('./utils/constants')
|
||||
const { estimateProfit } = require('./events/processAMBCollectedSignaturesMEV')
|
||||
|
||||
if (process.argv.length < 3) {
|
||||
logger.error('Please check the number of arguments, config file was not provided')
|
||||
process.exit(EXIT_CODES.GENERAL_ERROR)
|
||||
}
|
||||
|
||||
const config = require(path.join('../config/', process.argv[2]))
|
||||
|
||||
const { web3, mevForeign, validatorAddress } = config
|
||||
|
||||
let chainId = 0
|
||||
let flashbotsProvider
|
||||
|
||||
async function initialize() {
|
||||
try {
|
||||
const checkHttps = checkHTTPS(process.env.ORACLE_ALLOW_HTTP_FOR_RPC, logger)
|
||||
|
||||
web3.currentProvider.urls.forEach(checkHttps(config.id))
|
||||
|
||||
chainId = await getChainId(web3)
|
||||
flashbotsProvider = await mevForeign.getFlashbotsProvider(chainId)
|
||||
return runMain()
|
||||
} catch (e) {
|
||||
logger.error(e.message)
|
||||
process.exit(EXIT_CODES.GENERAL_ERROR)
|
||||
}
|
||||
}
|
||||
|
||||
async function runMain() {
|
||||
try {
|
||||
if (redis.status === 'ready') {
|
||||
if (config.maxProcessingTime) {
|
||||
await watchdog(main, config.maxProcessingTime, () => {
|
||||
logger.fatal('Max processing time reached')
|
||||
process.exit(EXIT_CODES.MAX_TIME_REACHED)
|
||||
})
|
||||
} else {
|
||||
await main()
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
}
|
||||
|
||||
setTimeout(runMain, config.pollingInterval)
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
const jobs = Object.values(await redis.hgetall(config.mevJobsRedisKey)).map(JSON.parse)
|
||||
const totalJobs = jobs.length
|
||||
|
||||
if (totalJobs === 0) {
|
||||
logger.debug('Nothing to process')
|
||||
return
|
||||
}
|
||||
|
||||
const { baseFeePerGas: pendingBaseFee, number: pendingBlockNumber } = await getBlock(web3, 'pending')
|
||||
const bestJob = pickBestJob(jobs, pendingBaseFee)
|
||||
|
||||
if (!bestJob) {
|
||||
logger.info({ totalJobs, pendingBaseFee }, 'No suitable job was found, waiting for a lower gas price')
|
||||
return
|
||||
}
|
||||
|
||||
const jobLogger = logger.child({ eventTransactionHash: bestJob.transactionReference })
|
||||
|
||||
const maxProfit = await estimateProfit(
|
||||
mevForeign.contract,
|
||||
mevForeign.minGasPrice,
|
||||
bestJob.executeData,
|
||||
bestJob.value
|
||||
)
|
||||
|
||||
if (maxProfit === '0') {
|
||||
jobLogger.info(`No MEV opportunity found when testing with min gas price ${mevForeign.minGasPrice}, removing job`)
|
||||
await redis.hdel(config.mevJobsRedisKey, bestJob.transactionReference)
|
||||
return
|
||||
}
|
||||
jobLogger.info(`Estimated profit of ${maxProfit} when simulating with ${mevForeign.minGasPrice} gas price`)
|
||||
bestJob.profit = maxProfit
|
||||
|
||||
if (new BigNumber(pendingBaseFee).gt(mevForeign.minGasPrice)) {
|
||||
const profit = await estimateProfit(mevForeign.contract, pendingBaseFee, bestJob.executeData, bestJob.value)
|
||||
if (profit === '0') {
|
||||
jobLogger.info(
|
||||
`No MEV opportunity found when testing with current gas price ${pendingBaseFee}, waiting for lower gas price`
|
||||
)
|
||||
bestJob.maxFeePerGas = pendingBaseFee
|
||||
await redis.hset(config.mevJobsRedisKey, bestJob.transactionReference, JSON.stringify(bestJob))
|
||||
return
|
||||
}
|
||||
jobLogger.info(`Estimated profit of ${profit} when simulating with ${pendingBaseFee} gas price`)
|
||||
}
|
||||
|
||||
let gasLimit
|
||||
if (typeof bestJob.extraGas === 'number') {
|
||||
gasLimit = addExtraGas(bestJob.gasEstimate + bestJob.extraGas, 0, MAX_GAS_LIMIT)
|
||||
} else {
|
||||
gasLimit = addExtraGas(bestJob.gasEstimate, EXTRA_GAS_PERCENTAGE, MAX_GAS_LIMIT)
|
||||
}
|
||||
|
||||
const nonce = await getNonce(web3, validatorAddress)
|
||||
jobLogger.info(
|
||||
{ nonce, fromBlock: pendingBlockNumber, toBlock: pendingBlockNumber + mevForeign.bundlesPerIteration - 1 },
|
||||
'Sending MEV bundles'
|
||||
)
|
||||
const txHash = await sendTx({
|
||||
data: bestJob.data,
|
||||
nonce,
|
||||
value: bestJob.value,
|
||||
gasLimit,
|
||||
privateKey: config.validatorPrivateKey,
|
||||
to: bestJob.to,
|
||||
chainId,
|
||||
web3,
|
||||
gasPriceOptions: {
|
||||
maxFeePerGas: bestJob.maxFeePerGas,
|
||||
maxPriorityFeePerGas: bestJob.maxPriorityFeePerGas
|
||||
},
|
||||
mevOptions: {
|
||||
provider: flashbotsProvider,
|
||||
fromBlock: pendingBlockNumber,
|
||||
toBlock: pendingBlockNumber + mevForeign.bundlesPerIteration - 1,
|
||||
logger
|
||||
}
|
||||
})
|
||||
|
||||
jobLogger.info({ txHash }, `Tx generated ${txHash} for event Tx ${bestJob.transactionReference}`)
|
||||
|
||||
await redis.hset(config.mevJobsRedisKey, bestJob.transactionReference, JSON.stringify(bestJob))
|
||||
jobLogger.debug(`Finished processing msg`)
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
function pickBestJob(jobs, feePerGas) {
|
||||
const feePerGasBN = new BigNumber(feePerGas)
|
||||
let best = null
|
||||
jobs.forEach(job => {
|
||||
if (feePerGasBN.lt(job.maxFeePerGas) && (!best || new BigNumber(best.profit).lt(job.profit))) {
|
||||
best = job
|
||||
}
|
||||
})
|
||||
return best
|
||||
}
|
||||
|
||||
initialize()
|
@ -1,251 +0,0 @@
|
||||
require('../env')
|
||||
const path = require('path')
|
||||
const { redis } = require('./services/redisClient')
|
||||
const logger = require('./services/logger')
|
||||
const { getBlockNumber, getRequiredBlockConfirmations, getEvents } = require('./tx/web3')
|
||||
const { checkHTTPS, watchdog, syncForEach } = require('./utils/utils')
|
||||
const { processCollectedSignaturesBuilder } = require('./events/processAMBCollectedSignaturesMEV')
|
||||
const {
|
||||
EXIT_CODES,
|
||||
BLOCK_NUMBER_PROGRESS_ITERATIONS_LIMIT,
|
||||
MAX_HISTORY_BLOCK_TO_REPROCESS
|
||||
} = require('./utils/constants')
|
||||
|
||||
if (process.argv.length < 3) {
|
||||
logger.error('Please check the number of arguments, config file was not provided')
|
||||
process.exit(EXIT_CODES.GENERAL_ERROR)
|
||||
}
|
||||
|
||||
const config = require(path.join('../config/', process.argv[2]))
|
||||
|
||||
const processAMBCollectedSignaturesMEV = processCollectedSignaturesBuilder(config)
|
||||
|
||||
const {
|
||||
web3,
|
||||
bridgeContract,
|
||||
eventContract,
|
||||
startBlock,
|
||||
pollingInterval,
|
||||
chain,
|
||||
reprocessingOptions,
|
||||
blockPollingLimit
|
||||
} = config.main
|
||||
const lastBlockRedisKey = `${config.id}:lastProcessedBlock`
|
||||
const lastReprocessedBlockRedisKey = `${config.id}:lastReprocessedBlock`
|
||||
const seenEventsRedisKey = `${config.id}:seenEvents`
|
||||
const mevJobsRedisKey = `${config.id}:mevJobs`
|
||||
let lastProcessedBlock = Math.max(startBlock - 1, 0)
|
||||
let lastReprocessedBlock
|
||||
let lastSeenBlockNumber = 0
|
||||
let sameBlockNumberCounter = 0
|
||||
|
||||
async function initialize() {
|
||||
try {
|
||||
const checkHttps = checkHTTPS(process.env.ORACLE_ALLOW_HTTP_FOR_RPC, logger)
|
||||
|
||||
web3.currentProvider.urls.forEach(checkHttps(chain))
|
||||
|
||||
await getLastProcessedBlock()
|
||||
await getLastReprocessedBlock()
|
||||
runMain({ sendToQueue: saveJobsToRedis })
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
process.exit(EXIT_CODES.GENERAL_ERROR)
|
||||
}
|
||||
}
|
||||
|
||||
async function runMain({ sendToQueue }) {
|
||||
try {
|
||||
if (redis.status === 'ready') {
|
||||
if (config.maxProcessingTime) {
|
||||
await watchdog(() => main({ sendToQueue }), config.maxProcessingTime, () => {
|
||||
logger.fatal('Max processing time reached')
|
||||
process.exit(EXIT_CODES.MAX_TIME_REACHED)
|
||||
})
|
||||
} else {
|
||||
await main({ sendToQueue })
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
runMain({ sendToQueue })
|
||||
}, pollingInterval)
|
||||
}
|
||||
|
||||
async function saveJobsToRedis(jobs) {
|
||||
return syncForEach(jobs, job => redis.hset(mevJobsRedisKey, job.transactionReference, JSON.stringify(job)))
|
||||
}
|
||||
|
||||
async function getLastProcessedBlock() {
|
||||
const result = await redis.get(lastBlockRedisKey)
|
||||
logger.debug({ fromRedis: result, fromConfig: lastProcessedBlock }, 'Last Processed block obtained')
|
||||
lastProcessedBlock = result ? parseInt(result, 10) : lastProcessedBlock
|
||||
}
|
||||
|
||||
async function getLastReprocessedBlock() {
|
||||
if (reprocessingOptions.enabled) {
|
||||
const result = await redis.get(lastReprocessedBlockRedisKey)
|
||||
if (result) {
|
||||
lastReprocessedBlock = Math.max(parseInt(result, 10), lastProcessedBlock - MAX_HISTORY_BLOCK_TO_REPROCESS)
|
||||
} else {
|
||||
lastReprocessedBlock = lastProcessedBlock
|
||||
}
|
||||
logger.debug({ block: lastReprocessedBlock }, 'Last reprocessed block obtained')
|
||||
} else {
|
||||
// when reprocessing is being enabled not for the first time,
|
||||
// we do not want to process blocks for which we didn't recorded seen events,
|
||||
// instead, we want to start from the current block.
|
||||
// Thus we should delete this reprocessing pointer once it is disabled.
|
||||
await redis.del(lastReprocessedBlockRedisKey)
|
||||
}
|
||||
}
|
||||
|
||||
function updateLastProcessedBlock(lastBlockNumber) {
|
||||
lastProcessedBlock = lastBlockNumber
|
||||
return redis.set(lastBlockRedisKey, lastProcessedBlock)
|
||||
}
|
||||
|
||||
function updateLastReprocessedBlock(lastBlockNumber) {
|
||||
lastReprocessedBlock = lastBlockNumber
|
||||
return redis.set(lastReprocessedBlockRedisKey, lastReprocessedBlock)
|
||||
}
|
||||
|
||||
function processEvents(events) {
|
||||
switch (config.id) {
|
||||
case 'amb-collected-signatures-mev':
|
||||
return processAMBCollectedSignaturesMEV(events)
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
const eventKey = e => `${e.transactionHash}-${e.logIndex}`
|
||||
|
||||
async function reprocessOldLogs(sendToQueue) {
|
||||
const fromBlock = lastReprocessedBlock + 1
|
||||
const toBlock = lastReprocessedBlock + reprocessingOptions.batchSize
|
||||
const events = await getEvents({
|
||||
contract: eventContract,
|
||||
event: config.event,
|
||||
fromBlock,
|
||||
toBlock,
|
||||
filter: config.eventFilter
|
||||
})
|
||||
const alreadySeenEvents = await getSeenEvents(fromBlock, toBlock)
|
||||
const missingEvents = events.filter(e => !alreadySeenEvents[eventKey(e)])
|
||||
if (missingEvents.length === 0) {
|
||||
logger.debug('No missed events were found')
|
||||
} else {
|
||||
logger.info(`Found ${missingEvents.length} ${config.event} missed events`)
|
||||
const job = await processEvents(missingEvents)
|
||||
logger.info('Missed events transactions to send:', job.length)
|
||||
if (job.length) {
|
||||
await sendToQueue(job)
|
||||
}
|
||||
}
|
||||
|
||||
await updateLastReprocessedBlock(toBlock)
|
||||
await deleteSeenEvents(0, toBlock)
|
||||
}
|
||||
|
||||
async function getSeenEvents(fromBlock, toBlock) {
|
||||
const keys = await redis.zrangebyscore(seenEventsRedisKey, fromBlock, toBlock)
|
||||
const res = {}
|
||||
keys.forEach(k => {
|
||||
res[k] = true
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
function deleteSeenEvents(fromBlock, toBlock) {
|
||||
return redis.zremrangebyscore(seenEventsRedisKey, fromBlock, toBlock)
|
||||
}
|
||||
|
||||
function addSeenEvents(events) {
|
||||
return redis.zadd(seenEventsRedisKey, ...events.flatMap(e => [e.blockNumber, eventKey(e)]))
|
||||
}
|
||||
|
||||
async function getLastBlockToProcess(web3, bridgeContract) {
|
||||
const [lastBlockNumber, requiredBlockConfirmations] = await Promise.all([
|
||||
getBlockNumber(web3),
|
||||
getRequiredBlockConfirmations(bridgeContract)
|
||||
])
|
||||
|
||||
if (lastBlockNumber < lastSeenBlockNumber) {
|
||||
sameBlockNumberCounter = 0
|
||||
logger.warn({ lastBlockNumber, lastSeenBlockNumber }, 'Received block number less than already seen block')
|
||||
web3.currentProvider.switchToFallbackRPC()
|
||||
} else if (lastBlockNumber === lastSeenBlockNumber) {
|
||||
sameBlockNumberCounter++
|
||||
if (sameBlockNumberCounter > 1) {
|
||||
logger.info({ lastBlockNumber, sameBlockNumberCounter }, 'Received the same block number more than twice')
|
||||
if (sameBlockNumberCounter >= BLOCK_NUMBER_PROGRESS_ITERATIONS_LIMIT) {
|
||||
sameBlockNumberCounter = 0
|
||||
logger.warn(
|
||||
{ lastBlockNumber, n: BLOCK_NUMBER_PROGRESS_ITERATIONS_LIMIT },
|
||||
'Received the same block number for too many times. Probably node is not synced anymore'
|
||||
)
|
||||
web3.currentProvider.switchToFallbackRPC()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sameBlockNumberCounter = 0
|
||||
lastSeenBlockNumber = lastBlockNumber
|
||||
}
|
||||
return lastBlockNumber - requiredBlockConfirmations
|
||||
}
|
||||
|
||||
async function main({ sendToQueue }) {
|
||||
try {
|
||||
const lastBlockToProcess = await getLastBlockToProcess(web3, bridgeContract)
|
||||
|
||||
if (reprocessingOptions.enabled) {
|
||||
if (lastReprocessedBlock + reprocessingOptions.batchSize + reprocessingOptions.blockDelay < lastBlockToProcess) {
|
||||
await reprocessOldLogs(sendToQueue)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (lastBlockToProcess <= lastProcessedBlock) {
|
||||
logger.debug('All blocks already processed')
|
||||
return
|
||||
}
|
||||
|
||||
const fromBlock = lastProcessedBlock + 1
|
||||
const rangeEndBlock = blockPollingLimit ? fromBlock + blockPollingLimit : lastBlockToProcess
|
||||
const toBlock = Math.min(lastBlockToProcess, rangeEndBlock)
|
||||
|
||||
const events = await getEvents({
|
||||
contract: eventContract,
|
||||
event: config.event,
|
||||
fromBlock,
|
||||
toBlock,
|
||||
filter: config.eventFilter
|
||||
})
|
||||
logger.info(`Found ${events.length} ${config.event} events`)
|
||||
|
||||
if (events.length) {
|
||||
const job = await processEvents(events)
|
||||
logger.info('Transactions to send:', job.length)
|
||||
|
||||
if (job.length) {
|
||||
await sendToQueue(job)
|
||||
}
|
||||
if (reprocessingOptions.enabled) {
|
||||
await addSeenEvents(events)
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug({ lastProcessedBlock: toBlock.toString() }, 'Updating last processed block')
|
||||
await updateLastProcessedBlock(toBlock)
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
}
|
||||
|
||||
logger.debug('Finished')
|
||||
}
|
||||
|
||||
initialize()
|
@ -9,8 +9,6 @@ const { sendTx } = require('./tx/sendTx')
|
||||
const { getNonce, getChainId } = require('./tx/web3')
|
||||
const {
|
||||
addExtraGas,
|
||||
applyMinGasFeeBump,
|
||||
chooseGasPriceOptions,
|
||||
checkHTTPS,
|
||||
syncForEach,
|
||||
waitForFunds,
|
||||
@ -21,7 +19,7 @@ const {
|
||||
isInsufficientBalanceError,
|
||||
isNonceError
|
||||
} = require('./utils/utils')
|
||||
const { EXIT_CODES, EXTRA_GAS_PERCENTAGE, MAX_GAS_LIMIT, MIN_GAS_PRICE_BUMP_FACTOR } = require('./utils/constants')
|
||||
const { EXIT_CODES, EXTRA_GAS_PERCENTAGE, MAX_GAS_LIMIT } = require('./utils/constants')
|
||||
|
||||
const { ORACLE_TX_REDUNDANCY } = process.env
|
||||
|
||||
@ -32,8 +30,8 @@ if (process.argv.length < 3) {
|
||||
|
||||
const config = require(path.join('../config/', process.argv[2]))
|
||||
|
||||
const { web3, web3Fallback, syncCheckInterval } = config.main
|
||||
const web3Redundant = ORACLE_TX_REDUNDANCY === 'true' ? config.main.web3Redundant : web3
|
||||
const { web3, web3Fallback } = config
|
||||
const web3Redundant = ORACLE_TX_REDUNDANCY === 'true' ? config.web3Redundant : web3
|
||||
|
||||
const nonceKey = `${config.id}:nonce`
|
||||
let chainId = 0
|
||||
@ -42,10 +40,9 @@ async function initialize() {
|
||||
try {
|
||||
const checkHttps = checkHTTPS(process.env.ORACLE_ALLOW_HTTP_FOR_RPC, logger)
|
||||
|
||||
web3.currentProvider.urls.forEach(checkHttps(config.id))
|
||||
web3.currentProvider.startSyncStateChecker(syncCheckInterval)
|
||||
web3.currentProvider.subProvider.urls.forEach(checkHttps(config.id))
|
||||
|
||||
GasPrice.start(config.id, web3)
|
||||
GasPrice.start(config.id)
|
||||
|
||||
chainId = await getChainId(web3)
|
||||
connectQueue()
|
||||
@ -58,6 +55,7 @@ async function initialize() {
|
||||
function connectQueue() {
|
||||
connectSenderToQueue({
|
||||
queueName: config.queue,
|
||||
oldQueueName: config.oldQueue,
|
||||
resendInterval: config.resendInterval,
|
||||
cb: options => {
|
||||
if (config.maxProcessingTime) {
|
||||
@ -123,7 +121,7 @@ async function main({ msg, ackMsg, nackMsg, channel, scheduleForRetry, scheduleT
|
||||
|
||||
const txArray = JSON.parse(msg.content)
|
||||
logger.debug(`Msg received with ${txArray.length} Tx to send`)
|
||||
const gasPriceOptions = GasPrice.gasPriceOptions()
|
||||
const gasPrice = GasPrice.getPrice().toString(10)
|
||||
|
||||
let nonce
|
||||
let insufficientFunds = false
|
||||
@ -149,7 +147,6 @@ async function main({ msg, ackMsg, nackMsg, channel, scheduleForRetry, scheduleT
|
||||
}
|
||||
|
||||
try {
|
||||
const newGasPriceOptions = chooseGasPriceOptions(gasPriceOptions, job.gasPriceOptions)
|
||||
if (isResend) {
|
||||
const tx = await web3Fallback.eth.getTransaction(job.txHash)
|
||||
|
||||
@ -162,26 +159,24 @@ async function main({ msg, ackMsg, nackMsg, channel, scheduleForRetry, scheduleT
|
||||
nonce = await readNonce(true)
|
||||
}
|
||||
|
||||
const oldGasPrice = JSON.stringify(job.gasPriceOptions)
|
||||
const newGasPrice = JSON.stringify(newGasPriceOptions)
|
||||
logger.info(`Transaction ${job.txHash} was not mined, updating gasPrice: ${oldGasPrice} -> ${newGasPrice}`)
|
||||
logger.info(`Transaction ${job.txHash} was not mined, updating gasPrice: ${job.gasPrice} -> ${gasPrice}`)
|
||||
}
|
||||
logger.info(`Sending transaction with nonce ${nonce}`)
|
||||
const txHash = await sendTx({
|
||||
data: job.data,
|
||||
nonce,
|
||||
value: '0',
|
||||
gasPrice,
|
||||
amount: '0',
|
||||
gasLimit,
|
||||
privateKey: config.validatorPrivateKey,
|
||||
to: job.to,
|
||||
chainId,
|
||||
web3: web3Redundant,
|
||||
gasPriceOptions: newGasPriceOptions
|
||||
web3: web3Redundant
|
||||
})
|
||||
const resendJob = {
|
||||
...job,
|
||||
txHash,
|
||||
gasPriceOptions: newGasPriceOptions
|
||||
gasPrice
|
||||
}
|
||||
resendJobs.push(resendJob)
|
||||
|
||||
@ -199,8 +194,8 @@ async function main({ msg, ackMsg, nackMsg, channel, scheduleForRetry, scheduleT
|
||||
|
||||
if (isGasPriceError(e)) {
|
||||
logger.info('Replacement transaction underpriced, forcing gas price update')
|
||||
GasPrice.start(config.id, web3)
|
||||
failedTx.push(applyMinGasFeeBump(job, MIN_GAS_PRICE_BUMP_FACTOR))
|
||||
GasPrice.start(config.id)
|
||||
failedTx.push(job)
|
||||
} else if (isResend || isSameTransactionError(e)) {
|
||||
resendJobs.push(job)
|
||||
} else {
|
||||
@ -213,7 +208,7 @@ async function main({ msg, ackMsg, nackMsg, channel, scheduleForRetry, scheduleT
|
||||
if (isInsufficientBalanceError(e)) {
|
||||
insufficientFunds = true
|
||||
const currentBalance = await web3.eth.getBalance(config.validatorAddress)
|
||||
minimumBalance = gasLimit.multipliedBy(gasPriceOptions.gasPrice || gasPriceOptions.maxFeePerGas)
|
||||
minimumBalance = gasLimit.multipliedBy(gasPrice)
|
||||
logger.error(
|
||||
`Insufficient funds: ${currentBalance}. Stop processing messages until the balance is at least ${minimumBalance}.`
|
||||
)
|
||||
|
@ -1,16 +1,11 @@
|
||||
const fetch = require('node-fetch')
|
||||
const promiseRetry = require('promise-retry')
|
||||
const { utils } = require('web3')
|
||||
const { FALLBACK_RPC_URL_SWITCH_TIMEOUT } = require('../utils/constants')
|
||||
|
||||
const { onInjected } = require('./injectedLogger')
|
||||
|
||||
const { ORACLE_JSONRPC_ERROR_CODES } = process.env
|
||||
|
||||
// From EIP-1474 and Infura documentation
|
||||
const JSONRPC_ERROR_CODES = ORACLE_JSONRPC_ERROR_CODES
|
||||
? ORACLE_JSONRPC_ERROR_CODES.split(',').map(s => parseInt(s, 10))
|
||||
: [-32603, -32002, -32005]
|
||||
const JSONRPC_ERROR_CODES = [-32603, -32002, -32005]
|
||||
|
||||
const defaultOptions = {
|
||||
name: 'main',
|
||||
@ -40,61 +35,12 @@ function HttpListProvider(urls, options = {}) {
|
||||
this.options = { ...defaultOptions, ...options }
|
||||
this.currentIndex = 0
|
||||
this.lastTimeUsedPrimary = 0
|
||||
this.latestBlock = 0
|
||||
this.syncStateCheckerIntervalId = 0
|
||||
|
||||
onInjected(logger => {
|
||||
this.logger = logger.child({ module: `HttpListProvider:${this.options.name}` })
|
||||
})
|
||||
}
|
||||
|
||||
HttpListProvider.prototype.startSyncStateChecker = function(syncCheckInterval) {
|
||||
if (this.urls.length > 1 && syncCheckInterval > 0 && this.syncStateCheckerIntervalId === 0) {
|
||||
this.syncStateCheckerIntervalId = setInterval(this.checkLatestBlock.bind(this), syncCheckInterval)
|
||||
}
|
||||
}
|
||||
|
||||
HttpListProvider.prototype.checkLatestBlock = function() {
|
||||
const payload = { jsonrpc: '2.0', id: 1, method: 'eth_blockNumber', params: [] }
|
||||
this.send(payload, (error, result) => {
|
||||
if (error) {
|
||||
this.logger.warn({ oldBlock: this.latestBlock }, 'Failed to request latest block from all RPC urls')
|
||||
} else if (result.error) {
|
||||
this.logger.warn(
|
||||
{ oldBlock: this.latestBlock, error: result.error.message },
|
||||
'Failed to make eth_blockNumber request due to unknown error, switching to fallback RPC'
|
||||
)
|
||||
this.switchToFallbackRPC()
|
||||
} else {
|
||||
const blockNumber = utils.hexToNumber(result.result)
|
||||
if (blockNumber > this.latestBlock) {
|
||||
this.logger.debug({ oldBlock: this.latestBlock, newBlock: blockNumber }, 'Updating latest block number')
|
||||
this.latestBlock = blockNumber
|
||||
} else {
|
||||
this.logger.warn(
|
||||
{ oldBlock: this.latestBlock, newBlock: blockNumber },
|
||||
'Latest block on the node was not updated since last request, switching to fallback RPC'
|
||||
)
|
||||
this.switchToFallbackRPC()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
HttpListProvider.prototype.switchToFallbackRPC = function(index) {
|
||||
const prevIndex = this.currentIndex
|
||||
const newIndex = index || (prevIndex + 1) % this.urls.length
|
||||
if (this.urls.length < 2 || prevIndex === newIndex) {
|
||||
return
|
||||
}
|
||||
|
||||
this.logger.info(
|
||||
{ index: newIndex, oldURL: this.urls[prevIndex], newURL: this.urls[newIndex] },
|
||||
'Switching to fallback JSON-RPC URL'
|
||||
)
|
||||
this.currentIndex = newIndex
|
||||
}
|
||||
|
||||
HttpListProvider.prototype.send = async function send(payload, callback) {
|
||||
// if fallback URL is being used for too long, switch back to the primary URL
|
||||
if (this.currentIndex > 0 && Date.now() - this.lastTimeUsedPrimary > FALLBACK_RPC_URL_SWITCH_TIMEOUT) {
|
||||
@ -116,7 +62,11 @@ HttpListProvider.prototype.send = async function send(payload, callback) {
|
||||
|
||||
// if some of URLs failed to respond, current URL index is updated to the first URL that responded
|
||||
if (currentIndex !== index) {
|
||||
this.switchToFallbackRPC(index)
|
||||
this.logger.info(
|
||||
{ index, oldURL: this.urls[currentIndex], newURL: this.urls[index] },
|
||||
'Switching to fallback JSON-RPC URL'
|
||||
)
|
||||
this.currentIndex = index
|
||||
}
|
||||
callback(null, result)
|
||||
} catch (e) {
|
||||
|
@ -1,35 +1,39 @@
|
||||
const { hexToNumber, isHexStrict } = require('web3').utils
|
||||
|
||||
const { onInjected } = require('./injectedLogger')
|
||||
|
||||
function SafeEthLogsProvider(provider) {
|
||||
const oldSend = provider.send.bind(provider)
|
||||
const newSend = function(payload, callback) {
|
||||
if (payload.method === 'eth_getLogs' && isHexStrict(payload.params[0].toBlock)) {
|
||||
this.logger.debug('Modifying eth_getLogs request to include batch eth_blockNumber request')
|
||||
this.subProvider = provider
|
||||
onInjected(logger => {
|
||||
this.logger = logger.child({ module: 'SafeEthLogsProvider' })
|
||||
})
|
||||
}
|
||||
|
||||
const newPayload = [payload, { jsonrpc: '2.0', id: payload.id + 1, method: 'eth_blockNumber', params: [] }]
|
||||
oldSend(newPayload, (err, res) => {
|
||||
if (err) {
|
||||
callback(err, null)
|
||||
SafeEthLogsProvider.prototype.send = function send(payload, callback) {
|
||||
if (payload.method === 'eth_getLogs' && isHexStrict(payload.params[0].toBlock)) {
|
||||
this.logger.debug('Modifying eth_getLogs request to include batch eth_blockNumber request')
|
||||
|
||||
const newPayload = [payload, { jsonrpc: '2.0', id: payload.id + 1, method: 'eth_blockNumber', params: [] }]
|
||||
this.subProvider.send(newPayload, (err, res) => {
|
||||
if (err) {
|
||||
callback(err, null)
|
||||
} else {
|
||||
const rawLogs = res.find(({ id }) => id === payload.id)
|
||||
const rawBlockNumber = res.find(({ id }) => id === payload.id + 1)
|
||||
const blockNumber = hexToNumber(rawBlockNumber.result)
|
||||
const toBlock = hexToNumber(payload.params[0].toBlock)
|
||||
|
||||
if (blockNumber < toBlock) {
|
||||
this.logger.warn({ toBlock, blockNumber }, 'Returned block number is less than the specified toBlock')
|
||||
callback(new Error('block number too low'), null)
|
||||
} else {
|
||||
const rawLogs = res.find(({ id }) => id === payload.id)
|
||||
const rawBlockNumber = res.find(({ id }) => id === payload.id + 1)
|
||||
const blockNumber = hexToNumber(rawBlockNumber.result)
|
||||
const toBlock = hexToNumber(payload.params[0].toBlock)
|
||||
|
||||
if (blockNumber < toBlock) {
|
||||
this.logger.warn({ toBlock, blockNumber }, 'Returned block number is less than the specified toBlock')
|
||||
callback(new Error('block number too low'), null)
|
||||
} else {
|
||||
callback(null, rawLogs)
|
||||
}
|
||||
callback(null, rawLogs)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
oldSend(payload, callback)
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.subProvider.send(payload, callback)
|
||||
}
|
||||
provider.send = newSend.bind(provider)
|
||||
return provider
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
@ -40,9 +40,23 @@ function connectWatcherToQueue({ queueName, cb }) {
|
||||
cb({ sendToQueue, channel: channelWrapper })
|
||||
}
|
||||
|
||||
function connectSenderToQueue({ queueName, cb, resendInterval }) {
|
||||
function connectSenderToQueue({ queueName, oldQueueName, cb, resendInterval }) {
|
||||
const deadLetterExchange = `${queueName}-retry`
|
||||
|
||||
async function resendMessagesToNewQueue(channel) {
|
||||
logger.info(`Trying to check messages in the old non-priority queue ${queueName}`)
|
||||
while (true) {
|
||||
const msg = await channel.get(oldQueueName)
|
||||
if (msg === false) {
|
||||
logger.info(`No messages in the old queue ${oldQueueName} left`)
|
||||
break
|
||||
}
|
||||
logger.debug(`Message in the old queue ${oldQueueName} was found, redirecting it to the new queue ${queueName}`)
|
||||
await channel.sendToQueue(queueName, msg.content, { persistent: true, priority: SENDER_QUEUE_SEND_PRIORITY })
|
||||
await channel.ack(msg)
|
||||
}
|
||||
}
|
||||
|
||||
const channelWrapper = connection.createChannel({
|
||||
json: true
|
||||
})
|
||||
@ -50,6 +64,7 @@ function connectSenderToQueue({ queueName, cb, resendInterval }) {
|
||||
channelWrapper.addSetup(async channel => {
|
||||
await channel.assertExchange(deadLetterExchange, 'fanout', { durable: true })
|
||||
await channel.assertQueue(queueName, { durable: true, maxPriority: SENDER_QUEUE_MAX_PRIORITY })
|
||||
await channel.assertQueue(oldQueueName, { durable: true }).then(() => resendMessagesToNewQueue(channel))
|
||||
await channel.bindQueue(queueName, deadLetterExchange)
|
||||
await channel.prefetch(1)
|
||||
await channel.consume(queueName, msg =>
|
||||
|
@ -20,21 +20,21 @@ const {
|
||||
COMMON_HOME_GAS_PRICE_FACTOR
|
||||
} = process.env
|
||||
|
||||
let cachedGasPriceOptions = null
|
||||
let cachedGasPrice = null
|
||||
|
||||
let fetchGasPriceInterval = null
|
||||
|
||||
const fetchGasPrice = async (speedType, factor, web3, bridgeContract, gasPriceSupplierUrl) => {
|
||||
const fetchGasPrice = async (speedType, factor, bridgeContract, gasPriceSupplierUrl) => {
|
||||
const contractOptions = { logger }
|
||||
const supplierOptions = { speedType, factor, limits: GAS_PRICE_BOUNDARIES, logger }
|
||||
cachedGasPriceOptions =
|
||||
(await gasPriceFromSupplier(web3, gasPriceSupplierUrl, supplierOptions)) ||
|
||||
cachedGasPrice =
|
||||
(await gasPriceFromSupplier(gasPriceSupplierUrl, supplierOptions)) ||
|
||||
(await gasPriceFromContract(bridgeContract, contractOptions)) ||
|
||||
cachedGasPriceOptions
|
||||
return cachedGasPriceOptions
|
||||
cachedGasPrice
|
||||
return cachedGasPrice
|
||||
}
|
||||
|
||||
async function start(chainId, web3, fetchOnce) {
|
||||
async function start(chainId, fetchOnce) {
|
||||
clearInterval(fetchGasPriceInterval)
|
||||
|
||||
let contract = null
|
||||
@ -49,7 +49,7 @@ async function start(chainId, web3, fetchOnce) {
|
||||
updateInterval = ORACLE_HOME_GAS_PRICE_UPDATE_INTERVAL || DEFAULT_UPDATE_INTERVAL
|
||||
factor = Number(COMMON_HOME_GAS_PRICE_FACTOR) || DEFAULT_GAS_PRICE_FACTOR
|
||||
|
||||
cachedGasPriceOptions = { gasPrice: COMMON_HOME_GAS_PRICE_FALLBACK }
|
||||
cachedGasPrice = COMMON_HOME_GAS_PRICE_FALLBACK
|
||||
} else if (chainId === 'foreign') {
|
||||
contract = foreign.bridgeContract
|
||||
gasPriceSupplierUrl = COMMON_FOREIGN_GAS_PRICE_SUPPLIER_URL
|
||||
@ -57,7 +57,7 @@ async function start(chainId, web3, fetchOnce) {
|
||||
updateInterval = ORACLE_FOREIGN_GAS_PRICE_UPDATE_INTERVAL || DEFAULT_UPDATE_INTERVAL
|
||||
factor = Number(COMMON_FOREIGN_GAS_PRICE_FACTOR) || DEFAULT_GAS_PRICE_FACTOR
|
||||
|
||||
cachedGasPriceOptions = { gasPrice: COMMON_FOREIGN_GAS_PRICE_FALLBACK }
|
||||
cachedGasPrice = COMMON_FOREIGN_GAS_PRICE_FALLBACK
|
||||
} else {
|
||||
throw new Error(`Unrecognized chainId '${chainId}'`)
|
||||
}
|
||||
@ -67,21 +67,21 @@ async function start(chainId, web3, fetchOnce) {
|
||||
}
|
||||
|
||||
if (fetchOnce) {
|
||||
await fetchGasPrice(speedType, factor, web3, contract, gasPriceSupplierUrl)
|
||||
await fetchGasPrice(speedType, factor, contract, gasPriceSupplierUrl)
|
||||
} else {
|
||||
fetchGasPriceInterval = await setIntervalAndRun(
|
||||
() => fetchGasPrice(speedType, factor, web3, contract, gasPriceSupplierUrl),
|
||||
() => fetchGasPrice(speedType, factor, contract, gasPriceSupplierUrl),
|
||||
updateInterval
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function gasPriceOptions() {
|
||||
return cachedGasPriceOptions
|
||||
function getPrice() {
|
||||
return cachedGasPrice
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
start,
|
||||
gasPriceOptions,
|
||||
getPrice,
|
||||
fetchGasPrice
|
||||
}
|
||||
|
@ -1,6 +1,4 @@
|
||||
const Web3 = require('web3')
|
||||
const ethers = require('ethers')
|
||||
const flashbots = require('@flashbots/ethers-provider-bundle')
|
||||
const { HttpListProvider } = require('./HttpListProvider')
|
||||
const { SafeEthLogsProvider } = require('./SafeEthLogsProvider')
|
||||
const { RedundantHttpListProvider } = require('./RedundantHttpListProvider')
|
||||
@ -11,8 +9,6 @@ const {
|
||||
COMMON_FOREIGN_RPC_URL,
|
||||
ORACLE_SIDE_RPC_URL,
|
||||
ORACLE_FOREIGN_ARCHIVE_RPC_URL,
|
||||
ORACLE_MEV_FOREIGN_FLASHBOTS_RPC_URL,
|
||||
ORACLE_MEV_FOREIGN_FLASHBOTS_AUTH_SIGNING_KEY,
|
||||
ORACLE_RPC_REQUEST_TIMEOUT,
|
||||
ORACLE_HOME_RPC_POLLING_INTERVAL,
|
||||
ORACLE_FOREIGN_RPC_POLLING_INTERVAL
|
||||
@ -42,10 +38,10 @@ const foreignOptions = {
|
||||
retry: RETRY_CONFIG
|
||||
}
|
||||
|
||||
const homeProvider = SafeEthLogsProvider(new HttpListProvider(homeUrls, homeOptions))
|
||||
const homeProvider = new SafeEthLogsProvider(new HttpListProvider(homeUrls, homeOptions))
|
||||
const web3Home = new Web3(homeProvider)
|
||||
|
||||
const foreignProvider = SafeEthLogsProvider(new HttpListProvider(foreignUrls, foreignOptions))
|
||||
const foreignProvider = new SafeEthLogsProvider(new HttpListProvider(foreignUrls, foreignOptions))
|
||||
const web3Foreign = new Web3(foreignProvider)
|
||||
|
||||
let web3ForeignArchive = null
|
||||
@ -98,15 +94,6 @@ if (foreignUrls.length > 1) {
|
||||
web3ForeignRedundant = new Web3(redundantProvider)
|
||||
}
|
||||
|
||||
let getFlashbotsProvider
|
||||
if (ORACLE_MEV_FOREIGN_FLASHBOTS_RPC_URL) {
|
||||
const provider = new ethers.providers.JsonRpcProvider(foreignUrls[0])
|
||||
const authSigner = new ethers.Wallet(ORACLE_MEV_FOREIGN_FLASHBOTS_AUTH_SIGNING_KEY, provider)
|
||||
|
||||
getFlashbotsProvider = chainId =>
|
||||
flashbots.FlashbotsBundleProvider.create(provider, authSigner, ORACLE_MEV_FOREIGN_FLASHBOTS_RPC_URL, chainId)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
web3Home,
|
||||
web3Foreign,
|
||||
@ -115,6 +102,5 @@ module.exports = {
|
||||
web3HomeRedundant,
|
||||
web3ForeignRedundant,
|
||||
web3HomeFallback,
|
||||
web3ForeignFallback,
|
||||
getFlashbotsProvider
|
||||
web3ForeignFallback
|
||||
}
|
||||
|
@ -1,38 +1,25 @@
|
||||
async function sendTx(opts) {
|
||||
const { privateKey, data, nonce, gasPrice, gasPriceOptions, value, gasLimit, to, chainId, web3, mevOptions } = opts
|
||||
const gasOpts = gasPriceOptions || { gasPrice }
|
||||
const { toWei } = require('web3').utils
|
||||
|
||||
async function sendTx({ privateKey, data, nonce, gasPrice, amount, gasLimit, to, chainId, web3 }) {
|
||||
const serializedTx = await web3.eth.accounts.signTransaction(
|
||||
{
|
||||
nonce: Number(nonce),
|
||||
chainId,
|
||||
to,
|
||||
data,
|
||||
value,
|
||||
gas: gasLimit,
|
||||
...gasOpts
|
||||
value: toWei(amount),
|
||||
gasPrice,
|
||||
gas: gasLimit
|
||||
},
|
||||
privateKey
|
||||
)
|
||||
|
||||
if (!mevOptions) {
|
||||
return new Promise((res, rej) =>
|
||||
web3.eth
|
||||
.sendSignedTransaction(serializedTx.rawTransaction)
|
||||
.once('transactionHash', res)
|
||||
.once('error', rej)
|
||||
)
|
||||
}
|
||||
|
||||
mevOptions.logger.debug(
|
||||
{ rawTx: serializedTx.rawTransaction, txHash: serializedTx.transactionHash },
|
||||
'Signed MEV helper transaction'
|
||||
return new Promise((res, rej) =>
|
||||
web3.eth
|
||||
.sendSignedTransaction(serializedTx.rawTransaction)
|
||||
.once('transactionHash', res)
|
||||
.once('error', rej)
|
||||
)
|
||||
|
||||
for (let blockNumber = mevOptions.fromBlock; blockNumber <= mevOptions.toBlock; blockNumber++) {
|
||||
mevOptions.logger.debug({ txHash: serializedTx.transactionHash, blockNumber }, 'Sending MEV bundle transaction')
|
||||
await mevOptions.provider.sendRawBundle([serializedTx.rawTransaction], blockNumber)
|
||||
}
|
||||
return Promise.resolve(serializedTx.transactionHash)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
@ -87,7 +87,7 @@ async function getEvents({ contract, event, fromBlock, toBlock, filter }) {
|
||||
)
|
||||
const pastEvents = await contract.getPastEvents(event, { fromBlock, toBlock, filter })
|
||||
logger.debug({ contractAddress, event, count: pastEvents.length }, 'Past events obtained')
|
||||
return pastEvents.sort((a, b) => a.blockNumber - b.blockNumber || a.transactionIndex - b.transactionIndex)
|
||||
return pastEvents
|
||||
} catch (e) {
|
||||
logger.error(e.message)
|
||||
throw new Error(`${event} events cannot be obtained`)
|
||||
|
@ -1,11 +1,10 @@
|
||||
module.exports = {
|
||||
EXTRA_GAS_PERCENTAGE: 4,
|
||||
EXTRA_GAS_ABSOLUTE: 250000,
|
||||
EXTRA_GAS_ABSOLUTE: 200000,
|
||||
AMB_AFFIRMATION_REQUEST_EXTRA_GAS_ESTIMATOR: len => Math.floor(0.0035 * len ** 2 + 40 * len),
|
||||
MIN_AMB_HEADER_LENGTH: 32 + 20 + 20 + 4 + 2 + 1 + 2,
|
||||
MAX_GAS_LIMIT: 10000000,
|
||||
MAX_CONCURRENT_EVENTS: 50,
|
||||
MAX_HISTORY_BLOCK_TO_REPROCESS: 10000,
|
||||
RETRY_CONFIG: {
|
||||
retries: 20,
|
||||
factor: 1.4,
|
||||
@ -24,22 +23,9 @@ module.exports = {
|
||||
MIN: 1,
|
||||
MAX: 1000
|
||||
},
|
||||
MIN_GAS_PRICE_BUMP_FACTOR: 0.1,
|
||||
DEFAULT_TRANSACTION_RESEND_INTERVAL: 20 * 60 * 1000,
|
||||
FALLBACK_RPC_URL_SWITCH_TIMEOUT: 60 * 60 * 1000,
|
||||
SENDER_QUEUE_MAX_PRIORITY: 10,
|
||||
SENDER_QUEUE_SEND_PRIORITY: 5,
|
||||
SENDER_QUEUE_CHECK_STATUS_PRIORITY: 1,
|
||||
ASYNC_CALL_ERRORS: {
|
||||
// requested transaction/block/receipt does not exist
|
||||
NOT_FOUND: '0x0000000000000000000000000000000000000000000000000000000000000000',
|
||||
// requested custom block does not exist yet or its timestamp is greater than the home block timestamp
|
||||
BLOCK_IS_IN_THE_FUTURE: '0x0000000000000000000000000000000000000000000000000000000000000001',
|
||||
// eth_call has reverted or finished with OOG error
|
||||
REVERT: '0x0000000000000000000000000000000000000000000000000000000000000002',
|
||||
// evaluated output length exceeds allowed length of 64 KB
|
||||
RESULT_IS_TOO_LONG: '0x0000000000000000000000000000000000000000000000000000000000000003'
|
||||
},
|
||||
MAX_ASYNC_CALL_RESULT_LENGTH: 64 * 1024,
|
||||
ASYNC_ETH_CALL_MAX_GAS_LIMIT: 100000000
|
||||
SENDER_QUEUE_CHECK_STATUS_PRIORITY: 1
|
||||
}
|
||||
|
@ -1,43 +0,0 @@
|
||||
const MEV_HELPER_ABI = [
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: '_data',
|
||||
type: 'bytes'
|
||||
}
|
||||
],
|
||||
name: 'execute',
|
||||
outputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: '_gasPrice',
|
||||
type: 'uint256'
|
||||
},
|
||||
{
|
||||
name: '_data',
|
||||
type: 'bytes'
|
||||
}
|
||||
],
|
||||
name: 'estimateProfit',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
payable: true,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
}
|
||||
]
|
||||
|
||||
module.exports = {
|
||||
MEV_HELPER_ABI
|
||||
}
|
@ -2,9 +2,6 @@ const fs = require('fs')
|
||||
const BigNumber = require('bignumber.js')
|
||||
const promiseRetry = require('promise-retry')
|
||||
const Web3 = require('web3')
|
||||
const { GAS_PRICE_BOUNDARIES } = require('./constants')
|
||||
|
||||
const { toBN, toWei } = Web3.utils
|
||||
|
||||
const retrySequence = [1, 2, 3, 5, 8, 13, 21, 34, 55, 60]
|
||||
|
||||
@ -37,8 +34,8 @@ const promiseRetryForever = f => promiseRetry(f, { forever: true, factor: 1 })
|
||||
async function waitForFunds(web3, address, minimumBalance, cb, logger) {
|
||||
promiseRetryForever(async retry => {
|
||||
logger.debug('Getting balance of validator account')
|
||||
const newBalance = toBN(await web3.eth.getBalance(address))
|
||||
if (newBalance.gte(toBN(minimumBalance.toString(10)))) {
|
||||
const newBalance = web3.utils.toBN(await web3.eth.getBalance(address))
|
||||
if (newBalance.gte(web3.utils.toBN(minimumBalance.toString(10)))) {
|
||||
logger.debug({ balance: newBalance, minimumBalance }, 'Validator has minimum necessary balance')
|
||||
cb(newBalance)
|
||||
} else {
|
||||
@ -67,48 +64,6 @@ function addExtraGas(gas, extraPercentage, maxGasLimit = Infinity) {
|
||||
return BigNumber.min(maxGasLimit, gasWithExtra)
|
||||
}
|
||||
|
||||
function applyMinGasFeeBump(job, bumpFactor = 0.1) {
|
||||
if (!job.gasPriceOptions) {
|
||||
return job
|
||||
}
|
||||
const { gasPrice, maxFeePerGas, maxPriorityFeePerGas } = job.gasPriceOptions
|
||||
const maxGasPrice = toWei(GAS_PRICE_BOUNDARIES.MAX.toString(), 'gwei')
|
||||
if (gasPrice) {
|
||||
return {
|
||||
...job,
|
||||
gasPriceOptions: {
|
||||
gasPrice: addExtraGas(gasPrice, bumpFactor, maxGasPrice).toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (maxFeePerGas && maxPriorityFeePerGas) {
|
||||
return {
|
||||
...job,
|
||||
gasPriceOptions: {
|
||||
maxFeePerGas: addExtraGas(maxFeePerGas, bumpFactor, maxGasPrice).toString(),
|
||||
maxPriorityFeePerGas: addExtraGas(maxPriorityFeePerGas, bumpFactor, maxGasPrice).toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
return job
|
||||
}
|
||||
|
||||
function chooseGasPriceOptions(a, b) {
|
||||
if (!a) {
|
||||
return b
|
||||
}
|
||||
if (a && b && a.gasPrice && b.gasPrice) {
|
||||
return { gasPrice: BigNumber.max(a.gasPrice, b.gasPrice).toString() }
|
||||
}
|
||||
if (a && b && a.maxFeePerGas && b.maxFeePerGas && a.maxPriorityFeePerGas && b.maxPriorityFeePerGas) {
|
||||
return {
|
||||
maxFeePerGas: BigNumber.max(a.maxFeePerGas, b.maxFeePerGas).toString(),
|
||||
maxPriorityFeePerGas: BigNumber.max(a.maxPriorityFeePerGas, b.maxPriorityFeePerGas).toString()
|
||||
}
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
async function setIntervalAndRun(f, interval) {
|
||||
const handler = setInterval(f, interval)
|
||||
await f()
|
||||
@ -151,11 +106,7 @@ function isGasPriceError(e) {
|
||||
|
||||
function isSameTransactionError(e) {
|
||||
const message = e.message.toLowerCase()
|
||||
return (
|
||||
message.includes('transaction with the same hash was already imported') ||
|
||||
message.includes('already known') ||
|
||||
message.includes('alreadyknown')
|
||||
)
|
||||
return message.includes('transaction with the same hash was already imported') || message.includes('already known')
|
||||
}
|
||||
|
||||
function isInsufficientBalanceError(e) {
|
||||
@ -168,21 +119,7 @@ function isNonceError(e) {
|
||||
return (
|
||||
message.includes('transaction nonce is too low') ||
|
||||
message.includes('nonce too low') ||
|
||||
message.includes('transaction with same nonce in the queue') ||
|
||||
message.includes('oldnonce') ||
|
||||
message.includes(`the tx doesn't have the correct nonce`)
|
||||
)
|
||||
}
|
||||
|
||||
function isRevertError(e) {
|
||||
const message = e.message.toLowerCase()
|
||||
// OE and NE returns "VM execution error"/"Transaction execution error"
|
||||
// Geth returns "out of gas"/"intrinsic gas too low"/"execution reverted"
|
||||
return (
|
||||
message.includes('execution error') ||
|
||||
message.includes('intrinsic gas too low') ||
|
||||
message.includes('out of gas') ||
|
||||
message.includes('execution reverted')
|
||||
message.includes('transaction with same nonce in the queue')
|
||||
)
|
||||
}
|
||||
|
||||
@ -229,8 +166,6 @@ module.exports = {
|
||||
waitForFunds,
|
||||
waitForUnsuspend,
|
||||
addExtraGas,
|
||||
chooseGasPriceOptions,
|
||||
applyMinGasFeeBump,
|
||||
setIntervalAndRun,
|
||||
watchdog,
|
||||
add0xPrefix,
|
||||
@ -239,7 +174,6 @@ module.exports = {
|
||||
isSameTransactionError,
|
||||
isInsufficientBalanceError,
|
||||
isNonceError,
|
||||
isRevertError,
|
||||
getRetrySequence,
|
||||
promiseAny,
|
||||
readAccessListFile,
|
||||
|
@ -6,7 +6,7 @@ const logger = require('./services/logger')
|
||||
const { getShutdownFlag } = require('./services/shutdownState')
|
||||
const { getBlockNumber, getRequiredBlockConfirmations, getEvents } = require('./tx/web3')
|
||||
const { checkHTTPS, watchdog } = require('./utils/utils')
|
||||
const { EXIT_CODES, MAX_HISTORY_BLOCK_TO_REPROCESS } = require('./utils/constants')
|
||||
const { EXIT_CODES } = require('./utils/constants')
|
||||
|
||||
if (process.argv.length < 3) {
|
||||
logger.error('Please check the number of arguments, config file was not provided')
|
||||
@ -26,33 +26,17 @@ const processAMBInformationRequests = require('./events/processAMBInformationReq
|
||||
|
||||
const { getTokensState } = require('./utils/tokenState')
|
||||
|
||||
const {
|
||||
web3,
|
||||
bridgeContract,
|
||||
eventContract,
|
||||
startBlock,
|
||||
pollingInterval,
|
||||
chain,
|
||||
reprocessingOptions,
|
||||
blockPollingLimit,
|
||||
syncCheckInterval
|
||||
} = config.main
|
||||
const { web3, bridgeContract, eventContract, startBlock, pollingInterval, chain } = config.main
|
||||
const lastBlockRedisKey = `${config.id}:lastProcessedBlock`
|
||||
const lastReprocessedBlockRedisKey = `${config.id}:lastReprocessedBlock`
|
||||
const seenEventsRedisKey = `${config.id}:seenEvents`
|
||||
let lastProcessedBlock = Math.max(startBlock - 1, 0)
|
||||
let lastReprocessedBlock
|
||||
|
||||
async function initialize() {
|
||||
try {
|
||||
const checkHttps = checkHTTPS(process.env.ORACLE_ALLOW_HTTP_FOR_RPC, logger)
|
||||
|
||||
web3.currentProvider.urls.forEach(checkHttps(chain))
|
||||
web3.currentProvider.startSyncStateChecker(syncCheckInterval)
|
||||
web3.currentProvider.subProvider.urls.forEach(checkHttps(chain))
|
||||
|
||||
await getLastProcessedBlock()
|
||||
await getLastReprocessedBlock()
|
||||
await checkConditions()
|
||||
connectWatcherToQueue({
|
||||
queueName: config.queue,
|
||||
cb: runMain
|
||||
@ -90,34 +74,11 @@ async function getLastProcessedBlock() {
|
||||
lastProcessedBlock = result ? parseInt(result, 10) : lastProcessedBlock
|
||||
}
|
||||
|
||||
async function getLastReprocessedBlock() {
|
||||
if (reprocessingOptions.enabled) {
|
||||
const result = await redis.get(lastReprocessedBlockRedisKey)
|
||||
if (result) {
|
||||
lastReprocessedBlock = Math.max(parseInt(result, 10), lastProcessedBlock - MAX_HISTORY_BLOCK_TO_REPROCESS)
|
||||
} else {
|
||||
lastReprocessedBlock = lastProcessedBlock
|
||||
}
|
||||
logger.debug({ block: lastReprocessedBlock }, 'Last reprocessed block obtained')
|
||||
} else {
|
||||
// when reprocessing is being enabled not for the first time,
|
||||
// we do not want to process blocks for which we didn't recorded seen events,
|
||||
// instead, we want to start from the current block.
|
||||
// Thus we should delete this reprocessing pointer once it is disabled.
|
||||
await redis.del(lastReprocessedBlockRedisKey)
|
||||
}
|
||||
}
|
||||
|
||||
function updateLastProcessedBlock(lastBlockNumber) {
|
||||
lastProcessedBlock = lastBlockNumber
|
||||
return redis.set(lastBlockRedisKey, lastProcessedBlock)
|
||||
}
|
||||
|
||||
function updateLastReprocessedBlock(lastBlockNumber) {
|
||||
lastReprocessedBlock = lastBlockNumber
|
||||
return redis.set(lastReprocessedBlockRedisKey, lastReprocessedBlock)
|
||||
}
|
||||
|
||||
function processEvents(events) {
|
||||
switch (config.id) {
|
||||
case 'erc-native-signature-request':
|
||||
@ -151,71 +112,6 @@ async function checkConditions() {
|
||||
}
|
||||
}
|
||||
|
||||
const eventKey = e => `${e.transactionHash}-${e.logIndex}`
|
||||
|
||||
async function reprocessOldLogs(sendToQueue) {
|
||||
const fromBlock = lastReprocessedBlock + 1
|
||||
let toBlock = lastReprocessedBlock + reprocessingOptions.batchSize
|
||||
const events = await getEvents({
|
||||
contract: eventContract,
|
||||
event: config.event,
|
||||
fromBlock,
|
||||
toBlock,
|
||||
filter: config.eventFilter
|
||||
})
|
||||
const alreadySeenEvents = await getSeenEvents(fromBlock, toBlock)
|
||||
const missingEvents = events.filter(e => !alreadySeenEvents[eventKey(e)])
|
||||
if (missingEvents.length === 0) {
|
||||
logger.debug('No missed events were found')
|
||||
} else {
|
||||
logger.info(`Found ${missingEvents.length} ${config.event} missed events`)
|
||||
let job
|
||||
if (config.id === 'amb-information-request') {
|
||||
// obtain block number and events from the earliest block
|
||||
const batchBlockNumber = missingEvents[0].blockNumber
|
||||
const batchEvents = missingEvents.filter(event => event.blockNumber === batchBlockNumber)
|
||||
|
||||
// if there are some other events in the later blocks,
|
||||
// adjust lastReprocessedBlock so that these events will be processed again on the next iteration
|
||||
if (batchEvents.length < missingEvents.length) {
|
||||
// pick event outside from the batch
|
||||
toBlock = missingEvents[batchEvents.length].blockNumber - 1
|
||||
}
|
||||
|
||||
job = await processAMBInformationRequests(batchEvents)
|
||||
if (job === null) {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
job = await processEvents(missingEvents)
|
||||
}
|
||||
logger.info('Missed events transactions to send:', job.length)
|
||||
if (job.length) {
|
||||
await sendToQueue(job)
|
||||
}
|
||||
}
|
||||
|
||||
await updateLastReprocessedBlock(toBlock)
|
||||
await deleteSeenEvents(0, toBlock)
|
||||
}
|
||||
|
||||
async function getSeenEvents(fromBlock, toBlock) {
|
||||
const keys = await redis.zrangebyscore(seenEventsRedisKey, fromBlock, toBlock)
|
||||
const res = {}
|
||||
keys.forEach(k => {
|
||||
res[k] = true
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
function deleteSeenEvents(fromBlock, toBlock) {
|
||||
return redis.zremrangebyscore(seenEventsRedisKey, fromBlock, toBlock)
|
||||
}
|
||||
|
||||
function addSeenEvents(events) {
|
||||
return redis.zadd(seenEventsRedisKey, ...events.flatMap(e => [e.blockNumber, eventKey(e)]))
|
||||
}
|
||||
|
||||
async function getLastBlockToProcess(web3, bridgeContract) {
|
||||
const [lastBlockNumber, requiredBlockConfirmations] = await Promise.all([
|
||||
getBlockNumber(web3),
|
||||
@ -236,14 +132,9 @@ async function main({ sendToQueue }) {
|
||||
logger.info(`Oracle watcher was unsuspended.`)
|
||||
}
|
||||
|
||||
const lastBlockToProcess = await getLastBlockToProcess(web3, bridgeContract)
|
||||
await checkConditions()
|
||||
|
||||
if (reprocessingOptions.enabled) {
|
||||
if (lastReprocessedBlock + reprocessingOptions.batchSize + reprocessingOptions.blockDelay < lastBlockToProcess) {
|
||||
await reprocessOldLogs(sendToQueue)
|
||||
return
|
||||
}
|
||||
}
|
||||
const lastBlockToProcess = await getLastBlockToProcess(web3, bridgeContract)
|
||||
|
||||
if (lastBlockToProcess <= lastProcessedBlock) {
|
||||
logger.debug('All blocks already processed')
|
||||
@ -251,16 +142,16 @@ async function main({ sendToQueue }) {
|
||||
}
|
||||
|
||||
const fromBlock = lastProcessedBlock + 1
|
||||
const rangeEndBlock = blockPollingLimit ? fromBlock + blockPollingLimit : lastBlockToProcess
|
||||
const rangeEndBlock = config.blockPollingLimit ? fromBlock + config.blockPollingLimit : lastBlockToProcess
|
||||
let toBlock = Math.min(lastBlockToProcess, rangeEndBlock)
|
||||
|
||||
let events = await getEvents({
|
||||
const events = (await getEvents({
|
||||
contract: eventContract,
|
||||
event: config.event,
|
||||
fromBlock,
|
||||
toBlock,
|
||||
filter: config.eventFilter
|
||||
})
|
||||
})).sort((a, b) => a.blockNumber - b.blockNumber)
|
||||
logger.info(`Found ${events.length} ${config.event} events`)
|
||||
|
||||
if (events.length) {
|
||||
@ -277,10 +168,9 @@ async function main({ sendToQueue }) {
|
||||
if (batchEvents.length < events.length) {
|
||||
// pick event outside from the batch
|
||||
toBlock = events[batchEvents.length].blockNumber - 1
|
||||
events = batchEvents
|
||||
}
|
||||
|
||||
job = await processAMBInformationRequests(events)
|
||||
job = await processAMBInformationRequests(batchEvents)
|
||||
if (job === null) {
|
||||
return
|
||||
}
|
||||
@ -292,9 +182,6 @@ async function main({ sendToQueue }) {
|
||||
if (job.length) {
|
||||
await sendToQueue(job)
|
||||
}
|
||||
if (reprocessingOptions.enabled) {
|
||||
await addSeenEvents(events)
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug({ lastProcessedBlock: toBlock.toString() }, 'Updating last processed block')
|
||||
|
@ -71,10 +71,10 @@ describe('gasPrice', () => {
|
||||
await gasPrice.start('home')
|
||||
|
||||
// when
|
||||
await gasPrice.fetchGasPrice('standard', 1, null, null, null)
|
||||
await gasPrice.fetchGasPrice('standard', 1, null, null)
|
||||
|
||||
// then
|
||||
expect(gasPrice.gasPriceOptions()).to.eql({ gasPrice: '101000000000' })
|
||||
expect(gasPrice.getPrice()).to.equal('101000000000')
|
||||
})
|
||||
|
||||
it('should fetch gas from supplier', async () => {
|
||||
@ -82,10 +82,10 @@ describe('gasPrice', () => {
|
||||
await gasPrice.start('home')
|
||||
|
||||
// when
|
||||
await gasPrice.fetchGasPrice('standard', 1, null, null, 'url')
|
||||
await gasPrice.fetchGasPrice('standard', 1, null, 'url')
|
||||
|
||||
// then
|
||||
expect(gasPrice.gasPriceOptions()).to.eql({ gasPrice: '103000000000' })
|
||||
expect(gasPrice.getPrice().toString()).to.equal('103000000000')
|
||||
})
|
||||
|
||||
it('should fetch gas from contract', async () => {
|
||||
@ -101,10 +101,10 @@ describe('gasPrice', () => {
|
||||
}
|
||||
|
||||
// when
|
||||
await gasPrice.fetchGasPrice('standard', 1, null, bridgeContractMock, null)
|
||||
await gasPrice.fetchGasPrice('standard', 1, bridgeContractMock, null)
|
||||
|
||||
// then
|
||||
expect(gasPrice.gasPriceOptions()).to.eql({ gasPrice: '102000000000' })
|
||||
expect(gasPrice.getPrice().toString()).to.equal('102000000000')
|
||||
})
|
||||
|
||||
it('should fetch the gas price from the oracle first', async () => {
|
||||
@ -120,10 +120,10 @@ describe('gasPrice', () => {
|
||||
}
|
||||
|
||||
// when
|
||||
await gasPrice.fetchGasPrice('standard', 1, null, bridgeContractMock, 'url')
|
||||
await gasPrice.fetchGasPrice('standard', 1, bridgeContractMock, 'url')
|
||||
|
||||
// then
|
||||
expect(gasPrice.gasPriceOptions()).to.eql({ gasPrice: '103000000000' })
|
||||
expect(gasPrice.getPrice().toString()).to.equal('103000000000')
|
||||
})
|
||||
|
||||
it('log error using the logger', async () => {
|
||||
@ -131,7 +131,7 @@ describe('gasPrice', () => {
|
||||
await gasPrice.start('home')
|
||||
|
||||
// when
|
||||
await gasPrice.fetchGasPrice('standard', 1, null, null, null)
|
||||
await gasPrice.fetchGasPrice('standard', 1, null, null)
|
||||
|
||||
// then
|
||||
expect(fakeLogger.warn.calledOnce).to.equal(true) // one warning
|
||||
|
@ -3,13 +3,7 @@ const chai = require('chai')
|
||||
const chaiAsPromised = require('chai-as-promised')
|
||||
const BigNumber = require('bignumber.js')
|
||||
const proxyquire = require('proxyquire')
|
||||
const {
|
||||
addExtraGas,
|
||||
applyMinGasFeeBump,
|
||||
chooseGasPriceOptions,
|
||||
syncForEach,
|
||||
promiseAny
|
||||
} = require('../src/utils/utils')
|
||||
const { addExtraGas, syncForEach, promiseAny } = require('../src/utils/utils')
|
||||
|
||||
chai.use(chaiAsPromised)
|
||||
chai.should()
|
||||
@ -179,43 +173,4 @@ describe('utils', () => {
|
||||
await promiseAny(array.map(f)).should.be.rejected
|
||||
})
|
||||
})
|
||||
|
||||
describe('applyMinGasFeeBump', () => {
|
||||
it('should bump pre-eip1559 fee', () => {
|
||||
const job = { gasPriceOptions: { gasPrice: '100000000000' } }
|
||||
const newJob = applyMinGasFeeBump(job)
|
||||
expect(newJob.gasPriceOptions.gasPrice).to.be.equal('110000000000')
|
||||
})
|
||||
|
||||
it('should bump eip1559 fee', () => {
|
||||
const job = { gasPriceOptions: { maxFeePerGas: '100000000000', maxPriorityFeePerGas: '20000000000' } }
|
||||
const newJob = applyMinGasFeeBump(job)
|
||||
expect(newJob.gasPriceOptions.maxFeePerGas).to.be.equal('110000000000')
|
||||
expect(newJob.gasPriceOptions.maxPriorityFeePerGas).to.be.equal('22000000000')
|
||||
})
|
||||
})
|
||||
|
||||
describe('chooseGasPriceOptions', () => {
|
||||
it('should choose max pre-eip1559 fee', () => {
|
||||
const opts1 = { gasPrice: '100000000000' }
|
||||
const opts2 = { gasPrice: '101000000000' }
|
||||
expect(chooseGasPriceOptions(opts1, opts2).gasPrice).to.be.equal('101000000000')
|
||||
expect(chooseGasPriceOptions(opts2, opts1).gasPrice).to.be.equal('101000000000')
|
||||
expect(chooseGasPriceOptions(opts2, undefined).gasPrice).to.be.equal('101000000000')
|
||||
expect(chooseGasPriceOptions(undefined, opts2).gasPrice).to.be.equal('101000000000')
|
||||
})
|
||||
|
||||
it('should choose max eip1559 fee', () => {
|
||||
const opts1 = { maxFeePerGas: '100000000000', maxPriorityFeePerGas: '21000000000' }
|
||||
const opts2 = { maxFeePerGas: '101000000000', maxPriorityFeePerGas: '20000000000' }
|
||||
expect(chooseGasPriceOptions(opts1, opts2).maxFeePerGas).to.be.equal('101000000000')
|
||||
expect(chooseGasPriceOptions(opts1, opts2).maxPriorityFeePerGas).to.be.equal('21000000000')
|
||||
expect(chooseGasPriceOptions(opts2, opts1).maxFeePerGas).to.be.equal('101000000000')
|
||||
expect(chooseGasPriceOptions(opts2, opts1).maxPriorityFeePerGas).to.be.equal('21000000000')
|
||||
expect(chooseGasPriceOptions(opts2, undefined).maxFeePerGas).to.be.equal('101000000000')
|
||||
expect(chooseGasPriceOptions(opts2, undefined).maxPriorityFeePerGas).to.be.equal('20000000000')
|
||||
expect(chooseGasPriceOptions(undefined, opts2).maxFeePerGas).to.be.equal('101000000000')
|
||||
expect(chooseGasPriceOptions(undefined, opts2).maxPriorityFeePerGas).to.be.equal('20000000000')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -45,8 +45,5 @@
|
||||
"compile:contracts": "yarn workspace tokenbridge-contracts run compile",
|
||||
"install:deploy": "cd contracts/deploy && npm install --unsafe-perm --silent",
|
||||
"postinstall": "test -n \"$NOYARNPOSTINSTALL\" || ln -sf $(pwd)/node_modules/openzeppelin-solidity/ contracts/node_modules/openzeppelin-solidity"
|
||||
},
|
||||
"resolutions": {
|
||||
"**/@mycrypto/eth-scan": "3.5.3"
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
@ -1,33 +0,0 @@
|
||||
pragma solidity 0.4.24;
|
||||
|
||||
interface IERC20 {
|
||||
function transferFrom(address from,address to,uint256 value) external;
|
||||
function transfer(address to,uint256 value) external;
|
||||
}
|
||||
|
||||
contract cDaiMock {
|
||||
IERC20 daiToken;
|
||||
|
||||
event Transfer(address indexed from, address indexed to, uint amount);
|
||||
event Mint(address minter, uint mintAmount, uint mintTokens);
|
||||
event Redeem(address redeemer, uint redeemAmount, uint redeemTokens);
|
||||
|
||||
|
||||
function mint(uint256 mintAmount) external returns (uint256) {
|
||||
daiToken.transferFrom(msg.sender, address(this), mintAmount);
|
||||
|
||||
emit Mint(msg.sender, mintAmount, mintAmount);
|
||||
emit Transfer(address(this), msg.sender, mintAmount);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
function redeemUnderlying(uint256 redeemAmount) external returns (uint256) {
|
||||
daiToken.transfer(msg.sender, redeemAmount);
|
||||
|
||||
emit Transfer(msg.sender, address(this), redeemAmount);
|
||||
emit Redeem(msg.sender, redeemAmount, redeemAmount);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user