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: unified address format #1782

Merged
merged 14 commits into from
Feb 6, 2025
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
5 changes: 5 additions & 0 deletions .changeset/nervous-walls-yell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@talismn/chaindata-provider": minor
---

oldPrefix property on Chain
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ jobs:
# SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
# SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
- name: Upload build artifacts
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: build
path: ./apps/extension/dist/chrome/talisman_extension_ci_${{ steps.vars.outputs.sha_short }}_chrome.zip
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { ArrowRightIcon, XIcon } from "@talismn/icons"
import { classNames } from "@talismn/util"
import { FC, useMemo } from "react"
import { Trans, useTranslation } from "react-i18next"
import { IconButton } from "talisman-ui"

import { useAccounts, useAppState, useFeatureFlag } from "@ui/state"

export const UnifiedAddressInfoBanner = () => {
const { t } = useTranslation()
const allowBanner = useFeatureFlag("UNIFIED_ADDRESS_BANNER")
const [hideBanner, setHideBanner] = useAppState("hideUnifiedAddressBanner")
const accounts = useAccounts()

const showBanner = useMemo(
() => allowBanner && !hideBanner && accounts.some((a) => a.type !== "ethereum"),
[accounts, allowBanner, hideBanner],
)

if (!showBanner) return null

return (
<div
className={classNames(
"relative z-0 overflow-hidden",
"text-tiny select-none rounded-sm px-8 py-6",
"bg-gradient-to-r from-[#9F7998] to-[#EB5D93]",
)}
>
<div className="relative z-10">
<div className="flex items-center gap-4 text-base">
<div className="grow font-bold">{t("Unified address format")}</div>
<div>
<IconButton
className="text-md text-body select-auto"
onClick={() => setHideBanner(true)}
>
<XIcon />
</IconButton>
</div>
</div>
<p>
<Trans
t={t}
defaults="Polkadot is unifying account formats across parachains.<br />Verify addresses during the transition to ensure smooth transfers."
></Trans>
</p>
<div className="text-tiny mt-5 flex items-center justify-between">
<div className="flex h-12 flex-col justify-center rounded-lg bg-white/10 px-6">
5EoJmkBANK...os4rNjjoTt
</div>
<ArrowRightIcon className="shrink-0 text-sm" />
<div className="flex h-12 flex-col justify-center rounded-lg bg-white/10 px-6">
13jbv5SEE6...oDcwQFvFg2
</div>
</div>
</div>
<BgIcon className="absolute -right-1 top-[2.2rem] h-[10.1rem] w-[16.5rem] fill-[#FF0067] opacity-20" />
</div>
)
}

const BgIcon: FC<{ className?: string }> = ({ className }) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 165 101" fill="none" className={className}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M115.516 18.1681C115.516 28.202 101.18 36.3361 83.495 36.3361C65.8106 36.3361 51.4743 28.202 51.4743 18.1681C51.4743 8.13412 65.8106 0 83.495 0C101.18 0 115.516 8.13412 115.516 18.1681ZM115.516 154.989C115.516 165.023 101.18 173.157 83.495 173.157C65.8106 173.157 51.4742 165.023 51.4742 154.989C51.4742 144.955 65.8106 136.821 83.495 136.821C101.18 136.821 115.516 144.955 115.516 154.989ZM38.8651 61.4555C47.7074 46.523 47.6525 30.3518 38.7422 25.3361C29.832 20.3204 15.4407 28.3596 6.59837 43.2921C-2.24396 58.2246 -2.18896 74.3955 6.72126 79.4114C15.6315 84.4273 30.0227 76.3879 38.8651 61.4555ZM160.278 93.7446C169.19 98.7612 169.248 114.933 160.405 129.866C151.563 144.798 137.17 152.836 128.257 147.82C119.345 142.803 119.288 126.63 128.13 111.698C136.973 96.7653 151.366 88.7272 160.278 93.7446ZM38.7427 147.824C47.6552 142.807 47.712 126.635 38.8696 111.703C30.0273 96.7702 15.6342 88.7321 6.72173 93.7488C-2.19075 98.7661 -2.2476 114.938 6.59473 129.871C15.4371 144.803 29.8302 152.841 38.7427 147.824ZM160.402 43.2863C169.245 58.2188 169.188 74.3912 160.275 79.4078C151.363 84.4252 136.97 76.3871 128.128 61.4543C119.285 46.5219 119.342 30.3496 128.254 25.3326C137.167 20.3156 151.56 28.3538 160.402 43.2863Z"
/>
</svg>
)
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { useBalances } from "@ui/state"

import { AuthorisedSiteToolbar } from "../../components/AuthorisedSiteToolbar"
import { useQuickSettingsOpenClose } from "../../components/Navigation/QuickSettings"
import { UnifiedAddressInfoBanner } from "../../components/UnifiedAddressInfoBanner"

