Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix date displayed with no Tx #434

Merged
merged 23 commits into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions packages/ui/src/components/CallInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import Expander from './Expander'
import { styled } from '@mui/material/styles'
import { AggregatedData } from './Transactions/TransactionList'
import { AnyJson } from '@polkadot/types/types'
import { ReactNode, useMemo } from 'react'
import { useApi } from '../contexts/ApiContext'
Expand All @@ -12,11 +11,11 @@ import { Link } from './library'
import { usePjsLinks } from '../hooks/usePjsLinks'
import { Alert } from '@mui/material'
import { ApiPromise } from '@polkadot/api'
import { isTypeBalance } from '../utils/isTypeBalance'
import { isTypeAccount } from '../utils/isTypeAccount'
import { isTypeBalance, isTypeAccount } from '../utils'
import { CallDataInfoFromChain } from '../hooks/usePendingTx'

interface Props {
aggregatedData: Omit<AggregatedData, 'from' | 'timestamp'>
aggregatedData: Omit<CallDataInfoFromChain, 'from' | 'timestamp'>
expanded?: boolean
children?: ReactNode
className?: string
Expand Down
4 changes: 2 additions & 2 deletions packages/ui/src/components/Transactions/Transaction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ import { styled } from '@mui/material/styles'
import CallInfo from '../CallInfo'
import { MdOutlineGesture as GestureIcon } from 'react-icons/md'
import { HiOutlineQuestionMarkCircle as QuestionMarkIcon } from 'react-icons/hi2'
import { AggregatedData } from './TransactionList'
import { useCallback, useMemo } from 'react'
import { isProxyCall } from '../../utils'
import { AccountBadge } from '../../types'
import TransactionProgress from './TransactionProgress'
import { useModals } from '../../contexts/ModalsContext'
import { CallDataInfoFromChain } from '../../hooks/usePendingTx'

interface Props {
className?: string
aggregatedData: AggregatedData
aggregatedData: CallDataInfoFromChain
isProposer: boolean
possibleSigners: string[]
multisigSignatories: string[]
Expand Down
194 changes: 12 additions & 182 deletions packages/ui/src/components/Transactions/TransactionList.tsx
Original file line number Diff line number Diff line change
@@ -1,203 +1,29 @@
import { Box, CircularProgress, Paper } from '@mui/material'
import { useEffect, useState } from 'react'
import { styled } from '@mui/material/styles'
import { PendingTx, usePendingTx } from '../../hooks/usePendingTx'
import { usePendingTx } from '../../hooks/usePendingTx'
import { useMultiProxy } from '../../contexts/MultiProxyContext'
import { ApiPromise } from '@polkadot/api'
import { useApi } from '../../contexts/ApiContext'
import { getDifference, getDisplayArgs, getIntersection, isProxyCall } from '../../utils'
import { getDifference, getIntersection } from '../../utils'
import { useAccounts } from '../../contexts/AccountsContext'
import { ISanitizedCall, parseGenericCall } from '../../utils'
import { GenericCall } from '@polkadot/types'
import { AnyJson, AnyTuple } from '@polkadot/types/types'
import { MdOutlineFlare as FlareIcon } from 'react-icons/md'
import Transaction from './Transaction'
import { HexString } from '../../types'
import dayjs from 'dayjs'
import localizedFormat from 'dayjs/plugin/localizedFormat'
dayjs.extend(localizedFormat)

export interface AggregatedData {
callData?: HexString
hash?: string
name?: string
args?: AnyJson
info?: PendingTx['info']
from: string
timestamp: Date | undefined
}

type AggGroupedByDate = { [index: string]: AggregatedData[] }

const sortByLatest = (a: AggregatedData, b: AggregatedData) => {
if (!a.timestamp || !b.timestamp) return 0

return b.timestamp.valueOf() - a.timestamp.valueOf()
}

interface Props {
className?: string
}

export const getMultisigInfo = (c: ISanitizedCall): Partial<AggregatedData>[] => {
const result: Partial<AggregatedData>[] = []

const getCallResult = (c: ISanitizedCall) => {
if (typeof c.method !== 'string' && c.method.pallet === 'multisig') {
if (c.method.method === 'asMulti' && typeof c.args.call?.method !== 'string') {
result.push({
name: `${c.args.call?.method?.pallet}.${c.args.call?.method.method}`,
hash: c.args.call?.hash,
callData: c.args.callData as AggregatedData['callData']
})
} else {
result.push({
name: 'Unknown call',
hash: (c.args?.call_hash as Uint8Array).toString() || undefined,
callData: undefined
})
}
// this is not a multisig call
// try to dig deeper
} else {
if (c.args.calls) {
for (const call of c.args.calls) {
getCallResult(call)
}
} else if (c.args.call) {
getCallResult(c.args.call)
}
}
}

getCallResult(c)
return result
}

const getAgregatedDataPromise = (pendingTxData: PendingTx[], api: ApiPromise) =>
pendingTxData.map(async (pendingTx) => {
const blockHash = await api.rpc.chain.getBlockHash(pendingTx.info.when.height)
const signedBlock = await api.rpc.chain.getBlock(blockHash)

let date: Date | undefined

// get the timestamp which is an unsigned extrinsic set by the validator in each block
// the information for each of the contained extrinsics
signedBlock.block.extrinsics.some(({ method: { args, method, section } }) => {
// check for timestamp.set
if (section === 'timestamp' && method === 'set') {
// extract the Option<Moment> as Moment
const moment = args.toString()

// convert to date (unix ms since epoch in Moment - exactly as per the Rust code)
date = new Date(Number(moment))
return true
}

return false
})

const ext = signedBlock.block.extrinsics[pendingTx.info.when.index]

const decoded = parseGenericCall(ext.method as GenericCall, ext.registry)
// console.log('pendingTxData', pendingTxData)
// console.log('decoded', decoded)
const multisigInfos = getMultisigInfo(decoded) || {}

const info = multisigInfos.find(({ name, hash, callData }) => {
if (!!hash && hash === pendingTx.hash) {
return { name, hash, callData }
}

return false
})

if (!info) {
console.log('oops we didnot find the right extrinsic', multisigInfos, pendingTx.hash)
return
}

const { name, hash, callData } = info

let call: false | GenericCall<AnyTuple> = false
try {
call = !!callData && !!hash && ext.registry.createType('Call', callData)
} catch (error) {
console.error('Error in TransactionList')
console.error(error)
}

return {
callData,
hash: hash || pendingTx.hash,
name,
args: getDisplayArgs(call),
info: pendingTx.info,
from: pendingTx.from,
timestamp: date
} as AggregatedData
})

const TransactionList = ({ className }: Props) => {
const [aggregatedData, setAggregatedData] = useState<AggGroupedByDate>({})
const { selectedMultiProxy, getMultisigByAddress } = useMultiProxy()
const {
data: pendingTxData,
isLoading: isLoadingPendingTxs,
refresh
} = usePendingTx(selectedMultiProxy)
const { api } = useApi()
const { getMultisigByAddress } = useMultiProxy()
const { txWithCallDataByDate, isLoading: isLoadingPendingTxs, refresh } = usePendingTx()
const { ownAddressList } = useAccounts()

useEffect(() => {
if (!api) {
return
}

if (!pendingTxData || !pendingTxData.length) {
setAggregatedData({})
return
}

const agregatedDataPromise = getAgregatedDataPromise(pendingTxData, api)

Promise.all(agregatedDataPromise)
.then((res) => {
const definedTxs = res.filter((agg) => agg !== undefined) as AggregatedData[]
const timestampObj: AggGroupedByDate = {}

// remove the proxy transaction that aren't for the selected proxy
const relevantTxs = definedTxs.filter((agg) => {
if (!isProxyCall(agg.name) || !agg?.args || !(agg.args as any).real.Id) {
return true
}

return (agg.args as any).real.Id === selectedMultiProxy?.proxy
})

// sort by date, the newest first
const sorted = relevantTxs.sort(sortByLatest)

// populate the object
sorted.forEach((data) => {
const date = dayjs(data.timestamp).format('LL')
const previousData = timestampObj[date] || []
timestampObj[date] = [...previousData, data]
})

setAggregatedData(timestampObj)
})
.catch(console.error)
}, [aggregatedData.args, api, pendingTxData, selectedMultiProxy])

return (
<Box className={className}>
{isLoadingPendingTxs && (
<Box className="loader">
<CircularProgress />
</Box>
)}
{Object.entries(aggregatedData).length === 0 && !isLoadingPendingTxs && (
{Object.entries(txWithCallDataByDate).length === 0 && !isLoadingPendingTxs && (
<Paper className="noCall">
<FlareIcon
size={24}
Expand All @@ -206,16 +32,20 @@ const TransactionList = ({ className }: Props) => {
<div className="noCallText">You're all set!</div>
</Paper>
)}
{Object.entries(aggregatedData).length !== 0 &&
Object.entries(aggregatedData).map(([date, aggregatedData]) => {
{Object.entries(txWithCallDataByDate).length !== 0 &&
Object.entries(txWithCallDataByDate).map(([date, aggregatedData]) => {
return (
<TransactionWrapper key={date}>
<DateContainerStyled>{date}</DateContainerStyled>
{aggregatedData.map((agg, index) => {
const { callData, info, from } = agg
const multisig = getMultisigByAddress(from)

if (!info || !multisig?.threshold) return null
// if the "from" is not a multisig from the
// currently selected multiProxy or we have no info
if (!info || !multisig?.threshold) {
return null
}

const multisigSignatories = multisig?.signatories || []
// if the threshold is met, but the transaction is still not executed
Expand Down
4 changes: 2 additions & 2 deletions packages/ui/src/components/modals/ProposalSigning.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { useAccounts } from '../../contexts/AccountsContext'
import { useApi } from '../../contexts/ApiContext'
import { useMultiProxy } from '../../contexts/MultiProxyContext'
import CallInfo from '../CallInfo'
import { AggregatedData } from '../Transactions/TransactionList'
import SignerSelection from '../select/SignerSelection'
import { SubmittableExtrinsic } from '@polkadot/api/types'
import { useToasts } from '../../contexts/ToastContext'
Expand All @@ -19,12 +18,13 @@ import { ModalCloseButton } from '../library/ModalCloseButton'
import { useGetSortAddress } from '../../hooks/useGetSortAddress'
import { useCheckBalance } from '../../hooks/useCheckBalance'
import BN from 'bn.js'
import { CallDataInfoFromChain } from '../../hooks/usePendingTx'

export interface SigningModalProps {
onClose: () => void
className?: string
possibleSigners: string[]
proposalData: AggregatedData
proposalData: CallDataInfoFromChain
onSuccess?: () => void
}

Expand Down
Loading
Loading