Add Alm project structure and transaction form (#353)

This commit is contained in:
Gerardo Nardelli 2020-06-09 10:37:43 -03:00 committed by GitHub
parent 8e10a5d609
commit bcdf691000
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 1400 additions and 80 deletions

@ -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>
)
}

@ -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>
)
}

@ -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>
)
}

@ -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>
)
}

@ -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>
)
}

@ -0,0 +1,5 @@
import styled from 'styled-components'
export const Button = styled.button`
height: 36px;
`

@ -0,0 +1,6 @@
import styled from 'styled-components'
export const ExplorerTxLink = styled.a`
color: var(--link-color);
text-decoration: underline;
`

@ -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>
)

@ -0,0 +1,9 @@
import styled from 'styled-components'
export const RadioButtonLabel = styled.label`
padding-left: 5px;
`
export const RadioButtonContainer = styled.div`
padding: 10px;
`

@ -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'
}

@ -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

@ -0,0 +1 @@
declare type Maybe<T> = T | null

@ -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
}
}

@ -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')
)

@ -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

@ -0,0 +1,9 @@
const theme = {
backgroundColor: '#121212',
fontColor: '#f5f5f5',
colorPrimary: '#272727',
colorGrey: '#272727',
colorLightGrey: '#272727',
linkColor: '#ffffff'
}
export default theme

@ -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

@ -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

@ -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",

645
yarn.lock

File diff suppressed because it is too large Load Diff