Skip to content

Commit

Permalink
Merge branch 'develop' into ui-fixes-contract-details
Browse files Browse the repository at this point in the history
  • Loading branch information
alongoni committed Nov 14, 2023
2 parents 594a19f + 6d315e4 commit a06cbb3
Show file tree
Hide file tree
Showing 12 changed files with 193 additions and 238 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,12 @@
"copy-to-clipboard": "^3.3.3",
"dexie": "^3.2.4",
"dotenv": "^16.3.1",
"json5": "^2.2.3",
"lodash": "^4.17.21",
"next": "13.4.19",
"next-plausible": "^3.11.3",
"react": "18.2.0",
"react-code-blocks": "^0.0.9-0",
"react-code-blocks": "^0.1.5",
"react-device-detect": "^2.2.2",
"react-dom": "18.2.0",
"react-dropzone": "^14.2.3",
Expand Down
3 changes: 2 additions & 1 deletion src/domain/UserContractDetails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ export interface UserContractDetails {
date: string
type: ContractType
abi?: Record<string, unknown>
external: boolean // Contracts not deployed by PCW are custom and external
// TODO
external: boolean // Represents a contract that has not been aggregated by the connected wallet accounts.
hidden: boolean
}

Expand Down
8 changes: 6 additions & 2 deletions src/domain/repositories/DeploymentRepository.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { ChainId } from '@/services/useink/chains'
import { TokenType } from '../TokenType'
import { UserContractDetailsDraft } from '../UserContractDetails'
import {
UserContractDetails,
UserContractDetailsDraft
} from '../UserContractDetails'

export type ContractType = TokenType | 'custom'
export type ContractType = TokenType | 'custom' // custom contracts are generally deployed outside the PCW
export type UpdateDeployment = Partial<UserContractDetailsDraft>

export interface IDeploymentsRepository<A, B> {
add: (deployment: UserContractDetailsDraft) => Promise<A>
findBy: (userAddress: string, network?: ChainId) => Promise<B>
updateBy: (deployment: UpdateDeployment) => Promise<A>
get(uuid: string): Promise<UserContractDetails | undefined>
}
32 changes: 26 additions & 6 deletions src/hooks/userContracts/useFindUserContract.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useLocalDbContext } from '@/context/LocalDbContext'
import { UserContractDetails } from '@/domain'
import { useEffect, useState } from 'react'
import { useCallback, useEffect, useState } from 'react'

