diff --git a/.changeset/base-network-registration.md b/.changeset/base-network-registration.md new file mode 100644 index 000000000..1a3f2cb2d --- /dev/null +++ b/.changeset/base-network-registration.md @@ -0,0 +1,8 @@ +--- +'@rosen-bridge/icons': minor +'@rosen-network/evm': minor +'@rosen-ui/constants': minor +'@rosen-ui/utils': minor +--- + +Register Base in the shared UI network registry, EVM RPC handling, explorer URL helpers, and network icons. diff --git a/.changeset/base-service-backend.md b/.changeset/base-service-backend.md new file mode 100644 index 000000000..61dc11aae --- /dev/null +++ b/.changeset/base-service-backend.md @@ -0,0 +1,6 @@ +--- +'@rosen-ui/asset-calculator': minor +'@rosen-bridge/rosen-service': minor +--- + +Add Base support to the asset calculator and rosen-service scanner, observation, event-trigger, config, and health-check flows. diff --git a/apps/rosen-service/config/default.yaml b/apps/rosen-service/config/default.yaml index b69144970..eb6d0487f 100644 --- a/apps/rosen-service/config/default.yaml +++ b/apps/rosen-service/config/default.yaml @@ -93,6 +93,18 @@ binance: initialHeight: rpcUrl: https://bsc-mainnet.public.blastapi.io/ # rpcAuthToken: +base: + addresses: + lock: + eventTrigger: + permit: + fraud: + commitment: + tokens: + rwt: + initialHeight: + rpcUrl: # use a production provider; the public Base endpoint is rate-limited + # rpcAuthToken: postgres: url: # postgresql://username:password@host:port/databasename logging: false @@ -107,6 +119,7 @@ calculator: doge: [] ethereum: [] binance: [] + base: [] healthCheck: ergoScannerWarnDiff: 3 ergoScannerCriticalDiff: 5 @@ -120,6 +133,8 @@ healthCheck: ethereumScannerCriticalDiff: 5 binanceScannerWarnDiff: 3 binanceScannerCriticalDiff: 5 + baseScannerWarnDiff: 3 + baseScannerCriticalDiff: 5 interval: 60 # health check update interval (in seconds) duration: 600 # log duration time check (in seconds) maxAllowedErrorCount: 1 # maximum allowed error log lines diff --git a/apps/rosen-service/package.json b/apps/rosen-service/package.json index d138fc738..2bad66cc8 100644 --- a/apps/rosen-service/package.json +++ b/apps/rosen-service/package.json @@ -36,10 +36,11 @@ "@rosen-bridge/discord-notification": "^1.0.0", "@rosen-bridge/ergo-observation-extractor": "^1.0.3", "@rosen-bridge/ergo-scanner": "^1.0.2", - "@rosen-bridge/evm-observation-extractor": "^6.0.3", + "@rosen-bridge/evm-observation-extractor": "^7.0.0", "@rosen-bridge/evm-scanner": "^1.0.2", "@rosen-bridge/health-check": "^8.0.0", "@rosen-bridge/log-level-check": "^4.0.0", + "@rosen-bridge/rosen-extractor": "^12.0.1", "@rosen-bridge/scanner-interfaces": "^0.2.2", "@rosen-bridge/scanner-sync-check": "^8.2.0", "@rosen-bridge/tokens": "^6.0.0", diff --git a/apps/rosen-service/src/calculator/calculator-service.ts b/apps/rosen-service/src/calculator/calculator-service.ts index fe48f0935..f3ab40734 100644 --- a/apps/rosen-service/src/calculator/calculator-service.ts +++ b/apps/rosen-service/src/calculator/calculator-service.ts @@ -74,6 +74,11 @@ const start = async () => { rpcUrl: config.binance.rpcUrl, authToken: config.binance.rpcAuthToken, }, + { + addresses: config.calculator.addresses.base, + rpcUrl: config.base.rpcUrl, + authToken: config.base.rpcAuthToken, + }, { addresses: config.calculator.addresses.doge, blockcypherUrl: config.doge.blockcypherUrl, diff --git a/apps/rosen-service/src/configs.ts b/apps/rosen-service/src/configs.ts index ca0972501..0684f10ad 100644 --- a/apps/rosen-service/src/configs.ts +++ b/apps/rosen-service/src/configs.ts @@ -123,6 +123,21 @@ const getConfig = () => { rpcUrl: nodeConfig.get('binance.rpcUrl'), rpcAuthToken: getOptionalString('binance.rpcAuthToken'), }, + base: { + addresses: { + lock: nodeConfig.get('base.addresses.lock'), + eventTrigger: nodeConfig.get('base.addresses.eventTrigger'), + permit: nodeConfig.get('base.addresses.permit'), + fraud: nodeConfig.get('base.addresses.fraud'), + commitment: nodeConfig.get('base.addresses.commitment'), + }, + initialHeight: nodeConfig.get('base.initialHeight'), + tokens: { + rwt: nodeConfig.get('base.tokens.rwt'), + }, + rpcUrl: nodeConfig.get('base.rpcUrl'), + rpcAuthToken: getOptionalString('base.rpcAuthToken'), + }, doge: { addresses: { lock: nodeConfig.get('doge.addresses.lock'), @@ -159,6 +174,7 @@ const getConfig = () => { ), ethereum: nodeConfig.get('calculator.addresses.ethereum'), binance: nodeConfig.get('calculator.addresses.binance'), + base: nodeConfig.get('calculator.addresses.base'), doge: nodeConfig.get('calculator.addresses.doge'), }, interval: nodeConfig.get('calculator.interval'), @@ -200,6 +216,12 @@ const getConfig = () => { binanceScannerCriticalDiff: nodeConfig.get( 'healthCheck.binanceScannerCriticalDiff', ), + baseScannerWarnDiff: nodeConfig.get( + 'healthCheck.baseScannerWarnDiff', + ), + baseScannerCriticalDiff: nodeConfig.get( + 'healthCheck.baseScannerCriticalDiff', + ), updateInterval: nodeConfig.get('healthCheck.interval'), logDuration: nodeConfig.get('healthCheck.duration'), errorLogAllowedCount: nodeConfig.get( diff --git a/apps/rosen-service/src/constants.ts b/apps/rosen-service/src/constants.ts index aea846ff5..fd4b204e7 100644 --- a/apps/rosen-service/src/constants.ts +++ b/apps/rosen-service/src/constants.ts @@ -6,6 +6,7 @@ export const BITCOIN_SCANNER_INTERVAL = 10 * 60 * 1000; export const DOGE_SCANNER_INTERVAL = 60 * 1000; export const ETHEREUM_SCANNER_INTERVAL = 60 * 1000; export const BINANCE_SCANNER_INTERVAL = 10 * 1000; +export const BASE_SCANNER_INTERVAL = 20 * 1000; export const ERGO_SCANNER_LOGGER_NAME = 'ergoScanner'; export const CARDANO_SCANNER_LOGGER_NAME = 'cardanoScanner'; @@ -13,11 +14,13 @@ export const BITCOIN_SCANNER_LOGGER_NAME = 'bitcoinScanner'; export const DOGE_SCANNER_LOGGER_NAME = 'dogeScanner'; export const ETHEREUM_SCANNER_LOGGER_NAME = 'ethereumScanner'; export const BINANCE_SCANNER_LOGGER_NAME = 'binanceScanner'; +export const BASE_SCANNER_LOGGER_NAME = 'baseScanner'; export const ERGO_BLOCK_TIME = 120; export const CARDANO_BLOCK_TIME = 20; export const BITCOIN_BLOCK_TIME = 600; export const ETHEREUM_BLOCK_TIME = 12; export const BINANCE_BLOCK_TIME = 3; +export const BASE_BLOCK_TIME = 2; export const DOGE_BLOCK_TIME = 60; export const BITCOIN_RUNES_CONFIG_KEY = 'bitcoinRunes'; diff --git a/apps/rosen-service/src/event-trigger/event-trigger-service.ts b/apps/rosen-service/src/event-trigger/event-trigger-service.ts index 69c47fd16..d995f4a6a 100644 --- a/apps/rosen-service/src/event-trigger/event-trigger-service.ts +++ b/apps/rosen-service/src/event-trigger/event-trigger-service.ts @@ -26,6 +26,9 @@ const ethereumEventTriggerExtractorLogger = logger.child( const binanceEventTriggerExtractorLogger = logger.child( 'binanceEventTriggerExtractor', ); +const baseEventTriggerExtractorLogger = logger.child( + 'baseEventTriggerExtractor', +); const dogeEventTriggerExtractorLogger = logger.child( 'dogeEventTriggerExtractor', ); @@ -113,6 +116,17 @@ export const registerExtractors = async (scanner: ErgoScanner) => { configs.binance.addresses.fraud, binanceEventTriggerExtractorLogger, ); + const baseEventTriggerExtractor = new EventTriggerExtractor( + 'base-extractor', + dataSource, + ErgoNetworkType.Explorer, + configs.ergo.explorerUrl, + configs.base.addresses.eventTrigger, + configs.base.tokens.rwt, + configs.base.addresses.permit, + configs.base.addresses.fraud, + baseEventTriggerExtractorLogger, + ); await scanner.registerExtractor(ergoEventTriggerExtractor); await scanner.registerExtractor(cardanoEventTriggerExtractor); await scanner.registerExtractor(bitcoinEventTriggerExtractor); @@ -120,6 +134,7 @@ export const registerExtractors = async (scanner: ErgoScanner) => { await scanner.registerExtractor(dogeEventTriggerExtractor); await scanner.registerExtractor(ethereumEventTriggerExtractor); await scanner.registerExtractor(binanceEventTriggerExtractor); + await scanner.registerExtractor(baseEventTriggerExtractor); logger.debug('event trigger extractors registered', { scannerName: scanner.name(), @@ -131,6 +146,7 @@ export const registerExtractors = async (scanner: ErgoScanner) => { dogeEventTriggerExtractor.getId(), ethereumEventTriggerExtractor.getId(), binanceEventTriggerExtractor.getId(), + baseEventTriggerExtractor.getId(), ], }); } catch (error) { diff --git a/apps/rosen-service/src/health-check/health-check-service.ts b/apps/rosen-service/src/health-check/health-check-service.ts index 90c290d0a..2e3591170 100644 --- a/apps/rosen-service/src/health-check/health-check-service.ts +++ b/apps/rosen-service/src/health-check/health-check-service.ts @@ -9,6 +9,8 @@ import path from 'node:path'; import config from '../configs'; import { + BASE_BLOCK_TIME, + BASE_SCANNER_INTERVAL, BINANCE_BLOCK_TIME, BINANCE_SCANNER_INTERVAL, BITCOIN_BLOCK_TIME, @@ -131,6 +133,17 @@ const registerAllHealthChecks = (healthCheck: HealthCheck) => { ), label: 'binance', }, + { + instance: new ScannerSyncHealthCheckParam( + scannerService.getBaseScanner().name(), + async () => getLastSavedBlock(scannerService.getBaseScanner().name()), + config.healthCheck.baseScannerWarnDiff, + config.healthCheck.baseScannerCriticalDiff, + BASE_BLOCK_TIME, + BASE_SCANNER_INTERVAL, + ), + label: 'base', + }, ]; for (const { instance, label } of checks) { diff --git a/apps/rosen-service/src/observation/chains/base.ts b/apps/rosen-service/src/observation/chains/base.ts new file mode 100644 index 000000000..2b6f7d11f --- /dev/null +++ b/apps/rosen-service/src/observation/chains/base.ts @@ -0,0 +1,41 @@ +import { DefaultLogger } from '@rosen-bridge/abstract-logger'; +import { BaseRpcObservationExtractor } from '@rosen-bridge/evm-observation-extractor'; +import { EvmRpcScanner } from '@rosen-bridge/evm-scanner'; + +import config from '../../configs'; +import dataSource from '../../data-source'; +import AppError from '../../errors/AppError'; +import { getTokenMap } from '../../utils'; + +const logger = DefaultLogger.getInstance().child(import.meta.url); + +/** + * register an observation extractor for the provided scanner + * @param scanner + */ +export const registerBaseExtractor = async (scanner: EvmRpcScanner) => { + try { + const observationExtractor = new BaseRpcObservationExtractor( + config.base.addresses.lock, + dataSource, + await getTokenMap(), + logger.child('baseRpcObservationExtractor'), + ); + + await scanner.registerExtractor(observationExtractor); + + logger.debug('base observation extractor registered', { + scannerName: scanner.name(), + }); + } catch (error) { + throw new AppError( + `cannot create or register base observation extractor due to error: ${error}`, + false, + 'error', + error instanceof Error ? error.stack : undefined, + { + scannerName: scanner.name(), + }, + ); + } +}; diff --git a/apps/rosen-service/src/observation/observation-service.ts b/apps/rosen-service/src/observation/observation-service.ts index 8d070f39f..792d01b13 100644 --- a/apps/rosen-service/src/observation/observation-service.ts +++ b/apps/rosen-service/src/observation/observation-service.ts @@ -1,3 +1,4 @@ +import { registerBaseExtractor } from './chains/base'; import { registerBinanceExtractor } from './chains/binance'; import { registerBitcoinExtractor } from './chains/bitcoin'; import { registerBitcoinRunesExtractor } from './chains/bitcoin-runes'; @@ -14,6 +15,7 @@ const observationService = { registerErgoExtractor, registerEthereumExtractor, registerBinanceExtractor, + registerBaseExtractor, }; export default observationService; diff --git a/apps/rosen-service/src/scanner/chains/base.ts b/apps/rosen-service/src/scanner/chains/base.ts new file mode 100644 index 000000000..f034b92a8 --- /dev/null +++ b/apps/rosen-service/src/scanner/chains/base.ts @@ -0,0 +1,72 @@ +import { DefaultLogger } from '@rosen-bridge/abstract-logger'; +import { + FailoverStrategy, + NetworkConnectorManager, +} from '@rosen-bridge/abstract-scanner'; +import { EvmRpcNetwork, EvmRpcScanner } from '@rosen-bridge/evm-scanner'; +import { TransactionResponse } from 'ethers'; + +import config from '../../configs'; +import { + BASE_SCANNER_INTERVAL, + BASE_SCANNER_LOGGER_NAME, + SCANNER_API_TIMEOUT, +} from '../../constants'; +import dataSource from '../../data-source'; +import AppError from '../../errors/AppError'; +import observationService from '../../observation/observation-service'; +import { startScanner } from '../scanner-utils'; + +const logger = DefaultLogger.getInstance().child(import.meta.url); +const scannerLogger = logger.child(BASE_SCANNER_LOGGER_NAME); + +/** + * Creates and configures a NetworkConnectorManager instance for base scanner + */ +export const createBaseNetworkConnectorManager = () => { + const networkConnectorManager = + new NetworkConnectorManager( + new FailoverStrategy(), + scannerLogger, + ); + + networkConnectorManager.addConnector( + new EvmRpcNetwork( + config.base.rpcUrl, + SCANNER_API_TIMEOUT * 1000, + config.base.rpcAuthToken, + ), + ); + + return networkConnectorManager; +}; + +/** + * create a base scanner, initializing it and calling its update method + * periodically + */ +export const startBaseScanner = async () => { + try { + const scanner = new EvmRpcScanner('base', { + dataSource, + initialHeight: config.base.initialHeight, + logger: scannerLogger, + network: createBaseNetworkConnectorManager(), + }); + + await observationService.registerBaseExtractor(scanner); + + startScanner(scanner, import.meta.url, BASE_SCANNER_INTERVAL); + + logger.debug('base scanner started'); + + return scanner; + } catch (error) { + throw new AppError( + `cannot create or start base scanner due to error: ${error}`, + false, + 'error', + error instanceof Error ? error.stack : undefined, + ); + } +}; diff --git a/apps/rosen-service/src/scanner/scanner-service.ts b/apps/rosen-service/src/scanner/scanner-service.ts index 5b589c910..28a7c6d67 100644 --- a/apps/rosen-service/src/scanner/scanner-service.ts +++ b/apps/rosen-service/src/scanner/scanner-service.ts @@ -8,6 +8,7 @@ import { ErgoScanner } from '@rosen-bridge/ergo-scanner'; import { EvmRpcScanner } from '@rosen-bridge/evm-scanner'; import { handleError } from '../utils'; +import { startBaseScanner } from './chains/base'; import { startBinanceScanner } from './chains/binance'; import { startBitcoinScanner } from './chains/bitcoin'; import { startCardanoScanner } from './chains/cardano'; @@ -23,6 +24,7 @@ let cardanoScanner: CardanoKoiosScanner; let bitcoinScanner: BitcoinRpcScanner; let ethereumScanner: EvmRpcScanner; let binanceScanner: EvmRpcScanner; +let baseScanner: EvmRpcScanner; let dogeScanner: DogeRpcScanner; /** @@ -36,6 +38,7 @@ const start = async () => { bitcoinScanner, ethereumScanner, binanceScanner, + baseScanner, dogeScanner, ] = await Promise.all([ startErgoScanner(), @@ -43,6 +46,7 @@ const start = async () => { startBitcoinScanner(), startEthereumScanner(), startBinanceScanner(), + startBaseScanner(), startDogeScanner(), ]); @@ -53,6 +57,7 @@ const start = async () => { bitcoinScanner.name(), ethereumScanner.name(), binanceScanner.name(), + baseScanner.name(), dogeScanner.name(), ], }); @@ -69,6 +74,7 @@ const scannerService = { getBitcoinScanner: () => bitcoinScanner, getEthereumScanner: () => ethereumScanner, getBinanceScanner: () => binanceScanner, + getBaseScanner: () => baseScanner, getDogeScanner: () => dogeScanner, }; diff --git a/apps/rosen/.env.example b/apps/rosen/.env.example index bd7003aa9..b5b1de2af 100644 --- a/apps/rosen/.env.example +++ b/apps/rosen/.env.example @@ -7,6 +7,7 @@ CARDANO_KOIOS_API='' ERGO_EXPLORER_API='' BITCOIN_ESPLORA_API='' DOGE_BLOCKCYPHER_API='' +BASE_RPC_API='' ETHEREUM_RPC_API='' BINANCE_RPC_API='' BITCOIN_RUNES_API='' diff --git a/apps/rosen/src/networks/base/client.ts b/apps/rosen/src/networks/base/client.ts new file mode 100644 index 000000000..7fb9a8d5f --- /dev/null +++ b/apps/rosen/src/networks/base/client.ts @@ -0,0 +1,69 @@ +import { Base as BaseIcon } from '@rosen-bridge/icons'; +import { Network, NetworkConfig } from '@rosen-network/base'; +import { generateLockData, generateTxParameters } from '@rosen-network/evm'; +import { NETWORKS } from '@rosen-ui/constants'; + +import { unwrapFromObject } from '@/safeServerAction'; + +import { LOCK_ADDRESSES } from '../../../configs'; +import * as actions from './server'; + +type BaseNetworkConfig = NetworkConfig & { + generateLockData: typeof generateLockData; + generateTxParameters: ReturnType; +}; + +class BaseNetwork implements Network { + public label = NETWORKS.base.label; + + public lockAddress: string; + + public logo = BaseIcon; + + public name = NETWORKS.base.key; + + public nextHeightInterval: number; + + constructor(protected config: BaseNetworkConfig) { + this.nextHeightInterval = config.nextHeightInterval; + this.lockAddress = config.lockAddress; + } + + public calculateFee: BaseNetworkConfig['calculateFee'] = (...args) => { + return this.config.calculateFee(...args); + }; + + public generateLockData: BaseNetworkConfig['generateLockData'] = ( + ...args + ) => { + return this.config.generateLockData(...args); + }; + + public generateTxParameters: BaseNetworkConfig['generateTxParameters'] = ( + ...args + ) => { + return this.config.generateTxParameters(...args); + }; + + public getMaxTransfer: BaseNetworkConfig['getMaxTransfer'] = (...args) => { + return this.config.getMaxTransfer(...args); + }; + + public getMinTransfer: BaseNetworkConfig['getMinTransfer'] = (...args) => { + return this.config.getMinTransfer(...args); + }; + + public toSafeAddress = (address: string): string => { + return address.toLowerCase(); + }; + + public validateAddress = (walletAddress: string): Promise => { + return this.config.validateAddress(this.name, walletAddress); + }; +} + +export const base = new BaseNetwork({ + lockAddress: LOCK_ADDRESSES.base, + nextHeightInterval: 50, + ...unwrapFromObject(actions), +}); diff --git a/apps/rosen/src/networks/base/index.ts b/apps/rosen/src/networks/base/index.ts new file mode 100644 index 000000000..4f1cce44f --- /dev/null +++ b/apps/rosen/src/networks/base/index.ts @@ -0,0 +1 @@ +export * from './client'; diff --git a/apps/rosen/src/networks/base/server.ts b/apps/rosen/src/networks/base/server.ts new file mode 100644 index 000000000..f10892fbc --- /dev/null +++ b/apps/rosen/src/networks/base/server.ts @@ -0,0 +1,63 @@ +'use server'; + +import { validateAddress as validateAddressCore } from '@rosen-network/base'; +import { + CalculateFee, + calculateFeeCreator, + getMinTransferCreator as getMinTransferCreatorBase, +} from '@rosen-network/base'; +import { + EvmChains, + generateLockData as generateLockDataCore, + generateTxParameters as generateTxParametersCore, + getHeight as getHeightCore, + getMaxTransferCreator as getMaxTransferCreatorBase, +} from '@rosen-network/evm'; +import { NETWORKS } from '@rosen-ui/constants'; + +import { wrap } from '@/safeServerAction'; +import { getTokenMap } from '@/tokenMap/getServerTokenMap'; + +const getHeight = async (): Promise => { + return await getHeightCore(EvmChains.BASE); +}; + +const calculateFeeCore: CalculateFee = calculateFeeCreator( + NETWORKS.base.key, + getHeight, +); + +export const calculateFee = wrap(calculateFeeCore, { + cache: 10 * 60 * 1000, + traceKey: 'base:calculateFee', +}); + +export const generateLockData = wrap(generateLockDataCore, { + traceKey: 'base:generateLockData', +}); + +export const generateTxParameters = wrap( + generateTxParametersCore(getTokenMap), + { + traceKey: 'base:generateTxParameters', + }, +); + +export const getMaxTransfer = wrap( + getMaxTransferCreatorBase(getTokenMap, EvmChains.BASE), + { + traceKey: 'base:getMaxTransfer', + }, +); + +export const getMinTransfer = wrap( + getMinTransferCreatorBase(NETWORKS.base.key, calculateFeeCore)(getTokenMap), + { + traceKey: 'base:getMinTransfer', + }, +); + +export const validateAddress = wrap(validateAddressCore, { + cache: Infinity, + traceKey: 'base:validateAddress', +}); diff --git a/apps/rosen/src/networks/index.ts b/apps/rosen/src/networks/index.ts index c56c522a0..4e922c8db 100644 --- a/apps/rosen/src/networks/index.ts +++ b/apps/rosen/src/networks/index.ts @@ -1,3 +1,4 @@ +export * from './base'; export * from './binance'; export * from './bitcoin'; export * from './bitcoin-runes'; diff --git a/apps/rosen/src/wallets/metaMask.ts b/apps/rosen/src/wallets/metaMask.ts index f18dad705..1d3e94996 100644 --- a/apps/rosen/src/wallets/metaMask.ts +++ b/apps/rosen/src/wallets/metaMask.ts @@ -1,9 +1,9 @@ import { MetaMaskWallet } from '@rosen-ui/metamask-wallet'; -import { binance, ethereum } from '@/networks'; +import { base, binance, ethereum } from '@/networks'; import { getTokenMap } from '@/tokenMap/getClientTokenMap'; export const metaMask = new MetaMaskWallet({ - networks: [binance, ethereum], + networks: [base, binance, ethereum], getTokenMap, }); diff --git a/apps/rosen/src/wallets/walletConnect.ts b/apps/rosen/src/wallets/walletConnect.ts index fa7c0ca4b..d00bbc4f0 100644 --- a/apps/rosen/src/wallets/walletConnect.ts +++ b/apps/rosen/src/wallets/walletConnect.ts @@ -1,10 +1,10 @@ import { WalletConnect } from '@rosen-ui/wallet-connect'; -import { binance, ethereum } from '@/networks'; +import { base, binance, ethereum } from '@/networks'; import { getTokenMap } from '@/tokenMap/getClientTokenMap'; export const walletConnect = new WalletConnect({ - networks: [binance, ethereum], + networks: [base, binance, ethereum], projectId: process.env.NEXT_PUBLIC_REOWN_PROJECT_ID!, getTokenMap, }); diff --git a/networks/evm/src/types.ts b/networks/evm/src/types.ts index 6bf40c7b6..7a8b53426 100644 --- a/networks/evm/src/types.ts +++ b/networks/evm/src/types.ts @@ -1,4 +1,5 @@ export enum EvmChains { + BASE = 'base', ETHEREUM = 'ethereum', BINANCE = 'binance', } diff --git a/networks/evm/src/utils.ts b/networks/evm/src/utils.ts index 7a64f61a1..b4024b1f9 100644 --- a/networks/evm/src/utils.ts +++ b/networks/evm/src/utils.ts @@ -73,6 +73,8 @@ export const isValidAddress = (addr: string) => { */ const getChainRpcUrl = (chain: EvmChains) => { switch (chain) { + case EvmChains.BASE: + return process.env.BASE_RPC_API; case EvmChains.ETHEREUM: return process.env.ETHEREUM_RPC_API; case EvmChains.BINANCE: diff --git a/package-lock.json b/package-lock.json index b519fff04..44a90fdcd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -147,10 +147,11 @@ "@rosen-bridge/discord-notification": "^1.0.0", "@rosen-bridge/ergo-observation-extractor": "^1.0.3", "@rosen-bridge/ergo-scanner": "^1.0.2", - "@rosen-bridge/evm-observation-extractor": "^6.0.3", + "@rosen-bridge/evm-observation-extractor": "^7.0.0", "@rosen-bridge/evm-scanner": "^1.0.2", "@rosen-bridge/health-check": "^8.0.0", "@rosen-bridge/log-level-check": "^4.0.0", + "@rosen-bridge/rosen-extractor": "^12.0.1", "@rosen-bridge/scanner-interfaces": "^0.2.2", "@rosen-bridge/scanner-sync-check": "^8.2.0", "@rosen-bridge/tokens": "^6.0.0", @@ -173,6 +174,238 @@ "npm": "11.6.2" } }, + "apps/rosen-service/node_modules/@blockfrost/openapi": { + "version": "0.1.86", + "resolved": "https://registry.npmjs.org/@blockfrost/openapi/-/openapi-0.1.86.tgz", + "integrity": "sha512-VWFshFtUVmg27jn/dSc+7FvGbGyuHBUyNCqyE4ah9EkE3+ZcF3mIC095TE+f8uS/4lMIryUEGBq/p2zrfF+dEw==", + "dependencies": { + "ajv": "^8.17.1", + "cbor": "^9.0.2", + "rimraf": "6.0.1", + "yaml": "^2.6.1" + }, + "engines": { + "node": ">=20" + } + }, + "apps/rosen-service/node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "apps/rosen-service/node_modules/@rosen-bridge/evm-observation-extractor": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@rosen-bridge/evm-observation-extractor/-/evm-observation-extractor-7.0.0.tgz", + "integrity": "sha512-2B37XzjGLws5JrC3DEmKYiUjaNZf1s506+pgDrIEQkmglGdNDron8GY3upxa+/06cE+ggkeSiYTlgv20YZy9OQ==", + "license": "MIT", + "dependencies": { + "@rosen-bridge/abstract-logger": "^4.0.0", + "@rosen-bridge/abstract-observation-extractor": "^1.0.5", + "@rosen-bridge/extended-typeorm": "^1.1.0", + "@rosen-bridge/rosen-extractor": "^12.0.1", + "@rosen-bridge/scanner-interfaces": "^0.2.2", + "@rosen-bridge/tokens": "^6.0.1", + "blakejs": "^1.2.1", + "ethers": "6.16.0" + }, + "engines": { + "node": ">=22.18.0", + "npm": "11.6.2" + } + }, + "apps/rosen-service/node_modules/@rosen-bridge/rosen-extractor": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@rosen-bridge/rosen-extractor/-/rosen-extractor-12.0.1.tgz", + "integrity": "sha512-L5gdANYeTfp17K+8t54RDkuvNV1KYV1bvqz2lCKtPFigHckkChk5qli90QHF7TVJprYe8B67mPymdXdSY8Jelw==", + "license": "MIT", + "dependencies": { + "@bitcoinerlab/secp256k1": "^1.2.0", + "@blockfrost/openapi": "^0.1.80", + "@cardano-ogmios/schema": "^6.0.3", + "@emurgo/cardano-serialization-lib-nodejs": "^13.2.1", + "@rosen-bridge/abstract-logger": "^4.0.0", + "@rosen-bridge/address-manager": "^0.1.0", + "@rosen-bridge/json-bigint": "^1.1.0", + "@rosen-bridge/tokens": "^6.0.1", + "bitcoinjs-lib": "^6.1.5", + "ergo-lib-wasm-nodejs": "^0.24.1", + "ethers": "6.16.0", + "lodash-es": "^4.17.21" + }, + "engines": { + "node": ">=22.18.0", + "npm": "11.6.2" + } + }, + "apps/rosen-service/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "apps/rosen-service/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "apps/rosen-service/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "apps/rosen-service/node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "apps/rosen-service/node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "apps/rosen-service/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "apps/rosen-service/node_modules/lru-cache": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", + "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "apps/rosen-service/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "apps/rosen-service/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "apps/rosen-service/node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "apps/rosen-service/node_modules/rimraf": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", + "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", + "license": "ISC", + "dependencies": { + "glob": "^11.0.0", + "package-json-from-dist": "^1.0.0" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "apps/rosen-service/node_modules/yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "apps/rosen-service2": { "name": "@rosen-bridge/rosen-service2", "version": "0.0.2", @@ -5090,17 +5323,17 @@ } }, "node_modules/@rosen-bridge/abstract-extractor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@rosen-bridge/abstract-extractor/-/abstract-extractor-3.1.0.tgz", - "integrity": "sha512-gqVeMCRiJNsu37wltUNvHFYvsEDuYClGvAiWerlHWWmBPQsIxvodfmEdlOf1FAukaq3O9FA13RAYINtGuQW/Yw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@rosen-bridge/abstract-extractor/-/abstract-extractor-3.1.2.tgz", + "integrity": "sha512-yOLyVz29yww0n9bW/wew7yVertvQJBSCFwC+mPYuoVECFaoScHUgHESc3f4RM1kkA37WiSLx+WZp1v3xsVy/+g==", "license": "MIT", "dependencies": { "@rosen-bridge/abstract-logger": "^4.0.0", - "@rosen-bridge/extended-typeorm": "^1.0.1", + "@rosen-bridge/extended-typeorm": "^1.1.0", "@rosen-bridge/json-bigint": "^1.1.0", "@rosen-bridge/scanner-interfaces": "^0.2.2", - "@rosen-clients/ergo-explorer": "^2.1.1", - "@rosen-clients/ergo-node": "^3.1.1", + "@rosen-clients/ergo-explorer": "^2.1.2", + "@rosen-clients/ergo-node": "^3.1.2", "await-semaphore": "^0.1.3", "lodash-es": "^4.17.21", "p-queue": "^9.0.1", @@ -5129,17 +5362,17 @@ } }, "node_modules/@rosen-bridge/abstract-observation-extractor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rosen-bridge/abstract-observation-extractor/-/abstract-observation-extractor-1.0.3.tgz", - "integrity": "sha512-I1ANvR9fx/9FB41QbQxVuJFtwqvuPPax1bSdywgLtvLJktdFUGTm94BoSQTc+JJQtHlAMcyp1kA/zUiFfh81FA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@rosen-bridge/abstract-observation-extractor/-/abstract-observation-extractor-1.0.5.tgz", + "integrity": "sha512-UL4YKX61J74FvNGfiSreLAspikLnonUMqV7Ki494CbPCEzQMwTlCKek2T+TkQyhLBqPkXev+WdAUBtiztZ1tbw==", "license": "MIT", "dependencies": { - "@rosen-bridge/abstract-extractor": "^3.1.0", + "@rosen-bridge/abstract-extractor": "^3.1.2", "@rosen-bridge/abstract-logger": "^4.0.0", - "@rosen-bridge/extended-typeorm": "^1.0.1", - "@rosen-bridge/rosen-extractor": "^11.3.0", + "@rosen-bridge/extended-typeorm": "^1.1.0", + "@rosen-bridge/rosen-extractor": "^12.0.1", "@rosen-bridge/scanner-interfaces": "^0.2.2", - "@rosen-bridge/tokens": "^6.0.0", + "@rosen-bridge/tokens": "^6.0.1", "blakejs": "^1.2.1" }, "engines": { @@ -5147,6 +5380,218 @@ "npm": "11.6.2" } }, + "node_modules/@rosen-bridge/abstract-observation-extractor/node_modules/@blockfrost/openapi": { + "version": "0.1.86", + "resolved": "https://registry.npmjs.org/@blockfrost/openapi/-/openapi-0.1.86.tgz", + "integrity": "sha512-VWFshFtUVmg27jn/dSc+7FvGbGyuHBUyNCqyE4ah9EkE3+ZcF3mIC095TE+f8uS/4lMIryUEGBq/p2zrfF+dEw==", + "dependencies": { + "ajv": "^8.17.1", + "cbor": "^9.0.2", + "rimraf": "6.0.1", + "yaml": "^2.6.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@rosen-bridge/abstract-observation-extractor/node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@rosen-bridge/abstract-observation-extractor/node_modules/@rosen-bridge/rosen-extractor": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@rosen-bridge/rosen-extractor/-/rosen-extractor-12.0.1.tgz", + "integrity": "sha512-L5gdANYeTfp17K+8t54RDkuvNV1KYV1bvqz2lCKtPFigHckkChk5qli90QHF7TVJprYe8B67mPymdXdSY8Jelw==", + "license": "MIT", + "dependencies": { + "@bitcoinerlab/secp256k1": "^1.2.0", + "@blockfrost/openapi": "^0.1.80", + "@cardano-ogmios/schema": "^6.0.3", + "@emurgo/cardano-serialization-lib-nodejs": "^13.2.1", + "@rosen-bridge/abstract-logger": "^4.0.0", + "@rosen-bridge/address-manager": "^0.1.0", + "@rosen-bridge/json-bigint": "^1.1.0", + "@rosen-bridge/tokens": "^6.0.1", + "bitcoinjs-lib": "^6.1.5", + "ergo-lib-wasm-nodejs": "^0.24.1", + "ethers": "6.16.0", + "lodash-es": "^4.17.21" + }, + "engines": { + "node": ">=22.18.0", + "npm": "11.6.2" + } + }, + "node_modules/@rosen-bridge/abstract-observation-extractor/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@rosen-bridge/abstract-observation-extractor/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@rosen-bridge/abstract-observation-extractor/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@rosen-bridge/abstract-observation-extractor/node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@rosen-bridge/abstract-observation-extractor/node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@rosen-bridge/abstract-observation-extractor/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/@rosen-bridge/abstract-observation-extractor/node_modules/lru-cache": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", + "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@rosen-bridge/abstract-observation-extractor/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@rosen-bridge/abstract-observation-extractor/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@rosen-bridge/abstract-observation-extractor/node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@rosen-bridge/abstract-observation-extractor/node_modules/rimraf": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", + "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", + "license": "ISC", + "dependencies": { + "glob": "^11.0.0", + "package-json-from-dist": "^1.0.0" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@rosen-bridge/abstract-observation-extractor/node_modules/yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/@rosen-bridge/abstract-scanner": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@rosen-bridge/abstract-scanner/-/abstract-scanner-1.0.2.tgz", @@ -5202,6 +5647,19 @@ "npm": "11.6.2" } }, + "node_modules/@rosen-bridge/address-manager": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@rosen-bridge/address-manager/-/address-manager-0.1.0.tgz", + "integrity": "sha512-yJaLWpRIsBCaxmX9rcubRuwf+dEsamkpDH7OpOEkk9Hv3jD/QYo+0QMD9Nx4OZcVGkKIoDjKZF8AQSrSS/8c9Q==", + "license": "MIT", + "dependencies": { + "@rosen-bridge/abstract-logger": "^4.0.0" + }, + "engines": { + "node": ">=22.18.0", + "npm": "11.6.2" + } + }, "node_modules/@rosen-bridge/bitcoin-observation-extractor": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@rosen-bridge/bitcoin-observation-extractor/-/bitcoin-observation-extractor-7.0.3.tgz", @@ -5836,15 +6294,21 @@ } }, "node_modules/@rosen-bridge/extended-typeorm": { - "version": "1.0.1", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rosen-bridge/extended-typeorm/-/extended-typeorm-1.1.0.tgz", + "integrity": "sha512-ln4heqek9wrtt0Bgy1am+f3jXwh7+wNMZ50m+f3B3CnUH5cdSksfhO/MRdcGXdRBb1ckiMXQ0OUPPauYHg0CKA==", "license": "MIT", "dependencies": { "async-mutex": "^0.4.0", "reflect-metadata": "0.2.2", "typeorm": "0.3.26" }, + "bin": { + "extended-typeorm": "dist/cli.js" + }, "engines": { - "node": ">=22.18.0" + "node": ">=22.18.0", + "npm": "11.6.2" }, "peerDependencies": { "sqlite3": "^5.1.6" @@ -6386,9 +6850,9 @@ } }, "node_modules/@rosen-bridge/tokens": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@rosen-bridge/tokens/-/tokens-6.0.0.tgz", - "integrity": "sha512-fXIWJSrYOvlux2d0U1FKhh9KyTA3+a9mnIkoTrWUqwN9snc+QC+UUG3VK/h7VitkjY7QrJXPcxU4RXl8fZ1HFA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@rosen-bridge/tokens/-/tokens-6.0.1.tgz", + "integrity": "sha512-MuInZSNqVnHlUmuszX9KJqcmFj4EnUNxP/o8rhDlDFKO+kxg8J73QNBdaC9Kak+xrWm61hgZOBbgxU3GGsrIBA==", "license": "MIT", "dependencies": { "@rosen-bridge/abstract-logger": "^4.0.0", @@ -6448,13 +6912,28 @@ } }, "node_modules/@rosen-clients/axios": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@rosen-clients/axios/-/axios-1.1.1.tgz", - "integrity": "sha512-whG91IfspeD9fZ4hRBrUCLn+FQGGPGz22y86VrnphLyo8VlrHgGXZQHn0H32dT8pPrQLyX4Fb4AekX4u90WOMw==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@rosen-clients/axios/-/axios-1.1.2.tgz", + "integrity": "sha512-w7oPmZwklo22YU9Ale+BwBJ9jnCmHTLqEXD1kCVqhH8nvU27KawNs5FlhGBZc3NJemd0L16MlKc5P9Cqa+kqcg==", "license": "MIT", "dependencies": { "@rosen-bridge/json-bigint": "^1.1.0", - "@rosen-clients/rate-limited-axios": "^1.1.1" + "@rosen-clients/rate-limited-axios": "^2.0.0" + }, + "engines": { + "node": ">=22.18.0", + "npm": "11.6.2" + } + }, + "node_modules/@rosen-clients/axios/node_modules/@rosen-clients/rate-limited-axios": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@rosen-clients/rate-limited-axios/-/rate-limited-axios-2.0.0.tgz", + "integrity": "sha512-2fYR5O+c57uvI8V0jXi2Ny6eZ8ykIHmtN6wlEg8SNhW1gvfJas8scIoF+AOLx9C+5Z9KlN6usV8fnaTa7OicaQ==", + "license": "MIT", + "dependencies": { + "@rosen-bridge/abstract-logger": "^4.0.0", + "await-semaphore": "^0.1.3", + "axios": "^1.13.2" }, "engines": { "node": ">=22.18.0", @@ -6475,12 +6954,12 @@ } }, "node_modules/@rosen-clients/ergo-explorer": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@rosen-clients/ergo-explorer/-/ergo-explorer-2.1.1.tgz", - "integrity": "sha512-o59VwrBogqCePbIaG23UCJk/5xSpM182QW1yhKOUF2LS1Wx3DplcufG2ZIqxaH3XVfFCy8Xe1g8/UZdOxlOzFw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@rosen-clients/ergo-explorer/-/ergo-explorer-2.1.2.tgz", + "integrity": "sha512-gB1K+jO3T391nQq69N/TBne+rjjDGmf7ChF9IQEk7t6Q7YHnwVGQAF8oOWJPY9NUPpAEogxfNfptTdoIc1k3rw==", "license": "MIT", "dependencies": { - "@rosen-clients/axios": "^1.1.1" + "@rosen-clients/axios": "^1.1.2" }, "engines": { "node": ">=22.18.0", @@ -6488,12 +6967,12 @@ } }, "node_modules/@rosen-clients/ergo-node": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@rosen-clients/ergo-node/-/ergo-node-3.1.1.tgz", - "integrity": "sha512-XMEJ8E65HocS2mT2mOQTWo61UwPjQqI5GMhBTK7mOW9chgPsrM5zgWbUdiyPC77dBsGDzDuywur8JuxFlhKztA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@rosen-clients/ergo-node/-/ergo-node-3.1.2.tgz", + "integrity": "sha512-cjoo9J+zyzvr6KLsfxcVmxGU+ot/qwd82u98po7geHWRTFHuUmh88SeLQIU3bi+VPfkAUh3iLOUeh0zSgMsEpw==", "license": "MIT", "dependencies": { - "@rosen-clients/axios": "^1.1.1" + "@rosen-clients/axios": "^1.1.2" }, "engines": { "node": ">=22.18.0", diff --git a/packages/asset-calculator/lib/asset-calculator.ts b/packages/asset-calculator/lib/asset-calculator.ts index 2ce8f0414..19c217216 100644 --- a/packages/asset-calculator/lib/asset-calculator.ts +++ b/packages/asset-calculator/lib/asset-calculator.ts @@ -48,6 +48,7 @@ class AssetCalculator { bitcoinRunesCalculator: BitcoinRunesCalculatorInterface, ethereumCalculator: EvmCalculatorInterface, binanceCalculator: EvmCalculatorInterface, + baseCalculator: EvmCalculatorInterface, dogeCalculator: DogeCalculatorInterface, dataSource: DataSource, protected readonly logger: AbstractLogger = new DummyLogger(), @@ -95,6 +96,14 @@ class AssetCalculator { binanceCalculator.authToken, logger.child('binanceCalculator'), ); + const baseAssetCalculator = new EvmCalculator( + NETWORKS.base.key, + this.tokens, + baseCalculator.addresses, + baseCalculator.rpcUrl, + baseCalculator.authToken, + logger.child('baseCalculator'), + ); const dogeAssetCalculator = new DogeCalculator( this.tokens, dogeCalculator.addresses, @@ -110,6 +119,7 @@ class AssetCalculator { ); this.calculatorMap.set(NETWORKS.ethereum.key, ethereumAssetCalculator); this.calculatorMap.set(NETWORKS.binance.key, binanceAssetCalculator); + this.calculatorMap.set(NETWORKS.base.key, baseAssetCalculator); this.calculatorMap.set(NETWORKS.doge.key, dogeAssetCalculator); this.bridgedAssetModel = new BridgedAssetModel( dataSource, diff --git a/packages/asset-calculator/tests/asset-calculator.spec.ts b/packages/asset-calculator/tests/asset-calculator.spec.ts index 7c6d1d068..a8ec3e0ad 100644 --- a/packages/asset-calculator/tests/asset-calculator.spec.ts +++ b/packages/asset-calculator/tests/asset-calculator.spec.ts @@ -9,6 +9,40 @@ import { bridgedAssets, lockedAssets, tokens } from './database/test-data'; import { tokenMapData } from './test-data'; describe('AssetCalculator', () => { + describe('constructor', () => { + /** + * @target AssetCalculator.constructor should register Base calculator + * @dependencies + * - NETWORKS + * @scenario + * - create AssetCalculator with Base EVM calculator config + * - read calculatorMap + * @expected + * - Base calculator should be available in calculatorMap + */ + it('should register Base calculator', async () => { + const dataSource = await initDatabase(); + const tokenMap = new TokenMap(); + await tokenMap.updateConfigByJson(tokenMapData); + const assetCalculator = new AssetCalculator( + tokenMap, + { addresses: ['Addr'], explorerUrl: 'explorerUrl' }, + { addresses: ['Addr'], koiosUrl: 'koiosUrl' }, + { addresses: ['Addr'], esploraUrl: 'esploraUrl' }, + { addresses: ['Addr'], unisatUrl: 'unisatUrl' }, + { addresses: ['Addr'], rpcUrl: 'rpcUrl' }, + { addresses: ['Addr'], rpcUrl: 'bnbRpcUrl' }, + { addresses: ['Addr'], rpcUrl: 'baseRpcUrl' }, + { addresses: ['Addr'], blockcypherUrl: 'blockcypherUrl' }, + dataSource, + ); + + expect(assetCalculator['calculatorMap'].has(NETWORKS.base.key)).toBe( + true, + ); + }); + }); + describe('calculateEmissionForChain', () => { /** * Mock database and create the AssetCalculator instance before each test @@ -29,6 +63,7 @@ describe('AssetCalculator', () => { { addresses: ['hotAddr', 'coldAddr'], unisatUrl: 'unisatUrl' }, { addresses: ['hotAddr', 'coldAddr'], rpcUrl: 'rpcUrl' }, { addresses: ['hotAddr', 'coldAddr'], rpcUrl: 'bnbRpcUrl' }, + { addresses: ['hotAddr', 'coldAddr'], rpcUrl: 'baseRpcUrl' }, { addresses: ['hotAddr', 'coldAddr'], blockcypherUrl: 'blockcypherUrl', @@ -116,6 +151,7 @@ describe('AssetCalculator', () => { { addresses: ['hotAddr', 'coldAddr'], unisatUrl: 'unisatUrl' }, { addresses: ['hotAddr', 'coldAddr'], rpcUrl: 'rpcUrl' }, { addresses: ['hotAddr', 'coldAddr'], rpcUrl: 'bnbRpcUrl' }, + { addresses: ['hotAddr', 'coldAddr'], rpcUrl: 'baseRpcUrl' }, { addresses: ['hotAddr', 'coldAddr'], blockcypherUrl: 'blockcypherUrl', @@ -192,6 +228,7 @@ describe('AssetCalculator', () => { { addresses: ['Addr'], unisatUrl: 'unisatUrl' }, { addresses: ['Addr'], rpcUrl: 'rpcUrl' }, { addresses: ['Addr'], rpcUrl: 'bnbRpcUrl' }, + { addresses: ['Addr'], rpcUrl: 'baseRpcUrl' }, { addresses: ['Addr'], blockcypherUrl: 'blockcypherUrl' }, dataSource, ); @@ -228,17 +265,21 @@ describe('AssetCalculator', () => { await assetCalculator['lockedAssetModel'].getAllStoredAssets(); const allStoredTokens = await assetCalculator['tokenModel'].getAllStoredTokens(); - expect(upsertBridgedAssetSpy).to.have.toBeCalledTimes(3); + expect(upsertBridgedAssetSpy).to.have.toBeCalledTimes(4); expect(upsertLockedAssetSpy).to.have.toBeCalledTimes(3); expect(removeBridgedAssetSpy).to.have.toBeCalledWith([]); expect(removeLockedAssetSpy).to.have.toBeCalledWith([]); - expect(insertTokenSpy).toBeCalledTimes(6); + expect(insertTokenSpy).toBeCalledTimes(7); expect( allStoredBridgedAssets.sort((a, b) => a.tokenId.localeCompare(b.tokenId), ), ).toEqual( [ + { + tokenId: tokenMapData[0].ergo.tokenId, + chain: NETWORKS.base.key, + }, { tokenId: tokenMapData[0].ergo.tokenId, chain: NETWORKS.cardano.key, @@ -267,6 +308,7 @@ describe('AssetCalculator', () => { expect(allStoredTokens.sort()).toEqual( [ tokenMapData[0].ergo.tokenId, + tokenMapData[0].base.tokenId, tokenMapData[1].ergo.tokenId, tokenMapData[1].cardano.tokenId, tokenMapData[2].cardano.tokenId, @@ -311,6 +353,7 @@ describe('AssetCalculator', () => { { addresses: ['Addr'], unisatUrl: 'unisatUrl' }, { addresses: ['Addr'], rpcUrl: 'rpcUrl' }, { addresses: ['Addr'], rpcUrl: 'bnbRpcUrl' }, + { addresses: ['Addr'], rpcUrl: 'baseRpcUrl' }, { addresses: ['Addr'], blockcypherUrl: 'blockcypherUrl' }, dataSource, ); @@ -348,7 +391,7 @@ describe('AssetCalculator', () => { await assetCalculator['bridgedAssetModel'].getAllStoredAssets(); const allStoredLockedAssets = await assetCalculator['lockedAssetModel'].getAllStoredAssets(); - expect(updateBridgedAssetSpy).to.have.toBeCalledTimes(3); + expect(updateBridgedAssetSpy).to.have.toBeCalledTimes(4); expect(updateLockedAssetSpy).to.have.toBeCalledTimes(3); expect(removeBridgedAssetsSpy).to.have.toBeCalledWith( bridgedAssets.map((asset) => ({ @@ -370,6 +413,10 @@ describe('AssetCalculator', () => { ), ).toEqual( [ + { + tokenId: tokenMapData[0].ergo.tokenId, + chain: NETWORKS.base.key, + }, { tokenId: tokenMapData[0].ergo.tokenId, chain: NETWORKS.cardano.key, @@ -418,6 +465,7 @@ describe('AssetCalculator', () => { { addresses: ['hotAddr', 'coldAddr'], unisatUrl: 'unisatUrl' }, { addresses: ['hotAddr', 'coldAddr'], rpcUrl: 'rpcUrl' }, { addresses: ['hotAddr', 'coldAddr'], rpcUrl: 'bnbRpcUrl' }, + { addresses: ['hotAddr', 'coldAddr'], rpcUrl: 'baseRpcUrl' }, { addresses: ['hotAddr', 'coldAddr'], blockcypherUrl: 'blockcypherUrl', @@ -447,10 +495,14 @@ describe('AssetCalculator', () => { const cardanoCalculator = { totalSupply: vitest.fn().mockResolvedValue(2000n), } as unknown as AbstractCalculator; + const baseCalculator = { + totalSupply: vitest.fn().mockResolvedValue(3000n), + } as unknown as AbstractCalculator; assetCalculator['calculatorMap'] = new Map([ [NETWORKS.ergo.key, ergoCalculator], [NETWORKS.cardano.key, cardanoCalculator], + [NETWORKS.base.key, baseCalculator], ]); await assetCalculator['storeAllTokenSupplies'](); @@ -470,9 +522,14 @@ describe('AssetCalculator', () => { 2000n, ); + // Base wrapped token should be stored + const baseToken1Key = `${NETWORKS.base.key}-${tokenMapData[0].base.tokenId}`; + expect(assetCalculator['totalSupplyMap'].get(baseToken1Key)).toBe(3000n); + // Verify totalSupply was called for wrapped tokens expect(ergoCalculator.totalSupply).toHaveBeenCalled(); expect(cardanoCalculator.totalSupply).toHaveBeenCalled(); + expect(baseCalculator.totalSupply).toHaveBeenCalled(); }); /** @@ -495,6 +552,7 @@ describe('AssetCalculator', () => { assetCalculator['calculatorMap'] = new Map([ [NETWORKS.ergo.key, ergoCalculator], [NETWORKS.cardano.key, ergoCalculator], + [NETWORKS.base.key, ergoCalculator], ]); await assetCalculator['storeAllTokenSupplies'](); diff --git a/packages/asset-calculator/tests/test-data.ts b/packages/asset-calculator/tests/test-data.ts index 1321bc883..aa04ac1e3 100644 --- a/packages/asset-calculator/tests/test-data.ts +++ b/packages/asset-calculator/tests/test-data.ts @@ -22,6 +22,14 @@ export const tokenMapData: RosenTokens = [ type: 'tokenType', residency: 'wrapped', }, + base: { + tokenId: '0x1111111111111111111111111111111111111111', + extra: {}, + name: 'asset1', + decimals: 0, + type: 'tokenType', + residency: 'wrapped', + }, }, { ergo: { diff --git a/packages/constants/src/index.ts b/packages/constants/src/index.ts index cfc149ac5..7fd591f3b 100644 --- a/packages/constants/src/index.ts +++ b/packages/constants/src/index.ts @@ -65,6 +65,14 @@ export const NETWORKS = { id: '', hasTokenSupport: false, }, + 'base': { + index: 9, + key: 'base', + label: 'Base', + nativeToken: 'eth', + id: '0x2105', + hasTokenSupport: true, + }, } as const; export const NETWORKS_KEYS = Object.values(NETWORKS).map( diff --git a/packages/icons/src/index.ts b/packages/icons/src/index.ts index 18d5961e9..28d0f35af 100644 --- a/packages/icons/src/index.ts +++ b/packages/icons/src/index.ts @@ -150,6 +150,8 @@ export { default as Unlock } from './icons/unlock.svg?react'; export { default as Wallet } from './icons/wallet.svg?react'; +export { default as Base } from './networks/base.svg?react'; + export { default as Binance } from './networks/binance.svg?react'; export { default as BitcoinRunes } from './networks/bitcoin-runes.svg?react'; diff --git a/packages/icons/src/networks/base.svg b/packages/icons/src/networks/base.svg new file mode 100644 index 000000000..579fb766f --- /dev/null +++ b/packages/icons/src/networks/base.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/utils/package.json b/packages/utils/package.json index d53771867..a22e89282 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -13,10 +13,12 @@ "types": "dist/index.d.ts", "scripts": { "build": "tsc --build", + "coverage": "npm run test -- --run --coverage", "lint": "eslint --fix . && npm run prettify", "lint:check": "eslint . && npm run prettify:check", "prettify": "prettier --write . --ignore-path ../../.gitignore", "prettify:check": "prettier --check . --ignore-path ../../.gitignore", + "test": "NODE_OPTIONS='--import tsx' vitest", "type-check": "tsc --noEmit" }, "dependencies": { diff --git a/packages/utils/src/getAddressUrl.ts b/packages/utils/src/getAddressUrl.ts index 366a64e60..f135ca214 100644 --- a/packages/utils/src/getAddressUrl.ts +++ b/packages/utils/src/getAddressUrl.ts @@ -2,6 +2,7 @@ import { NETWORKS } from '@rosen-ui/constants'; import { Network } from '@rosen-ui/types'; const baseAddressURLs: { [key in Network]: string } = { + [NETWORKS.base.key]: 'https://basescan.org/address', [NETWORKS.binance.key]: 'https://bscscan.com/address', [NETWORKS.ergo.key]: 'https://explorer.ergoplatform.com/en/addresses', [NETWORKS.cardano.key]: 'https://cardanoscan.io/address', diff --git a/packages/utils/src/getTokenUrl.ts b/packages/utils/src/getTokenUrl.ts index 66cb748be..b0d172e97 100644 --- a/packages/utils/src/getTokenUrl.ts +++ b/packages/utils/src/getTokenUrl.ts @@ -2,6 +2,7 @@ import { NETWORKS } from '@rosen-ui/constants'; import { Network } from '@rosen-ui/types'; const baseTokenURLs: { [key in Network]: string } = { + [NETWORKS.base.key]: 'https://basescan.org/token', [NETWORKS.binance.key]: 'https://bscscan.com/token', [NETWORKS.ergo.key]: 'https://explorer.ergoplatform.com/en/token', [NETWORKS.cardano.key]: 'https://cardanoscan.io/token', diff --git a/packages/utils/src/getTxUrl.ts b/packages/utils/src/getTxUrl.ts index 7cc3f95bb..ef26abef0 100644 --- a/packages/utils/src/getTxUrl.ts +++ b/packages/utils/src/getTxUrl.ts @@ -11,6 +11,7 @@ import { Network } from '@rosen-ui/types'; type HttpsURL = `https://${string}`; const baseTxURLs: { [key in Network]: HttpsURL } = { + [NETWORKS.base.key]: 'https://basescan.org/tx', [NETWORKS.binance.key]: 'https://bscscan.com/tx', [NETWORKS.ergo.key]: 'https://explorer.ergoplatform.com/transactions', [NETWORKS.cardano.key]: 'https://cardanoscan.io/transaction', diff --git a/packages/utils/tests/networkUrls.test.ts b/packages/utils/tests/networkUrls.test.ts new file mode 100644 index 000000000..6bdcd7c8a --- /dev/null +++ b/packages/utils/tests/networkUrls.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from 'vitest'; + +import { getAddressUrl } from '../src/getAddressUrl'; +import { getTokenUrl } from '../src/getTokenUrl'; +import { getTxURL } from '../src/getTxUrl'; + +describe('Base explorer URLs', () => { + it('should return the Base tx explorer URL', () => { + expect(getTxURL('base', '0x4200000000000000000000000000000000000006')).toBe( + 'https://basescan.org/tx/0x4200000000000000000000000000000000000006', + ); + }); + + it('should return the Base address explorer URL', () => { + expect( + getAddressUrl('base', '0x4200000000000000000000000000000000000006'), + ).toBe( + 'https://basescan.org/address/0x4200000000000000000000000000000000000006', + ); + }); + + it('should return the Base token explorer URL', () => { + expect( + getTokenUrl('base', '0x4200000000000000000000000000000000000006'), + ).toBe( + 'https://basescan.org/token/0x4200000000000000000000000000000000000006', + ); + }); +});