Skip to content
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
71 changes: 67 additions & 4 deletions src/features/accounts/api/get-multisig-txn-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,77 @@ import { useQuery } from "react-query"
import { useNetworkContext } from "features/network"
import { MultisigInfoResponse } from "@liftedinit/many-js"

const transactionNotFound = new Error("The transaction cannot be found.")
type ResponseOrError = MultisigInfoResponse | Error
type MultisigInfoFromNetworks = {
isLegacyOnly: boolean
info: MultisigInfoResponse[]
}

export function useGetMultisigTxnInfo(token?: ArrayBuffer) {
const [n] = useNetworkContext()
const [n, , l] = useNetworkContext()
// Create a network list from n and l, excluding undefined values and empty arrays
// `n` is the active network
// `l` is the legacy network list
const networkList = [...(n ? [n] : []), ...(l || [])]

return useQuery<MultisigInfoResponse, Error>({
return useQuery<MultisigInfoFromNetworks, Error>({
queryKey: ["multisigTxnInfo", token],
queryFn: async () => {
const res = await n?.account.multisigInfo(token)
return res
if (networkList.length === 0) {
throw new Error("Network context is empty")
}

// Try to find the transaction on the active network
const resActive: ResponseOrError = await n?.account
.multisigInfo(token)
.catch((err: Error) => {
if (err.message === transactionNotFound.message) {
return transactionNotFound
}
return err
})

// Try to find the transaction on the legacy networks
let resLegacy = await Promise.all(
(l || []).map(item =>
item.account.multisigInfo(token).catch((err: Error) => {
if (err.message === transactionNotFound.message) {
return transactionNotFound
}
return err
}),
),
)

// Remove the transactionNotFound errors from the result
resLegacy = resLegacy.filter(
item =>
!(
item instanceof Error &&
item.message === transactionNotFound.message
),
)

// Initialize the result with the active network result if it is not an error, else initialize it with an empty array
let res: MultisigInfoResponse[] = !(resActive instanceof Error)
? [resActive]
: []

// Add the legacy network results to the result
if (resLegacy.length > 0) {
res = [...res, ...resLegacy]
}

// Check if the result comes from legacy networks only
let legacyOnly = res.length > 0 && resActive instanceof Error

// If the result is empty, then the transaction really is not found on any network
if (res.length === 0) {
throw transactionNotFound
}

return { isLegacyOnly: legacyOnly, info: res }
},
enabled: !!token,
})
Expand Down
24 changes: 19 additions & 5 deletions src/features/network/network-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ import {
import { useNetworkStore } from "./store"
import { useAccountsStore } from "features/accounts"

const NetworkContext = React.createContext<[Network?, Network?]>([
undefined,
undefined,
const NetworkContext = React.createContext<[Network?, Network?, Network[]?]>([
undefined, // Query network
undefined, // Legacy networks
undefined, // Command network
])

export function NetworkProvider({ children }: React.PropsWithChildren<{}>) {
const activeNetwork = useNetworkStore(state => state.getActiveNetwork())
const legacyNetworks = useNetworkStore(state => state.getLegacyNetworks())
const activeAccount = useAccountsStore(state =>
state.byId.get(state.activeId),
)!
Expand All @@ -30,8 +32,20 @@ export function NetworkProvider({ children }: React.PropsWithChildren<{}>) {
queryNetwork.apply([Ledger, IdStore, Account, Events, Base])
const cmdNetwork = new Network(url, identity)
cmdNetwork.apply([Ledger, IdStore, Account])
return [queryNetwork, cmdNetwork] as [Network, Network]
}, [activeNetwork, activeAccount])
const eventNetworks =
activeNetwork?.name.toLowerCase() === "manifest ledger" // FIXME: Filtering by the network name is dumb. Improve me.
? legacyNetworks?.map(params => {
const network = new Network(params.url, anonIdentity)
network.apply([Account, Events])
return network
})
: []
return [queryNetwork, cmdNetwork, eventNetworks] as [
Network,
Network,
Network[],
]
}, [activeNetwork, legacyNetworks, activeAccount])

return (
<NetworkContext.Provider value={network}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
MultisigSetDefaultsEvent,
BurnEvent,
MintEvent,
MultisigTransactionInfo,
} from "@liftedinit/many-js"
import {
CheckCircleIcon,
Expand Down Expand Up @@ -187,63 +188,91 @@ export function useMultisigActions({
const rolesForIdentity =
accountInfoData?.accountInfo?.roles?.get(identityAddress)

const { data: multisigTxnInfoData } = useGetMultisigTxnInfo(txnToken)
const { data: maybeMultisigTxnInfoData } = useGetMultisigTxnInfo(txnToken)
const { isLegacyOnly, info: multisigTxnInfoData } =
maybeMultisigTxnInfoData ?? {}

const approvers = multisigTxnInfoData?.info?.approvers ?? new Map()
// Get the first result and compare it to the rest of the array, if any
const maybeFirst =
multisigTxnInfoData?.[0]?.info ?? ({} as MultisigTransactionInfo)

const approvers = maybeFirst.approvers ?? new Map<string, boolean>()
const isApprover = rolesForIdentity?.some(r => approverRoles.includes(r))

const isSubmitter = multisigTxnInfoData?.info?.submitter === identityAddress
const isSubmitter = maybeFirst.submitter === identityAddress
const state = maybeFirst.state

const isOwner = !!accountInfoData?.accountInfo?.roles
?.get(identityAddress)
?.includes(AccountRole[AccountRole.owner])

const isThresholdReached =
Array.from(multisigTxnInfoData?.info?.approvers ?? new Map()).reduce(
(acc, approver) => {
const [, hasApproved] = approver
if (hasApproved) acc += 1
return acc
},
Array.from(approvers).reduce(
(acc, [, hasApproved]) => acc + (hasApproved ? 1 : 0),
0,
) >= (multisigTxnInfoData?.info?.threshold ?? Infinity)
) >= (maybeFirst.threshold ?? Infinity)

// Are all the results the same?
const isSame = multisigTxnInfoData?.every(({ info }) => info === maybeFirst)
if (isSame === false) {
throw new Error(
"Discrepancy of the Multisig Transaction Info between networks.",
)
}

const createMutateHook = (hook: any) => {
const { reset, mutate, isLoading, error } = hook(txnToken)
return {
reset,
mutate,
isLoading,
error,
}
}

const {
reset: resetWithdraw,
mutate: doWithdraw,
isLoading: isWithdrawLoading,
error: withdrawError,
} = useMultisigWithdraw(txnToken)
const canWithdraw = isSubmitter || isOwner

} = createMutateHook(useMultisigWithdraw)
const {
reset: resetApprove,
mutate: doApprove,
isLoading: isApproveLoading,
error: approveError,
} = useMultisigApprove(txnToken)
const canApprove = approvers?.get(identityAddress)
? false
: !!rolesForIdentity?.some(r => approverRoles.includes(r))

} = createMutateHook(useMultisigApprove)
const {
reset: resetRevoke,
mutate: doRevoke,
isLoading: isRevokeLoading,
error: revokeError,
} = useMultisigRevoke(txnToken)
const canRevoke =
isApprover &&
(!approvers.has(identityAddress) || approvers.get(identityAddress) === true)

} = createMutateHook(useMultisigRevoke)
const {
reset: resetExecute,
mutate: doExecute,
isLoading: isExecuteLoading,
error: executeError,
} = useMultisigExecute(txnToken)
const canExecute = isThresholdReached && (isSubmitter || isOwner)
} = createMutateHook(useMultisigExecute)

const txIsPending = state === "pending" // TODO: Use MultisigTransactionState enum from many-js

const canWithdraw = (isSubmitter || isOwner) && !isLegacyOnly && txIsPending
const canApprove =
!approvers?.get(identityAddress) &&
isApprover &&
!isLegacyOnly &&
txIsPending
const canRevoke =
isApprover &&
(!approvers.has(identityAddress) ||
approvers.get(identityAddress) === true) &&
!isLegacyOnly &&
txIsPending
const canExecute =
isThresholdReached &&
(isSubmitter || isOwner) &&
!isLegacyOnly &&
txIsPending

const isLoading =
isApproveLoading || isRevokeLoading || isWithdrawLoading || isExecuteLoading
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,10 @@ export function MultisigTxnListItem({ txn }: { txn: MultisigEvent }) {
const { actionLabel, actorAddress, txnLabel, TxnIcon, iconProps } =
useMultisigTxn(txn)

const { data: multisigTxnInfoData } = useGetMultisigTxnInfo(token)
const { state } = multisigTxnInfoData?.info ?? {}
const { data: maybeMultisigTxnInfoData } = useGetMultisigTxnInfo(token)
const { info: multisigTxnInfoData } = maybeMultisigTxnInfoData ?? {}
let states = new Set(multisigTxnInfoData?.map(item => item.info?.state))
let state = states.size === 1 ? [...states][0] : undefined // FIXME: State can be different
const stateText = getTxnStateText(state)

const getContactName = useGetContactName()
Expand Down Expand Up @@ -196,31 +198,59 @@ export function SubmittedMultisigTxnDetails({
useGetAccountInfo(multisigTxn.account)

const {
data: multisigTxnInfoData,
data: maybeMultisigTxnInfoData,
isLoading: isMultisigTxnInfoLoading,
error: multisigTxnInfoError,
} = useGetMultisigTxnInfo(token)
const multisigTxnInfoData = maybeMultisigTxnInfoData?.info || []

const { memo, executeAutomatically, threshold, transaction, submitter } =
(multisigTxnInfoData?.info ?? {}) as MultisigTransactionInfo
// Get the first result and compare it to the rest of the array, if any
const maybeFirst =
multisigTxnInfoData?.[0]?.info ?? ({} as MultisigTransactionInfo)

// Are all the results the same?
const isSame = multisigTxnInfoData?.every(({ info }) => info === maybeFirst)

// If not, throw an error
const maybeError = isSame
? undefined
: new Error(
"Discrepancy of the Multisig Transaction Info between networks.",
)

// Get the first result as the MultisigTransactionInfo
// The destructured elements will be undefined if the map is empty
const {
memo,
executeAutomatically,
threshold,
transaction,
submitter,
expireDate,
} = maybeFirst

const memoStr = memo && typeof memo[0] === "string" ? memo[0] : ""

const submitterContactName = getContactName(submitter)

const approvers = makeApproversMap(
accountInfoData?.accountInfo?.roles,
multisigTxnInfoData?.info?.approvers,
maybeFirst.approvers,
getContactName,
)

if (getAccountInfoError || multisigTxnInfoError) {
const hasError = getAccountInfoError || multisigTxnInfoError || maybeError
const isExpireDateExist = maybeFirst.expireDate

if (hasError) {
return (
<Alert status="warning">
<AlertIcon />
<AlertDescription>
<Text>
{getAccountInfoError?.message ?? multisigTxnInfoError?.message}
{getAccountInfoError?.message ??
multisigTxnInfoError?.message ??
maybeError?.message}
</Text>
</AlertDescription>
</Alert>
Expand All @@ -247,10 +277,8 @@ export function SubmittedMultisigTxnDetails({
<DataField
label="Expire"
value={
multisigTxnInfoData?.info?.expireDate
? new Date(
multisigTxnInfoData?.info.expireDate * 1000, // Date expects milliseconds
).toLocaleString()
isExpireDateExist
? new Date(expireDate * 1000).toLocaleString() // Date expects milliseconds
: ""
}
/>
Expand Down Expand Up @@ -475,7 +503,7 @@ export function MultisigActions({
onSuccess: () => {
onActionDone("success", title, "Transaction was approved")
},
onError: err => {
onError: (err: { message: string }) => {
onActionDone("warning", title, err?.message)
},
})
Expand All @@ -500,7 +528,7 @@ export function MultisigActions({
onSuccess: () => {
onActionDone("success", title, "Transaction was revoked")
},
onError: err => {
onError: (err: { message: string }) => {
onActionDone("warning", title, err?.message)
},
})
Expand All @@ -524,7 +552,7 @@ export function MultisigActions({
onSuccess: () => {
onActionDone("success", title, "Transaction was withdrawn")
},
onError: err => {
onError: (err: { message: string }) => {
onActionDone("warning", title, err?.message)
},
})
Expand All @@ -548,7 +576,7 @@ export function MultisigActions({
onSuccess: () => {
onActionDone("success", title, "Transaction was executed")
},
onError: err => {
onError: (err: { message: string }) => {
onActionDone("warning", title, err?.message)
},
})
Expand Down