From d9323d7bce849837e0979f84dab173cb3c4b4a43 Mon Sep 17 00:00:00 2001 From: Daedalus <0xDaedalus@users.noreply.github.com> Date: Tue, 17 Jan 2023 08:05:17 -0700 Subject: [PATCH 01/20] Separate wallet_addEthereumChain from wallet_switchEthereumChain. Now that we're building in the ability to add custom networks we need to deliniate these two methods. Lets keep wallet_switchEthereumChain functionality inside of wallet_addEthereumChain in case users try to add networks that are already supported - but give a separate path for the wallet_addEthereumChain method to interact with chainService. --- background/services/chain/index.ts | 5 + .../internal-ethereum-provider/index.ts | 103 ++++++++++++++++-- window-provider/index.ts | 2 +- 3 files changed, 102 insertions(+), 8 deletions(-) diff --git a/background/services/chain/index.ts b/background/services/chain/index.ts index 72ca9d4660..e33b498c12 100644 --- a/background/services/chain/index.ts +++ b/background/services/chain/index.ts @@ -62,6 +62,7 @@ import { OPTIMISM_GAS_ORACLE_ADDRESS, } from "./utils/optimismGasPriceOracle" import KeyringService from "../keyring" +import type { ValidatedAddEthereumChainParameter } from "../internal-ethereum-provider" // The number of blocks to query at a time for historic asset transfers. // Unfortunately there's no "right" answer here that works well across different @@ -1863,4 +1864,8 @@ export default class ChainService extends BaseService { ) } } + + addCustomChain(param: ValidatedAddEthereumChainParameter) { + console.log(param) + } } diff --git a/background/services/internal-ethereum-provider/index.ts b/background/services/internal-ethereum-provider/index.ts index f8d8802e87..ed0f552d3f 100644 --- a/background/services/internal-ethereum-provider/index.ts +++ b/background/services/internal-ethereum-provider/index.ts @@ -59,6 +59,74 @@ export type SwitchEthereumChainParameter = { chainId: string } +// https://eips.ethereum.org/EIPS/eip-3085 +export type AddEthereumChainParameter = { + chainId: string + blockExplorerUrls?: string[] + chainName?: string + iconUrls?: string[] + nativeCurrency?: { + name: string + symbol: string + decimals: number + } + rpcUrls?: string[] +} + +// Lets start with all required and work backwards +export type ValidatedAddEthereumChainParameter = { + chainId: string + blockExplorerUrl: string + chainName: string + iconUrl: string + nativeCurrency: { + name: string + symbol: string + decimals: number + } + rpcUrls: string[] +} + +const validateAddEthereumChainParameter = ({ + chainId, + chainName, + blockExplorerUrls, + iconUrls, + nativeCurrency, + rpcUrls, +}: AddEthereumChainParameter): ValidatedAddEthereumChainParameter => { + if ( + !chainId || + !chainName || + !nativeCurrency || + !blockExplorerUrls || + !blockExplorerUrls.length || + !iconUrls || + !iconUrls.length || + !rpcUrls || + !rpcUrls.length + ) { + throw new Error("Missing Chain Property") + } + + if ( + !nativeCurrency.decimals || + !nativeCurrency.name || + !nativeCurrency.symbol + ) { + throw new Error("Missing Currency Property") + } + + return { + chainId, + chainName, + nativeCurrency, + blockExplorerUrl: blockExplorerUrls[0], + iconUrl: iconUrls[0], + rpcUrls, + } +} + type DAppRequestEvent = { payload: T resolver: (result: E | PromiseLike) => void @@ -245,21 +313,33 @@ export default class InternalEthereumProviderService extends BaseService ) // TODO - actually allow adding a new ethereum chain - for now wallet_addEthereumChain // will just switch to a chain if we already support it - but not add a new one - case "wallet_addEthereumChain": + case "wallet_addEthereumChain": { + const chainInfo = params[0] as AddEthereumChainParameter + const { chainId } = chainInfo + const supportedNetwork = await this.getTrackedNetworkByChainId(chainId) + if (supportedNetwork) { + this.switchToSupportedNetwork(supportedNetwork) + return null + } + try { + const validatedParam = validateAddEthereumChainParameter(chainInfo) + return this.chainService.addCustomChain(validatedParam) + } catch (e) { + logger.error(e) + throw new EIP1193Error(EIP1193_ERROR_CODES.userRejectedRequest) + } + throw new EIP1193Error(EIP1193_ERROR_CODES.userRejectedRequest) + } case "wallet_switchEthereumChain": { const newChainId = (params[0] as SwitchEthereumChainParameter).chainId const supportedNetwork = await this.getTrackedNetworkByChainId( newChainId ) if (supportedNetwork) { - const { address } = await this.preferenceService.getSelectedAccount() - await this.chainService.markAccountActivity({ - address, - network: supportedNetwork, - }) - await this.db.setCurrentChainIdForOrigin(origin, supportedNetwork) + this.switchToSupportedNetwork(supportedNetwork) return null } + throw new EIP1193Error(EIP1193_ERROR_CODES.chainDisconnected) } case "metamask_getProviderState": // --- important MM only methods --- @@ -391,6 +471,15 @@ export default class InternalEthereumProviderService extends BaseService }) } + private async switchToSupportedNetwork(supportedNetwork: EVMNetwork) { + const { address } = await this.preferenceService.getSelectedAccount() + await this.chainService.markAccountActivity({ + address, + network: supportedNetwork, + }) + await this.db.setCurrentChainIdForOrigin(origin, supportedNetwork) + } + private async signData( { input, diff --git a/window-provider/index.ts b/window-provider/index.ts index 5dc5ecbf52..d3c3dc1d41 100644 --- a/window-provider/index.ts +++ b/window-provider/index.ts @@ -214,7 +214,7 @@ export default class TallyWindowProvider extends EventEmitter { reject(result) } - // let's emmit connected on the first successful response from background + // let's emit connected on the first successful response from background if (!this.connected) { this.connected = true this.emit("connect", { chainId: this.chainId }) From 40de8589f2f5feebbb50590c672b9fbc7c31d38e Mon Sep 17 00:00:00 2001 From: Daedalus <0xDaedalus@users.noreply.github.com> Date: Tue, 17 Jan 2023 08:23:26 -0700 Subject: [PATCH 02/20] Persist EVM Networks --- background/networks.ts | 2 +- background/services/chain/db.ts | 20 +++++++++++++++++++ background/services/chain/index.ts | 10 ++++++++-- .../internal-ethereum-provider/index.ts | 19 ++++++++++++------ 4 files changed, 42 insertions(+), 9 deletions(-) diff --git a/background/networks.ts b/background/networks.ts index 675eca1b37..3f34431aae 100644 --- a/background/networks.ts +++ b/background/networks.ts @@ -34,7 +34,7 @@ export type Network = { baseAsset: NetworkBaseAsset & CoinGeckoAsset family: NetworkFamily chainID?: string - coingeckoPlatformID: string + coingeckoPlatformID?: string } /** diff --git a/background/services/chain/db.ts b/background/services/chain/db.ts index 894bf56649..e2870a91d5 100644 --- a/background/services/chain/db.ts +++ b/background/services/chain/db.ts @@ -183,6 +183,26 @@ export class ChainDatabase extends Dexie { ) } + async addEVMNetwork( + chainName: string, + chainID: string, + decimals: number, + symbol: string, + assetName: string + ): Promise { + this.networks.put({ + name: chainName, + chainID, + family: "EVM", + baseAsset: { + decimals, + symbol, + name: assetName, + chainID, + }, + }) + } + async getAllEVMNetworks(): Promise { return this.networks.where("family").equals("EVM").toArray() } diff --git a/background/services/chain/index.ts b/background/services/chain/index.ts index e33b498c12..38bb36f55f 100644 --- a/background/services/chain/index.ts +++ b/background/services/chain/index.ts @@ -1865,7 +1865,13 @@ export default class ChainService extends BaseService { } } - addCustomChain(param: ValidatedAddEthereumChainParameter) { - console.log(param) + async addCustomChain(param: ValidatedAddEthereumChainParameter) { + await this.db.addEVMNetwork( + param.chainName, + param.chainId, + param.nativeCurrency.decimals, + param.nativeCurrency.symbol, + param.nativeCurrency.name + ) } } diff --git a/background/services/internal-ethereum-provider/index.ts b/background/services/internal-ethereum-provider/index.ts index ed0f552d3f..df8cd8b704 100644 --- a/background/services/internal-ethereum-provider/index.ts +++ b/background/services/internal-ethereum-provider/index.ts @@ -78,7 +78,7 @@ export type ValidatedAddEthereumChainParameter = { chainId: string blockExplorerUrl: string chainName: string - iconUrl: string + iconUrl?: string nativeCurrency: { name: string symbol: string @@ -101,11 +101,17 @@ const validateAddEthereumChainParameter = ({ !nativeCurrency || !blockExplorerUrls || !blockExplorerUrls.length || - !iconUrls || - !iconUrls.length || !rpcUrls || !rpcUrls.length ) { + console.log({ + chainId, + chainName, + blockExplorerUrls, + iconUrls, + nativeCurrency, + rpcUrls, + }) throw new Error("Missing Chain Property") } @@ -118,11 +124,11 @@ const validateAddEthereumChainParameter = ({ } return { - chainId, + chainId: chainId.startsWith("0x") ? String(parseInt(chainId, 16)) : chainId, chainName, nativeCurrency, blockExplorerUrl: blockExplorerUrls[0], - iconUrl: iconUrls[0], + iconUrl: iconUrls && iconUrls[0], rpcUrls, } } @@ -323,7 +329,8 @@ export default class InternalEthereumProviderService extends BaseService } try { const validatedParam = validateAddEthereumChainParameter(chainInfo) - return this.chainService.addCustomChain(validatedParam) + await this.chainService.addCustomChain(validatedParam) + return null } catch (e) { logger.error(e) throw new EIP1193Error(EIP1193_ERROR_CODES.userRejectedRequest) From 436d9999ea01c4614636e136f668bea573f27dc6 Mon Sep 17 00:00:00 2001 From: Daedalus <0xDaedalus@users.noreply.github.com> Date: Tue, 17 Jan 2023 09:05:44 -0700 Subject: [PATCH 03/20] Persist and retrieve RPC URLs for all networks --- background/services/chain/db.ts | 72 +++++++++++- background/services/chain/index.ts | 105 +++++++++++------- .../chain/serial-fallback-provider.ts | 14 +-- .../internal-ethereum-provider/index.ts | 1 + 4 files changed, 138 insertions(+), 54 deletions(-) diff --git a/background/services/chain/db.ts b/background/services/chain/db.ts index e2870a91d5..fc47a82faa 100644 --- a/background/services/chain/db.ts +++ b/background/services/chain/db.ts @@ -10,7 +10,13 @@ import { NetworkBaseAsset, } from "../../networks" import { FungibleAsset } from "../../assets" -import { BASE_ASSETS, DEFAULT_NETWORKS, GOERLI, POLYGON } from "../../constants" +import { + BASE_ASSETS, + CHAIN_ID_TO_RPC_URLS, + DEFAULT_NETWORKS, + GOERLI, + POLYGON, +} from "../../constants" export type Transaction = AnyEVMTransaction & { dataSource: "alchemy" | "local" @@ -73,6 +79,8 @@ export class ChainDatabase extends Dexie { private baseAssets!: Dexie.Table + private rpcUrls!: Dexie.Table<{ chainId: string; rpcUrls: string[] }, string> + constructor(options?: DexieOptions) { super("tally/chain", options) this.version(1).stores({ @@ -153,6 +161,10 @@ export class ChainDatabase extends Dexie { this.version(6).stores({ baseAssets: "&chainID,symbol,name", }) + + this.version(7).stores({ + rpcUrls: "&chainId, rpcUrls", + }) } async getLatestBlock(network: Network): Promise { @@ -188,9 +200,10 @@ export class ChainDatabase extends Dexie { chainID: string, decimals: number, symbol: string, - assetName: string + assetName: string, + rpcUrls: string[] ): Promise { - this.networks.put({ + await this.networks.put({ name: chainName, chainID, family: "EVM", @@ -201,16 +214,42 @@ export class ChainDatabase extends Dexie { chainID, }, }) + await this.addBaseAsset(assetName, symbol, chainID, decimals) + await this.addRpcUrls(chainID, rpcUrls) } async getAllEVMNetworks(): Promise { return this.networks.where("family").equals("EVM").toArray() } + private async addBaseAsset( + name: string, + symbol: string, + chainID: string, + decimals: number + ) { + await this.baseAssets.put({ + decimals, + name, + symbol, + chainID, + }) + } + async getAllBaseAssets(): Promise { return this.baseAssets.toArray() } + async initializeRPCs(): Promise { + await Promise.all( + Object.entries(CHAIN_ID_TO_RPC_URLS).map(async ([chainId, rpcUrls]) => { + if (rpcUrls) { + await this.addRpcUrls(chainId, rpcUrls) + } + }) + ) + } + async initializeBaseAssets(): Promise { await this.updateBaseAssets(BASE_ASSETS) } @@ -230,6 +269,33 @@ export class ChainDatabase extends Dexie { ) } + async getRpcUrlsByChainId(chainId: string): Promise { + const rpcUrls = await this.rpcUrls.where({ chainId }).first() + if (rpcUrls) { + return rpcUrls.rpcUrls + } + throw new Error(`No RPC Found for ${chainId}`) + } + + async addRpcUrls(chainId: string, rpcUrls: string[]): Promise { + const existingRpcUrlsForChain = await this.rpcUrls.get(chainId) + if (existingRpcUrlsForChain) { + existingRpcUrlsForChain.rpcUrls.push(...rpcUrls) + existingRpcUrlsForChain.rpcUrls = [ + ...new Set(existingRpcUrlsForChain.rpcUrls), + ] + console.log("about to put") + await this.rpcUrls.put(existingRpcUrlsForChain) + } else { + console.log("new put", { chainId, rpcUrls }) + await this.rpcUrls.put({ chainId, rpcUrls }) + } + } + + async getAllRpcUrls(): Promise<{ chainId: string; rpcUrls: string[] }[]> { + return this.rpcUrls.toArray() + } + async getAllSavedTransactionHashes(): Promise { return this.chainTransactions.orderBy("hash").keys() } diff --git a/background/services/chain/index.ts b/background/services/chain/index.ts index 38bb36f55f..66233e7e49 100644 --- a/background/services/chain/index.ts +++ b/background/services/chain/index.ts @@ -300,57 +300,72 @@ export default class ChainService extends BaseService { override async internalStartService(): Promise { await super.internalStartService() - await this.initializeBaseAssets() - await this.initializeNetworks() - const accounts = await this.getAccountsToTrack() - const trackedNetworks = await this.getTrackedNetworks() - const transactions = await this.db.getAllTransactions() - - this.emitter.emit("initializeActivities", { transactions, accounts }) - - // get the latest blocks and subscribe for all active networks - - Promise.allSettled( - accounts - .flatMap((an) => [ - // subscribe to all account transactions - this.subscribeToAccountTransactions(an).catch((e) => { - logger.error(e) - }), - // do a base-asset balance check for every account - this.getLatestBaseAccountBalance(an).catch((e) => { - logger.error(e) - }), - ]) - .concat( - // Schedule any stored unconfirmed transactions for - // retrieval---either to confirm they no longer exist, or to - // read/monitor their status. - trackedNetworks.map((network) => - this.db - .getNetworkPendingTransactions(network) - .then((pendingTransactions) => { - pendingTransactions.forEach(({ hash, firstSeen }) => { - logger.debug( - `Queuing pending transaction ${hash} for status lookup.` - ) - this.queueTransactionHashToRetrieve(network, hash, firstSeen) + try { + await this.initializeRPCs() + await this.initializeBaseAssets() + await this.initializeNetworks() + const accounts = await this.getAccountsToTrack() + const trackedNetworks = await this.getTrackedNetworks() + + const transactions = await this.db.getAllTransactions() + + this.emitter.emit("initializeActivities", { transactions, accounts }) + + // get the latest blocks and subscribe for all active networks + + Promise.allSettled( + accounts + .flatMap((an) => [ + // subscribe to all account transactions + this.subscribeToAccountTransactions(an).catch((e) => { + logger.error(e) + }), + // do a base-asset balance check for every account + this.getLatestBaseAccountBalance(an).catch((e) => { + logger.error(e) + }), + ]) + .concat( + // Schedule any stored unconfirmed transactions for + // retrieval---either to confirm they no longer exist, or to + // read/monitor their status. + trackedNetworks.map((network) => + this.db + .getNetworkPendingTransactions(network) + .then((pendingTransactions) => { + pendingTransactions.forEach(({ hash, firstSeen }) => { + logger.debug( + `Queuing pending transaction ${hash} for status lookup.` + ) + this.queueTransactionHashToRetrieve( + network, + hash, + firstSeen + ) + }) }) - }) - .catch((e) => { - logger.error(e) - }) + .catch((e) => { + logger.error(e) + }) + ) ) - ) - ) + ) + } catch (e) { + console.error(e) + } } async initializeBaseAssets(): Promise { await this.db.initializeBaseAssets() } + async initializeRPCs(): Promise { + await this.db.initializeRPCs() + } + async initializeNetworks(): Promise { await this.db.initializeEVMNetworks() + const rpcUrls = await this.db.getAllRpcUrls() if (!this.supportedNetworks.length) { this.supportedNetworks = await this.db.getAllEVMNetworks() } @@ -363,7 +378,10 @@ export default class ChainService extends BaseService { evm: Object.fromEntries( this.supportedNetworks.map((network) => [ network.chainID, - makeSerialFallbackProvider(network), + makeSerialFallbackProvider( + network, + rpcUrls.find((v) => v.chainId === network.chainID)?.rpcUrls || [] + ), ]) ), } @@ -1871,7 +1889,8 @@ export default class ChainService extends BaseService { param.chainId, param.nativeCurrency.decimals, param.nativeCurrency.symbol, - param.nativeCurrency.name + param.nativeCurrency.name, + param.rpcUrls ) } } diff --git a/background/services/chain/serial-fallback-provider.ts b/background/services/chain/serial-fallback-provider.ts index a099b4a232..4cb24267d0 100644 --- a/background/services/chain/serial-fallback-provider.ts +++ b/background/services/chain/serial-fallback-provider.ts @@ -10,7 +10,6 @@ import { utils } from "ethers" import { getNetwork } from "@ethersproject/networks" import { SECOND, - CHAIN_ID_TO_RPC_URLS, ALCHEMY_SUPPORTED_CHAIN_IDS, RPC_METHOD_PROVIDER_ROUTING, } from "../../constants" @@ -931,7 +930,8 @@ export default class SerialFallbackProvider extends JsonRpcProvider { } export function makeSerialFallbackProvider( - network: EVMNetwork + network: EVMNetwork, + rpcUrls: string[] ): SerialFallbackProvider { const alchemyProviderCreators = ALCHEMY_SUPPORTED_CHAIN_IDS.has( network.chainID @@ -956,12 +956,10 @@ export function makeSerialFallbackProvider( ] : [] - const genericProviders = (CHAIN_ID_TO_RPC_URLS[network.chainID] || []).map( - (rpcUrl) => ({ - type: "generic" as const, - creator: () => new JsonRpcProvider(rpcUrl), - }) - ) + const genericProviders = (rpcUrls || []).map((rpcUrl) => ({ + type: "generic" as const, + creator: () => new JsonRpcProvider(rpcUrl), + })) return new SerialFallbackProvider(network, [ // Prefer alchemy as the primary provider when available diff --git a/background/services/internal-ethereum-provider/index.ts b/background/services/internal-ethereum-provider/index.ts index df8cd8b704..90c1e1153a 100644 --- a/background/services/internal-ethereum-provider/index.ts +++ b/background/services/internal-ethereum-provider/index.ts @@ -327,6 +327,7 @@ export default class InternalEthereumProviderService extends BaseService this.switchToSupportedNetwork(supportedNetwork) return null } + // @TODO Feature Flag This try { const validatedParam = validateAddEthereumChainParameter(chainInfo) await this.chainService.addCustomChain(validatedParam) From 9dca2ea17b914a6d0b86122604e6d01da445e945 Mon Sep 17 00:00:00 2001 From: Daedalus <0xDaedalus@users.noreply.github.com> Date: Tue, 17 Jan 2023 09:12:47 -0700 Subject: [PATCH 04/20] Consistent Casing --- background/services/chain/db.ts | 14 ++++++-------- background/services/chain/index.ts | 3 ++- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/background/services/chain/db.ts b/background/services/chain/db.ts index fc47a82faa..7c10176cb8 100644 --- a/background/services/chain/db.ts +++ b/background/services/chain/db.ts @@ -79,7 +79,7 @@ export class ChainDatabase extends Dexie { private baseAssets!: Dexie.Table - private rpcUrls!: Dexie.Table<{ chainId: string; rpcUrls: string[] }, string> + private rpcUrls!: Dexie.Table<{ chainID: string; rpcUrls: string[] }, string> constructor(options?: DexieOptions) { super("tally/chain", options) @@ -163,7 +163,7 @@ export class ChainDatabase extends Dexie { }) this.version(7).stores({ - rpcUrls: "&chainId, rpcUrls", + rpcUrls: "&chainID, rpcUrls", }) } @@ -277,22 +277,20 @@ export class ChainDatabase extends Dexie { throw new Error(`No RPC Found for ${chainId}`) } - async addRpcUrls(chainId: string, rpcUrls: string[]): Promise { - const existingRpcUrlsForChain = await this.rpcUrls.get(chainId) + async addRpcUrls(chainID: string, rpcUrls: string[]): Promise { + const existingRpcUrlsForChain = await this.rpcUrls.get(chainID) if (existingRpcUrlsForChain) { existingRpcUrlsForChain.rpcUrls.push(...rpcUrls) existingRpcUrlsForChain.rpcUrls = [ ...new Set(existingRpcUrlsForChain.rpcUrls), ] - console.log("about to put") await this.rpcUrls.put(existingRpcUrlsForChain) } else { - console.log("new put", { chainId, rpcUrls }) - await this.rpcUrls.put({ chainId, rpcUrls }) + await this.rpcUrls.put({ chainID, rpcUrls }) } } - async getAllRpcUrls(): Promise<{ chainId: string; rpcUrls: string[] }[]> { + async getAllRpcUrls(): Promise<{ chainID: string; rpcUrls: string[] }[]> { return this.rpcUrls.toArray() } diff --git a/background/services/chain/index.ts b/background/services/chain/index.ts index 66233e7e49..73938896de 100644 --- a/background/services/chain/index.ts +++ b/background/services/chain/index.ts @@ -380,7 +380,7 @@ export default class ChainService extends BaseService { network.chainID, makeSerialFallbackProvider( network, - rpcUrls.find((v) => v.chainId === network.chainID)?.rpcUrls || [] + rpcUrls.find((v) => v.chainID === network.chainID)?.rpcUrls || [] ), ]) ), @@ -392,6 +392,7 @@ export default class ChainService extends BaseService { * provider exists. */ providerForNetwork(network: EVMNetwork): SerialFallbackProvider | undefined { + console.log(this.providers.evm) return isEnabled(FeatureFlags.USE_MAINNET_FORK) ? this.providers.evm[ETHEREUM.chainID] : this.providers.evm[network.chainID] From 0c8e78eeca18a2a9f8613213c03c90b3f374bd5c Mon Sep 17 00:00:00 2001 From: Daedalus <0xDaedalus@users.noreply.github.com> Date: Tue, 17 Jan 2023 09:38:48 -0700 Subject: [PATCH 05/20] Remove BTC from networks and assets. No plans to use BTC anytime soon as we are solely focused on EVM networks for the foreseeable future - lets remove the cruft. --- background/accounts.ts | 2 +- background/constants/base-assets.ts | 11 ----------- background/constants/coin-types.ts | 2 -- background/constants/currencies.ts | 11 ----------- background/constants/networks.ts | 10 +--------- background/networks.ts | 2 +- background/services/chain/db.ts | 2 +- background/tests/prices.test.ts | 6 +++--- 8 files changed, 7 insertions(+), 39 deletions(-) diff --git a/background/accounts.ts b/background/accounts.ts index b12bbb2411..631465661a 100644 --- a/background/accounts.ts +++ b/background/accounts.ts @@ -4,7 +4,7 @@ import { HexString } from "./types" /** * An account balance at a particular time and block height, on a particular - * network. Flexible enough to represent base assets like ETH and BTC as well + * network. Flexible enough to represent base assets like ETH as well * application-layer tokens like ERC-20s. */ export type AccountBalance = { diff --git a/background/constants/base-assets.ts b/background/constants/base-assets.ts index 4f68250f95..dd38dbe833 100644 --- a/background/constants/base-assets.ts +++ b/background/constants/base-assets.ts @@ -55,19 +55,8 @@ const BNB: NetworkBaseAsset = { decimals: 18, } -const BTC: NetworkBaseAsset = { - /** - * To persist base asset to indexDB chainID must be declared. - */ - chainID: "", - name: "Bitcoin", - symbol: "BTC", - decimals: 8, -} - export const BASE_ASSETS_BY_CUSTOM_NAME = { ETH, - BTC, MATIC, RBTC, AVAX, diff --git a/background/constants/coin-types.ts b/background/constants/coin-types.ts index 2352b5c554..0dab19b232 100644 --- a/background/constants/coin-types.ts +++ b/background/constants/coin-types.ts @@ -5,8 +5,6 @@ * Limited extension-specific list of coin types by asset symbol. */ export const coinTypesByAssetSymbol = { - BTC: 0, - "Testnet BTC": 1, ETH: 60, RBTC: 137, MATIC: 966, diff --git a/background/constants/currencies.ts b/background/constants/currencies.ts index e809fd2f48..2212272fde 100644 --- a/background/constants/currencies.ts +++ b/background/constants/currencies.ts @@ -90,19 +90,8 @@ export const BNB: NetworkBaseAsset & Required = { }, } -export const BTC: NetworkBaseAsset & Required = { - ...BASE_ASSETS_BY_CUSTOM_NAME.BTC, - coinType: coinTypesByAssetSymbol.BTC, - metadata: { - coinGeckoID: "bitcoin", - tokenLists: [], - websiteURL: "https://bitcoin.org", - }, -} - export const BUILT_IN_NETWORK_BASE_ASSETS = [ ETH, - BTC, MATIC, RBTC, OPTIMISTIC_ETH, diff --git a/background/constants/networks.ts b/background/constants/networks.ts index 621afa97c6..6e3c090c2d 100644 --- a/background/constants/networks.ts +++ b/background/constants/networks.ts @@ -1,11 +1,10 @@ import { FeatureFlags, isEnabled } from "../features" -import { EVMNetwork, Network } from "../networks" +import { EVMNetwork } from "../networks" import { ARBITRUM_NOVA_ETH, ARBITRUM_ONE_ETH, AVAX, BNB, - BTC, ETH, GOERLI_ETH, MATIC, @@ -85,13 +84,6 @@ export const GOERLI: EVMNetwork = { coingeckoPlatformID: "ethereum", } -export const BITCOIN: Network = { - name: "Bitcoin", - baseAsset: BTC, - family: "BTC", - coingeckoPlatformID: "bitcoin", -} - export const DEFAULT_NETWORKS = [ ETHEREUM, POLYGON, diff --git a/background/networks.ts b/background/networks.ts index 3f34431aae..1f800abe40 100644 --- a/background/networks.ts +++ b/background/networks.ts @@ -14,7 +14,7 @@ import type { * Each supported network family is generally incompatible with others from a * transaction, consensus, and/or wire format perspective. */ -export type NetworkFamily = "EVM" | "BTC" +export type NetworkFamily = "EVM" // Should be structurally compatible with FungibleAsset or much code will // likely explode. diff --git a/background/services/chain/db.ts b/background/services/chain/db.ts index 7c10176cb8..533ba7c87f 100644 --- a/background/services/chain/db.ts +++ b/background/services/chain/db.ts @@ -277,7 +277,7 @@ export class ChainDatabase extends Dexie { throw new Error(`No RPC Found for ${chainId}`) } - async addRpcUrls(chainID: string, rpcUrls: string[]): Promise { + private async addRpcUrls(chainID: string, rpcUrls: string[]): Promise { const existingRpcUrlsForChain = await this.rpcUrls.get(chainID) if (existingRpcUrlsForChain) { existingRpcUrlsForChain.rpcUrls.push(...rpcUrls) diff --git a/background/tests/prices.test.ts b/background/tests/prices.test.ts index 897b8a1e16..dd8e4a7b60 100644 --- a/background/tests/prices.test.ts +++ b/background/tests/prices.test.ts @@ -2,7 +2,7 @@ import * as ethers from "@ethersproject/web" // << THIS IS THE IMPORTANT TRICK import logger from "../lib/logger" -import { BTC, ETH, FIAT_CURRENCIES, USD } from "../constants" +import { ETH, FIAT_CURRENCIES, USD } from "../constants" import { getPrices } from "../lib/prices" import { isValidCoinGeckoPriceResponse } from "../lib/validate" @@ -151,7 +151,7 @@ describe("lib/prices.ts", () => { amounts: [639090000000000n, 100000000n], pair: [ { decimals: 10, name: "United States Dollar", symbol: "USD" }, - BTC, + ETH, ], time: dateNow, }, @@ -167,7 +167,7 @@ describe("lib/prices.ts", () => { jest.spyOn(ethers, "fetchJson").mockResolvedValue(fetchJsonResponse) - await expect(getPrices([BTC, ETH], FIAT_CURRENCIES)).resolves.toEqual( + await expect(getPrices([ETH], FIAT_CURRENCIES)).resolves.toEqual( getPricesResponse ) expect(ethers.fetchJson).toHaveBeenCalledTimes(1) From 2f967aeada42ba9540ef05acc1b400596042abbf Mon Sep 17 00:00:00 2001 From: Daedalus <0xDaedalus@users.noreply.github.com> Date: Tue, 17 Jan 2023 12:09:22 -0700 Subject: [PATCH 06/20] Cleanup --- background/services/chain/index.ts | 109 ++++++++---------- .../chain/serial-fallback-provider.ts | 2 +- .../internal-ethereum-provider/index.ts | 16 +-- 3 files changed, 57 insertions(+), 70 deletions(-) diff --git a/background/services/chain/index.ts b/background/services/chain/index.ts index 73938896de..f00c521556 100644 --- a/background/services/chain/index.ts +++ b/background/services/chain/index.ts @@ -300,59 +300,51 @@ export default class ChainService extends BaseService { override async internalStartService(): Promise { await super.internalStartService() - try { - await this.initializeRPCs() - await this.initializeBaseAssets() - await this.initializeNetworks() - const accounts = await this.getAccountsToTrack() - const trackedNetworks = await this.getTrackedNetworks() - - const transactions = await this.db.getAllTransactions() - - this.emitter.emit("initializeActivities", { transactions, accounts }) - - // get the latest blocks and subscribe for all active networks - - Promise.allSettled( - accounts - .flatMap((an) => [ - // subscribe to all account transactions - this.subscribeToAccountTransactions(an).catch((e) => { - logger.error(e) - }), - // do a base-asset balance check for every account - this.getLatestBaseAccountBalance(an).catch((e) => { - logger.error(e) - }), - ]) - .concat( - // Schedule any stored unconfirmed transactions for - // retrieval---either to confirm they no longer exist, or to - // read/monitor their status. - trackedNetworks.map((network) => - this.db - .getNetworkPendingTransactions(network) - .then((pendingTransactions) => { - pendingTransactions.forEach(({ hash, firstSeen }) => { - logger.debug( - `Queuing pending transaction ${hash} for status lookup.` - ) - this.queueTransactionHashToRetrieve( - network, - hash, - firstSeen - ) - }) - }) - .catch((e) => { - logger.error(e) + await this.initializeRPCs() + await this.initializeBaseAssets() + await this.initializeNetworks() + const accounts = await this.getAccountsToTrack() + const trackedNetworks = await this.getTrackedNetworks() + + const transactions = await this.db.getAllTransactions() + + this.emitter.emit("initializeActivities", { transactions, accounts }) + + // get the latest blocks and subscribe for all active networks + + Promise.allSettled( + accounts + .flatMap((an) => [ + // subscribe to all account transactions + this.subscribeToAccountTransactions(an).catch((e) => { + logger.error(e) + }), + // do a base-asset balance check for every account + this.getLatestBaseAccountBalance(an).catch((e) => { + logger.error(e) + }), + ]) + .concat( + // Schedule any stored unconfirmed transactions for + // retrieval---either to confirm they no longer exist, or to + // read/monitor their status. + trackedNetworks.map((network) => + this.db + .getNetworkPendingTransactions(network) + .then((pendingTransactions) => { + pendingTransactions.forEach(({ hash, firstSeen }) => { + logger.debug( + `Queuing pending transaction ${hash} for status lookup.` + ) + this.queueTransactionHashToRetrieve(network, hash, firstSeen) }) - ) + }) + .catch((e) => { + logger.error(e) + }) ) - ) - } catch (e) { - console.error(e) - } + ) + ) } async initializeBaseAssets(): Promise { @@ -392,7 +384,6 @@ export default class ChainService extends BaseService { * provider exists. */ providerForNetwork(network: EVMNetwork): SerialFallbackProvider | undefined { - console.log(this.providers.evm) return isEnabled(FeatureFlags.USE_MAINNET_FORK) ? this.providers.evm[ETHEREUM.chainID] : this.providers.evm[network.chainID] @@ -1884,14 +1875,14 @@ export default class ChainService extends BaseService { } } - async addCustomChain(param: ValidatedAddEthereumChainParameter) { + async addCustomChain(chainInfo: ValidatedAddEthereumChainParameter) { await this.db.addEVMNetwork( - param.chainName, - param.chainId, - param.nativeCurrency.decimals, - param.nativeCurrency.symbol, - param.nativeCurrency.name, - param.rpcUrls + chainInfo.chainName, + chainInfo.chainId, + chainInfo.nativeCurrency.decimals, + chainInfo.nativeCurrency.symbol, + chainInfo.nativeCurrency.name, + chainInfo.rpcUrls ) } } diff --git a/background/services/chain/serial-fallback-provider.ts b/background/services/chain/serial-fallback-provider.ts index 4cb24267d0..cc0651d8e4 100644 --- a/background/services/chain/serial-fallback-provider.ts +++ b/background/services/chain/serial-fallback-provider.ts @@ -956,7 +956,7 @@ export function makeSerialFallbackProvider( ] : [] - const genericProviders = (rpcUrls || []).map((rpcUrl) => ({ + const genericProviders = rpcUrls.map((rpcUrl) => ({ type: "generic" as const, creator: () => new JsonRpcProvider(rpcUrl), })) diff --git a/background/services/internal-ethereum-provider/index.ts b/background/services/internal-ethereum-provider/index.ts index 90c1e1153a..bd349d716e 100644 --- a/background/services/internal-ethereum-provider/index.ts +++ b/background/services/internal-ethereum-provider/index.ts @@ -32,6 +32,7 @@ import { TransactionAnnotation, } from "../enrichment" import { decodeJSON } from "../../lib/utils" +import { FeatureFlags } from "../../features" // A type representing the transaction requests that come in over JSON-RPC // requests like eth_sendTransaction and eth_signTransaction. These are very @@ -95,6 +96,7 @@ const validateAddEthereumChainParameter = ({ nativeCurrency, rpcUrls, }: AddEthereumChainParameter): ValidatedAddEthereumChainParameter => { + // @TODO Use AJV if ( !chainId || !chainName || @@ -104,14 +106,6 @@ const validateAddEthereumChainParameter = ({ !rpcUrls || !rpcUrls.length ) { - console.log({ - chainId, - chainName, - blockExplorerUrls, - iconUrls, - nativeCurrency, - rpcUrls, - }) throw new Error("Missing Chain Property") } @@ -327,7 +321,10 @@ export default class InternalEthereumProviderService extends BaseService this.switchToSupportedNetwork(supportedNetwork) return null } - // @TODO Feature Flag This + if (!FeatureFlags.SUPPORT_CUSTOM_NETWORKS) { + // Dissallow adding new chains until feature flag is turned on. + throw new EIP1193Error(EIP1193_ERROR_CODES.userRejectedRequest) + } try { const validatedParam = validateAddEthereumChainParameter(chainInfo) await this.chainService.addCustomChain(validatedParam) @@ -336,7 +333,6 @@ export default class InternalEthereumProviderService extends BaseService logger.error(e) throw new EIP1193Error(EIP1193_ERROR_CODES.userRejectedRequest) } - throw new EIP1193Error(EIP1193_ERROR_CODES.userRejectedRequest) } case "wallet_switchEthereumChain": { const newChainId = (params[0] as SwitchEthereumChainParameter).chainId From c9bbe77f2196c8e12c2f3ae9d40c2c2841edb9cd Mon Sep 17 00:00:00 2001 From: Daedalus <0xDaedalus@users.noreply.github.com> Date: Tue, 17 Jan 2023 13:14:28 -0700 Subject: [PATCH 07/20] Add tests for initialization and happy-path adding of a chain --- background/services/chain/db.ts | 2 + background/services/chain/index.ts | 6 ++- .../chain/tests/index.integration.test.ts | 21 +++++++++ .../tests/index.integration.test.ts | 45 +++++++++++++++++++ background/tests/factories.ts | 13 ++++++ 5 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 background/services/internal-ethereum-provider/tests/index.integration.test.ts diff --git a/background/services/chain/db.ts b/background/services/chain/db.ts index 533ba7c87f..e66b3a7884 100644 --- a/background/services/chain/db.ts +++ b/background/services/chain/db.ts @@ -214,6 +214,8 @@ export class ChainDatabase extends Dexie { chainID, }, }) + // A bit awkward that we are adding the base asset to the network as well + // as to its own separate table - but lets forge on for now. await this.addBaseAsset(assetName, symbol, chainID, decimals) await this.addRpcUrls(chainID, rpcUrls) } diff --git a/background/services/chain/index.ts b/background/services/chain/index.ts index f00c521556..72bb0607f5 100644 --- a/background/services/chain/index.ts +++ b/background/services/chain/index.ts @@ -1875,7 +1875,10 @@ export default class ChainService extends BaseService { } } - async addCustomChain(chainInfo: ValidatedAddEthereumChainParameter) { + // Used to add non-default chains via wallet_addEthereumChain + async addCustomChain( + chainInfo: ValidatedAddEthereumChainParameter + ): Promise { await this.db.addEVMNetwork( chainInfo.chainName, chainInfo.chainId, @@ -1884,5 +1887,6 @@ export default class ChainService extends BaseService { chainInfo.nativeCurrency.name, chainInfo.rpcUrls ) + this.supportedNetworks = await this.db.getAllEVMNetworks() } } diff --git a/background/services/chain/tests/index.integration.test.ts b/background/services/chain/tests/index.integration.test.ts index ecb76bbd37..9d418775b1 100644 --- a/background/services/chain/tests/index.integration.test.ts +++ b/background/services/chain/tests/index.integration.test.ts @@ -46,6 +46,27 @@ describe("ChainService", () => { ) ).toHaveLength(1) }) + + it("should initialize persisted data in the correct order", async () => { + const chainServiceInstance = await createChainService() + const initializeRPCs = sandbox.spy(chainServiceInstance, "initializeRPCs") + + const initializeBaseAssets = sandbox.spy( + chainServiceInstance, + "initializeBaseAssets" + ) + + const initializeNetworks = sandbox.spy( + chainServiceInstance, + "initializeNetworks" + ) + + await chainServiceInstance.internalStartService() + + expect(initializeRPCs.calledBefore(initializeBaseAssets)).toBe(true) + expect(initializeBaseAssets.calledBefore(initializeNetworks)).toBe(true) + expect(initializeNetworks.called).toBe(true) + }) }) it("handlePendingTransactions on chains without mempool should subscribe to transaction confirmations, and persist the transaction to indexedDB", async () => { diff --git a/background/services/internal-ethereum-provider/tests/index.integration.test.ts b/background/services/internal-ethereum-provider/tests/index.integration.test.ts new file mode 100644 index 0000000000..0a941f4d50 --- /dev/null +++ b/background/services/internal-ethereum-provider/tests/index.integration.test.ts @@ -0,0 +1,45 @@ +import sinon from "sinon" +import InternalEthereumProviderService from ".." +import { EVMNetwork } from "../../../networks" + +import { + createChainService, + createInternalEthereumProviderService, +} from "../../../tests/factories" + +describe("Internal Ethereum Provider Service", () => { + const sandbox = sinon.createSandbox() + let IEPService: InternalEthereumProviderService + + beforeEach(async () => { + sandbox.restore() + IEPService = await createInternalEthereumProviderService() + await IEPService.startService() + }) + + afterEach(async () => { + await IEPService.stopService() + }) + + it("should correctly persist chains sent in via wallet_addEthereumChain", async () => { + const chainService = createChainService() + + IEPService = await createInternalEthereumProviderService({ chainService }) + const startedChainService = await chainService + await startedChainService.startService() + await IEPService.startService() + const METHOD = "wallet_addEthereumChain" + const ORIGIN = "https://chainlist.org" + + // prettier-ignore + const EIP3085_PARAMS = [ { chainId: "0xfa", chainName: "Fantom Opera", nativeCurrency: { name: "Fantom", symbol: "FTM", decimals: 18, }, rpcUrls: [ "https://fantom-mainnet.gateway.pokt.network/v1/lb/62759259ea1b320039c9e7ac", "https://rpc.ftm.tools", "https://rpc.ankr.com/fantom", "https://rpc.fantom.network", "https://rpc2.fantom.network", "https://rpc3.fantom.network", "https://rpcapi.fantom.network", "https://fantom-mainnet.public.blastapi.io", "https://1rpc.io/ftm", ], blockExplorerUrls: ["https://ftmscan.com"], }, "0xd8da6bf26964af9d7eed9e03e53415d37aa96045", ] + + await IEPService.routeSafeRPCRequest(METHOD, EIP3085_PARAMS, ORIGIN) + + expect( + startedChainService.supportedNetworks.find( + (network: EVMNetwork) => network.name === "Fantom Opera" + ) + ).toBeTruthy() + }) +}) diff --git a/background/tests/factories.ts b/background/tests/factories.ts index c9eb65f875..dbc4e9263d 100644 --- a/background/tests/factories.ts +++ b/background/tests/factories.ts @@ -35,6 +35,7 @@ import { AnalyticsService, ChainService, IndexingService, + InternalEthereumProviderService, KeyringService, LedgerService, NameService, @@ -131,6 +132,18 @@ export const createSigningService = async ( ) } +export const createInternalEthereumProviderService = async ( + overrides: { + chainService?: Promise + preferenceService?: Promise + } = {} +): Promise => { + return InternalEthereumProviderService.create( + overrides.chainService ?? createChainService(), + overrides.preferenceService ?? createPreferenceService() + ) +} + // Copied from a legacy Optimism transaction generated with our test wallet. export const createLegacyTransactionRequest = ( overrides: Partial = {} From bf9af2fa8f52f48d38007ea49b1a9fa2c5f45d38 Mon Sep 17 00:00:00 2001 From: Karolina Kosiorowska Date: Wed, 18 Jan 2023 16:04:47 +0100 Subject: [PATCH 08/20] Add notification dot for tab bar element --- ui/components/TabBar/TabBar.tsx | 21 ++++++++++++++++-- ui/components/TabBar/TabBarIconButton.tsx | 26 ++++++++++++++++++++++- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/ui/components/TabBar/TabBar.tsx b/ui/components/TabBar/TabBar.tsx index 6b90b55caa..7d256f7e44 100644 --- a/ui/components/TabBar/TabBar.tsx +++ b/ui/components/TabBar/TabBar.tsx @@ -1,7 +1,10 @@ -import React, { ReactElement } from "react" +import React, { ReactElement, useCallback } from "react" import { matchPath, useHistory, useLocation } from "react-router-dom" -import { selectCurrentNetwork } from "@tallyho/tally-background/redux-slices/selectors" +import { + selectAbilityCount, + selectCurrentNetwork, +} from "@tallyho/tally-background/redux-slices/selectors" import { NETWORKS_SUPPORTING_SWAPS } from "@tallyho/tally-background/constants/networks" import { EVMNetwork } from "@tallyho/tally-background/networks" import { useTranslation } from "react-i18next" @@ -21,6 +24,7 @@ const isTabSupportedByNetwork = (tab: TabInfo, network: EVMNetwork) => { export default function TabBar(): ReactElement { const location = useLocation() const selectedNetwork = useBackgroundSelector(selectCurrentNetwork) + const abilityCount = useBackgroundSelector(selectAbilityCount) const history = useHistory() const { t } = useTranslation() @@ -33,6 +37,18 @@ export default function TabBar(): ReactElement { matchPath(location.pathname, { path, exact: false }) ) ?? defaultTab + const hasNotifications = useCallback( + (path: string): boolean => { + switch (path) { + case "/portfolio": + return abilityCount > 0 + default: + return false + } + }, + [abilityCount] + ) + return (