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

Add selector for memo type and fix stellar-expert fetch #1211

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
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
8 changes: 8 additions & 0 deletions i18n/locales/en/generic.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
{
"asset-selector": {
"placeholder": "Select an asset",
"select": "Select"
},
"dialog-actions": {
"close": {
"label": "Close"
Expand Down Expand Up @@ -78,6 +82,10 @@
"cancel": "Cancel"
}
},
"memo-selector": {
"placeholder": "Select a memo",
"select": "Select"
},
"submission-progress": {
"pending": "Submitting to network ...",
"success": {
Expand Down
3 changes: 1 addition & 2 deletions i18n/locales/en/payment.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
"memo-metadata": {
"label": {
"default": "Memo",
"id": "Memo (ID)",
"text": "Memo (Text)"
"required": "Memo (Required)"
},
"placeholder": {
"optional": "Description (optional)",
Expand Down
8 changes: 4 additions & 4 deletions src/Assets/components/AddAssetDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ const AddAssetDialog = React.memo(function AddAssetDialog(props: AddAssetDialogP
const allAssets = useTickerAssets(props.account.testnet)
const router = useRouter()
const { t } = useTranslation()
const wellKnownAccounts = useWellKnownAccounts(props.account.testnet)
const wellKnownAccounts = useWellKnownAccounts()
const [customTrustlineDialogOpen, setCustomTrustlineDialogOpen] = React.useState(false)
const [searchFieldValue, setSearchFieldValue] = React.useState("")
const [txCreationPending, setTxCreationPending] = React.useState(false)
Expand Down Expand Up @@ -283,15 +283,15 @@ const AddAssetDialog = React.memo(function AddAssetDialog(props: AddAssetDialogP
}

const wellknownAccountMatches = React.useCallback(
(accountID: string, search: string) => {
async (accountID: string, search: string) => {
const lowerCasedSearch = search.toLowerCase()
const record = wellKnownAccounts.lookup(accountID)
const record = await wellKnownAccounts.lookup(accountID)

if (!record) {
return false
}
return (
record.domain.toLowerCase().includes(lowerCasedSearch) || record.name.toLowerCase().includes(lowerCasedSearch)
record.domain?.toLowerCase().includes(lowerCasedSearch) || record.name.toLowerCase().includes(lowerCasedSearch)
)
},
[wellKnownAccounts]
Expand Down
8 changes: 5 additions & 3 deletions src/Generic/components/AssetSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from "react"
import { useTranslation } from "react-i18next"
import { Asset, Horizon } from "stellar-sdk"
import ListItemIcon from "@material-ui/core/ListItemIcon"
import ListItemText from "@material-ui/core/ListItemText"
Expand Down Expand Up @@ -90,6 +91,7 @@ interface AssetSelectorProps {
function AssetSelector(props: AssetSelectorProps) {
const { onChange } = props
const classes = useAssetSelectorStyles()
const { t } = useTranslation()

const assets = React.useMemo(
() => [
Expand Down Expand Up @@ -129,7 +131,7 @@ function AssetSelector(props: AssetSelectorProps) {
margin={props.margin}
onChange={handleChange as any}
name={props.name}
placeholder="Select an asset"
placeholder={t("generic.asset-selector.placeholder")}
select
style={{ flexShrink: 0, ...props.style }}
value={props.value ? props.value.getCode() : ""}
Expand All @@ -151,12 +153,12 @@ function AssetSelector(props: AssetSelectorProps) {
},
displayEmpty: !props.value,
disableUnderline: props.disableUnderline,
renderValue: () => (props.value ? props.value.getCode() : "Select")
renderValue: () => (props.value ? props.value.getCode() : t("generic.asset-selector.select"))
}}
>
{props.value ? null : (
<MenuItem disabled value="">
Select an asset
{t("generic.asset-selector.placeholder")}
</MenuItem>
)}
{props.showXLM ? (
Expand Down
10 changes: 7 additions & 3 deletions src/Generic/components/Fetchers.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from "react"
import { useAccountHomeDomainSafe } from "../hooks/stellar"
import { useWellKnownAccounts } from "../hooks/stellar-ecosystem"
import { AccountRecord, useWellKnownAccounts } from "../hooks/stellar-ecosystem"
import { Address } from "./PublicKey"

interface AccountNameProps {
Expand All @@ -9,9 +9,13 @@ interface AccountNameProps {
}

export const AccountName = React.memo(function AccountName(props: AccountNameProps) {
const wellknownAccounts = useWellKnownAccounts(props.testnet)
const wellknownAccounts = useWellKnownAccounts()
const homeDomain = useAccountHomeDomainSafe(props.publicKey, props.testnet, true)
const record = wellknownAccounts.lookup(props.publicKey)
const [record, setRecord] = React.useState<AccountRecord | undefined>(undefined)

React.useEffect(() => {
wellknownAccounts.lookup(props.publicKey).then(setRecord)
}, [props.publicKey, wellknownAccounts])

if (record && record.domain) {
return <span style={{ userSelect: "text" }}>{record.domain}</span>
Expand Down
35 changes: 35 additions & 0 deletions src/Generic/components/FormFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,41 @@ export const PriceInput = React.memo(function PriceInput(props: PriceInputProps)
)
})

type MemoInputProps = TextFieldProps & {
memoSelector: React.ReactNode
memoStyle?: React.CSSProperties
readOnly?: boolean
}

export const MemoInput = React.memo(function MemoInput(props: MemoInputProps) {
const { memoSelector, memoStyle, readOnly, ...textfieldProps } = props
const InputField = readOnly ? ReadOnlyTextfield : TextField
return (
<InputField
{...textfieldProps}
InputProps={{
startAdornment: (
<InputAdornment
disableTypography
position="start"
style={{
pointerEvents: typeof memoSelector === "string" ? "none" : undefined,
...memoStyle
}}
>
{memoSelector}
</InputAdornment>
),
...textfieldProps.InputProps
}}
style={{
pointerEvents: props.readOnly ? "none" : undefined,
...textfieldProps.style
}}
/>
)
})

const useReadOnlyTextfieldStyles = makeStyles({
root: {
"&:focus": {
Expand Down
142 changes: 142 additions & 0 deletions src/Generic/components/MemoSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import React from "react"
import { useTranslation } from "react-i18next"
import { MemoHash, MemoID, MemoNone, MemoReturn, MemoText, MemoType } from "stellar-sdk"
import ListItemText from "@material-ui/core/ListItemText"
import MenuItem from "@material-ui/core/MenuItem"
import { makeStyles } from "@material-ui/core/styles"
import TextField, { TextFieldProps } from "@material-ui/core/TextField"

interface MemoItemProps {
disabled?: boolean
key: string
memoType: MemoType
value: string
}

function capitalizeFirstLetter(word: string) {
return word.charAt(0).toUpperCase() + word.slice(1)
}

const MemoItem = React.memo(
React.forwardRef(function MemoItem(props: MemoItemProps, ref: React.Ref<HTMLLIElement>) {
const { memoType, ...reducedProps } = props

return (
<MenuItem {...reducedProps} key={props.key} ref={ref} value={props.value}>
<ListItemText>{capitalizeFirstLetter(memoType)}</ListItemText>
</MenuItem>
)
})
)

const useMemoSelectorStyles = makeStyles({
helperText: {
maxWidth: 100,
whiteSpace: "nowrap"
},
input: {
minWidth: 72
},
select: {
fontSize: 18,
fontWeight: 400
},
unselected: {
opacity: 0.5
}
})

interface MemoSelectorProps {
autoFocus?: TextFieldProps["autoFocus"]
children?: React.ReactNode
className?: string
disabledMemos?: MemoType[]
disableUnderline?: boolean
helperText?: TextFieldProps["helperText"]
inputError?: string
label?: TextFieldProps["label"]
margin?: TextFieldProps["margin"]
minWidth?: number | string
name?: string
onChange?: (memoType: MemoType) => void
style?: React.CSSProperties
value?: MemoType
}

function MemoSelector(props: MemoSelectorProps) {
const { onChange } = props
const classes = useMemoSelectorStyles()

const { t } = useTranslation()

const memoTypes = React.useMemo<MemoType[]>(() => [MemoNone, MemoText, MemoID, MemoHash, MemoReturn], [])

const handleChange = React.useCallback(
(event: React.ChangeEvent<{ name?: any; value: any }>, child: React.ComponentElement<MemoItemProps, any>) => {
const matchingMemo = memoTypes.find(memoType => memoType === child.props.memoType)

if (matchingMemo) {
if (onChange) {
onChange(matchingMemo)
}
} else {
// tslint:disable-next-line no-console
console.error(`Invariant violation: Trustline ${child.props.memoType} selected, but no matching memo found.`)
}
},
[memoTypes, onChange]
)

return (
<TextField
autoFocus={props.autoFocus}
className={props.className}
error={Boolean(props.inputError)}
helperText={props.helperText}
label={props.inputError ? props.inputError : props.label}
margin={props.margin}
onChange={handleChange as any}
name={props.name}
placeholder={t("generic.memo-selector.select")}
select
style={{ flexShrink: 0, ...props.style }}
value={props.value || ""}
FormHelperTextProps={{
className: classes.helperText
}}
InputProps={{
classes: {
root: classes.input
},
style: {
minWidth: props.minWidth
}
}}
SelectProps={{
classes: {
root: props.value ? undefined : classes.unselected,
select: classes.select
},
displayEmpty: !props.value,
disableUnderline: props.disableUnderline,
renderValue: () => (props.value ? capitalizeFirstLetter(props.value) : t("generic.memo-selector.select"))
}}
>
{props.value ? null : (
<MenuItem disabled value="">
{t("generic.memo-selector.placeholder")}
</MenuItem>
)}
{memoTypes.map(memoType => (
<MemoItem
memoType={memoType}
disabled={props.disabledMemos && props.disabledMemos.some(someMemo => someMemo === memoType)}
key={memoType}
value={memoType}
/>
))}
</TextField>
)
}

export default React.memo(MemoSelector)
33 changes: 6 additions & 27 deletions src/Generic/hooks/stellar-ecosystem.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import React from "react"
import { trackError } from "~App/contexts/notifications"
import { AccountRecord, fetchWellknownAccounts } from "../lib/stellar-expert"
import { AccountRecord, fetchWellKnownAccount } from "../lib/stellar-expert"
import { AssetRecord, fetchAllAssets } from "../lib/stellar-ticker"
import { tickerAssetsCache, wellKnownAccountsCache } from "./_caches"
import { useForceRerender } from "./util"
import { tickerAssetsCache } from "./_caches"

export { AccountRecord, AssetRecord }

Expand All @@ -12,33 +10,14 @@ export function useTickerAssets(testnet: boolean) {
return tickerAssetsCache.get(testnet) || tickerAssetsCache.suspend(testnet, fetchAssets)
}

export function useWellKnownAccounts(testnet: boolean) {
let accounts: AccountRecord[]

const forceRerender = useForceRerender()
const fetchAccounts = () => fetchWellknownAccounts(testnet)

try {
accounts = wellKnownAccountsCache.get(testnet) || wellKnownAccountsCache.suspend(testnet, fetchAccounts)
} catch (thrown) {
if (thrown && typeof thrown.then === "function") {
// Promise thrown to suspend component – prevent suspension
accounts = []
thrown.then(forceRerender, trackError)
} else {
// It's an error – re-throw
throw thrown
}
}

export function useWellKnownAccounts() {
const wellknownAccounts = React.useMemo(() => {
return {
accounts,
lookup(publicKey: string): AccountRecord | undefined {
return accounts.find(account => account.address === publicKey)
lookup(publicKey: string): Promise<AccountRecord | undefined> {
return fetchWellKnownAccount(publicKey)
}
}
}, [accounts])
}, [])

return wellknownAccounts
}
28 changes: 17 additions & 11 deletions src/Generic/lib/stellar-expert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,32 @@ export interface AccountRecord {
paging_token: string
name: string
tags: string[]
domain: string
accepts?: {
memo: "MEMO_TEXT" | "MEMO_ID"
}
domain?: string
}

const wellKnownAccountsCache = createPersistentCache<AccountRecord[]>("known-accounts", { expiresIn: 24 * 60 * 60_000 })

export async function fetchWellknownAccounts(testnet: boolean): Promise<AccountRecord[]> {
const cacheKey = testnet ? "testnet" : "pubnet"
export async function fetchWellKnownAccount(accountID: string): Promise<AccountRecord | undefined> {
const cacheKey = "all"
const cachedAccounts = wellKnownAccountsCache.read(cacheKey)

const { netWorker } = await workers

if (cachedAccounts) {
return cachedAccounts
const cachedAccount = cachedAccounts && cachedAccounts.find(account => account.address === accountID)

if (cachedAccount) {
return cachedAccount
} else {
const knownAccounts = await netWorker.fetchWellknownAccounts(testnet)
const fetchedAccount = await netWorker.fetchWellknownAccount(accountID)

if (fetchedAccount) {
const newKnownAccounts: AccountRecord[] = cachedAccounts
? cachedAccounts.concat(fetchedAccount)
: [fetchedAccount]

wellKnownAccountsCache.save(cacheKey, newKnownAccounts)
}

wellKnownAccountsCache.save(cacheKey, knownAccounts)
return knownAccounts
return fetchedAccount || undefined
}
}
Loading