Add Alm project structure and transaction form (#353)
This commit is contained in:
parent
8e10a5d609
commit
bcdf691000
@ -3,3 +3,9 @@ COMMON_FOREIGN_BRIDGE_ADDRESS=0xFe446bEF1DbF7AFE24E81e05BC8B271C1BA9a560
|
|||||||
|
|
||||||
COMMON_HOME_RPC_URL=https://sokol.poa.network
|
COMMON_HOME_RPC_URL=https://sokol.poa.network
|
||||||
COMMON_FOREIGN_RPC_URL=https://kovan.infura.io/v3/
|
COMMON_FOREIGN_RPC_URL=https://kovan.infura.io/v3/
|
||||||
|
|
||||||
|
ALM_HOME_NETWORK_NAME=Sokol Testnet
|
||||||
|
ALM_FOREIGN_NETWORK_NAME=Kovan Testnet
|
||||||
|
|
||||||
|
ALM_HOME_EXPLORER_TX_TEMPLATE=https://blockscout.com/poa/sokol/tx/%s
|
||||||
|
ALM_FOREIGN_EXPLORER_TX_TEMPLATE=https://blockscout.com/eth/kovan/tx/%s
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@react-hook/window-size": "^3.0.6",
|
||||||
"@testing-library/jest-dom": "^4.2.4",
|
"@testing-library/jest-dom": "^4.2.4",
|
||||||
"@testing-library/react": "^9.3.2",
|
"@testing-library/react": "^9.3.2",
|
||||||
"@testing-library/user-event": "^7.1.2",
|
"@testing-library/user-event": "^7.1.2",
|
||||||
@ -10,12 +11,19 @@
|
|||||||
"@types/node": "^12.0.0",
|
"@types/node": "^12.0.0",
|
||||||
"@types/react": "^16.9.0",
|
"@types/react": "^16.9.0",
|
||||||
"@types/react-dom": "^16.9.0",
|
"@types/react-dom": "^16.9.0",
|
||||||
|
"@types/react-router-dom": "^5.1.5",
|
||||||
|
"@types/styled-components": "^5.1.0",
|
||||||
"customize-cra": "^1.0.0",
|
"customize-cra": "^1.0.0",
|
||||||
|
"date-fns": "^2.14.0",
|
||||||
|
"fast-memoize": "^2.5.2",
|
||||||
"react": "^16.13.1",
|
"react": "^16.13.1",
|
||||||
"react-app-rewired": "^2.1.6",
|
"react-app-rewired": "^2.1.6",
|
||||||
"react-dom": "^16.13.1",
|
"react-dom": "^16.13.1",
|
||||||
|
"react-router-dom": "^5.2.0",
|
||||||
"react-scripts": "3.0.1",
|
"react-scripts": "3.0.1",
|
||||||
"typescript": "^3.5.2"
|
"styled-components": "^5.1.1",
|
||||||
|
"typescript": "^3.5.2",
|
||||||
|
"web3": "^1.2.8"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "./load-env.sh react-app-rewired start",
|
"start": "./load-env.sh react-app-rewired start",
|
||||||
|
@ -7,8 +7,9 @@
|
|||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="Web site created using create-react-app"
|
content="AMB Live Monitoring"
|
||||||
/>
|
/>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/chota@latest">
|
||||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||||
<!--
|
<!--
|
||||||
manifest.json provides metadata used when your web app is installed on a
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
Before Width: | Height: | Size: 9.4 KiB |
@ -1,21 +1,11 @@
|
|||||||
{
|
{
|
||||||
"short_name": "React App",
|
"short_name": "ALM",
|
||||||
"name": "Create React App Sample",
|
"name": "AMB Live Monitoring",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "favicon.ico",
|
"src": "favicon.ico",
|
||||||
"sizes": "64x64 32x32 24x24 16x16",
|
"sizes": "64x64 32x32 24x24 16x16",
|
||||||
"type": "image/x-icon"
|
"type": "image/x-icon"
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "logo192.png",
|
|
||||||
"type": "image/png",
|
|
||||||
"sizes": "192x192"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "logo512.png",
|
|
||||||
"type": "image/png",
|
|
||||||
"sizes": "512x512"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"start_url": ".",
|
"start_url": ".",
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
.App {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.App-header {
|
|
||||||
background-color: #282c34;
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: calc(10px + 2vmin);
|
|
||||||
color: white;
|
|
||||||
}
|
|
@ -1,13 +1,12 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import './App.css'
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
|
import { MainPage } from './components/MainPage'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<div className="App">
|
<BrowserRouter>
|
||||||
<header className="App-header">
|
<MainPage />
|
||||||
<p>AMB Live Monitoring</p>
|
</BrowserRouter>
|
||||||
</header>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
90
alm/src/components/Form.tsx
Normal file
90
alm/src/components/Form.tsx
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import React, { useState, FormEvent, useEffect } from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
import { FormSubmitParams } from './MainPage'
|
||||||
|
import { useStateProvider } from '../state/StateProvider'
|
||||||
|
import { useParams } from 'react-router-dom'
|
||||||
|
import { Button } from './commons/Button'
|
||||||
|
import { RadioButtonLabel, RadioButtonContainer } from './commons/RadioButton'
|
||||||
|
|
||||||
|
const LabelText = styled.label`
|
||||||
|
line-height: 36px;
|
||||||
|
max-width: 140px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Input = styled.input`
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
color: var(--font-color);
|
||||||
|
`
|
||||||
|
|
||||||
|
export const Form = ({ onSubmit }: { onSubmit: ({ chainId, txHash }: FormSubmitParams) => void }) => {
|
||||||
|
const { home, foreign, loading } = useStateProvider()
|
||||||
|
const { chainId: paramChainId, txHash: paramTxHash } = useParams()
|
||||||
|
const [chainId, setChainId] = useState(0)
|
||||||
|
const [txHash, setTxHash] = useState('')
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => {
|
||||||
|
if (!paramChainId) {
|
||||||
|
setChainId(foreign.chainId)
|
||||||
|
} else {
|
||||||
|
setChainId(parseInt(paramChainId))
|
||||||
|
setTxHash(paramTxHash)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[foreign.chainId, paramChainId, paramTxHash]
|
||||||
|
)
|
||||||
|
|
||||||
|
const formSubmit = (e: FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
onSubmit({ chainId, txHash })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={formSubmit}>
|
||||||
|
<div className="row is-center">
|
||||||
|
<LabelText className="col-2">Bridgeable tx hash:</LabelText>
|
||||||
|
<div className="col-7">
|
||||||
|
<Input
|
||||||
|
placeholder="Enter transaction hash"
|
||||||
|
type="text"
|
||||||
|
onChange={e => setTxHash(e.target.value)}
|
||||||
|
required
|
||||||
|
pattern="^0x[a-fA-F0-9]{64}$"
|
||||||
|
value={txHash}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-1">
|
||||||
|
<Button className="button dark" type="submit">
|
||||||
|
Check
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!loading && (
|
||||||
|
<div className="row is-center">
|
||||||
|
<RadioButtonContainer className="is-vertical-align" onClick={() => setChainId(foreign.chainId)}>
|
||||||
|
<input
|
||||||
|
className="is-marginless"
|
||||||
|
type="radio"
|
||||||
|
name="network"
|
||||||
|
value={foreign.name}
|
||||||
|
checked={chainId === foreign.chainId}
|
||||||
|
onChange={() => setChainId(foreign.chainId)}
|
||||||
|
/>
|
||||||
|
<RadioButtonLabel htmlFor={foreign.name}>{foreign.name}</RadioButtonLabel>
|
||||||
|
</RadioButtonContainer>
|
||||||
|
<RadioButtonContainer className="is-vertical-align" onClick={() => setChainId(home.chainId)}>
|
||||||
|
<input
|
||||||
|
className="is-marginless"
|
||||||
|
type="radio"
|
||||||
|
name="network"
|
||||||
|
value={home.name}
|
||||||
|
checked={chainId === home.chainId}
|
||||||
|
onChange={() => setChainId(home.chainId)}
|
||||||
|
/>
|
||||||
|
<RadioButtonLabel htmlFor={home.name}>{home.name}</RadioButtonLabel>
|
||||||
|
</RadioButtonContainer>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
48
alm/src/components/MainPage.tsx
Normal file
48
alm/src/components/MainPage.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
import { Route, useHistory } from 'react-router-dom'
|
||||||
|
import { Form } from './Form'
|
||||||
|
import { StatusContainer } from './StatusContainer'
|
||||||
|
import { StateProvider } from '../state/StateProvider'
|
||||||
|
|
||||||
|
const StyledMainPage = styled.div`
|
||||||
|
text-align: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Header = styled.header`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: calc(10px + 2vmin);
|
||||||
|
`
|
||||||
|
|
||||||
|
export interface FormSubmitParams {
|
||||||
|
chainId: number
|
||||||
|
txHash: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MainPage = () => {
|
||||||
|
const history = useHistory()
|
||||||
|
const onFormSubmit = ({ chainId, txHash }: FormSubmitParams) => {
|
||||||
|
history.push(`/${chainId}/${txHash}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StateProvider>
|
||||||
|
<StyledMainPage>
|
||||||
|
<Header>
|
||||||
|
<p>AMB Live Monitoring</p>
|
||||||
|
</Header>
|
||||||
|
<div className="container">
|
||||||
|
<Route
|
||||||
|
path={['/:chainId/:txHash/:messageIdParam', '/:chainId/:txHash', '/']}
|
||||||
|
children={<Form onSubmit={onFormSubmit} />}
|
||||||
|
/>
|
||||||
|
<Route path={['/:chainId/:txHash/:messageIdParam', '/:chainId/:txHash']} children={<StatusContainer />} />
|
||||||
|
</div>
|
||||||
|
</StyledMainPage>
|
||||||
|
</StateProvider>
|
||||||
|
)
|
||||||
|
}
|
46
alm/src/components/MessageSelector.tsx
Normal file
46
alm/src/components/MessageSelector.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import React, { useState } from 'react'
|
||||||
|
import { Button } from './commons/Button'
|
||||||
|
import { RadioButtonLabel, RadioButtonContainer } from './commons/RadioButton'
|
||||||
|
import { useWindowWidth } from '@react-hook/window-size'
|
||||||
|
import { formatTxHashExtended } from '../utils/networks'
|
||||||
|
|
||||||
|
export interface MessageSelectorParams {
|
||||||
|
messages: Array<string>
|
||||||
|
onMessageSelected: (index: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MessageSelector = ({ messages, onMessageSelected }: MessageSelectorParams) => {
|
||||||
|
const [messageIndex, setMessageIndex] = useState(0)
|
||||||
|
const windowWidth = useWindowWidth()
|
||||||
|
|
||||||
|
const onSelect = () => {
|
||||||
|
onMessageSelected(messageIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="row is-center">
|
||||||
|
<div className="col-7-lg col-12 is-marginless">
|
||||||
|
{messages.map((message, i) => (
|
||||||
|
<RadioButtonContainer className="row is-center is-vertical-align" key={i} onClick={() => setMessageIndex(i)}>
|
||||||
|
<input
|
||||||
|
className="is-marginless"
|
||||||
|
type="radio"
|
||||||
|
name="message"
|
||||||
|
value={i}
|
||||||
|
checked={i === messageIndex}
|
||||||
|
onChange={() => setMessageIndex(i)}
|
||||||
|
/>
|
||||||
|
<RadioButtonLabel htmlFor={i.toString()}>
|
||||||
|
{windowWidth < 700 ? formatTxHashExtended(message) : message}
|
||||||
|
</RadioButtonLabel>
|
||||||
|
</RadioButtonContainer>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="col-1-lg col-12 is-marginless">
|
||||||
|
<Button className="button dark" onClick={onSelect}>
|
||||||
|
Select
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
76
alm/src/components/StatusContainer.tsx
Normal file
76
alm/src/components/StatusContainer.tsx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { useHistory, useParams } from 'react-router-dom'
|
||||||
|
import { useTransactionStatus } from '../hooks/useTransactionStatus'
|
||||||
|
import { formatTxHash, getExplorerTxUrl, getTransactionStatusDescription, validTxHash } from '../utils/networks'
|
||||||
|
import { TRANSACTION_STATUS } from '../config/constants'
|
||||||
|
import { MessageSelector } from './MessageSelector'
|
||||||
|
import { Loading } from './commons/Loading'
|
||||||
|
import { useStateProvider } from '../state/StateProvider'
|
||||||
|
import { ExplorerTxLink } from './commons/ExplorerTxLink'
|
||||||
|
|
||||||
|
export const StatusContainer = () => {
|
||||||
|
const { home, foreign } = useStateProvider()
|
||||||
|
const history = useHistory()
|
||||||
|
const { chainId, txHash, messageIdParam } = useParams()
|
||||||
|
const validChainId = chainId === home.chainId.toString() || chainId === foreign.chainId.toString()
|
||||||
|
const validParameters = validChainId && validTxHash(txHash)
|
||||||
|
|
||||||
|
const { messagesId, status, description, timestamp, loading } = useTransactionStatus({
|
||||||
|
txHash: validParameters ? txHash : '',
|
||||||
|
chainId: validParameters ? parseInt(chainId) : 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedMessageId =
|
||||||
|
messageIdParam === undefined || messagesId[messageIdParam] === undefined ? -1 : messageIdParam
|
||||||
|
|
||||||
|
if (!validParameters) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
Chain Id: {chainId} and/or Transaction Hash: {txHash} are not valid
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <Loading />
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMessageSelected = (messageId: number) => {
|
||||||
|
history.push(`/${chainId}/${txHash}/${messageId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayMessageSelector = status === TRANSACTION_STATUS.SUCCESS_MULTIPLE_MESSAGES && selectedMessageId === -1
|
||||||
|
const multiMessageSelected = status === TRANSACTION_STATUS.SUCCESS_MULTIPLE_MESSAGES && selectedMessageId !== -1
|
||||||
|
const displayReference = multiMessageSelected ? messagesId[selectedMessageId] : txHash
|
||||||
|
const formattedMessageId = formatTxHash(displayReference)
|
||||||
|
|
||||||
|
const displayedDescription = multiMessageSelected
|
||||||
|
? getTransactionStatusDescription(TRANSACTION_STATUS.SUCCESS_ONE_MESSAGE, timestamp)
|
||||||
|
: description
|
||||||
|
|
||||||
|
const isHome = chainId === home.chainId.toString()
|
||||||
|
const txExplorerLink = getExplorerTxUrl(txHash, isHome)
|
||||||
|
const displayExplorerLink = status !== TRANSACTION_STATUS.NOT_FOUND
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{status && (
|
||||||
|
<p>
|
||||||
|
The request{' '}
|
||||||
|
<i>
|
||||||
|
{displayExplorerLink && (
|
||||||
|
<ExplorerTxLink href={txExplorerLink} target="blank">
|
||||||
|
{formattedMessageId}
|
||||||
|
</ExplorerTxLink>
|
||||||
|
)}
|
||||||
|
{!displayExplorerLink && <label>{formattedMessageId}</label>}
|
||||||
|
</i>{' '}
|
||||||
|
{displayedDescription}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{displayMessageSelector && <MessageSelector messages={messagesId} onMessageSelected={onMessageSelected} />}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
5
alm/src/components/commons/Button.tsx
Normal file
5
alm/src/components/commons/Button.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
export const Button = styled.button`
|
||||||
|
height: 36px;
|
||||||
|
`
|
6
alm/src/components/commons/ExplorerTxLink.tsx
Normal file
6
alm/src/components/commons/ExplorerTxLink.tsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
export const ExplorerTxLink = styled.a`
|
||||||
|
color: var(--link-color);
|
||||||
|
text-decoration: underline;
|
||||||
|
`
|
153
alm/src/components/commons/Loading.tsx
Normal file
153
alm/src/components/commons/Loading.tsx
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export const Loading = () => (
|
||||||
|
<div className="row is-center">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
style={{ background: 'none', display: 'block', shapeRendering: 'auto' }}
|
||||||
|
width="50px"
|
||||||
|
height="50px"
|
||||||
|
viewBox="0 0 100 100"
|
||||||
|
preserveAspectRatio="xMidYMid"
|
||||||
|
>
|
||||||
|
<g transform="rotate(0 50 50)">
|
||||||
|
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#f5f5f5">
|
||||||
|
<animate
|
||||||
|
attributeName="opacity"
|
||||||
|
values="1;0"
|
||||||
|
keyTimes="0;1"
|
||||||
|
dur="1s"
|
||||||
|
begin="-0.9166666666666666s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
|
</rect>
|
||||||
|
</g>
|
||||||
|
<g transform="rotate(30 50 50)">
|
||||||
|
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#f5f5f5">
|
||||||
|
<animate
|
||||||
|
attributeName="opacity"
|
||||||
|
values="1;0"
|
||||||
|
keyTimes="0;1"
|
||||||
|
dur="1s"
|
||||||
|
begin="-0.8333333333333334s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
|
</rect>
|
||||||
|
</g>
|
||||||
|
<g transform="rotate(60 50 50)">
|
||||||
|
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#f5f5f5">
|
||||||
|
<animate
|
||||||
|
attributeName="opacity"
|
||||||
|
values="1;0"
|
||||||
|
keyTimes="0;1"
|
||||||
|
dur="1s"
|
||||||
|
begin="-0.75s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
|
</rect>
|
||||||
|
</g>
|
||||||
|
<g transform="rotate(90 50 50)">
|
||||||
|
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#f5f5f5">
|
||||||
|
<animate
|
||||||
|
attributeName="opacity"
|
||||||
|
values="1;0"
|
||||||
|
keyTimes="0;1"
|
||||||
|
dur="1s"
|
||||||
|
begin="-0.6666666666666666s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
|
</rect>
|
||||||
|
</g>
|
||||||
|
<g transform="rotate(120 50 50)">
|
||||||
|
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#f5f5f5">
|
||||||
|
<animate
|
||||||
|
attributeName="opacity"
|
||||||
|
values="1;0"
|
||||||
|
keyTimes="0;1"
|
||||||
|
dur="1s"
|
||||||
|
begin="-0.5833333333333334s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
|
</rect>
|
||||||
|
</g>
|
||||||
|
<g transform="rotate(150 50 50)">
|
||||||
|
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#f5f5f5">
|
||||||
|
<animate
|
||||||
|
attributeName="opacity"
|
||||||
|
values="1;0"
|
||||||
|
keyTimes="0;1"
|
||||||
|
dur="1s"
|
||||||
|
begin="-0.5s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
|
</rect>
|
||||||
|
</g>
|
||||||
|
<g transform="rotate(180 50 50)">
|
||||||
|
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#f5f5f5">
|
||||||
|
<animate
|
||||||
|
attributeName="opacity"
|
||||||
|
values="1;0"
|
||||||
|
keyTimes="0;1"
|
||||||
|
dur="1s"
|
||||||
|
begin="-0.4166666666666667s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
|
</rect>
|
||||||
|
</g>
|
||||||
|
<g transform="rotate(210 50 50)">
|
||||||
|
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#f5f5f5">
|
||||||
|
<animate
|
||||||
|
attributeName="opacity"
|
||||||
|
values="1;0"
|
||||||
|
keyTimes="0;1"
|
||||||
|
dur="1s"
|
||||||
|
begin="-0.3333333333333333s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
|
</rect>
|
||||||
|
</g>
|
||||||
|
<g transform="rotate(240 50 50)">
|
||||||
|
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#f5f5f5">
|
||||||
|
<animate
|
||||||
|
attributeName="opacity"
|
||||||
|
values="1;0"
|
||||||
|
keyTimes="0;1"
|
||||||
|
dur="1s"
|
||||||
|
begin="-0.25s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
|
</rect>
|
||||||
|
</g>
|
||||||
|
<g transform="rotate(270 50 50)">
|
||||||
|
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#f5f5f5">
|
||||||
|
<animate
|
||||||
|
attributeName="opacity"
|
||||||
|
values="1;0"
|
||||||
|
keyTimes="0;1"
|
||||||
|
dur="1s"
|
||||||
|
begin="-0.16666666666666666s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
|
</rect>
|
||||||
|
</g>
|
||||||
|
<g transform="rotate(300 50 50)">
|
||||||
|
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#f5f5f5">
|
||||||
|
<animate
|
||||||
|
attributeName="opacity"
|
||||||
|
values="1;0"
|
||||||
|
keyTimes="0;1"
|
||||||
|
dur="1s"
|
||||||
|
begin="-0.08333333333333333s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
|
</rect>
|
||||||
|
</g>
|
||||||
|
<g transform="rotate(330 50 50)">
|
||||||
|
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#f5f5f5">
|
||||||
|
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="0s" repeatCount="indefinite" />
|
||||||
|
</rect>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
<label>Loading...</label>
|
||||||
|
</div>
|
||||||
|
)
|
9
alm/src/components/commons/RadioButton.tsx
Normal file
9
alm/src/components/commons/RadioButton.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
export const RadioButtonLabel = styled.label`
|
||||||
|
padding-left: 5px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const RadioButtonContainer = styled.div`
|
||||||
|
padding: 10px;
|
||||||
|
`
|
19
alm/src/config/constants.ts
Normal file
19
alm/src/config/constants.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
export const HOME_BRIDGE_ADDRESS: string = process.env.REACT_APP_COMMON_HOME_BRIDGE_ADDRESS || ''
|
||||||
|
export const FOREIGN_BRIDGE_ADDRESS: string = process.env.REACT_APP_COMMON_FOREIGN_BRIDGE_ADDRESS || ''
|
||||||
|
|
||||||
|
export const HOME_RPC_URL: string = process.env.REACT_APP_COMMON_HOME_RPC_URL || ''
|
||||||
|
export const FOREIGN_RPC_URL: string = process.env.REACT_APP_COMMON_FOREIGN_RPC_URL || ''
|
||||||
|
|
||||||
|
export const HOME_NETWORK_NAME: string = process.env.REACT_APP_ALM_HOME_NETWORK_NAME || ''
|
||||||
|
export const FOREIGN_NETWORK_NAME: string = process.env.REACT_APP_ALM_FOREIGN_NETWORK_NAME || ''
|
||||||
|
|
||||||
|
export const HOME_EXPLORER_TX_TEMPLATE: string = process.env.REACT_APP_ALM_HOME_EXPLORER_TX_TEMPLATE || ''
|
||||||
|
export const FOREIGN_EXPLORER_TX_TEMPLATE: string = process.env.REACT_APP_ALM_FOREIGN_EXPLORER_TX_TEMPLATE || ''
|
||||||
|
|
||||||
|
export const TRANSACTION_STATUS = {
|
||||||
|
SUCCESS_MULTIPLE_MESSAGES: 'SUCCESS_MULTIPLE_MESSAGES',
|
||||||
|
SUCCESS_ONE_MESSAGE: 'SUCCESS_ONE_MESSAGE',
|
||||||
|
SUCCESS_NO_MESSAGES: 'SUCCESS_NO_MESSAGES',
|
||||||
|
FAILED: 'FAILED',
|
||||||
|
NOT_FOUND: 'NOT_FOUND'
|
||||||
|
}
|
8
alm/src/config/descriptions.ts
Normal file
8
alm/src/config/descriptions.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// %t will be replaced by the time -> x minutes/hours/days ago
|
||||||
|
export const TRANSACTION_STATUS_DESCRIPTION: { [key: string]: string } = {
|
||||||
|
SUCCESS_MULTIPLE_MESSAGES: 'was initiated %t and contains several bridge messages. Specify one of them:',
|
||||||
|
SUCCESS_ONE_MESSAGE: 'was initiated %t',
|
||||||
|
SUCCESS_NO_MESSAGES: 'execution succeeded %t but it does not contain any bridge messages',
|
||||||
|
FAILED: 'failed %t',
|
||||||
|
NOT_FOUND: 'was not found'
|
||||||
|
}
|
1
alm/src/global.d.ts
vendored
Normal file
1
alm/src/global.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
declare type Maybe<T> = T | null
|
27
alm/src/hooks/useNetwork.ts
Normal file
27
alm/src/hooks/useNetwork.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { getWeb3 } from '../utils/web3'
|
||||||
|
|
||||||
|
export const useNetwork = (url: string) => {
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [chainId, setChainId] = useState(0)
|
||||||
|
const web3 = getWeb3(url)
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => {
|
||||||
|
setLoading(true)
|
||||||
|
const getChainId = async () => {
|
||||||
|
const id = await web3.eth.getChainId()
|
||||||
|
setChainId(id)
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
getChainId()
|
||||||
|
},
|
||||||
|
[web3.eth]
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
web3,
|
||||||
|
chainId,
|
||||||
|
loading
|
||||||
|
}
|
||||||
|
}
|
99
alm/src/hooks/useTransactionStatus.ts
Normal file
99
alm/src/hooks/useTransactionStatus.ts
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { TransactionReceipt } from 'web3-eth'
|
||||||
|
import { TRANSACTION_STATUS } from '../config/constants'
|
||||||
|
import { getTransactionStatusDescription } from '../utils/networks'
|
||||||
|
import { useStateProvider } from '../state/StateProvider'
|
||||||
|
import { getHomeMessagesFromReceipt, getForeignMessagesFromReceipt } from '../utils/web3'
|
||||||
|
|
||||||
|
export const useTransactionStatus = ({ txHash, chainId }: { txHash: string; chainId: number }) => {
|
||||||
|
const { home, foreign } = useStateProvider()
|
||||||
|
const [messagesId, setMessagesId] = useState<Array<string>>([])
|
||||||
|
const [status, setStatus] = useState('')
|
||||||
|
const [description, setDescription] = useState('')
|
||||||
|
const [receipt, setReceipt] = useState<Maybe<TransactionReceipt>>(null)
|
||||||
|
const [timestamp, setTimestamp] = useState(0)
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => {
|
||||||
|
const subscriptions: Array<number> = []
|
||||||
|
|
||||||
|
const unsubscribe = () => {
|
||||||
|
subscriptions.forEach(s => {
|
||||||
|
clearTimeout(s)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getReceipt = async () => {
|
||||||
|
if (!chainId || !txHash || !home.chainId || !foreign.chainId || !home.web3 || !foreign.web3) return
|
||||||
|
setLoading(true)
|
||||||
|
const isHome = chainId === home.chainId
|
||||||
|
const web3 = isHome ? home.web3 : foreign.web3
|
||||||
|
|
||||||
|
const txReceipt = await web3.eth.getTransactionReceipt(txHash)
|
||||||
|
setReceipt(txReceipt)
|
||||||
|
|
||||||
|
if (!txReceipt) {
|
||||||
|
setStatus(TRANSACTION_STATUS.NOT_FOUND)
|
||||||
|
setDescription(getTransactionStatusDescription(TRANSACTION_STATUS.NOT_FOUND))
|
||||||
|
setMessagesId([txHash])
|
||||||
|
const timeoutId = setTimeout(() => getReceipt(), 5000)
|
||||||
|
subscriptions.push(timeoutId)
|
||||||
|
} else {
|
||||||
|
const blockNumber = txReceipt.blockNumber
|
||||||
|
const block = await web3.eth.getBlock(blockNumber)
|
||||||
|
const blockTimestamp = typeof block.timestamp === 'string' ? parseInt(block.timestamp) : block.timestamp
|
||||||
|
setTimestamp(blockTimestamp)
|
||||||
|
|
||||||
|
if (txReceipt.status) {
|
||||||
|
let bridgeMessagesId
|
||||||
|
if (isHome) {
|
||||||
|
bridgeMessagesId = getHomeMessagesFromReceipt(txReceipt, home.web3, home.bridgeAddress)
|
||||||
|
} else {
|
||||||
|
bridgeMessagesId = getForeignMessagesFromReceipt(txReceipt, foreign.web3, foreign.bridgeAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bridgeMessagesId.length === 0) {
|
||||||
|
setMessagesId([txHash])
|
||||||
|
setStatus(TRANSACTION_STATUS.SUCCESS_NO_MESSAGES)
|
||||||
|
setDescription(getTransactionStatusDescription(TRANSACTION_STATUS.SUCCESS_NO_MESSAGES, blockTimestamp))
|
||||||
|
} else if (bridgeMessagesId.length === 1) {
|
||||||
|
setMessagesId(bridgeMessagesId)
|
||||||
|
setStatus(TRANSACTION_STATUS.SUCCESS_ONE_MESSAGE)
|
||||||
|
setDescription(getTransactionStatusDescription(TRANSACTION_STATUS.SUCCESS_ONE_MESSAGE, blockTimestamp))
|
||||||
|
} else {
|
||||||
|
setMessagesId(bridgeMessagesId)
|
||||||
|
setStatus(TRANSACTION_STATUS.SUCCESS_MULTIPLE_MESSAGES)
|
||||||
|
setDescription(
|
||||||
|
getTransactionStatusDescription(TRANSACTION_STATUS.SUCCESS_MULTIPLE_MESSAGES, blockTimestamp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setStatus(TRANSACTION_STATUS.FAILED)
|
||||||
|
setDescription(getTransactionStatusDescription(TRANSACTION_STATUS.FAILED, blockTimestamp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// unsubscribe from previous txHash
|
||||||
|
unsubscribe()
|
||||||
|
|
||||||
|
getReceipt()
|
||||||
|
return () => {
|
||||||
|
// unsubscribe when unmount component
|
||||||
|
unsubscribe()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[txHash, chainId, home.chainId, foreign.chainId, home.web3, foreign.web3, home.bridgeAddress, foreign.bridgeAddress]
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
messagesId,
|
||||||
|
status,
|
||||||
|
description,
|
||||||
|
receipt,
|
||||||
|
timestamp,
|
||||||
|
loading
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +0,0 @@
|
|||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
|
||||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
|
||||||
sans-serif;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
@ -1,11 +1,16 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import './index.css'
|
import { ThemeProvider } from 'styled-components'
|
||||||
|
import { GlobalStyle } from './themes/GlobalStyle'
|
||||||
import App from './App'
|
import App from './App'
|
||||||
|
import Dark from './themes/Dark'
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<ThemeProvider theme={Dark}>
|
||||||
|
<GlobalStyle />
|
||||||
|
<App />
|
||||||
|
</ThemeProvider>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
document.getElementById('root')
|
document.getElementById('root')
|
||||||
)
|
)
|
||||||
|
67
alm/src/state/StateProvider.tsx
Normal file
67
alm/src/state/StateProvider.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import React, { createContext, ReactNode } from 'react'
|
||||||
|
import { useNetwork } from '../hooks/useNetwork'
|
||||||
|
import {
|
||||||
|
HOME_RPC_URL,
|
||||||
|
FOREIGN_RPC_URL,
|
||||||
|
HOME_BRIDGE_ADDRESS,
|
||||||
|
FOREIGN_BRIDGE_ADDRESS,
|
||||||
|
HOME_NETWORK_NAME,
|
||||||
|
FOREIGN_NETWORK_NAME
|
||||||
|
} from '../config/constants'
|
||||||
|
import Web3 from 'web3'
|
||||||
|
|
||||||
|
export interface NetworkParams {
|
||||||
|
chainId: number
|
||||||
|
name: string
|
||||||
|
web3: Maybe<Web3>
|
||||||
|
bridgeAddress: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StateContext {
|
||||||
|
home: NetworkParams
|
||||||
|
foreign: NetworkParams
|
||||||
|
loading: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
home: {
|
||||||
|
chainId: 0,
|
||||||
|
name: '',
|
||||||
|
web3: null,
|
||||||
|
bridgeAddress: HOME_BRIDGE_ADDRESS
|
||||||
|
},
|
||||||
|
foreign: {
|
||||||
|
chainId: 0,
|
||||||
|
name: '',
|
||||||
|
web3: null,
|
||||||
|
bridgeAddress: FOREIGN_BRIDGE_ADDRESS
|
||||||
|
},
|
||||||
|
loading: true
|
||||||
|
}
|
||||||
|
|
||||||
|
const StateContext = createContext<StateContext>(initialState)
|
||||||
|
|
||||||
|
export const StateProvider = ({ children }: { children: ReactNode }) => {
|
||||||
|
const homeNetwork = useNetwork(HOME_RPC_URL)
|
||||||
|
const foreignNetwork = useNetwork(FOREIGN_RPC_URL)
|
||||||
|
|
||||||
|
const value = {
|
||||||
|
home: {
|
||||||
|
bridgeAddress: HOME_BRIDGE_ADDRESS,
|
||||||
|
name: HOME_NETWORK_NAME,
|
||||||
|
...homeNetwork
|
||||||
|
},
|
||||||
|
foreign: {
|
||||||
|
bridgeAddress: FOREIGN_BRIDGE_ADDRESS,
|
||||||
|
name: FOREIGN_NETWORK_NAME,
|
||||||
|
...foreignNetwork
|
||||||
|
},
|
||||||
|
loading: homeNetwork.loading || foreignNetwork.loading
|
||||||
|
}
|
||||||
|
|
||||||
|
return <StateContext.Provider value={value}>{children}</StateContext.Provider>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useStateProvider = (): StateContext => {
|
||||||
|
return React.useContext(StateContext)
|
||||||
|
}
|
9
alm/src/themes/Dark.tsx
Normal file
9
alm/src/themes/Dark.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
const theme = {
|
||||||
|
backgroundColor: '#121212',
|
||||||
|
fontColor: '#f5f5f5',
|
||||||
|
colorPrimary: '#272727',
|
||||||
|
colorGrey: '#272727',
|
||||||
|
colorLightGrey: '#272727',
|
||||||
|
linkColor: '#ffffff'
|
||||||
|
}
|
||||||
|
export default theme
|
25
alm/src/themes/GlobalStyle.tsx
Normal file
25
alm/src/themes/GlobalStyle.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { createGlobalStyle } from 'styled-components'
|
||||||
|
|
||||||
|
import theme from './Dark'
|
||||||
|
|
||||||
|
type ThemeType = typeof theme
|
||||||
|
|
||||||
|
export const GlobalStyle = createGlobalStyle<{ theme: ThemeType }>`
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg-color: ${props => props.theme.backgroundColor};
|
||||||
|
--font-color: ${props => props.theme.fontColor};
|
||||||
|
--color-primary: ${props => props.theme.colorPrimary};
|
||||||
|
--color-grey: ${props => props.theme.colorGrey};
|
||||||
|
--color-lightGrey: ${props => props.theme.colorLightGrey};
|
||||||
|
--link-color: ${props => props.theme.linkColor}
|
||||||
|
}
|
||||||
|
`
|
32
alm/src/utils/networks.ts
Normal file
32
alm/src/utils/networks.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { formatDistance } from 'date-fns'
|
||||||
|
import { TRANSACTION_STATUS_DESCRIPTION } from '../config/descriptions'
|
||||||
|
import { FOREIGN_EXPLORER_TX_TEMPLATE, HOME_EXPLORER_TX_TEMPLATE } from '../config/constants'
|
||||||
|
|
||||||
|
export const validTxHash = (txHash: string) => /^0x[a-fA-F0-9]{64}$/.test(txHash)
|
||||||
|
|
||||||
|
export const formatTxHash = (txHash: string) => `${txHash.substring(0, 6)}...${txHash.substring(txHash.length - 4)}`
|
||||||
|
|
||||||
|
export const getExplorerTxUrl = (txHash: string, isHome: boolean) => {
|
||||||
|
const template = isHome ? HOME_EXPLORER_TX_TEMPLATE : FOREIGN_EXPLORER_TX_TEMPLATE
|
||||||
|
return template.replace('%s', txHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const formatTxHashExtended = (txHash: string) =>
|
||||||
|
`${txHash.substring(0, 10)}...${txHash.substring(txHash.length - 8)}`
|
||||||
|
|
||||||
|
export const formatTimestamp = (timestamp: number): string => {
|
||||||
|
const txDate = new Date(0).setUTCSeconds(timestamp)
|
||||||
|
return formatDistance(txDate, new Date(), {
|
||||||
|
addSuffix: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getTransactionStatusDescription = (status: string, timestamp: Maybe<number> = null) => {
|
||||||
|
let description = TRANSACTION_STATUS_DESCRIPTION[status]
|
||||||
|
|
||||||
|
if (timestamp) {
|
||||||
|
description = description.replace('%t', formatTimestamp(timestamp))
|
||||||
|
}
|
||||||
|
|
||||||
|
return description
|
||||||
|
}
|
36
alm/src/utils/web3.ts
Normal file
36
alm/src/utils/web3.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import Web3 from 'web3'
|
||||||
|
import { TransactionReceipt } from 'web3-eth'
|
||||||
|
import { AbiItem } from 'web3-utils'
|
||||||
|
import memoize from 'fast-memoize'
|
||||||
|
import { HOME_AMB_ABI, FOREIGN_AMB_ABI } from '../../../commons'
|
||||||
|
|
||||||
|
const rawGetWeb3 = (url: string) => new Web3(new Web3.providers.HttpProvider(url))
|
||||||
|
const memoized = memoize(rawGetWeb3)
|
||||||
|
|
||||||
|
export const getWeb3 = (url: string) => memoized(url)
|
||||||
|
|
||||||
|
export const filterEventsByAbi = (
|
||||||
|
txReceipt: TransactionReceipt,
|
||||||
|
web3: Web3,
|
||||||
|
bridgeAddress: string,
|
||||||
|
eventAbi: AbiItem
|
||||||
|
) => {
|
||||||
|
const eventHash = web3.eth.abi.encodeEventSignature(eventAbi)
|
||||||
|
const events = txReceipt.logs.filter(e => e.address === bridgeAddress && e.topics[0] === eventHash)
|
||||||
|
|
||||||
|
return events.map(e => e.topics[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getHomeMessagesFromReceipt = (txReceipt: TransactionReceipt, web3: Web3, bridgeAddress: string) => {
|
||||||
|
const UserRequestForSignatureAbi: AbiItem = HOME_AMB_ABI.filter(
|
||||||
|
(e: AbiItem) => e.type === 'event' && e.name === 'UserRequestForSignature'
|
||||||
|
)[0]
|
||||||
|
return filterEventsByAbi(txReceipt, web3, bridgeAddress, UserRequestForSignatureAbi)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getForeignMessagesFromReceipt = (txReceipt: TransactionReceipt, web3: Web3, bridgeAddress: string) => {
|
||||||
|
const userRequestForAffirmationAbi: AbiItem = FOREIGN_AMB_ABI.filter(
|
||||||
|
(e: AbiItem) => e.type === 'event' && e.name === 'UserRequestForAffirmation'
|
||||||
|
)[0]
|
||||||
|
return filterEventsByAbi(txReceipt, web3, bridgeAddress, userRequestForAffirmationAbi)
|
||||||
|
}
|
@ -14,11 +14,11 @@
|
|||||||
"mobx-react": "^5.0.0",
|
"mobx-react": "^5.0.0",
|
||||||
"node-sass-chokidar": "^1.0.1",
|
"node-sass-chokidar": "^1.0.1",
|
||||||
"numeral": "^2.0.6",
|
"numeral": "^2.0.6",
|
||||||
"react": "^16.2.0",
|
"react": "16.13.1",
|
||||||
"react-app-rewire-mobx": "^1.0.7",
|
"react-app-rewire-mobx": "^1.0.7",
|
||||||
"react-app-rewired": "^2.0.3",
|
"react-app-rewired": "^2.0.3",
|
||||||
"react-copy-to-clipboard": "^5.0.1",
|
"react-copy-to-clipboard": "^5.0.1",
|
||||||
"react-dom": "^16.2.0",
|
"react-dom": "16.13.1",
|
||||||
"react-router": "^4.3.1",
|
"react-router": "^4.3.1",
|
||||||
"react-router-dom": "^4.2.2",
|
"react-router-dom": "^4.2.2",
|
||||||
"react-scripts": "3.0.1",
|
"react-scripts": "3.0.1",
|
||||||
|
Loading…
Reference in New Issue
Block a user