Skip to content

Commit

Permalink
Merge branch 'dev' into refactor/distributed-metadata
Browse files Browse the repository at this point in the history
  • Loading branch information
alecdwm committed Nov 6, 2023
2 parents 249b9d4 + 6fb73c3 commit 2052ef5
Show file tree
Hide file tree
Showing 37 changed files with 1,932 additions and 677 deletions.
5 changes: 5 additions & 0 deletions .changeset/sweet-trains-pump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@talismn/scale": patch
---

fix: workaround for devices with no wasm simd support
2 changes: 1 addition & 1 deletion apps/extension/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "extension",
"version": "1.19.4",
"version": "1.19.5",
"private": true,
"license": "GPL-3.0-or-later",
"dependencies": {
Expand Down
8 changes: 7 additions & 1 deletion apps/extension/src/core/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import keyring from "@polkadot/ui-keyring"
import { assert } from "@polkadot/util"
import { cryptoWaitReady } from "@polkadot/util-crypto"
import * as Sentry from "@sentry/browser"
import { watCryptoWaitReady } from "@talismn/scale"
import Browser, { Runtime } from "webextension-polyfill"

import { passwordStore } from "./domains/app"
Expand Down Expand Up @@ -92,7 +93,12 @@ Browser.runtime.onConnect.addListener((_port): void => {
!DEBUG && Browser.runtime.setUninstallURL("https://goto.talisman.xyz/uninstall")

// initial setup
cryptoWaitReady()
Promise.all([
// wait for `@polkadot/util-crypto` to be ready (it needs to load some wasm)
cryptoWaitReady(),
// wait for `@talismn/scale` to be ready (it needs to load some wasm)
watCryptoWaitReady(),
])
.then((): void => {
// load all the keyring data
keyring.loadAll({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { AnalyticsPage } from "@ui/api/analytics"
import { EvmNetworkForm, SubNetworkForm } from "@ui/domains/Settings/ManageNetworks/NetworkForm"
import {
EvmNetworkForm,
SubNetworkFormAdd,
SubNetworkFormEdit,
} from "@ui/domains/Settings/ManageNetworks/NetworkForm"
import { useAnalyticsPageView } from "@ui/hooks/useAnalyticsPageView"
import { useCallback } from "react"
import { useTranslation } from "react-i18next"
Expand Down Expand Up @@ -38,7 +42,12 @@ export const NetworkPage = () => {

return (
<DashboardLayout analytics={ANALYTICS_PAGE} withBack centered>
{isChain && <SubNetworkForm chainId={id} onSubmitted={handleSubmitted} />}
{isChain && (
<>
{id && <SubNetworkFormEdit chainId={id} onSubmitted={handleSubmitted} />}
{!id && <SubNetworkFormAdd onSubmitted={handleSubmitted} />}
</>
)}
{isEvmNetwork && <EvmNetworkForm evmNetworkId={id} onSubmitted={handleSubmitted} />}
</DashboardLayout>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ import { Trans, useTranslation } from "react-i18next"
import { useDebounce } from "react-use"
import { Button, Checkbox, FormFieldContainer, FormFieldInputText, Toggle } from "talisman-ui"

import { getEvmNetworkFormSchema } from "./getEvmNetworkFormSchema"
import { NetworkRpcsListField } from "../NetworkRpcsListField"
import { getEvmRpcChainId } from "./helpers"
import { NetworkRpcsListField } from "./NetworkRpcsListField"
import { RemoveEvmNetworkButton } from "./RemoveEvmNetworkButton"
import { ResetEvmNetworkButton } from "./ResetEvmNetworkButton"
import { evmNetworkFormSchema } from "./schema"

type EvmNetworkFormProps = {
evmNetworkId?: EvmNetworkId
Expand Down Expand Up @@ -63,13 +63,11 @@ export const EvmNetworkForm: FC<EvmNetworkFormProps> = ({ evmNetworkId, onSubmit
const { defaultValues, isCustom, isEditMode, evmNetwork } = useEditMode(evmNetworkId)
const tEditMode = evmNetworkId ? t("Edit") : t("Add")

const schema = useMemo(() => getEvmNetworkFormSchema(evmNetworkId), [evmNetworkId])

// because of the RPC checks, do not validate on each change
const formProps = useForm<RequestUpsertCustomEvmNetwork>({
mode: "onBlur",
mode: "all",
defaultValues,
resolver: yupResolver(schema),
context: { evmNetworkId },
resolver: yupResolver(evmNetworkFormSchema),
})

const {
Expand All @@ -81,7 +79,6 @@ export const EvmNetworkForm: FC<EvmNetworkFormProps> = ({ evmNetworkId, onSubmit
clearErrors,
setError,
reset,
trigger,
formState: { errors, isValid, isSubmitting, isDirty, touchedFields },
} = formProps

Expand All @@ -92,10 +89,9 @@ export const EvmNetworkForm: FC<EvmNetworkFormProps> = ({ evmNetworkId, onSubmit
useEffect(() => {
if (evmNetworkId && defaultValues && !initialized.current) {
reset(defaultValues)
trigger("rpcs")
initialized.current = true
}
}, [defaultValues, evmNetworkId, reset, trigger])
}, [defaultValues, evmNetworkId, reset])

// auto detect chain id based on RPC url (add mode only)
const rpcChainId = useRpcChainId(rpcs?.[0]?.url)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { EvmNetworkId } from "@talismn/chaindata-provider"
import { ethers } from "ethers"

// because of validation the same query is done 3 times minimum per url, make all await same promise
const rpcChainIdCache = new Map<string, Promise<EvmNetworkId | null>>()

export const getEvmRpcChainId = (rpcUrl: string): Promise<string | null> => {
// check if valid url
if (!rpcUrl || !/^https?:\/\/.+$/.test(rpcUrl)) return Promise.resolve(null)

const cached = rpcChainIdCache.get(rpcUrl)
if (cached) return cached

const provider = new ethers.providers.StaticJsonRpcProvider(rpcUrl)
const request = provider
.send("eth_chainId", [])
.then((hexChainId) => String(parseInt(hexChainId, 16)))
.catch(() => null)
rpcChainIdCache.set(rpcUrl, request)

return request
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import i18next from "@core/i18nConfig"
import * as yup from "yup"

import { getEvmRpcChainId } from "./helpers"

export const evmNetworkFormSchema = yup
.object({
id: yup.string().required(""),
isTestnet: yup.boolean().required(),
name: yup.string().required(i18next.t("Required")),
tokenSymbol: yup
.string()
.trim()
.required(i18next.t("Required"))
.min(2, i18next.t("2-6 characters"))
.max(6, i18next.t("2-6 characters")),
tokenDecimals: yup
.number()
.typeError(i18next.t("Must be a number"))
.required(i18next.t("Required"))
.integer(i18next.t("Must be a number")),
blockExplorerUrl: yup.string().url(i18next.t("Invalid URL")),
rpcs: yup
.array()
.of(
yup
.object({ url: yup.string().trim().required(i18next.t("Required")) })
.test(
"rpc-valid",
i18next.t("Chain ID mismatch"),
async function (rpc, { path, options, createError }) {
if (!rpc || !rpc.url) return true
let targetId = options.context?.evmNetworkId as string | undefined
try {
const chainId = await getEvmRpcChainId(rpc.url)
if (!chainId)
return createError({
message: i18next.t("Failed to connect"),
path: `${path}.url`,
})
if (!targetId) targetId = chainId
if (chainId !== targetId)
return createError({
message: i18next.t("Chain ID mismatch"),
path: `${path}.url`,
})
} catch (error) {
return createError({
message: i18next.t("Failed to connect"),
path: `${path}.url`,
})
}
return true
}
)
)
.required(i18next.t("Required"))
.min(1, i18next.t("RPC URL required"))
.test("rpcs-unique", i18next.t("Must be unique"), function (rpcs) {
if (!rpcs?.length) return true
const urls = rpcs.map((rpc) => rpc.url)
const duplicate = urls.filter((url, i) => {
const prevUrls = urls.slice(0, i)
return prevUrls.includes(url)
})

if (duplicate.length) {
return this.createError({
message: i18next.t("Must be unique"),
path: `rpcs[${urls.lastIndexOf(duplicate[0])}].url`,
})
}

return true
}),
})
.required()
Original file line number Diff line number Diff line change
Expand Up @@ -8,48 +8,48 @@ import {
useSensor,
useSensors,
} from "@dnd-kit/core"
import { SortableContext, sortableKeyboardCoordinates } from "@dnd-kit/sortable"
import { useSortable } from "@dnd-kit/sortable"
import { SortableContext, sortableKeyboardCoordinates, useSortable } from "@dnd-kit/sortable"
import { CSS } from "@dnd-kit/utilities"
import { DragIcon, PlusIcon, TrashIcon } from "@talismn/icons"
import { DragIcon, LoaderIcon, PlusIcon, TrashIcon } from "@talismn/icons"
import { FC, useCallback, useMemo } from "react"
import {
FieldArrayWithId,
FieldErrors,
UseFormRegister,
UseFormTrigger,
useFieldArray,
useFormContext,
} from "react-hook-form"
import { FieldArrayWithId, useFieldArray, useFormContext } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { FormFieldContainer, FormFieldInputText } from "talisman-ui"

import { useRegisterFieldWithDebouncedValidation } from "./useRegisterFieldWithDebouncedValidation"
import { SubNetworkFormData } from "./Substrate/types"
import {
ExtraValidationCb,
useRegisterFieldWithDebouncedValidation,
} from "./useRegisterFieldWithDebouncedValidation"

type RpcFormData = SubNetworkFormData | RequestUpsertCustomEvmNetwork

type SortableRpcItemProps = {
register: UseFormRegister<RequestUpsertCustomEvmNetwork>
trigger: UseFormTrigger<RequestUpsertCustomEvmNetwork>
rpc: FieldArrayWithId<RequestUpsertCustomEvmNetwork, "rpcs", "id">
errors?: FieldErrors<RequestUpsertCustomEvmNetwork>["rpcs"]
export type SortableRpcItemProps = {
rpc: FieldArrayWithId<RpcFormData, "rpcs", "id">
canDelete?: boolean
canDrag?: boolean
onDelete?: () => void
index: number
placeholder: string
isLoading?: boolean
extraValidationCb?: ExtraValidationCb
}

const SortableRpcField: FC<SortableRpcItemProps> = ({
export const SortableRpcField: FC<SortableRpcItemProps> = ({
rpc,
index,
errors,
register,
trigger,
canDelete,
canDrag,
onDelete,
placeholder,
isLoading = false,
extraValidationCb,
}) => {
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: rpc.id })
const {
register,
formState: { errors },
} = useFormContext<RpcFormData>()

const style = {
transform: CSS.Transform.toString(transform),
Expand All @@ -62,8 +62,8 @@ const SortableRpcField: FC<SortableRpcItemProps> = ({
const fieldRegistration = useRegisterFieldWithDebouncedValidation(
`rpcs.${index}.url`,
250,
trigger,
register
register,
extraValidationCb
)

return (
Expand All @@ -86,36 +86,40 @@ const SortableRpcField: FC<SortableRpcItemProps> = ({
<button
type="button"
className="allow-focus text-md mr-[-1.2rem] px-2 opacity-80 outline-none hover:opacity-100 focus:opacity-100 disabled:opacity-50"
disabled={isLoading}
onClick={onDelete}
>
<TrashIcon className="transition-none" />
{!isLoading && <TrashIcon className="transition-none" />}
{isLoading && <LoaderIcon className="animate-spin-slow transition-none" />}
</button>
)
}
/>
<div className="text-alert-warn h-8 max-w-full overflow-hidden text-ellipsis whitespace-nowrap py-2 text-right text-xs uppercase leading-none">
{errors?.[index]?.url?.message}
{errors?.rpcs?.[index]?.url?.message}
</div>
</div>
)
}

export const NetworkRpcsListField = ({ placeholder = "https://" }: { placeholder?: string }) => {
export const NetworkRpcsListField = ({
placeholder = "https://",
FieldComponent = SortableRpcField,
}: {
placeholder?: string
FieldComponent?: React.ComponentType<SortableRpcItemProps>
}) => {
const { t } = useTranslation("admin")
const {
register,
trigger,
formState: { errors },
watch,
} = useFormContext<RequestUpsertCustomEvmNetwork>()
const { watch, control } = useFormContext<RpcFormData>()

const {
fields: rpcs,
append,
remove,
move,
} = useFieldArray<RequestUpsertCustomEvmNetwork>({
} = useFieldArray<RpcFormData>({
name: "rpcs",
control,
})

const handleRemove = useCallback(
Expand Down Expand Up @@ -160,16 +164,13 @@ export const NetworkRpcsListField = ({ placeholder = "https://" }: { placeholder
<SortableContext items={rpcIds}>
<div className="flex w-full flex-col gap-2">
{rpcs.map((rpc, index, arr) => (
<SortableRpcField
<FieldComponent
key={rpc.id}
index={index}
register={register}
trigger={trigger}
rpc={rpc}
canDelete={canDelete}
canDrag={arr.length > 1}
onDelete={handleRemove(index)}
errors={errors.rpcs}
placeholder={placeholder}
/>
))}
Expand Down
Loading

0 comments on commit 2052ef5

Please sign in to comment.