interface UseFindUserContract {
userContract: UserContractDetails | undefined
Expand All @@ -14,15 +14,30 @@ export function useFindUserContract(uuid: string): UseFindUserContract {
>()
const [isLoading, setIsLoading] = useState(false)
const [requested, setRequested] = useState(false)
const { userContractsRepository, apiCompileContractRepository } =
useLocalDbContext()
const {
userContractsRepository,
apiCompileContractRepository,
apiDeploymentsRepository
} = useLocalDbContext()

const getUserContract = useCallback(
async (uuid: UserContractDetails['uuid']) => {
let knownUserContract = await userContractsRepository.get(uuid)

if (!knownUserContract) {
knownUserContract = await apiDeploymentsRepository.get(uuid)
}

return knownUserContract
},
[apiDeploymentsRepository, userContractsRepository]
)

useEffect(() => {
if (!uuid) return

setIsLoading(true)
userContractsRepository
.get(uuid)
getUserContract(uuid)
.then(async response => {
if (response && !response?.abi) {
const compiled = await apiCompileContractRepository.search(
Expand All @@ -36,7 +51,12 @@ export function useFindUserContract(uuid: string): UseFindUserContract {
setRequested(true)
})
.finally(() => setIsLoading(false))
}, [apiCompileContractRepository, userContractsRepository, uuid])
}, [
apiCompileContractRepository,
getUserContract,
userContractsRepository,
uuid
])

return { userContract, isLoading, requested }
}
12 changes: 1 addition & 11 deletions src/pages/contract-detail/index.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
import { ContractDetailSkeleton } from '@/view/CustomContracts/detail/SkeletonContractDetail'
import MainContainer from '@/view/layout/MainContainer'
import ContractDetail from '@/view/ContractDetailView'
import { useModalBehaviour } from '@/hooks/useModalBehaviour'
import { useRouter } from 'next/router'
import { useFindUserContract } from '@/hooks/userContracts/useFindUserContract'
import { useHasMounted } from '@/hooks/useHasMounted'
import { useDownloadMetadata } from '@/view/components/ContractsTable/useDownloadMetadata'

export default function CustomContractDetailPage() {
const router = useRouter()
const { uuid } = router.query
const { userContract, requested, isLoading } = useFindUserContract(
uuid as string
)
const { onDownloadSource } = useDownloadMetadata(userContract)
const modalBehaviour = useModalBehaviour()
const hasMounted = useHasMounted()

if (!userContract || !hasMounted || isLoading) {
Expand All @@ -28,13 +24,7 @@ export default function CustomContractDetailPage() {

return (
<MainContainer>
{userContract && (
<ContractDetail
modalBehaviour={modalBehaviour}
onDownloadSource={onDownloadSource}
userContract={userContract}
/>
)}
{userContract && <ContractDetail userContract={userContract} />}
</MainContainer>
)
}
12 changes: 12 additions & 0 deletions src/services/substrate/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@ export type {
RegistryError,
TypeDef
} from '@polkadot/types/types'
export type {
ContractInstantiateResult,
DispatchError,
EventRecord,
Weight,
WeightV2,
ChainType,
Hash,
ContractExecResult,
Balance,
ContractReturnFlags
} from '@polkadot/types/interfaces'

// classes
export { ApiPromise, SubmittableResult } from '@polkadot/api'
Expand Down
89 changes: 89 additions & 0 deletions src/utils/contractExecResult.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import JSON5 from 'json5'
import {
AbiMessage,
AnyJson,
Bytes,
ContractExecResult,
ContractReturnFlags,
Registry,
TypeDef
} from '@/services/substrate/types'

type ContractResult = {
Err?: AnyJson
Ok?: AnyJson
}

function isContractResult(obj: unknown): obj is ContractResult {
return (
typeof obj === 'object' && obj !== null && ('Err' in obj || 'Ok' in obj)
)
}

function getReturnTypeName(type: TypeDef | null | undefined): string {
return type?.lookupName || type?.type || ''
}

function stringify(obj: unknown): string {
return JSON5.stringify(obj, null, 2)
}

function decodeReturnValue(
returnType: TypeDef | null | undefined,
data: Bytes,
registry: Registry
): AnyJson {
if (!returnType) return '()'
const returnTypeName = getReturnTypeName(returnType)
try {
return registry.createTypeUnsafe(returnTypeName, [data]).toHuman()
} catch (exception) {
console.error(exception)
return 'Decoding error'
}
}

function checkRevertFlag(flags: ContractReturnFlags): boolean {
return flags.toHuman().includes('Revert')
}

function extractOutcome(returnValue: AnyJson): AnyJson {
if (!isContractResult(returnValue)) return returnValue
return returnValue.Err ?? returnValue.Ok ?? returnValue
}

function getOutcomeText(outcome: AnyJson): string {
if (!isContractResult(outcome)) {
return typeof outcome === 'object' && outcome !== null
? stringify(outcome)
: outcome?.toString() ?? 'Error'
}

const outcomeJson = outcome.Err ?? outcome.Ok
return typeof outcomeJson === 'object' && outcomeJson !== null
? stringify(outcomeJson)
: outcomeJson?.toString() ?? 'Error'
}

export function getDecodedOutput(
{ result }: Pick<ContractExecResult, 'result' | 'debugMessage'>,
{ returnType }: AbiMessage,
registry: Registry
): {
decodedOutput: string
isError: boolean
} {
if (!result.isOk) return { decodedOutput: 'Error', isError: true }

const isError = checkRevertFlag(result.asOk.flags)
const returnValue = decodeReturnValue(returnType, result.asOk.data, registry)
const outcome = extractOutcome(returnValue)
const decodedOutput = isError
? getOutcomeText(outcome)
: getOutcomeText(outcome) || '<empty>'

return {
decodedOutput,
isError
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,10 @@ import { AbiParam } from '@/services/substrate/types'
import { ButtonCall } from './styled'
import { useContractCaller } from '@/hooks/useContractCaller'
import { CopyBlock, atomOneDark } from 'react-code-blocks'
import { getDecodedOutput } from '@/utils/contractExecResult'

type Props = React.PropsWithChildren<
Pick<
ContractInteractionProps,
'abiMessage' | 'expanded' | 'contractPromise'
> & {
Omit<ContractInteractionProps, 'type'> & {
abiParams: AbiParam[]
inputData: unknown[] | undefined
}
Expand All @@ -23,21 +21,31 @@ export function ReadMethodsForm({
abiMessage,
abiParams,
contractPromise,
substrateRegistry,
expanded
}: Props) {
const { caller } = useContractCaller(contractPromise, abiMessage.method)
const [outcome, setOutcome] = useState<string>('')

useEffect(() => {
if (!expanded) return
caller.send(inputData)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [inputData])
}, [inputData, expanded])

useEffect(() => {
if (caller.result?.ok) {
setOutcome(caller.result.value.decoded)
const { decodedOutput, isError } = getDecodedOutput(
{
debugMessage: caller.result.value.raw.debugMessage,
result: caller.result.value.raw.result
},
abiMessage,
substrateRegistry
)
setOutcome(decodedOutput)
}
}, [caller.result])
}, [abiMessage, caller.result, substrateRegistry])

return (
<Stack
Expand Down Expand Up @@ -73,10 +81,12 @@ export function ReadMethodsForm({
language="text"
theme={atomOneDark}
showLineNumbers={false}
codeBlock
wrapLongLines={true}
/>
)}
</Box>
<ButtonCall onClick={() => caller.send(inputData)}>Call</ButtonCall>
<ButtonCall onClick={() => caller.send(inputData)}>Recall</ButtonCall>
</Stack>
</Box>
<Box sx={{ maxWidth: '45%', minWidth: '40%' }}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export function ContractInteractionForm({
contractPromise={contractPromise}
abiParams={abiParams}
inputData={inputData}
substrateRegistry={substrateRegistry}
>
<ArgumentsForm
argValues={argValues}
Expand Down
2 changes: 1 addition & 1 deletion src/view/ContractDetailView/ContractsTabInteraction.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Box, Typography } from '@mui/material'
import React, { useMemo } from 'react'
import { Box, Typography } from '@mui/material'
import { ContractTabType, UserContractDetailsWithAbi } from '@/domain'
import BasicTabs from '@/components/Tabs'
import SimpleAccordion from '@/components/Accordion'
Expand Down
14 changes: 5 additions & 9 deletions src/view/ContractDetailView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import ShareIcon from '@mui/icons-material/Share'
import DownloadIcon from '@mui/icons-material/Download'
import { getChain } from '@/constants/chains'
import NetworkBadge from '@/view/components/NetworkBadge'
import { UseModalBehaviour } from '@/hooks/useModalBehaviour'
import { UserContractDetails, UserContractDetailsWithAbi } from '@/domain'
import {
isoDate,
Expand All @@ -31,19 +30,15 @@ import CancelIcon from '@mui/icons-material/Cancel'

import { UpdateDeployment } from '@/domain/repositories/DeploymentRepository'
import { useUpdateUserContracts } from '@/hooks/userContracts/useUpdateUserContracts'
import { UserContractTableItem } from '@/domain/wizard/ContractTableItem'
import { useDownloadMetadata } from '@/components/ContractsTable/useDownloadMetadata'

interface Props {
modalBehaviour: UseModalBehaviour
userContract: UserContractDetails
onDownloadSource: (contract: UserContractTableItem) => void
}
interface AbiSource {
source: { language: string }
}
export default function ContractDetail({
userContract,
onDownloadSource
}: Props): JSX.Element {
export default function ContractDetail({ userContract }: Props): JSX.Element {
const [openShareModal, setOpenShareModal] = React.useState(false)
const url = getUserContractUrl(userContract)
const { accountConnected } = useNetworkAccountsContext()
Expand All @@ -57,6 +52,7 @@ export default function ContractDetail({
const anyInvalidField: boolean = Object.values(formData).some(
field => (field.required && !field.value) || field.error !== null
)
const { onDownloadSource } = useDownloadMetadata(userContract)

const handleUpdateContractName = () => {
const updatedContract: UpdateDeployment = {
Expand Down Expand Up @@ -253,7 +249,7 @@ export default function ContractDetail({
</Tooltip>
</Typography>
<Stack direction="row" alignItems="center">
<Typography variant="caption">Deployed by</Typography>
<Typography variant="caption">Added by</Typography>
{''}
<MonoTypography sx={{ fontSize: '0.8rem' }}>
{truncateAddress(userContract.userAddress, 4)}
Expand Down
Loading

0 comments on commit a06cbb3

Please sign in to comment.