const portfolioAccountsSearch$ = new BehaviorSubject("")

Expand Down Expand Up @@ -296,6 +297,7 @@ const Accounts = ({
<>
<AllAccountsHeader accounts={accounts} />
<NewFeaturesButton />
<UnifiedAddressInfoBanner />
</>
)}

Expand Down
124 changes: 105 additions & 19 deletions apps/extension/src/ui/domains/CopyAddress/CopyAddressChainForm.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { ChainId } from "@talismn/chaindata-provider"
import { CopyIcon, QrIcon } from "@talismn/icons"
import { ArrowUpRightIcon, CopyIcon, PolkadotIcon, QrIcon } from "@talismn/icons"
import { isEthereumAddress } from "@talismn/util"
import { SubstrateLedgerAppType } from "extension-core"
import { useCallback, useMemo, useState } from "react"
import { log } from "extension-shared"
import { FC, useCallback, useMemo, useState } from "react"
import { useTranslation } from "react-i18next"
import { IconButton, Tooltip, TooltipContent, TooltipTrigger, useOpenClose } from "talisman-ui"

Expand All @@ -11,41 +11,77 @@ import { SearchInput } from "@talisman/components/SearchInput"
import { convertAddress } from "@talisman/util/convertAddress"
import { shortenAddress } from "@talisman/util/shortenAddress"
import { useBalancesFiatTotalPerNetwork } from "@ui/hooks/useBalancesFiatTotalPerNetwork"
import { useAccountByAddress, useBalancesByAddress, useChains, useSetting } from "@ui/state"
import {
useAccountByAddress,
useBalancesByAddress,
useChains,
useFeatureFlag,
useRemoteConfig,
useSetting,
} from "@ui/state"

import { AccountIcon } from "../Account/AccountIcon"
import { ChainLogo } from "../Asset/ChainLogo"
import { CopyAddressExchangeWarning } from "./CopyAddressExchangeWarning"
import {
ChainFormat,
CopyAddressFormatPickerDrawer,
isMigratedFormat,
MigratedChainFormat,
} from "./CopyAddressFormatPickerDrawer"
import { CopyAddressLayout } from "./CopyAddressLayout"
import { useCopyAddressWizard } from "./useCopyAddressWizard"

type ChainFormat = {
key: string
chainId: ChainId | null
prefix: number | null
name: string
address: string
}

const ChainFormatButton = ({ format }: { format: ChainFormat }) => {
const { t } = useTranslation()
const { setChainId, copySpecific } = useCopyAddressWizard()
const { open: openWarning, isOpen: isWarningOpen, close: closeWarning } = useOpenClose()

const handleQrClick = useCallback(() => {
setChainId(format.chainId)
}, [format.chainId, setChainId])
const [migratedFormatPicker, setMigratedFormatPicker] = useState<{
format: MigratedChainFormat
mode: "copy" | "qr"
}>()

const { open: openWarning, isOpen: isWarningOpen, close: closeWarning } = useOpenClose()
const handleQrClick = useCallback(() => {
if (isMigratedFormat(format)) setMigratedFormatPicker({ format, mode: "qr" })
else setChainId(format.chainId)
}, [format, setChainId])

const handleCopyClick = useCallback(() => {
if (format.chainId === null && !isEthereumAddress(format.address)) openWarning()
else copySpecific(format.address, format.chainId)
}, [copySpecific, format.address, format.chainId, openWarning])
if (format.chainId === null && !isEthereumAddress(format.address)) {
openWarning()
} else if (isMigratedFormat(format)) {
setMigratedFormatPicker({ format, mode: "copy" })
} else {
copySpecific(format.address, format.chainId)
}
}, [copySpecific, format, openWarning])

const handleWarningContinueClick = useCallback(() => {
copySpecific(format.address, format.chainId)
}, [copySpecific, format.address, format.chainId])

const handleFormatPickerSelect = useCallback(
(legacyFormat: boolean) => {
if (!migratedFormatPicker) return
const { format, mode } = migratedFormatPicker

if (mode === "copy")
copySpecific(
legacyFormat ? format.oldAddress : format.address,
format.chainId,
legacyFormat,
)
if (mode === "qr") {
setChainId(format.chainId, legacyFormat)
}

// close drawer
setMigratedFormatPicker(undefined)
},
[copySpecific, migratedFormatPicker, setChainId],
)

