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_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",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@react-hook/window-size": "^3.0.6",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
@ -10,12 +11,19 @@
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/react": "^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",
|
||||
"date-fns": "^2.14.0",
|
||||
"fast-memoize": "^2.5.2",
|
||||
"react": "^16.13.1",
|
||||
"react-app-rewired": "^2.1.6",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "3.0.1",
|
||||
"typescript": "^3.5.2"
|
||||
"styled-components": "^5.1.1",
|
||||
"typescript": "^3.5.2",
|
||||
"web3": "^1.2.8"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "./load-env.sh react-app-rewired start",
|
||||
|
@ -7,8 +7,9 @@
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
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" />
|
||||
<!--
|
||||
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",
|
||||
"name": "Create React App Sample",
|
||||
"short_name": "ALM",
|
||||
"name": "AMB Live Monitoring",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"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 './App.css'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import { MainPage } from './components/MainPage'
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="App">
|
||||
<header className="App-header">
|
||||
<p>AMB Live Monitoring</p>
|
||||
</header>
|
||||
</div>
|
||||
<BrowserRouter>
|
||||
<MainPage />
|
||||
</BrowserRouter>
|
||||
)
|
||||
}
|
||||
|
||||
|
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 ReactDOM from 'react-dom'
|
||||
import './index.css'
|
||||
import { ThemeProvider } from 'styled-components'
|
||||
import { GlobalStyle } from './themes/GlobalStyle'
|
||||
import App from './App'
|
||||
import Dark from './themes/Dark'
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
<ThemeProvider theme={Dark}>
|
||||
<GlobalStyle />
|
||||
<App />
|
||||
</ThemeProvider>
|
||||
</React.StrictMode>,
|
||||
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",
|
||||
"node-sass-chokidar": "^1.0.1",
|
||||
"numeral": "^2.0.6",
|
||||
"react": "^16.2.0",
|
||||
"react": "16.13.1",
|
||||
"react-app-rewire-mobx": "^1.0.7",
|
||||
"react-app-rewired": "^2.0.3",
|
||||
"react-copy-to-clipboard": "^5.0.1",
|
||||
"react-dom": "^16.2.0",
|
||||
"react-dom": "16.13.1",
|
||||
"react-router": "^4.3.1",
|
||||
"react-router-dom": "^4.2.2",
|
||||
"react-scripts": "3.0.1",
|
||||
|
Loading…
Reference in New Issue
Block a user