From b29941167b2dbf2f813435e6a70f881c23c72cdf Mon Sep 17 00:00:00 2001 From: Lucemans Date: Tue, 19 Mar 2024 13:25:31 +0000 Subject: [PATCH] Introduce Aggregates & Transactions --- web/src/App.tsx | 68 ++++++++- web/src/main.tsx | 5 + web/src/txHistory/TransactionEntry.tsx | 176 ++++++----------------- web/src/txHistory/TransactionHistory.tsx | 31 ++-- web/src/utils/aggregateTotals.ts | 41 ++++++ web/src/utils/decodeTransaction.ts | 98 +++++++++++++ 6 files changed, 264 insertions(+), 155 deletions(-) create mode 100644 web/src/utils/aggregateTotals.ts create mode 100644 web/src/utils/decodeTransaction.ts diff --git a/web/src/App.tsx b/web/src/App.tsx index bfe47a1..00f2995 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,8 +1,40 @@ +import { useMemo } from 'react'; +import useSWR from 'swr'; + +import { getTransactions } from './etherscan/getTransactions'; import { TransactionHistory } from './txHistory/TransactionHistory'; +import { Aggregates, aggregateTotals } from './utils/aggregateTotals'; +import { + AllMultiReturnTypes, + decodeTransaction, +} from './utils/decodeTransaction'; +import { formatThousands } from './utils/formatThousands'; -const resolverAddress = '0xFC0a4A934410F34A9bb8b4F28bEd6b960C943a7E'; +const contractAddress = '0xFC0a4A934410F34A9bb8b4F28bEd6b960C943a7E'; export const App = () => { + const { data, isLoading, error } = useSWR( + '/transactions', + async () => await getTransactions(contractAddress), + { + keepPreviousData: true, + } + ); + + const decodedTransactions = useMemo( + () => + data?.result.map(decodeTransaction).filter(Boolean) as + | AllMultiReturnTypes[] + | undefined, + [data] + ); + + const totals = useMemo(() => { + if (!decodedTransactions) return; + + return aggregateTotals(decodedTransactions) as Aggregates; + }, [decodedTransactions]); + return (
@@ -18,7 +50,7 @@ export const App = () => { {
Commit Fee
-
0.00
+ {totals?.commitAverage ? ( +
+ {formatThousands(totals.commitAverage)} +
+ ) : ( +
Loading...
+ )}
Registration Fee
-
0.00
+ {totals?.registerAverage ? ( +
+ {formatThousands(totals.registerAverage)} +
+ ) : ( +
Loading...
+ )}{' '}
Renewal Fee
-
0.00
+ {totals?.renewAverage ? ( +
+ {formatThousands(totals.renewAverage)} +
+ ) : ( +
Loading...
+ )}{' '}
- +
); diff --git a/web/src/main.tsx b/web/src/main.tsx index dcd0251..23a8157 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -5,6 +5,11 @@ import ReactDOM from 'react-dom/client'; import { App } from './App'; +// @ts-ignore +BigInt.prototype.toJSON = function () { + return this.toString(); +}; + ReactDOM.createRoot(document.querySelector('#root') as HTMLElement).render( diff --git a/web/src/txHistory/TransactionEntry.tsx b/web/src/txHistory/TransactionEntry.tsx index 2e7fd79..0daffb1 100644 --- a/web/src/txHistory/TransactionEntry.tsx +++ b/web/src/txHistory/TransactionEntry.tsx @@ -3,105 +3,22 @@ import { FC } from 'react'; import { BsFuelPump } from 'react-icons/bs'; import { FiBox, FiChevronDown } from 'react-icons/fi'; import { LuFlame } from 'react-icons/lu'; -import { decodeFunctionData } from 'viem'; -import { ultraBulkAbi } from '../abi'; -import { EtherscanTx } from '../etherscan/getTransactions'; import { formatFullAndRelativeDate } from '../utils/date'; +import { + AllMultiReturnTypes, + deriveLabelFromFunctionName, +} from '../utils/decodeTransaction'; import { useEthUsd } from '../utils/ethUsd'; import { formatThousands } from '../utils/formatThousands'; import { gasToEth } from '../utils/gas'; -type DecodedFunction = { functionName: K; args: V }; -type MultiRegisterType = DecodedFunction< - 'multiRegister', - [string[], string[], bigint, string, string] ->; -type MultiCommitType = DecodedFunction<'multiCommit', [string[]]>; -type MultiRenewType = DecodedFunction<'renewAll', [string[], bigint, bigint]>; - -type AllMultiReturnTypes = MultiRegisterType | MultiCommitType | MultiRenewType; - -const decodeFunctionInput = ( - inputData: string, - to: string -): AllMultiReturnTypes | undefined => { - // If contract Create - if (to == '') { - return; - } - - try { - const { args, functionName } = decodeFunctionData({ - abi: ultraBulkAbi, - data: inputData as '0x{string}', - }); - - if (functionName == 'multiRegister' && args) { - return { - functionName, - args, - } as MultiRegisterType; - } - - if (functionName == 'multiCommit' && args) { - return { - functionName, - args, - } as MultiCommitType; - } - - if (functionName == 'renewAll' && args) { - return { - functionName, - args, - } as MultiRenewType; - } - } catch (error) { - console.error({ e: error }); - } -}; - -const deriveLabelFromFunctionName = (functionName: string, to: string) => { - if (to == '') return 'Deploy'; - - if (functionName.startsWith('multiRegister(')) return 'Register'; - - if (functionName.startsWith('multiCommit(')) return 'Commit'; - - if (functionName.startsWith('renewAll(')) return 'Renew'; - - return `Unknown (${functionName.split('(').shift()})`; -}; - -const getNameLength = (inputData?: AllMultiReturnTypes) => { - if (!inputData) return; - - if (inputData.functionName == 'multiRegister') { - return inputData.args[0].length; - } - - if (inputData.functionName == 'multiCommit') { - return inputData.args[0].length; - } - - if (inputData.functionName == 'renewAll') { - return inputData.args[0].length; - } -}; - -export const TransactionEntry: FC<{ txHash: EtherscanTx }> = ({ txHash }) => { - const actionLabel = deriveLabelFromFunctionName( - txHash.functionName, - txHash.to - ); - const inputData = decodeFunctionInput(txHash.input, txHash.to); - const ethUsd = useEthUsd(Number(txHash.timeStamp) * 1000); - - const namesLength = getNameLength(inputData); +export const TransactionEntry: FC<{ tx: AllMultiReturnTypes }> = ({ tx }) => { + const actionLabel = deriveLabelFromFunctionName(tx.functionName, tx.tx.to); + const ethUsd = useEthUsd(Number(tx.tx.timeStamp) * 1000); const { full, relative } = formatFullAndRelativeDate( - new Date(Number(txHash.timeStamp) * 1000) + new Date(Number(tx.tx.timeStamp) * 1000) ); return ( @@ -109,10 +26,10 @@ export const TransactionEntry: FC<{ txHash: EtherscanTx }> = ({ txHash }) => {
- {txHash.hash.slice(0, 8)}... + {tx.tx.hash.slice(0, 8)}...
= ({ txHash }) => { title="Blocknumber" > - {txHash.blockNumber} + {tx.tx.blockNumber}
{relative}
{actionLabel}
- {namesLength && ( + {tx.length > 0 && (
-
{namesLength}
+
{tx.length}
names
)} - {namesLength && ( + {tx.length > 0 && (
{formatThousands( - BigInt(txHash.gasUsed) / BigInt(namesLength) + BigInt(tx.tx.gasUsed) / BigInt(tx.length) )}
Per Name
@@ -146,15 +63,15 @@ export const TransactionEntry: FC<{ txHash: EtherscanTx }> = ({ txHash }) => {
-
{formatThousands(BigInt(txHash.gasUsed))}
+
{formatThousands(BigInt(tx.tx.gasUsed))}
{ethUsd.data && (
{( ethUsd.data * gasToEth( - BigInt(txHash.gasUsed), - BigInt(txHash.gasPrice) + BigInt(tx.tx.gasUsed), + BigInt(tx.tx.gasPrice) ) ).toFixed(2)}{' '} USD @@ -163,12 +80,12 @@ export const TransactionEntry: FC<{ txHash: EtherscanTx }> = ({ txHash }) => {
- {(Number(BigInt(txHash.gasPrice) / 100_000_000n) / 10) + {(Number(BigInt(tx.tx.gasPrice) / 100_000_000n) / 10) .toPrecision(2) .toString()}
- {inputData?.functionName === 'multiRegister' || - (inputData?.functionName === 'renewAll' && ( -
-
Names
-
-
    - {inputData?.args[0].map((name, _index) => ( -
  • - - {name} - -
  • - ))} -
-
+ {['renewAll', 'multiRegister'].includes(tx.functionName) && ( +
+
Names
+
+
    + {tx.args[0].map((name, _index) => ( +
  • + + {name} + +
  • + ))} +
- ))} - {inputData?.functionName === 'multiCommit' && ( +
+ )} + {tx.functionName === 'multiCommit' && (
Commitments
    - {inputData?.args[0].map( - (commitment, _index) => ( -
  • {commitment}
  • - ) - )} + {tx.args[0].map((commitment, _index) => ( +
  • {commitment}
  • + ))}
)}
- {JSON.stringify(txHash)} + {JSON.stringify(tx)}
diff --git a/web/src/txHistory/TransactionHistory.tsx b/web/src/txHistory/TransactionHistory.tsx index d9d36ee..a2d0caa 100644 --- a/web/src/txHistory/TransactionHistory.tsx +++ b/web/src/txHistory/TransactionHistory.tsx @@ -1,20 +1,15 @@ import { FC } from 'react'; -import useSWR from 'swr'; +import { AllMultiReturnTypes } from 'src/utils/decodeTransaction'; -import { getTransactions } from '../etherscan/getTransactions'; import { TransactionEntry } from './TransactionEntry'; -export const TransactionHistory: FC<{ contractAddress: string }> = ({ - contractAddress, -}) => { - const { data, isLoading, error } = useSWR( - '/transactions', - async () => await getTransactions(contractAddress), - { - keepPreviousData: true, - } - ); - +export const TransactionHistory: FC<{ + contractAddress: string; + txs?: (AllMultiReturnTypes | undefined)[]; + totals: any; + error: any; + isLoading: boolean; +}> = ({ txs, error, isLoading }) => { return (

@@ -31,17 +26,17 @@ export const TransactionHistory: FC<{ contractAddress: string }> = ({ {error.message}

)} - {data && ( + {txs && ( <> - {data.result.length === 0 && ( + {txs.length === 0 && (

No Txs found

)} - {data.result.length > 0 && ( + {txs.length > 0 && (
- {data.result.map((tx) => ( - + {txs.filter(Boolean).map((tx) => ( + ))}
)} diff --git a/web/src/utils/aggregateTotals.ts b/web/src/utils/aggregateTotals.ts new file mode 100644 index 0000000..fc004bd --- /dev/null +++ b/web/src/utils/aggregateTotals.ts @@ -0,0 +1,41 @@ +import { AllMultiReturnTypes } from './decodeTransaction'; + +export const aggregateTotals = (txs: AllMultiReturnTypes[]) => { + const aggregates = txs.reduce( + (aggregate, current) => { + if (current?.functionName == 'renewAll') { + aggregate.renewCount += BigInt(current.length); + aggregate.renewTotal += BigInt(current.tx.gasUsed); + } + + if (current?.functionName == 'multiCommit') { + aggregate.commitCount += BigInt(current.length); + aggregate.commitTotal += BigInt(current.tx.gasUsed); + } + + if (current?.functionName == 'multiRegister') { + aggregate.registerCount += BigInt(current.length); + aggregate.registerTotal += BigInt(current.tx.gasUsed); + } + + return aggregate; + }, + { + commitTotal: 0n, + commitCount: 0n, + renewTotal: 0n, + renewCount: 0n, + registerTotal: 0n, + registerCount: 0n, + } + ); + + return { + commitAverage: aggregates.commitTotal / aggregates.commitCount, + registerAverage: aggregates.registerTotal / aggregates.registerCount, + renewAverage: aggregates.renewTotal / aggregates?.renewCount, + ...aggregates, + }; +}; + +export type Aggregates = ReturnType; diff --git a/web/src/utils/decodeTransaction.ts b/web/src/utils/decodeTransaction.ts new file mode 100644 index 0000000..c679f22 --- /dev/null +++ b/web/src/utils/decodeTransaction.ts @@ -0,0 +1,98 @@ +import { decodeFunctionData } from 'viem'; + +import { ultraBulkAbi } from '../abi'; +import { EtherscanTx } from '../etherscan/getTransactions'; + +type DecodedFunction = { + functionName: K; + args: V; + length: number; + tx: EtherscanTx; +}; +type MultiRegisterType = DecodedFunction< + 'multiRegister', + [string[], string[], bigint, string, string] +>; +type MultiCommitType = DecodedFunction<'multiCommit', [string[]]>; +type MultiRenewType = DecodedFunction<'renewAll', [string[], bigint, bigint]>; + +export type AllMultiReturnTypes = + | MultiRegisterType + | MultiCommitType + | MultiRenewType; + +export const decodeTransaction = ( + tx: EtherscanTx +): AllMultiReturnTypes | undefined => { + // If contract Create + if (tx.to == '') { + return; + } + + try { + const { args, functionName } = decodeFunctionData({ + abi: ultraBulkAbi, + data: tx.input as '0x{string}', + }); + + const length = getNameLength(functionName, args as any); + + if (functionName == 'multiRegister' && args) { + return { + functionName, + args, + length, + tx, + } as MultiRegisterType; + } + + if (functionName == 'multiCommit' && args) { + return { + functionName, + args, + length, + tx, + } as MultiCommitType; + } + + if (functionName == 'renewAll' && args) { + return { + functionName, + args, + length, + tx, + } as MultiRenewType; + } + } catch (error) { + console.error({ e: error }); + } +}; + +export const deriveLabelFromFunctionName = ( + functionName: string, + to: string +) => { + if (to == '') return 'Deploy'; + + if (functionName.startsWith('multiRegister')) return 'Register'; + + if (functionName.startsWith('multiCommit')) return 'Commit'; + + if (functionName.startsWith('renewAll')) return 'Renew'; + + return `Unknown (${functionName.split('(').shift()})`; +}; + +const getNameLength = (functionName: string, arguments_: unknown[][]) => { + if (functionName == 'multiRegister') { + return arguments_[0].length; + } + + if (functionName == 'multiCommit') { + return arguments_[0].length; + } + + if (functionName == 'renewAll') { + return arguments_[0].length; + } +};