Skip to content
This repository has been archived by the owner on Apr 4, 2022. It is now read-only.

Commit

Permalink
Implementation of the batch transanction view (#1061)
Browse files Browse the repository at this point in the history
# Summary

Closes #1058

Implement the visualization of a batch of transactions through a node graph.

[Cytoscape](https://js.cytoscape.org/) has been used as interaction library due to its maturity and large community. But we had to apply styles in a way outside the project format.

![image](https://user-images.githubusercontent.com/622217/160895346-383451db-c590-464b-8892-ccf1f0eeb43b.png)

### To Test:
- Go to [mainnet/tx/0xc2eb04b549d68c770062591e2561c5b1534ea665c6179d33a779c52376c9161d](https://pr1061--gpui.review.gnosisdev.com/tx/0xc2eb04b549d68c770062591e2561c5b1534ea665c6179d33a779c52376c9161d)
- Go to [mainnet/tx/0x51757dd21436d695824ceb13d21ec3c3ef5e5e5454a847951b1ea91e1c89b0b7](https://pr1061--gpui.review.gnosisdev.com/tx/0x51757dd21436d695824ceb13d21ec3c3ef5e5e5454a847951b1ea91e1c89b0b7)
  • Loading branch information
henrypalacios committed Apr 1, 2022
1 parent 57e5868 commit 0bee3f7
Show file tree
Hide file tree
Showing 19 changed files with 745 additions and 26 deletions.
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@
"bignumber.js": "^9.0.0",
"bn.js": "^4.11.9",
"combine-reducers": "^1.0.0",
"cytoscape": "^3.21.0",
"cytoscape-popper": "^2.0.0",
"date-fns": "^2.9.0",
"detect-browser": "^5.1.0",
"eth-json-rpc-middleware": "^4.4.1",
Expand All @@ -90,6 +92,7 @@
"qrcode.react": "^1.0.0",
"react": "^16.12.0",
"react-copy-to-clipboard": "^5.0.2",
"react-cytoscapejs": "^1.2.1",
"react-device-detect": "^1.15.0",
"react-dom": "^16.12.0",
"react-ga": "^3.3.0",
Expand Down Expand Up @@ -124,13 +127,15 @@
"@truffle/hdwallet-provider": "^1.2.0",
"@types/bn.js": "^4.11.6",
"@types/combine-reducers": "^1.0.0",
"@types/cytoscape": "^3.19.4",
"@types/enzyme": "^3.10.4",
"@types/enzyme-adapter-react-16": "^1.0.5",
"@types/hapi__joi": "^16.0.12",
"@types/jest": "^26.0.8",
"@types/node": "^14.0.14",
"@types/qrcode.react": "^1.0.0",
"@types/react-copy-to-clipboard": "^4.3.0",
"@types/react-cytoscapejs": "^1.2.2",
"@types/react-dom": "^16.9.5",
"@types/react-router": "^5.1.4",
"@types/react-router-dom": "^5.1.3",
Expand Down
2 changes: 1 addition & 1 deletion src/api/tenderly/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export * from './tenderlyApi'

export { PublicTrade as Trade, Transfer, Account } from './types'
export type { PublicTrade as Trade, Transfer, Account } from './types'
35 changes: 21 additions & 14 deletions src/apps/explorer/components/TransactionsTableWidget/index.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,29 @@
import React, { useState, useEffect } from 'react'

import { BlockchainNetwork, TransactionsTableContext } from './context/TransactionsTableContext'
import { useGetTxOrders } from 'hooks/useGetOrders'
import { useGetTxOrders, useTxOrderExplorerLink } from 'hooks/useGetOrders'
import RedirectToSearch from 'components/RedirectToSearch'
import Spinner from 'components/common/Spinner'
import { RedirectToNetwork, useNetworkId } from 'state/network'
import { Order } from 'api/operator'
import { useTxOrderExplorerLink } from 'hooks/useGetOrders'
import { TransactionsTableWithData } from 'apps/explorer/components/TransactionsTableWidget/TransactionsTableWithData'
import { TabItemInterface } from 'components/common/Tabs/Tabs'
import ExplorerTabs from '../common/ExplorerTabs/ExplorerTab'
import styled from 'styled-components'
import { TitleAddress } from 'apps/explorer/pages/styled'
import { TitleAddress, FlexContainer, StyledTabLoader, BVButton, Title } from 'apps/explorer/pages/styled'
import { BlockExplorerLink } from 'components/common/BlockExplorerLink'
import { ConnectionStatus } from 'components/ConnectionStatus'
import { Notification } from 'components/Notification'
import { useTxBatchTrades } from 'hooks/useTxBatchTrades'
import { faListUl, faProjectDiagram } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import TransactionBatchGraph from 'apps/explorer/components/TransanctionBatchGraph'

interface Props {
txHash: string
networkId: BlockchainNetwork
transactions?: Order[]
}

const StyledTabLoader = styled.span`
padding-left: 4px;
`

const tabItems = (isLoadingOrders: boolean): TabItemInterface[] => {
return [
{
Expand All @@ -46,12 +43,11 @@ export const TransactionsTableWidget: React.FC<Props> = ({ txHash }) => {
const { orders, isLoading: isTxLoading, errorTxPresentInNetworkId, error } = useGetTxOrders(txHash)
const networkId = useNetworkId() || undefined
const [redirectTo, setRedirectTo] = useState(false)
const [batchViewer, setBatchViewer] = useState(false)
const txHashParams = { networkId, txHash }
const isZeroOrders = !!(orders && orders.length === 0)
const notGpv2ExplorerData = useTxOrderExplorerLink(txHash, isZeroOrders)
// TODO use on draw tx view
const txBatchTrades = useTxBatchTrades(networkId, txHash, orders && orders.length)
console.log('txBatchTrades', txBatchTrades)

// Avoid redirecting until another network is searched again
useEffect(() => {
Expand All @@ -75,15 +71,22 @@ export const TransactionsTableWidget: React.FC<Props> = ({ txHash }) => {
return <Spinner spin size="3x" />
}

const batchViewerButtonName = batchViewer ? 'Show Transactions list' : 'Show Batch Viewer'
const batchViewerButtonIcon = batchViewer ? faListUl : faProjectDiagram

return (
<>
<h1>
Transaction details
<FlexContainer>
<Title>Transaction details</Title>
<TitleAddress
textToCopy={txHash}
contentsToDisplay={<BlockExplorerLink type="tx" networkId={networkId} identifier={txHash} showLogo />}
/>
</h1>
<BVButton onClick={(): void => setBatchViewer(!batchViewer)}>
<FontAwesomeIcon icon={batchViewerButtonIcon} />
{batchViewerButtonName}
</BVButton>
</FlexContainer>
<ConnectionStatus />
{error && <Notification type={error.type} message={error.message} />}
<TransactionsTableContext.Provider
Expand All @@ -94,7 +97,11 @@ export const TransactionsTableWidget: React.FC<Props> = ({ txHash }) => {
isTxLoading,
}}
>
<ExplorerTabs tabItems={tabItems(isTxLoading)} />
{batchViewer ? (
<TransactionBatchGraph txBatchData={txBatchTrades} networkId={networkId} />
) : (
<ExplorerTabs tabItems={tabItems(isTxLoading)} />
)}
</TransactionsTableContext.Provider>
</>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { ElementDefinition } from 'cytoscape'
import { InfoTooltip, Node, TypeNodeOnTx } from './types'

export default class ElementsBuilder {
_center: ElementDefinition | null = null
_nodes: ElementDefinition[] = []
_edges: ElementDefinition[] = []
_SIZE: number
_countTypes: Map<string, number>

constructor(heighSize?: number) {
this._SIZE = heighSize || 600
this._countTypes = new Map()
}

_increaseCounType = (_type: string): void => {
const count = this._countTypes.get(_type) || 0
this._countTypes.set(_type, count + 1)
}
_createNodeElement = (node: Node, parent?: string): ElementDefinition => {
this._increaseCounType(node.type)
return {
group: 'nodes',
data: {
id: `${node.type}:${node.id}`,
label: node.entity.alias,
type: node.type,
parent: parent ? `${TypeNodeOnTx.NetworkNode}:${parent}` : undefined,
},
}
}

center(node: Node, parent?: string): this {
this._center = this._createNodeElement(node, parent)
return this
}

node(node: Node, parent?: string): this {
this._nodes.push(this._createNodeElement(node, parent))
return this
}

edge(
source: Pick<Node, 'type' | 'id'>,
target: Pick<Node, 'type' | 'id'>,
label: string,
tooltip?: InfoTooltip,
): this {
this._edges.push({
group: 'edges',
data: {
id: `${source.type}:${source.id}->${target.type}:${target.id}`,
source: `${source.type}:${source.id}`,
target: `${target.type}:${target.id}`,
label,
tooltip,
},
})
return this
}

build(customLayoutNodes?: CustomLayoutNodes): ElementDefinition[] {
if (!customLayoutNodes) {
return this._buildCoseLayout()
} else {
const { center, nodes } = customLayoutNodes
return [center, ...nodes, ...this._edges]
}
}

_buildCoseLayout(): ElementDefinition[] {
if (!this._center) {
throw new Error('Center node is required')
}
const center = {
...this._center,
position: { x: 0, y: 0 },
}
const nTypes = this._countTypes.size

const r = this._SIZE / nTypes - 100 // get radio

const nodes = this._nodes.map((node: ElementDefinition, index: number) => {
return {
...node,
position: {
x: r * Math.cos((nTypes * Math.PI * index) / this._nodes.length),
y: r * Math.sin((nTypes * Math.PI * index) / this._nodes.length),
},
}
})

return [center, ...nodes, ...this._edges]
}

getById(id: string): ElementDefinition | undefined {
// split <type>:<id> and find by <id>
if (this._center) {
return [this._center, ...this._nodes].find((node) => node.data.id?.split(':')[1] === id)
}

return this._nodes.find((node) => node.data.id?.split(':')[1] === id)
}
}

interface CustomLayoutNodes {
center: ElementDefinition
nodes: ElementDefinition[]
}

export function getGridPosition(type: TypeNodeOnTx, traderRowsLength: number, dexRowsLenght: number): number {
let col
const batchOf = 5
// Add a column for each batch of n
const leftPaddingCol = 1 + Math.round(traderRowsLength / batchOf)
const rightPaddingCol = leftPaddingCol + 1 + Math.round(dexRowsLenght / batchOf)

if (type === TypeNodeOnTx.Trader) {
col = 0 // first Column
} else if (type === TypeNodeOnTx.CowProtocol) {
col = leftPaddingCol
} else {
col = rightPaddingCol
}
return col
}

/**
* Build a grid layout using the 'position' attribute
* using 'x' for the columns and 'y' for the rows.
*/
export function buildGridLayout(
countTypes: Map<TypeNodeOnTx, number>,
center: ElementDefinition | null,
nodes: ElementDefinition[],
): { center: ElementDefinition; nodes: ElementDefinition[] } {
if (!center) {
throw new Error('Center node is required')
}

const maxRows = Math.max(...countTypes.values())
const middleOfTotalRows = Math.floor(maxRows / 2)
const traders = countTypes.get(TypeNodeOnTx.Trader) || 0
const dexes = countTypes.get(TypeNodeOnTx.Dex) || 0
const _center = {
...center,
position: { y: middleOfTotalRows, x: getGridPosition(center.data.type, traders, dexes) },
}

let counterRows = { [TypeNodeOnTx.Trader]: 0, [TypeNodeOnTx.Dex]: 0 }
if (traders > dexes) {
const difference = (traders - dexes) / 2
counterRows[TypeNodeOnTx.Dex] = Math.floor(difference)
} else if (traders < dexes) {
const difference = (dexes - traders) / 2
counterRows[TypeNodeOnTx.Trader] = Math.floor(difference)
}

const _nodes = nodes.map((node) => {
const _node = {
...node,
position: {
y: counterRows[node.data.type],
x: getGridPosition(node.data.type, traders, dexes),
},
}

if (node.data.type === TypeNodeOnTx.Trader) {
counterRows = { ...counterRows, [TypeNodeOnTx.Trader]: counterRows[TypeNodeOnTx.Trader] + 1 }
} else if (node.data.type === TypeNodeOnTx.Dex) {
counterRows = { ...counterRows, [TypeNodeOnTx.Dex]: counterRows[TypeNodeOnTx.Dex] + 1 }
}

return _node
})

return { center: _center, nodes: _nodes }
}

0 comments on commit 0bee3f7

Please sign in to comment.