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

feat: improve UX for transaction type arguments #271

Merged
merged 33 commits into from
Oct 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
406c9f8
wip
PatrickDinh Oct 3, 2024
c0716f6
single level txn arg works
PatrickDinh Oct 4, 2024
b08ec84
fix edit placeholder
PatrickDinh Oct 4, 2024
5292960
Merge remote-tracking branch 'origin/main' into spike-txn-type-arg
PatrickDinh Oct 4, 2024
6887657
cleaning up
PatrickDinh Oct 4, 2024
ceab5bc
wip - ux
PatrickDinh Oct 4, 2024
6762d0f
wip - ux
PatrickDinh Oct 4, 2024
d50fa97
show chain icons
PatrickDinh Oct 4, 2024
ea50b8d
make the edit link optional
PatrickDinh Oct 4, 2024
488014e
Merge remote-tracking branch 'origin/main' into spike-txn-type-arg
PatrickDinh Oct 6, 2024
b2aacef
fix check-types
PatrickDinh Oct 7, 2024
6a05347
wip - fixing tests
PatrickDinh Oct 7, 2024
d4b3bb6
wip - fixing tests
PatrickDinh Oct 7, 2024
84ec855
fix test
PatrickDinh Oct 7, 2024
c2c2e51
add tests
PatrickDinh Oct 7, 2024
c55188c
clean up
PatrickDinh Oct 7, 2024
1926799
wip - index the table
PatrickDinh Oct 7, 2024
5104c07
simplify the logic a bit
PatrickDinh Oct 7, 2024
2e3ef83
tweak UI
PatrickDinh Oct 7, 2024
e658aca
clean up
PatrickDinh Oct 8, 2024
5255541
wip - clean up
PatrickDinh Oct 8, 2024
6abbf45
fix text
PatrickDinh Oct 8, 2024
f4b4684
refactor method call form
PatrickDinh Oct 8, 2024
be2b221
clean up
PatrickDinh Oct 8, 2024
48dd96a
PR review
PatrickDinh Oct 8, 2024
beeae35
replace waitFor getBy by findBy
PatrickDinh Oct 8, 2024
9610f42
feat: Add NFD into search (#263)
negar-abbasi Oct 9, 2024
4351b46
chore: utf-8 decode box info where applicable (#272)
neilcampbell Oct 9, 2024
77a33ec
chore: temporarily lock nfd resolution to mainnet (#274)
neilcampbell Oct 9, 2024
38faa37
chore: support testnet NFDs (#275)
neilcampbell Oct 9, 2024
dcac66d
Merge branch 'app-lab' into spike-txn-type-arg
PatrickDinh Oct 9, 2024
6129fd1
fox bug where resources aren't saved
PatrickDinh Oct 9, 2024
7d85242
chore: tweak styling of the placeholder transaction
neilcampbell Oct 10, 2024
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: 1 addition & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -190,10 +190,5 @@
"ws@>7.0.0 <7.5.9": "7.5.10",
"@algorandfoundation/algokit-utils@<7.0.0": "^7.0.0-beta.8",
"path-to-regexp@>= 0.2.0 <8.0.0": "8.0.0"
},
"madge": {
"excludeRegExp": [
"method-call-transaction-builder.tsx"
]
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,20 @@ import { ApplicationId } from '../data/types'
import { RenderLoadable } from '@/features/common/components/render-loadable'
import { useLoadableApplicationBox } from '../data/application-boxes'
import { Dialog, DialogContent, DialogHeader, DialogTrigger, MediumSizeDialogBody } from '@/features/common/components/dialog'
import { base64ToUtf8IfValid } from '@/utils/base64-to-utf8'

type Props = { applicationId: ApplicationId; boxName: string }

const dialogTitle = 'Application Box'
export function ApplicationBoxDetailsDialog({ applicationId, boxName }: Props) {
const decodedBoxName = useMemo(() => {
return base64ToUtf8IfValid(boxName)
}, [boxName])

return (
<Dialog modal={true}>
<DialogTrigger>
<label className={cn('text-primary underline cursor-pointer')}>{boxName}</label>
<label className={cn('text-primary underline cursor-pointer')}>{decodedBoxName}</label>
</DialogTrigger>
<DialogContent>
<DialogHeader>
Expand All @@ -40,23 +45,25 @@ function InternalDialogContent({ applicationId, boxName }: Props) {
}

function ApplicationBoxDetails({ applicationBox }: { applicationBox: ApplicationBox }) {
const items = useMemo(
() => [
const items = useMemo(() => {
const decodedBoxName = base64ToUtf8IfValid(applicationBox.name)
const decodedBoxValue = base64ToUtf8IfValid(applicationBox.value)

return [
{
dt: applicationBoxNameLabel,
dd: applicationBox.name,
dd: decodedBoxName,
},
{
dt: applicationBoxValueLabel,
dd: (
<div className="grid">
<div className="overflow-y-auto break-words"> {applicationBox.value}</div>
<div className="overflow-y-auto break-words"> {decodedBoxValue}</div>
</div>
),
},
],
[applicationBox.name, applicationBox.value]
)
]
}, [applicationBox.name, applicationBox.value])

return <DescriptionList items={items} />
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ function Method({ method, applicationId, readonly }: MethodProps) {
transactionType: algosdk.ABITransactionType.appl,
transaction: {
applicationId: applicationId,
methodName: method.name,
method: method.abiMethod,
onComplete:
method.callConfig && method.callConfig.call.length > 0
? (method.callConfig.call[0] as algosdk.OnApplicationComplete as BuildAppCallTransactionResult['onComplete'])
Expand All @@ -83,7 +83,7 @@ function Method({ method, applicationId, readonly }: MethodProps) {
if (transaction && transaction.type === BuildableTransactionType.MethodCall) {
setTransaction(transaction)
}
}, [applicationId, method.callConfig, method.name, open])
}, [applicationId, method, open])

const handleTransactionSent = useCallback((transactions: Transaction[]) => {
const appCallTransactions = transactions.filter((txn) => txn.type === TransactionType.AppCall)
Expand Down
11 changes: 2 additions & 9 deletions src/features/applications/mappers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { Arc32AppSpec, Arc4AppSpec } from '@/features/app-interfaces/data/types'
import { isArc32AppSpec } from '@/features/common/utils'
import { CallConfigValue } from '@algorandfoundation/algokit-utils/types/app-spec'
import { Hint } from '@/features/app-interfaces/data/types/arc-32/application'
import { base64ToUtf8IfValid } from '@/utils/base64-to-utf8'

export const asApplicationSummary = (application: ApplicationResult): ApplicationSummary => {
return {
Expand Down Expand Up @@ -95,15 +96,7 @@ const getValue = (bytes: string) => {
if (buf.length === 32) {
return encodeAddress(new Uint8Array(buf))
}

if (isUtf8(buf)) {
const utf8Decoded = buf.toString('utf8')
// Check if the string contains any unprintable characters
if (!utf8Decoded.match(/[\p{Cc}\p{Cn}\p{Cs}]+/gu)) {
return utf8Decoded
}
}
return buf.toString('base64')
return base64ToUtf8IfValid(bytes)
}

const callValues: CallConfigValue[] = ['ALL', 'CALL']
Expand Down
13 changes: 7 additions & 6 deletions src/features/common/data/algo-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,19 @@ const shouldCreateKmdClient = (config: NetworkConfig) => {
}

// Init the network config from local storage
const networkConfig = settingsStore.get(networkConfigAtom)
export let networkConfig = settingsStore.get(networkConfigAtom)
export let indexer = ClientManager.getIndexerClient(networkConfig.indexer)
export let algod = ClientManager.getAlgodClient(networkConfig.algod)
export let kmd: algosdk.Kmd | undefined = shouldCreateKmdClient(networkConfig) ? ClientManager.getKmdClient(networkConfig.kmd!) : undefined
export let algorandClient = AlgorandClient.fromClients({ algod, indexer, kmd })

export const updateClientConfig = (networkConfig: NetworkConfigWithId) => {
indexer = ClientManager.getIndexerClient(networkConfig.indexer)
algod = ClientManager.getAlgodClient(networkConfig.algod)
kmd = shouldCreateKmdClient(networkConfig) ? ClientManager.getKmdClient(networkConfig.kmd!) : undefined
export const updateClientConfig = (_networkConfig: NetworkConfigWithId) => {
networkConfig = _networkConfig
indexer = ClientManager.getIndexerClient(_networkConfig.indexer)
algod = ClientManager.getAlgodClient(_networkConfig.algod)
kmd = shouldCreateKmdClient(_networkConfig) ? ClientManager.getKmdClient(_networkConfig.kmd!) : undefined
algorandClient = AlgorandClient.fromClients({ algod, indexer, kmd })
if (networkConfig.id !== localnetId) {
if (_networkConfig.id !== localnetId) {
algorandClient.setDefaultValidityWindow(30)
}
}
3 changes: 3 additions & 0 deletions src/features/common/data/state-cleanup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { accountResultsAtom } from '@/features/accounts/data'
import { applicationMetadataResultsAtom } from '@/features/applications/data/application-metadata'
import { applicationResultsAtom } from '@/features/applications/data'
import { assetMetadataResultsAtom, assetResultsAtom } from '@/features/assets/data'
import { forwardNfdResultsAtom, reverseNfdsAtom } from '@/features/nfd/data/nfd-result'

const cleanUpIntervalMillis = 600_000 // 10 minutes
export const cachedDataExpirationMillis = 1_800_000 // 30 minutes
Expand All @@ -32,6 +33,8 @@ const stateCleanupEffect = atomEffect((get, set) => {
removeExpired(applicationResultsAtom)
removeExpired(assetMetadataResultsAtom)
removeExpired(assetResultsAtom)
removeExpired(forwardNfdResultsAtom)
removeExpired(reverseNfdsAtom)
}, cleanUpIntervalMillis)

return () => clearInterval(cleanup)
Expand Down
57 changes: 1 addition & 56 deletions src/features/forms/components/transaction-form-item.tsx
Original file line number Diff line number Diff line change
@@ -1,75 +1,20 @@
import algosdk from 'algosdk'
import { Path, PathValue, useFormContext } from 'react-hook-form'
import { FormItemProps } from '@/features/forms/components/form-item'
import { Button } from '@/features/common/components/button'
import { useMemo } from 'react'
import { DescriptionList } from '@/features/common/components/description-list'
import { HintText } from './hint-text'
import { useFormFieldError } from '../hooks/use-form-field-error'
import { BuildTransactionResult } from '@/features/transaction-wizard/models'
import { asDescriptionListItems } from '@/features/transaction-wizard/mappers'
import { EllipsisVertical, Plus } from 'lucide-react'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/features/common/components/dropdown-menu'

export const transactionTypeLabel = 'Transaction type'

export interface TransactionFormItemProps<TSchema extends Record<string, unknown> = Record<string, unknown>>
extends Omit<FormItemProps<TSchema>, 'children'> {
transactionType: algosdk.ABITransactionType
placeholder?: string
onEdit: () => void
}
extends Omit<FormItemProps<TSchema>, 'children'> {}

export function TransactionFormItem<TSchema extends Record<string, unknown> = Record<string, unknown>>({
transactionType,
field,
helpText,
onEdit,
}: TransactionFormItemProps<TSchema>) {
const { watch, setValue } = useFormContext<TSchema>()
const error = useFormFieldError(field)
const fieldValue = watch(field) as BuildTransactionResult | undefined

const transactionFields = useMemo(() => {
if (fieldValue) {
return asDescriptionListItems(fieldValue)
} else {
return []
}
}, [fieldValue])

return (
<>
{transactionFields.length > 0 && (
<div className="relative">
<DescriptionList items={transactionFields} dtClassName="w-24 truncate" />
<div className="absolute right-0 top-0 w-8">
<DropdownMenu>
<DropdownMenuTrigger className="flex w-full items-center justify-center py-4">
<EllipsisVertical size={16} />
</DropdownMenuTrigger>
<DropdownMenuContent align="start" side="left">
<DropdownMenuItem onClick={() => onEdit()}>Edit</DropdownMenuItem>
<DropdownMenuItem onClick={() => setValue(field, undefined as PathValue<TSchema, Path<TSchema>>)}>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
)}
{transactionFields.length === 0 && (
<div className="absolute right-0 top-6">
<Button
variant="outline-secondary"
type="button"
onClick={() => onEdit()}
disabled={transactionType === algosdk.ABITransactionType.appl}
disabledReason="App call transaction arguments are currently not supported"
icon={<Plus size={16} />}
>
Add Transaction
</Button>
</div>
)}
<div>
<HintText errorText={error?.message} helpText={helpText} />
</div>
Expand Down
2 changes: 2 additions & 0 deletions src/features/network/data/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export const defaultNetworkConfigs: Record<NetworkId, NetworkConfig> = {
url: config.testNetDispenserApiUrl,
address: config.testNetDispenserAddress,
},
nfdApiUrl: 'https://api.testnet.nf.domains',
},
[mainnetId]: {
name: 'MainNet',
Expand All @@ -98,6 +99,7 @@ export const defaultNetworkConfigs: Record<NetworkId, NetworkConfig> = {
port: 443,
},
walletProviders: nonLocalnetWalletProviders,
nfdApiUrl: 'https://api.nf.domains',
},
}

Expand Down
1 change: 1 addition & 0 deletions src/features/network/data/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export type NetworkConfig = {
url: string
address: Address
}
nfdApiUrl?: string
}

export type NetworkConfigWithId = {
Expand Down
3 changes: 3 additions & 0 deletions src/features/nfd/data/is-nfd.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const nfdRegex = /^(.+\.algo)$/

export const isNFD = (name: string) => nfdRegex.test(name)
114 changes: 114 additions & 0 deletions src/features/nfd/data/nfd-result.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { createReadOnlyAtomAndTimestamp, readOnlyAtomCache } from '@/features/common/data'
import { Atom, atom, Getter, Setter } from 'jotai'
import { Nfd, NfdLookup, NfdResult } from './types'
import { Address } from '@/features/accounts/data/types'
import { networkConfig } from '@/features/common/data/algo-client'

const getReverseLookupNfd = async (_: Getter, set: Setter, address: Address, nfdApiUrl: string): Promise<Nfd | null> => {
try {
const response = await fetch(`${nfdApiUrl}/nfd/lookup?address=${address}`, {
method: 'GET',
headers: {
accept: 'application/json',
},
})

if (!response.ok) {
return null
}

const body = await response.json()
const nfd = body[address] as NfdResult
const nfdResult = {
name: nfd.name,
depositAccount: nfd.depositAccount,
caAlgo: nfd.caAlgo ?? [],
} satisfies NfdResult

// Cache the NFD result for forward lookups. Reverse lookups will also use this cache
set(forwardNfdResultsAtom, (prev) => {
if (!prev.has(nfdResult.name)) {
const next = new Map(prev)
next.set(nfdResult.name, createReadOnlyAtomAndTimestamp(nfdResult))
return next
}
return prev
})

// Cache all *other* addresses associated with this NFD for reverse lookups
set(reverseNfdsAtom, (prev) => {
const next = new Map(prev)
const addressesToAdd = new Set([nfdResult.depositAccount, ...nfdResult.caAlgo].filter((a) => a !== address))
addressesToAdd.forEach((address) => {
if (!prev.has(address)) {
next.set(address, createReadOnlyAtomAndTimestamp(nfdResult.name))
}
})
return next
})

return nfdResult.name
} catch (e: unknown) {
return null
}
}

const getForwardLookupNfdResult = async (_: Getter, set: Setter, nfd: Nfd, nfdApiUrl: string): Promise<NfdResult | null> => {
try {
const response = await fetch(`${nfdApiUrl}/nfd/${nfd}?view=tiny`, {
method: 'GET',
headers: {
accept: 'application/json',
},
})

if (!response.ok) {
return null
}

const body = (await response.json()) as NfdResult
const nfdResult = {
name: body.name,
depositAccount: body.depositAccount,
caAlgo: body.caAlgo ?? [],
} satisfies NfdResult

// Cache all addresses associated with this NFD for reverse lookups
set(reverseNfdsAtom, (prev) => {
const next = new Map(prev)
const addressesToAdd = new Set([nfdResult.depositAccount, ...nfdResult.caAlgo])
addressesToAdd.forEach((address) => {
if (!prev.has(address)) {
next.set(address, createReadOnlyAtomAndTimestamp(nfdResult.name))
}
})
return next
})

return nfdResult
} catch (e: unknown) {
return null
}
}

export const getNfdResultAtom = (nfdLookup: NfdLookup): Atom<Promise<NfdResult | null>> => {
return atom(async (get) => {
if (!networkConfig.nfdApiUrl) {
return null
}

if ('nfd' in nfdLookup) {
return await get(getForwardNfdResultAtom(nfdLookup.nfd, networkConfig.nfdApiUrl, { skipTimestampUpdate: true }))
}

const nfd = await get(getReverseNfdAtom(nfdLookup.address, networkConfig.nfdApiUrl, { skipTimestampUpdate: true }))
if (nfd) {
return await get(getForwardNfdResultAtom(nfd, networkConfig.nfdApiUrl, { skipTimestampUpdate: true }))
}
return null
})
}

const [forwardNfdResultsAtom, getForwardNfdResultAtom] = readOnlyAtomCache(getForwardLookupNfdResult, (nfd) => nfd)
const [reverseNfdsAtom, getReverseNfdAtom] = readOnlyAtomCache(getReverseLookupNfd, (address) => address)
export { forwardNfdResultsAtom, reverseNfdsAtom }
18 changes: 18 additions & 0 deletions src/features/nfd/data/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Address } from '@/features/accounts/data/types'

export type Nfd = string

export type ForwardNfdLookup = {
nfd: Nfd
}

export type ReverseNfdLookpup = {
address: string
}

export type NfdLookup = ReverseNfdLookpup | ForwardNfdLookup
export type NfdResult = {
name: Nfd
depositAccount: Address
caAlgo: Address[]
}
Loading
Loading