diff --git a/moralis-scan/README.md b/moralis-scan/README.md index 07d0b8ccf..caf77706f 100644 --- a/moralis-scan/README.md +++ b/moralis-scan/README.md @@ -16,6 +16,8 @@ [Part 7 - I Cloned Etherscan in 5 hours - Pagination Component](https://youtu.be/3x5eQdZo9i0) +[Part 8 - I Cloned Etherscan in 5 hours - Token Transactions](https://youtu.be/lzjMcjn6XCk) + # Getting Started with Create React App This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). diff --git a/moralis-scan/src/App.js b/moralis-scan/src/App.js index 0f7e22c25..b70be6960 100644 --- a/moralis-scan/src/App.js +++ b/moralis-scan/src/App.js @@ -4,7 +4,7 @@ import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; import "./App.css"; import Home from "./components/Home"; import Header from "./Header"; -import Transactions from "./components/Transactions"; +import AddressResults from "./components/AddressResults"; Moralis.initialize(process.env.REACT_APP_MORALIS_APPLICATION_ID); Moralis.serverURL = process.env.REACT_APP_MORALIS_SERVER_URL; @@ -15,7 +15,7 @@ function App() {
- + diff --git a/moralis-scan/src/components/AddressResults.jsx b/moralis-scan/src/components/AddressResults.jsx new file mode 100644 index 000000000..b276d3b98 --- /dev/null +++ b/moralis-scan/src/components/AddressResults.jsx @@ -0,0 +1,52 @@ +import React from 'react' +import { + BrowserRouter as Router, + Switch, + Route, + NavLink, + useParams, +} from "react-router-dom"; +import Transactions from './Transactions'; + +export default function AddressResults() { + const {address} = useParams(); + return ( +
+ +
+
+
    +
  • + + Transactions + +
  • +
  • + + ERC20 Token Txns + +
  • +
+
+
+ + + +
+
+
+
+ ); +} diff --git a/moralis-scan/src/components/Search.jsx b/moralis-scan/src/components/Search.jsx index b52d01479..92676a1e9 100644 --- a/moralis-scan/src/components/Search.jsx +++ b/moralis-scan/src/components/Search.jsx @@ -1,6 +1,6 @@ -import React, { useMemo, useState } from 'react' -import { useHistory } from 'react-router'; -import { useMoralisCloudQuery } from '../hooks/cloudQuery'; +import React, { useMemo, useState } from "react"; +import { useHistory } from "react-router"; +import { useMoralisCloudQuery } from "../hooks/cloudQuery"; export default function Search() { const [searchTxt, setSearchTxt] = useState(""); @@ -10,11 +10,18 @@ export default function Search() { const onSearchTextChanged = (e) => setSearchTxt(e.target.value); - const watchParams = useMemo(()=> ({ - params: {address}, // query params - onSuccess: () => history.push(`/address/${address}`), - }), [address, history]); - const {loading} = useMoralisCloudQuery("searchEthAddress", watchParams); + const watchParams = useMemo( + () => ({ + params: { address }, // query params + onSuccess: () => { + if (address) { + history.push(`/address/${address}/main`); + } + }, + }), + [address, history] + ); + const { loading } = useMoralisCloudQuery("searchEthAddress", watchParams); const submitSearch = async (e) => { e.preventDefault(); diff --git a/moralis-scan/src/components/TransResults.jsx b/moralis-scan/src/components/TransResults.jsx index 1bc3ce1e1..7da8096b9 100644 --- a/moralis-scan/src/components/TransResults.jsx +++ b/moralis-scan/src/components/TransResults.jsx @@ -1,18 +1,10 @@ +import { useTransType } from "../hooks/useTransType"; import { useResultContext } from "./Paginator"; -const cols = [ - { colName: "Txn Hash", key: "hash" }, - { colName: "Block", key: "block_number" }, - { colName: "Age", key: "ago" }, - { colName: "From", key: "from_address" }, - { colName: "To", key: "to_address" }, - { colName: "Value", key: "value" }, - { colName: "Txn Fee", key: "gas_price" }, -]; - export default function TransResults() { const { results } = useResultContext(); - if (!results) { + const { cols } = useTransType(); + if (!results || !cols) { return null; } diff --git a/moralis-scan/src/components/Transactions.jsx b/moralis-scan/src/components/Transactions.jsx index 04ea1214a..9a7f0d24c 100644 --- a/moralis-scan/src/components/Transactions.jsx +++ b/moralis-scan/src/components/Transactions.jsx @@ -1,11 +1,12 @@ import React from 'react' import { useParams } from 'react-router'; -import { processTransaction } from '../queries/transactions'; +import { useTransType } from '../hooks/useTransType'; import Paginator from './Paginator'; import TransResults from './TransResults'; export default function Transactions() { const {address } = useParams(); + const {methodName, postProcess} = useTransType(); if (!address) { return null; } @@ -14,8 +15,8 @@ export default function Transactions() {
diff --git a/moralis-scan/src/hooks/useTransType.js b/moralis-scan/src/hooks/useTransType.js new file mode 100644 index 000000000..03505f14c --- /dev/null +++ b/moralis-scan/src/hooks/useTransType.js @@ -0,0 +1,43 @@ +import { useEffect, useState } from "react" +import { useParams } from "react-router"; +import { processTokenTransfer } from "../queries/tokenTransfer"; +import { processTransaction } from "../queries/transactions"; + +const TransTypeOptions = { + main: { + cols: [ + { colName: "Txn Hash", key: "hash" }, + { colName: "Block", key: "block_number" }, + { colName: "Age", key: "ago" }, + { colName: "From", key: "from_address" }, + { colName: "To", key: "to_address" }, + { colName: "Value", key: "value" }, + { colName: "Txn Fee", key: "gas_price" }, + ], + methodName: "getTransactions", + postProcess: processTransaction, + }, + erc20: { + cols: [ + { colName: "Txn Hash", key: "transaction_hash" }, + { colName: "Age", key: "ago" }, + { colName: "From", key: "from_address" }, + { colName: "To", key: "to_address" }, + { colName: "Value", key: "value" }, + { colName: "Token", key: "name" }, + ], + methodName: "getTokenTranfers", + postProcess: processTokenTransfer, + }, +}; + +export const useTransType = () => { + const [state, setState] = useState({}); + const { transType } = useParams(); + + useEffect(()=>{ + setState(TransTypeOptions[transType]) + }, [transType]) + + return state; +} diff --git a/moralis-scan/src/queries/cloud-queries.js b/moralis-scan/src/queries/cloud-queries.js new file mode 100644 index 000000000..dced33686 --- /dev/null +++ b/moralis-scan/src/queries/cloud-queries.js @@ -0,0 +1,89 @@ +import Moralis from "moralis"; + +Moralis.Cloud.define("searchEthAddress", async function(request) { + const { address } = request.params + if (!address) { + return null + } + + // find out if address is already watched + const query = new Moralis.Query("WatchedEthAddress"); + query.equalTo("address", address) + const watchCount = await query.count(); + + if (watchCount > 0) { + // already watched don't sync again + return null; + } + + return Moralis.Cloud.run("watchEthAddress", {address}); +}); + +Moralis.Cloud.define("getTransactions", function(request) { + const {userAddress, pageSize, pageNum } = request.params; + const offset = (pageNum - 1) * pageSize; + + const query = new Moralis.Query("EthTransactions"); + query.equalTo("from_address", userAddress); + query.descending("block_number"); + query.withCount(); + query.skip(offset) + query.limit(pageSize); + + return query.find(); +}); + +Moralis.Cloud.define("getTokenTranfers", async (request) => { + const {userAddress, pageSize, pageNum } = request.params; + const offset = (pageNum - 1) * pageSize; + const output = { + results: [], + count: 0, + }; + + // count results + const matchPipeline = { + match: { + $expr: { + $or: [ + { $eq: ["$from_address", userAddress] }, + { $eq: ["$to_address", userAddress] }, + ], + }, + }, + sort: { block_number: -1 }, + count: "count", + }; + const query = new Moralis.Query("EthTokenTransfers"); + const countResult = await query.aggregate(matchPipeline); + output.count = countResult[0].count; + + // get page results + const lookupPipeline = { + ...matchPipeline, + skip: offset, + limit: pageSize, + lookup: { + from: "EthTokenBalance", + let: { tokenAddress: "$token_address", userAddress }, + pipeline: [ + { + $match: { + $expr: { + $and: [ + { $eq: ["$token_address", "$$tokenAddress"] }, + { $eq: ["$address", "$$userAddress"] }, + ], + }, + }, + }, + ], + as: "EthTokenBalance", + }, + unwind: "$EthTokenBalance", + }; + delete lookupPipeline.count; + + output.results = await query.aggregate(lookupPipeline); + return output; +}); diff --git a/moralis-scan/src/queries/tokenTransfer.js b/moralis-scan/src/queries/tokenTransfer.js new file mode 100644 index 000000000..d18ddb35c --- /dev/null +++ b/moralis-scan/src/queries/tokenTransfer.js @@ -0,0 +1,14 @@ +import { agoTxt, getEllipsisTxt, tokenValueTxt } from "./utils"; + +export const processTokenTransfer = (r) => ({ + transaction_hash: getEllipsisTxt(r.transaction_hash), + ago: agoTxt(r.block_timestamp.valueOf()), + from_address: getEllipsisTxt(r.from_address), + to_address: getEllipsisTxt(r.to_address), + value: tokenValueTxt( + r.value, + r.EthTokenBalance?.decimals, + r.EthTokenBalance?.symbol + ), + name: r.EthTokenBalance?.name || "", + });