import React, { useContext, useState, useEffect, useRef } from "react"; import { Spinner } from "react-bootstrap"; import { NetworkContext } from "src/services/network/networkProvider"; import { hexAddrToZilAddr, stripHexPrefix } from "src/utils/Utils"; import { useQuery, gql } from "@apollo/client"; import * as d3 from "d3"; import TransitionModal from "./TransitionModal"; import TransitionFlowDetails from "./TransactionFlowDetails"; import "./TransactionFlow.css"; interface IProps { hash: string; txn: any; } const TransactionFlow: React.FC = ({ hash, txn }) => { const [transaction, setTransaction] = useState(undefined); const [isLoading, setIsLoading] = useState(false); const [isError, setIsError] = useState(false); const [modalDisplay, setModalDisplay] = useState(false); const [modalData, setModalData] = useState(undefined); const [nodes, setNodes] = useState([]); const [links, setLinks] = useState([]); const networkContext = useContext(NetworkContext); if (!networkContext) { return (
); } const { dataService, isIsolatedServer, apolloUrl } = networkContext; const ref: any = useRef(); const TRANSACTION_QUERY = gql` query GetTransaction($customId: String!) { txFindByCustomId(customId: $customId) { fromAddr toAddr amount receipt { event_logs { address _eventname params { vname type value } } transitions { accepted addr depth msg { _tag _amount _recipient params { vname type value } } } } } } `; const { loading, error, data } = useQuery(TRANSACTION_QUERY, { variables: { customId: stripHexPrefix(hash) }, context: { uri: apolloUrl, }, fetchPolicy: "cache-and-network", }); useEffect(() => { setIsLoading(false); if (data) { if ( data.txFindByCustomId.length && data.txFindByCustomId[0] !== transaction ) { setTransaction(data.txFindByCustomId[0]); } else { setIsError( new Error("transaction was not found on the apollo-server.") ); } } }, [data]); const getNodeColor = async (node: { id: string; type?: string; color?: string; }) => { if (!dataService || isIsolatedServer === null) return "#035992"; const colors = [ { type: "caller", color: "#666", }, { type: "contract", color: "orange", }, { type: "user", color: "#035992", }, ]; try { if (node.type === undefined) { const targetContract = await dataService?.isContractAddr(node.id); node.type = targetContract ? "contract" : "user"; } const exists = colors.find( (item: { type: string; color: string }) => item.type === node.type ); return exists ? exists.color : "#035992"; } catch (error) { return "#035992"; } }; const openModal = (d: any, link: any) => { document.body.classList.add("has-modal-open"); console.log(link); setModalData(link); setModalDisplay(true); }; const openNode = (d: any, node: any) => { window.location.href = `/address/${node.id}${window.location.search}`; }; useEffect(() => { if (transaction !== undefined) { const links: any = [ { source: hexAddrToZilAddr(transaction.fromAddr), target: hexAddrToZilAddr(transaction.toAddr), amount: transaction.amount, index: 0, txData: transaction, receipt: transaction.receipt, }, ]; const nodes: any = [ { id: hexAddrToZilAddr(transaction.fromAddr), type: "caller", }, { id: hexAddrToZilAddr(transaction.toAddr), type: "contract", }, ]; console.log("nodes", nodes); console.log("links", links); if (transaction.receipt.transitions.length) { let ioo = 0; transaction.receipt.transitions.forEach(async (tr: any) => { ioo++; if ( !nodes.find((node: any) => node.id === hexAddrToZilAddr(tr.addr)) ) { nodes.push({ id: hexAddrToZilAddr(tr.addr), }); } if ( !nodes.find( (node: any) => node.id === hexAddrToZilAddr(tr.msg._recipient) ) ) { nodes.push({ id: hexAddrToZilAddr(tr.msg._recipient), }); } //console.log(transaction.receipt.event_logs, tr.msg._recipient); const events = transaction.receipt.event_logs.filter((evv: any) => { return evv.address === tr.msg._recipient; }); links.push({ source: hexAddrToZilAddr(tr.addr), target: hexAddrToZilAddr(tr.msg._recipient), data: { ...tr }, txData: transaction, index: ioo, events: events, }); }); } Promise.all( nodes.map( async (element: { id: string; type?: string; color?: string }) => { element.color = await getNodeColor(element); return element; } ) ).then(() => { setNodes(nodes); setLinks(links); }); } }, [transaction]); useEffect(() => { setIsLoading(true); if (!dataService || isIsolatedServer === null) return; d3.select(ref.current).selectAll("*").remove(); if (nodes.length) { const nodeWidth = 362; const nodeHeight = 40; // set the dimensions and margins of the graph const width = 1200, height = 570; // append the svg object to the body of the page const svg = d3 .select(ref.current) .append("svg") .attr("width", width) .attr("height", height); const link = svg .append("g") .attr("fill", "none") .attr("stroke-width", 1.5) .selectAll("path") .data(links); const linkLine = link .join("path") .attr("stroke", "#aaa") .attr("id", (d: any, index: number) => { return `linkLine-${index}`; }) .attr("marker-end", (d: any) => { return `url(${new URL(`#arrow-${d.target}`, window.location.href)})`; }) .on("click", openModal); const linkTextContainer = link .enter() .append("text") .attr("transform", "translate(0,0)") .style("cursor", "pointer") .on("click", openModal); linkTextContainer.append("title").text(function (d: any) { if (d.index === 0) { return d.index + 1; } if (d.data && d.data.msg && d.data.msg._tag) { return d.data.msg._tag.length > 15 ? `${d.index + 1}.${d.data.msg._tag.substring(0, 15)}...` : `${d.index + 1}.${d.data.msg._tag}`; } return d.index + 1; }); linkTextContainer .append("textPath") .attr("class", "linkText") .attr("transform", "translate(56,0)") .text(function (d: any) { if (d.index === 0) { return d.index + 1; } if (d.data && d.data.msg && d.data.msg._tag) { return d.data.msg._tag.length > 15 ? `${d.index + 1}.${d.data.msg._tag.substring(0, 15)}...` : `${d.index + 1}.${d.data.msg._tag}`; } return d.index + 1; }) .style("font-size", "16px") .style("font-family", "monospace") .attr("fill", "#fff") .attr("xlink:xlink:href", (d: any, index: number) => { return `#linkLine-${index}`; }); // Initialize the nodes const nodeg = svg .selectAll("rect") .data(nodes) .enter() .append("g"); const node = nodeg .append("rect") .attr("class", "node-rect") .attr("width", nodeWidth) .attr("height", nodeHeight) .attr("stroke-width", 2) .attr("fill", (d: any) => d.color) .attr("x", -500) .attr("y", -500); node.on("click", openNode); const label = nodeg .append("text") .text(function (d: any) { return d.id; }) .style("font-size", "14px") .style("font-family", "monospace") .style("cursor", "pointer") .attr("fill", "#fff") .attr("x", -500) .attr("y", -500) .on("click", openNode); node.append("title").text(function (d: any) { return d.id; }); // Per-type markers, as they don't inherit styles. svg .append("defs") .selectAll("marker") .data(nodes) .join("marker") .attr("id", (d: any) => `arrow-${d.id}`) .attr("viewBox", "0 -5 10 10") .attr("refX", 10) .attr("refY", 0) .attr("markerWidth", 5) .attr("markerHeight", 5) .attr("orient", "auto") .append("path") .attr("fill", "#fff") .attr("d", "M0,-5L10,0L0,5"); const arcs: string[] = []; const linkArc = (d: any) => { const fromx = d.source.x + nodeWidth / 2; const fromy = d.target.y > d.source.y ? d.source.y + nodeHeight : d.source.y; let tox = d.target.x + nodeWidth / 2; const toy = d.target.y > d.source.y ? d.target.y - 3 : d.target.y + nodeHeight + 3; if ( arcs.includes( `${d.source.x}-${d.source.y}-${d.target.x}-${d.target.y}` ) ) { tox = tox + 70; } arcs.push(`${d.source.x}-${d.source.y}-${d.target.x}-${d.target.y}`); return ` M${fromx},${fromy} A0,0 0 0,1 ${tox},${toy} `; }; // This function is run at each iteration of the force algorithm, updating the nodes position. const ticked = () => { node .attr("x", function (d: any) { return d.x; }) .attr("y", function (d: any) { return d.y; }); linkLine.attr("d", linkArc); label .attr("x", function (d: any) { return d.x + 4; }) .attr("y", function (d: any) { return d.y + nodeHeight / 2 + 5; }); linkTextContainer.attr("dx", function () { return 50; }); linkTextContainer.attr("dy", function () { return -5; }); setIsLoading(false); }; // Let's list the force we wanna apply on the network d3.forceSimulation(nodes) // Force algorithm is applied to data.nodes .force( "link", d3 .forceLink() // This force provides links between nodes .id(function (d: any) { return d.id; }) // This provide the id of a node .links(links) // and this the list of links ) //.force("charge", d3.forceManyBody().strength(-300)) // This adds repulsion between nodes. Play with the -400 for the repulsion strength .force("center", d3.forceCenter(width / 3, height / 2)) // This force attracts nodes to the center of the svg area //.force("x", d3.forceX()) //.force("y", d3.forceY()) .force("collide", d3.forceCollide(160)) .on("end", ticked); } }, [nodes, links]); const closeModal = () => { document.body.classList.remove("has-modal-open"); setModalDisplay(false); }; return (
{loading || isLoading ? (
) : null} {error || isError ? (
Transaction Flow error:{" "} {error ? error.message : isError ? isError.message : null}
) : ( transaction && (

Transaction call-graph

) )}
); }; export default TransactionFlow;