return (
<div className="text-body-secondary hover:text-body hover:bg-grey-800 flex h-32 w-full items-center gap-6 px-12">
{format.chainId ? (
Expand Down Expand Up @@ -91,6 +127,11 @@ const ChainFormatButton = ({ format }: { format: ChainFormat }) => {
onDismiss={closeWarning}
onContinue={handleWarningContinueClick}
/>
<CopyAddressFormatPickerDrawer
format={migratedFormatPicker?.format}
onDismiss={() => setMigratedFormatPicker(undefined)}
onSelect={handleFormatPickerSelect}
/>
</div>
)
}
Expand Down Expand Up @@ -149,8 +190,13 @@ export const CopyAddressChainForm = () => {
key: chain.id,
chainId: chain.id,
prefix: chain.prefix,
oldPrefix: chain.oldPrefix,
name: chain.name ?? "unknown",
address: convertAddress(address, chain.prefix),
oldAddress:
typeof chain.oldPrefix === "number"
? convertAddress(address, chain.oldPrefix)
: undefined,
})),
].filter((f) => !accountChain || accountChain.id === f.chainId)
}, [address, chains, SUBSTRATE_FORMAT, account?.ledgerApp, balancesPerNetwork, accountChain])
Expand All @@ -169,9 +215,49 @@ export const CopyAddressChainForm = () => {
<SearchInput onChange={setSearch} placeholder={t("Search by network name")} autoFocus />
</div>
<ScrollContainer className="bg-black-secondary border-grey-700 scrollable h-full w-full grow overflow-x-hidden border-t">
<UnifiedAddressMigrationBanner formats={filteredFormats} />
<ChainFormatsList formats={filteredFormats} />
</ScrollContainer>
</div>
</CopyAddressLayout>
)
}

export const UnifiedAddressMigrationBanner: FC<{ formats: ChainFormat[] }> = ({ formats }) => {
const { t } = useTranslation()
const allowBanner = useFeatureFlag("UNIFIED_ADDRESS_BANNER")
const remoteConfig = useRemoteConfig()

const showBanner = useMemo(
() => allowBanner && formats.some(isMigratedFormat),
[allowBanner, formats],
)

const handleClick = useCallback(() => {
try {
window.open(
remoteConfig.documentation.unifiedAddressDocsUrl,
"_blank",
"nooppener noreferrer",
)
} catch (err) {
log.error("Unable to open unified address docs", { cause: err })
}
}, [remoteConfig.documentation.unifiedAddressDocsUrl])

if (!showBanner) return null

return (
<button
type="button"
onClick={handleClick}
className="text-body flex w-full items-center gap-4 bg-gradient-to-r from-[#9F7998] to-[#EB5D93] px-12 py-4 text-left text-sm"
>
<div className="grow">
<PolkadotIcon className="mr-2 inline-block shrink-0 align-text-top" />
{t("Polkadot introduces new address formatting")}
</div>
<ArrowUpRightIcon className="text-body shrink-0 text-[2rem]" />
</button>
)
}
38 changes: 36 additions & 2 deletions apps/extension/src/ui/domains/CopyAddress/CopyAddressCopyForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,14 +170,22 @@ export const CopyAddressCopyForm = () => {
logo,
chain,
isLogoLoaded,
legacyFormat,
goToAddressPage,
goToNetworkOrTokenPage,
goToNetworkPage,
} = useCopyAddressWizard()

const isEthereum = useMemo(
() => !chain && formattedAddress && isEthereumAddress(formattedAddress),
[chain, formattedAddress],
)

const isMigratedChain = useMemo(() => {
if (!chain) return false
const { oldPrefix, prefix } = chain
return typeof oldPrefix === "number" && typeof prefix === "number" && oldPrefix !== prefix
}, [chain])

const genesisHash = chain?.genesisHash

const { t } = useTranslation()
Expand All @@ -204,12 +212,20 @@ export const CopyAddressCopyForm = () => {
<div>
<NetworkPillButton
chainId={networkId}
onClick={goToNetworkOrTokenPage}
onClick={goToNetworkPage}
address={formattedAddress}
/>
</div>
</div>
)}
{isMigratedChain && (
<div className="text-body-secondary flex h-16 w-full items-center justify-between">
<div>{t("Format")}</div>
<div>
<FormatIndicator legacyFormat={legacyFormat} />
</div>
</div>
)}
</div>
<div className="flex w-full grow flex-col items-center justify-center gap-12">
<div className="h-[21rem] w-[21rem] rounded-lg bg-[#ffffff] p-8">
Expand Down Expand Up @@ -334,3 +350,21 @@ export const CopyAddressCopyForm = () => {
</CopyAddressLayout>
)
}

const FormatIndicator: FC<{ legacyFormat?: boolean }> = ({ legacyFormat }) => {
const { t } = useTranslation()

return (
<Tooltip>
<TooltipTrigger asChild>
<div className="text-body flex items-center gap-2">
<span>{legacyFormat ? t("Legacy format") : t("New format")}</span>
<InfoIcon />
</div>
</TooltipTrigger>
<TooltipContent>
{t("You may need to use legacy format when sending from some exchanges.")}
</TooltipContent>
</Tooltip>
)
}
Loading