{salePoints.map((salePoint) => {
- const chainName = getChainConfig(
+ const chainKey = getChainKey(
salePoint.chainId,
- ).name
+ )
- const formattedChainName =
- formatChainName(chainName)
+ const chainName = getChainDisplayName(
+ salePoint.chainId,
+ )
- const beneficiaryErrorKey = `salePoints.${chainName}.beneficiaryAddress`
+ const beneficiaryErrorKey = `salePoints.${chainKey}.beneficiaryAddress`
const beneficiaryInvalid =
!!errors[beneficiaryErrorKey]
@@ -172,7 +171,7 @@ export default function ProjectEditorPage() {
>
Set the payment token and price on
- the {formattedChainName} chain
+ the {chainName} chain
You can set a price for others to
@@ -191,7 +190,7 @@ export default function ProjectEditorPage() {
This wallet address receives the
payments for this product on{' '}
- {formattedChainName} chain.
+ {chainName} chain.
Set the payment token and
price on the
- {formatChainName(
- getChainConfig(
- salePoint.chainId,
- ).name,
+ {getChainDisplayName(
+ salePoint.chainId,
)}{' '}
chain
>
diff --git a/src/parsers/ProjectParser.ts b/src/parsers/ProjectParser.ts
index e7da300a36..985a26bc98 100644
--- a/src/parsers/ProjectParser.ts
+++ b/src/parsers/ProjectParser.ts
@@ -7,14 +7,15 @@ import { getMostRelevantTimeUnit } from '~/marketplace/utils/price'
import { ProjectType, SalePoint } from '~/shared/types'
import {
TimeUnit,
- timeUnitSecondsMultiplierMap,
timeUnits,
+ timeUnitSecondsMultiplierMap,
} from '~/shared/utils/timeUnit'
import { toBigInt } from '~/utils/bn'
import {
getChainConfig,
- getChainConfigExtension,
+ getChainKey,
getCurrentChainId,
+ getMarketplaceChainConfigs,
} from '~/utils/chains'
import { getContractAddress } from '~/utils/contracts'
import { getTokenInfo } from '~/utils/tokens'
@@ -149,13 +150,12 @@ export function parseProject(value: unknown, options: ParseProjectOptions) {
}
}
- const chains: Chain[] =
- getChainConfigExtension(chainId).marketplaceChains.map(getChainConfig)
+ const chains: Chain[] = getMarketplaceChainConfigs(chainId)
const salePoints: Record = {}
- chains.map(({ id, name: chainName }) => {
- salePoints[chainName] = {
+ chains.map(({ id }) => {
+ salePoints[getChainKey(id)] = {
beneficiaryAddress: '',
chainId: id,
enabled: false,
@@ -172,9 +172,11 @@ export function parseProject(value: unknown, options: ParseProjectOptions) {
const { domainId, pricingTokenAddress, pricePerSecond, beneficiary } =
paymentDetails[i]
- const { id: chainId, name: chainName } = getChainConfig(
- Number(domainId),
- )
+ /**
+ * @todo Make sure we can trust domainId's type. It says it's a number thus
+ * the explicit type coercion *should be* redundant.
+ */
+ const { id: chainId } = getChainConfig(Number(domainId))
const { decimals } = await getTokenInfo(pricingTokenAddress, chainId)
@@ -189,7 +191,7 @@ export function parseProject(value: unknown, options: ParseProjectOptions) {
throw new Error('Invalid multiplier')
}
- salePoints[chainName] = {
+ salePoints[getChainKey(chainId)] = {
beneficiaryAddress: isOpenData
? address0
: beneficiary.toLowerCase(),
@@ -239,13 +241,12 @@ export function parseProject(value: unknown, options: ParseProjectOptions) {
export type ParsedProject = Awaited>
function getEmptySalePoints(chainId: number) {
- const chains: Chain[] =
- getChainConfigExtension(chainId).marketplaceChains.map(getChainConfig)
+ const marketplaceChainConfigs: Chain[] = getMarketplaceChainConfigs(chainId)
const salePoints: Record = {}
- chains.map(({ id, name: chainName }) => {
- salePoints[chainName] = {
+ marketplaceChainConfigs.map(({ id }) => {
+ salePoints[getChainKey(id)] = {
beneficiaryAddress: '',
chainId: id,
enabled: false,
diff --git a/src/shared/utils/constants.ts b/src/shared/utils/constants.ts
index 45206bfdc3..2c249c05f3 100644
--- a/src/shared/utils/constants.ts
+++ b/src/shared/utils/constants.ts
@@ -19,18 +19,4 @@ export const paymentCurrencies = {
NATIVE: 'NATIVE',
}
-export const ethereumNetworks = {
- '1': 'Ethereum Mainnet',
- '3': 'Ropsten',
- '4': 'Rinkeby',
- '5': 'Görli',
- '42': 'Kovan',
- '100': 'Gnosis',
- '137': 'Polygon',
- '8995': 'Dev0',
- '8997': 'Dev1',
- '31337': 'Dev2',
- '80002': 'Amoy',
-}
-
export const maxFileSizeForImageUpload = 5242880
diff --git a/src/shared/utils/tokenAssets.ts b/src/shared/utils/tokenAssets.ts
index ba61aeecf8..bb85b923ee 100644
--- a/src/shared/utils/tokenAssets.ts
+++ b/src/shared/utils/tokenAssets.ts
@@ -1,4 +1,4 @@
-import { getChainConfig } from '~/utils/chains'
+import { getCoingeckoNetworkId } from '~/utils/chains'
const BASE_URL = 'https://streamr-public.s3.amazonaws.com/truswallet-assets/blockchains'
@@ -6,21 +6,11 @@ export const getTokenLogoUrl = (
tokenContractAddress: string,
chainId: number,
): string => {
- const network = (() => {
- switch (chainId) {
- case 100:
- return 'xdai'
- case 8995:
- case 8996:
- return 'ethereum'
- default:
- return getChainConfig(chainId).name
- }
- })()
+ const networkId = getCoingeckoNetworkId(chainId)
/**
* For more details see:
* https://api.coingecko.com/api/v3/asset_platforms
*/
- return `${BASE_URL}/${network}/assets/${tokenContractAddress}/logo.png`
+ return `${BASE_URL}/${networkId}/assets/${tokenContractAddress}/logo.png`
}
diff --git a/src/types/projects.ts b/src/types/projects.ts
index d17bf64991..9ef2042797 100644
--- a/src/types/projects.ts
+++ b/src/types/projects.ts
@@ -3,16 +3,23 @@ import { z } from 'zod'
import { address0 } from '~/consts'
import { ProjectType, SalePoint } from '~/shared/types'
import { timeUnits } from '~/shared/utils/timeUnit'
-import { formatChainName } from '~/utils'
-import { getCurrentChain } from '~/utils/chains'
+import {
+ getChainDisplayName,
+ getChainKey,
+ getCurrentChain,
+ isChainKey,
+} from '~/utils/chains'
import { getContractAddress } from '~/utils/contracts'
-function getFormattedChainNameFromContext({ path: [, chainName] }: z.RefinementCtx) {
- if (typeof chainName !== 'string' || !chainName) {
- return ''
- }
+function getFormattedChainNameFromContext({ path: [, chainKey] }: z.RefinementCtx) {
+ const chainName =
+ typeof chainKey === 'number'
+ ? `#${chainKey}`
+ : isChainKey(chainKey)
+ ? getChainDisplayName(chainKey)
+ : `"${chainKey}"`
- return `for ${formatChainName(chainName)} network`
+ return `for ${chainName} network`
}
export const SalePointsPayload = z.record(
@@ -192,10 +199,10 @@ export const OpenDataPayload = z.object({
.transform((v) => v || undefined),
}),
salePoints: SalePointsPayload.transform(() => {
- const { name: chainName, id: chainId } = getCurrentChain()
+ const { id: chainId } = getCurrentChain()
return {
- [chainName]: {
+ [getChainKey(chainId)]: {
beneficiaryAddress: address0,
chainId,
enabled: true,
diff --git a/src/utils/chainConfigExtension.ts b/src/utils/chainConfigExtension.ts
index 9916629ac5..e90c71339d 100644
--- a/src/utils/chainConfigExtension.ts
+++ b/src/utils/chainConfigExtension.ts
@@ -5,6 +5,7 @@ import config from '~/config/chains.toml'
import formatConfigUrl from '~/utils/formatConfigUrl'
const ChainConfigExtension = z.object({
+ coingeckoNetworkId: z.string().optional(),
dataUnionJoinServerUrl: z.string().optional(),
dataunionGraphNames: z
.array(
@@ -15,6 +16,7 @@ const ChainConfigExtension = z.object({
)
.optional()
.default([]),
+ displayName: z.string().optional(),
dockerHost: z.string().optional(),
ipfs: z
.object({
diff --git a/src/utils/chains.test.ts b/src/utils/chains.test.ts
new file mode 100644
index 0000000000..ada534fb3d
--- /dev/null
+++ b/src/utils/chains.test.ts
@@ -0,0 +1,74 @@
+import { config } from '@streamr/config'
+import { defaultChainKey } from '../consts'
+import { parsedChainConfigExtension } from './chainConfigExtension'
+import {
+ getChainDisplayName,
+ getChainKey,
+ getMarketplaceChainConfigs,
+ isChainKey,
+} from './chains'
+
+describe('getChainKey', () => {
+ it('defaults to the default chain key', () => {
+ expect(getChainKey('whatever')).toEqual(defaultChainKey)
+
+ expect(getChainKey(0)).toEqual(defaultChainKey)
+ })
+
+ it('extracts chain key by slug', () => {
+ expect(getChainKey('amoy')).toEqual('polygonAmoy')
+ })
+
+ it('is case insensitive', () => {
+ expect(getChainKey('AMOY')).toEqual('polygonAmoy')
+
+ expect(getChainKey('POlyGON')).toEqual('polygon')
+
+ expect(getChainKey('polygonamoy')).toEqual('polygonAmoy')
+ })
+
+ it('resolves a number to a chain key', () => {
+ expect(getChainKey(137)).toEqual('polygon')
+
+ expect(getChainKey(100)).toEqual('gnosis')
+ })
+})
+
+describe('isChainKey', () => {
+ it('correctly identifies chain keys', () => {
+ expect(isChainKey('whatever')).toBe(false)
+
+ expect(isChainKey('polygon')).toBe(true)
+
+ expect(isChainKey('gnosis')).toBe(true)
+ })
+})
+
+describe('getChainDisplayName', () => {
+ it('used custom naming for chains that we provide it for', () => {
+ // Local config extension provides a custom value
+ expect(parsedChainConfigExtension['polygonAmoy']?.displayName).toEqual('Amoy')
+
+ // Config provides a diffrent value
+ expect(config.polygonAmoy.name).not.toEqual('Amoy')
+
+ // Ultimately local name counts
+ expect(getChainDisplayName('polygonAmoy')).toEqual('Amoy')
+ })
+})
+
+describe('getMarketplaceChainConfigs', () => {
+ it('gives a list of configs for given keys', () => {
+ const [config0, config1, config2] = getMarketplaceChainConfigs('polygon')
+
+ expect(config0.id).toEqual(config.gnosis.id)
+
+ expect(config1.id).toEqual(config.polygon.id)
+
+ expect(config2).toBeUndefined()
+ })
+
+ it('gives an empty list of configs if there are no marketplace chain keys provided', () => {
+ expect(getMarketplaceChainConfigs('ethereum').length).toEqual(0)
+ })
+})
diff --git a/src/utils/chains.ts b/src/utils/chains.ts
index 6bc8014aa4..7c78cf5bc5 100644
--- a/src/utils/chains.ts
+++ b/src/utils/chains.ts
@@ -3,7 +3,6 @@ import { produce } from 'immer'
import { useMemo } from 'react'
import { useSearchParams } from 'react-router-dom'
import { defaultChainKey } from '~/consts'
-import { ethereumNetworks } from '~/shared/utils/constants'
import {
ChainConfigExtension,
fallbackChainConfigExtension,
@@ -67,35 +66,35 @@ export function getChainKey(candidate: string | number): ChainKey {
return defaultChainKey
}
-export function getCurrentChain() {
+export function getCurrentChain(): Chain {
return getChainConfig(
new URLSearchParams(window.location.search).get('chain') || defaultChainKey,
)
}
-export function getCurrentChainId() {
+export function getCurrentChainId(): number {
return getCurrentChain().id
}
-export function useCurrentChain() {
+export function useCurrentChain(): Chain {
const chainName = useSearchParams()[0].get('chain') || defaultChainKey
return useMemo(() => getChainConfig(chainName), [chainName])
}
-export function useCurrentChainId() {
+export function useCurrentChainId(): number {
return useCurrentChain().id
}
-export function useCurrentChainKey() {
+export function useCurrentChainKey(): ChainKey {
return getChainKey(useCurrentChainId())
}
/**
* @todo rename to `useCurrentFullChainName`.
*/
-export function useCurrentChainFullName() {
- return getChainConfig(useCurrentChainId()).name
+export function useCurrentChainFullName(): string {
+ return getChainDisplayName(useCurrentChainId())
}
interface ChainEntry {
@@ -119,8 +118,6 @@ function getChainEntry(chainKey: ChainKey): ChainEntry {
const { dockerHost } = configExtension
const sanitizedConfig = produce(config, (draft) => {
- draft.name = ethereumNetworks[config.id] || config.name
-
for (const rpc of draft.rpcEndpoints) {
rpc.url = formatConfigUrl(rpc.url, {
dockerHost,
@@ -170,6 +167,39 @@ export function getChainSlug(chainIdOrChainKey: ChainKey | number): string {
* @param candidate Any string.
* @returns `true` if the given string is config's own key.
*/
-function isChainKey(candidate: string): candidate is ChainKey {
+export function isChainKey(candidate: string): candidate is ChainKey {
return Object.prototype.hasOwnProperty.call(configs, candidate)
}
+
+export function isKnownChainId(candidate: number): boolean {
+ return Object.entries(configs).some(([, { id }]) => id === candidate)
+}
+
+export function getChainDisplayName(chainIdOrChainKey: ChainKey | number): string {
+ const { config, configExtension } = getChainEntry(getChainKey(chainIdOrChainKey))
+
+ return configExtension.displayName || config.name
+}
+
+export function getMarketplaceChainConfigs(
+ chainIdOrChainKey: ChainKey | number,
+): Chain[] {
+ const marketplaceChainKeys = getChainEntry(getChainKey(chainIdOrChainKey))
+ .configExtension.marketplaceChains
+
+ const result: Chain[] = []
+
+ for (const key of marketplaceChainKeys) {
+ if (isChainKey(key)) {
+ result.push(getChainConfig(key))
+ }
+ }
+
+ return result
+}
+
+export function getCoingeckoNetworkId(chainIdOrChainKey: ChainKey | number) {
+ const { config, configExtension } = getChainEntry(getChainKey(chainIdOrChainKey))
+
+ return configExtension.coingeckoNetworkId || config.name
+}
diff --git a/src/utils/index.tsx b/src/utils/index.tsx
index 7bee2d3d3f..c9f54fb7c5 100644
--- a/src/utils/index.tsx
+++ b/src/utils/index.tsx
@@ -130,27 +130,6 @@ export async function sleep(millis: number) {
await new Promise((resolve) => void setTimeout(resolve, millis))
}
-/**
- * Turns `abc`, `ABC`, `aBc` into `Abc`.
- */
-function titleize(value: string): string {
- return value.toLowerCase().replace(/\w/, (firstLetter) => firstLetter.toUpperCase())
-}
-
-/**
- * Converts a string into a good-looking display-ready chain name.
- */
-export function formatChainName(chainName: string): string {
- switch (chainName.toLowerCase()) {
- case 'xdai':
- return formatChainName('gnosis')
- case 'bsc':
- return 'Binance Smart Chain'
- default:
- return titleize(chainName)
- }
-}
-
/**
* Takes the user back in history, but only if they've already navigated
* somewhere within the app.
diff --git a/src/utils/networkPreflight.ts b/src/utils/networkPreflight.ts
index 8fafd11fd6..1286e92f72 100644
--- a/src/utils/networkPreflight.ts
+++ b/src/utils/networkPreflight.ts
@@ -2,7 +2,7 @@ import { toaster } from 'toasterhea'
import SwitchNetworkModal from '~/modals/SwitchNetworkModal'
import { getWalletProvider } from '~/shared/stores/wallet'
import { Layer } from '~/utils/Layer'
-import { getChainConfig } from '~/utils/chains'
+import { getChainConfig, getChainDisplayName } from '~/utils/chains'
import getChainId from '~/utils/web3/getChainId'
/**
@@ -14,15 +14,15 @@ export default async function networkPreflight(expectedChainId: number) {
const provider = await getWalletProvider()
try {
- const currentChainId = await getChainId()
+ const actualChainId = await getChainId()
- if (currentChainId === expectedChainId) {
+ if (actualChainId === expectedChainId) {
return false
}
await toaster(SwitchNetworkModal, Layer.Modal).pop({
- expectedNetwork: expectedChainId,
- actualNetwork: currentChainId,
+ expectedChainId,
+ actualChainId,
})
await provider.request({
@@ -45,7 +45,7 @@ export default async function networkPreflight(expectedChainId: number) {
params: [
{
chainId: `0x${chainConfig.id.toString(16)}`,
- chainName: chainConfig.name,
+ chainName: getChainDisplayName(chainConfig.id),
rpcUrls: chainConfig.rpcEndpoints.map(({ url }) => url),
nativeCurrency: chainConfig.nativeCurrency,
blockExplorerUrls: [chainConfig.blockExplorerUrl].filter(