diff --git a/.github/workflows/mainnet-deploy.yaml.disabled b/.github/workflows/mainnet-deploy.yaml.disabled index 0b69f65c88..d69e4bbb0c 100644 --- a/.github/workflows/mainnet-deploy.yaml.disabled +++ b/.github/workflows/mainnet-deploy.yaml.disabled @@ -9,7 +9,7 @@ on: jobs: deploy: - name: Ping deploy + name: Pocket deploy runs-on: mainnet steps: - name: Environment diff --git a/.github/workflows/testnet-deploy.yaml b/.github/workflows/testnet-deploy.yaml index 55391d0122..13de232a81 100644 --- a/.github/workflows/testnet-deploy.yaml +++ b/.github/workflows/testnet-deploy.yaml @@ -9,7 +9,7 @@ on: jobs: deploy: - name: Ping deploy + name: Pocket deploy runs-on: testnet steps: - name: print diff --git a/.gitignore b/.gitignore index 212ccd26db..ae6715d0fb 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,7 @@ node_modules/ **/.vscode yarn-error.log dist -.idea \ No newline at end of file +.idea +.env + +server/datastore/* \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..39255af19e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +# Use the official Node.js image as base +FROM node:latest + + +# Set the working directory inside the container +WORKDIR /usr/src/app + +# Copy package.json and package-lock.json to the working directory +COPY package*.json ./ +COPY yarn.lock ./ + +# Install dependencies +RUN npm install --legacy-peer-deps + +# Copy the rest of the application code to the working directory +COPY . . + +# Change ownership of the working directory to the 'node' user +RUN chown -R node:node . + +# Switch to the 'node' user +USER node + +# Expose the port your app runs on +EXPOSE 5173 + +RUN yarn build +# Command to run your application +CMD ["yarn", "serve", "--host"] diff --git a/POCKET_NETWORK_API_README.md b/POCKET_NETWORK_API_README.md new file mode 100644 index 0000000000..f732db14bb --- /dev/null +++ b/POCKET_NETWORK_API_README.md @@ -0,0 +1,413 @@ +# Pocket Network Explorer Indexer API Documentation + +This document outlines the REST API endpoints that an **indexer** needs to implement to serve a Pocket Network explorer. The indexer will fetch data from Pocket Network blockchain nodes and external sources, then expose its own standardized REST API endpoints for the explorer to consume. + +## Overview + +Pocket Network is a decentralized infrastructure network that provides RPC services to blockchain applications. The **indexer** acts as an intermediary layer that: + +1. **Fetches data** from Pocket Network blockchain nodes and external sources +2. **Processes and indexes** the data for fast retrieval +3. **Exposes REST API endpoints** for the explorer to consume +4. **Provides real-time updates** and historical data + +The explorer will use the indexer's API instead of making direct RPC calls to blockchain nodes, ensuring better performance, reliability, and data consistency. + +## Architecture Overview + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ Pocket │ │ Indexer │ │ Explorer │ +│ Network │───▶│ │───▶│ │ +│ Blockchain │ │ • Fetches data │ │ • Consumes API │ +│ Nodes │ │ • Processes │ │ • Displays UI │ +│ │ │ • Indexes │ │ • User queries │ +└─────────────────┘ │ • Exposes API │ └─────────────────┘ + └─────────────────┘ +``` + +## Base URL Structure + +The indexer will expose these endpoints under its own domain: + +``` +{indexer_endpoint}/pokt-network/poktroll/{module}/{resource} # Pocket Network specific +{indexer_endpoint}/cosmos/{module}/v1beta1/{resource} # Core blockchain data +``` + +**Note**: These are the endpoints the **indexer** will expose, not the endpoints it fetches from. + +## Core Blockchain Data (Required for Pocket Network) + +### Authentication & Authorization + +#### Get All Accounts +- **Endpoint**: `GET /cosmos/auth/v1beta1/accounts` +- **Query Parameters**: + - `pagination.limit` (optional): Number of accounts per page + - `pagination.offset` (optional): Offset for pagination + - `pagination.count_total` (optional): Whether to count total records +- **Description**: Get all accounts with pagination +- **Response**: Paginated list of accounts + +#### Get Account by Address +- **Endpoint**: `GET /cosmos/auth/v1beta1/accounts/{address}` +- **Path Parameters**: `address` - Account address +- **Description**: Get specific account information +- **Response**: Account details + +### Bank Module + +#### Get Account Balances +- **Endpoint**: `GET /cosmos/bank/v1beta1/balances/{address}` +- **Path Parameters**: `address` - Account address +- **Description**: Get account balances +- **Response**: Paginated list of coin balances + +#### Get Total Supply +- **Endpoint**: `GET /cosmos/bank/v1beta1/supply` +- **Query Parameters**: Standard pagination parameters +- **Description**: Get total supply of all tokens +- **Response**: Paginated list of coin supplies + +### Staking Module + +#### Get All Validators +- **Endpoint**: `GET /cosmos/staking/v1beta1/validators` +- **Query Parameters**: + - `status` - Validator status (bonded, unbonded, unbonding) + - `pagination.limit` - Number of validators per page +- **Description**: Get all validators +- **Response**: Paginated list of validators + +#### Get Validator by Address +- **Endpoint**: `GET /cosmos/staking/v1beta1/validators/{validator_addr}` +- **Path Parameters**: `validator_addr` - Validator address +- **Description**: Get specific validator information +- **Response**: Validator details + +#### Get Delegator Delegations +- **Endpoint**: `GET /cosmos/staking/v1beta1/delegations/{delegator_addr}` +- **Path Parameters**: `delegator_addr` - Delegator address +- **Description**: Get delegations for specific delegator +- **Response**: Paginated list of delegations + +#### Get Delegator Unbonding +- **Endpoint**: `GET /cosmos/staking/v1beta1/delegators/{delegator_addr}/unbonding_delegations` +- **Path Parameters**: `delegator_addr` - Delegator address +- **Description**: Get unbonding delegations for specific delegator +- **Response**: Paginated list of unbonding delegations + +### Distribution Module + +#### Get Delegator Rewards +- **Endpoint**: `GET /cosmos/distribution/v1beta1/delegators/{delegator_addr}/rewards` +- **Path Parameters**: `delegator_addr` - Delegator address +- **Description**: Get delegator rewards +- **Response**: Rewards by validator and total + +### Tendermint/Blockchain Data + +#### Get Latest Block +- **Endpoint**: `GET /cosmos/base/tendermint/v1beta1/blocks/latest` +- **Description**: Get latest block information +- **Response**: Latest block data + +#### Get Block by Height +- **Endpoint**: `GET /cosmos/base/tendermint/v1beta1/blocks/{height}` +- **Path Parameters**: `height` - Block height +- **Description**: Get block at specific height +- **Response**: Block data + +### Transaction Module + +#### Get Transactions +- **Endpoint**: `GET /cosmos/tx/v1beta1/txs` +- **Query Parameters**: + - `order_by` - Order by field (1: ascending, 2: descending) + - `query` - Query filter (e.g., message.sender='address') + - `pagination.limit` - Number of transactions per page + - `pagination.offset` - Offset for pagination +- **Description**: Get transactions with filtering and pagination +- **Response**: Paginated list of transactions + +#### Get Transaction by Hash +- **Endpoint**: `GET /cosmos/tx/v1beta1/txs/{hash}` +- **Path Parameters**: `hash` - Transaction hash +- **Description**: Get specific transaction details +- **Response**: Transaction and transaction response + +## Pocket Network Specific Modules + +### Applications + +#### Get All Applications +- **Endpoint**: `GET /pokt-network/poktroll/application/application` +- **Query Parameters**: Standard pagination parameters +- **Description**: Get all applications that use Pocket Network services +- **Response**: Paginated list of applications + +#### Get Application by Address +- **Endpoint**: `GET /pokt-network/poktroll/application/application/{address}` +- **Path Parameters**: `address` - Application address +- **Description**: Get specific application information including stake and service usage +- **Response**: Application details + +### Gateways + +#### Get All Gateways +- **Endpoint**: `GET /pokt-network/poktroll/gateway/gateway` +- **Query Parameters**: Standard pagination parameters +- **Description**: Get all gateways that route RPC requests +- **Response**: Paginated list of gateways + +#### Get Gateway by Address +- **Endpoint**: `GET /pokt-network/poktroll/gateway/gateway/{address}` +- **Path Parameters**: `address` - Gateway address +- **Description**: Get specific gateway information including stake and routing data +- **Response**: Gateway details + +### Suppliers + +#### Get All Suppliers +- **Endpoint**: `GET /pokt-network/poktroll/supplier/supplier` +- **Query Parameters**: Standard pagination parameters +- **Description**: Get all suppliers that provide RPC services +- **Response**: Paginated list of suppliers + +#### Get Supplier by Address +- **Endpoint**: `GET /pokt-network/poktroll/supplier/supplier/{address}` +- **Path Parameters**: `address` - Supplier address +- **Description**: Get specific supplier information including stake and service offerings +- **Response**: Supplier details + +### Services + +#### Get All Services +- **Endpoint**: `GET /pokt-network/poktroll/service/service` +- **Query Parameters**: Standard pagination parameters +- **Description**: Get all RPC services available on the network +- **Response**: Paginated list of services + +#### Get Service by Address +- **Endpoint**: `GET /pokt-network/poktroll/service/service/{address}` +- **Path Parameters**: `address` - Service address +- **Description**: Get specific service information +- **Response**: Service details + +#### Get Relay Mining Difficulty +- **Endpoint**: `GET /pokt-network/poktroll/service/relay_mining_difficulty` +- **Query Parameters**: Standard pagination parameters +- **Description**: Get relay mining difficulty information for service distribution +- **Response**: Paginated list of relay mining difficulty data + +## Standard Pagination Parameters + +Most endpoints support the following pagination parameters: + +- `pagination.limit` - Number of items per page +- `pagination.offset` - Offset for pagination +- `pagination.count_total` - Whether to count total records +- `pagination.reverse` - Whether to reverse order + +## Response Format + +All endpoints return JSON responses with the following structure: + +```json +{ + "data": "actual response data", + "pagination": { + "next_key": "next page key", + "total": "total count if count_total=true" + } +} +``` + +## Error Handling + +The API should return appropriate HTTP status codes: + +- `200` - Success +- `400` - Bad Request +- `404` - Not Found +- `500` - Internal Server Error + +Error responses should include: + +```json +{ + "error": "Error message", + "code": "Error code" +} +``` + +## Rate Limiting + +Consider implementing rate limiting to prevent abuse: + +- **Rate Limit**: 1000 requests per minute per IP +- **Headers**: Include `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` + +## Caching + +Implement appropriate caching headers: + +- **Cache-Control**: `max-age=60` for frequently changing data +- **ETag**: For conditional requests +- **Last-Modified**: For resource modification tracking + +## Security Considerations + +- Implement CORS policies +- Validate all input parameters +- Sanitize query strings +- Use HTTPS in production +- Implement request logging and monitoring + +## Implementation Notes + +### Indexer Responsibilities + +1. **Data Fetching**: The indexer must fetch data from these sources: + - Pocket Network blockchain nodes (RPC endpoints) + - External APIs for additional data + - Real-time blockchain events + +2. **Data Processing**: + - Parse and validate blockchain data + - Transform data into consistent formats + - Calculate derived metrics and statistics + +3. **Data Storage**: + - Maintain a synchronized database with all Pocket Network data + - Implement efficient indexing for fast queries + - Store historical data for time-based analysis + +4. **API Exposure**: + - Expose the documented endpoints for the explorer to consume + - Implement proper caching and rate limiting + - Provide real-time updates via WebSocket (optional) + +### Data Sources for Indexer + +The indexer will need to fetch data from these **source endpoints**: + +#### Pocket Network Blockchain Nodes +- Node urls chain wise are available in env file. + +#### External Data Sources +- **CoinGecko API**: For POKT token price and market data +- **Pocket Network Stats**: For network-wide statistics +- **Service Provider APIs**: For additional service information + +## Example Usage + +### Explorer Consuming Indexer API + +```javascript +// Get all applications from the indexer +const applicationsResponse = await fetch('https://indexer.example.com/pokt-network/poktroll/application/application?pagination.limit=50'); +const applications = await applicationsResponse.json(); + +// Get specific supplier information from the indexer +const supplierResponse = await fetch('https://indexer.example.com/pokt-network/poktroll/supplier/supplier/supplier123'); +const supplier = await supplierResponse.json(); + +// Get account balances from the indexer +const balanceResponse = await fetch('https://indexer.example.com/cosmos/bank/v1beta1/balances/address123'); +const balances = await balanceResponse.json(); + +// Get latest block from the indexer +const blockResponse = await fetch('https://indexer.example.com/cosmos/base/tendermint/v1beta1/blocks/latest'); +const block = await blockResponse.json(); +``` + +### Indexer Fetching from Blockchain (Internal Implementation) + +```javascript +// Indexer internally fetching from Pocket Network blockchain +const blockchainResponse = await fetch('https://shannon-grove-rpc.mainnet.poktroll.com', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + jsonrpc: '2.0', + method: 'abci_query', + params: { path: '/pokt-network/poktroll/application/application' }, + id: 1 + }) +}); + +// Indexer processing and storing the data +const applications = await blockchainResponse.json(); +await database.storeApplications(applications); + +// Indexer then exposing the processed data via its own API +app.get('/pokt-network/poktroll/application/application', async (req, res) => { + const apps = await database.getApplications(req.query); + res.json(apps); +}); +``` + +## Health Check Endpoints + +### Basic Health Check +- **Endpoint**: `GET /health` +- **Description**: Basic health status of the indexer +- **Response**: Health status and basic metrics + +### Detailed Health Check +- **Endpoint**: `GET /health/detailed` +- **Description**: Detailed health information including database status +- **Response**: Comprehensive health metrics + +### Metrics Endpoint +- **Endpoint**: `GET /metrics` +- **Description**: Prometheus-compatible metrics +- **Response**: Metrics in Prometheus format + +## Data Synchronization + +### Indexer Data Flow + +The indexer should implement this data flow: + +1. **Block Processing**: + - Monitor new blocks as they arrive on the blockchain + - Process block data and extract relevant information + - Update internal database with new block data + +2. **Transaction Indexing**: + - Index all transactions with proper metadata + - Categorize transactions by type (staking, application, gateway, supplier) + - Extract relevant data for Pocket Network operations + +3. **State Tracking**: + - Maintain current state of all Pocket Network modules + - Track application stakes, gateway performance, supplier availability + - Monitor service health and relay mining difficulty + +4. **Historical Data**: + - Store historical data for time-based queries + - Maintain time-series data for analytics + - Implement data retention policies + +5. **Real-time Updates**: + - Provide real-time data updates via WebSocket (optional) + - Implement event-driven architecture for immediate updates + - Cache frequently accessed data for performance + +### Synchronization Strategy + +- **Block-based**: Sync on every new block +- **Event-driven**: Listen for specific blockchain events +- **Batch processing**: Process multiple transactions in batches +- **Incremental updates**: Only update changed data +- **Fallback mechanisms**: Handle blockchain node failures gracefully + +## API Endpoint Summary + +This comprehensive API documentation covers **25+ endpoints** specifically for Pocket Network: + +- **Core Blockchain Data**: 15 endpoints (accounts, balances, staking, blocks, transactions) +- **Pocket Network Specific**: 10 endpoints (applications, gateways, suppliers, services) diff --git a/README.md b/README.md index 342d5900e3..8a0b1bae2e 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@
-![Ping Wallet](./public/logo.svg) +![Pocket Wallet](./public/logo.svg) -

Ping Dashboard

+

Pocket Dashboard

-**Ping Dashboard is not only an explorer but also a wallet and more ... 🛠** +**Pocket Dashboard is not only an explorer but also a wallet and more ... 🛠** [![version](https://img.shields.io/github/tag/ping-pub/explorer.svg)](https://github.com/ping-pub/explorer/releases/latest) [![GitHub](https://img.shields.io/github/license/ping-pub/explorer.svg)](https://github.com/ping-pub/explorer/blob/master/LICENSE) @@ -14,21 +14,21 @@
-`Ping Dashboard` is a light explorer for Cosmos-based Blockchains. https://ping.pub . +`Pocket Dashboard` is a light explorer for Cosmos-based Blockchains. https://ping.pub . -## What sets Ping Dashboard apart from other explorers? -**Ping Dashboard** stands out by providing a real-time exploration of blockchain data without relying on caching or pre-processing. It exclusively fetches data from the Cosmos full node via LCD/RPC endpoints, ensuring a truly authentic experience. This approach is referred to as the "Light Explorer." +## What sets Pocket Dashboard apart from other explorers? +**Pocket Dashboard** stands out by providing a real-time exploration of blockchain data without relying on caching or pre-processing. It exclusively fetches data from the Cosmos full node via LCD/RPC endpoints, ensuring a truly authentic experience. This approach is referred to as the "Light Explorer." ## Are you interested in listing your blockchain on https://ping.pub? To make this repository clean, please submit your request to https://github.com/ping-pub/ping.pub.git -## Why does Ping Dashboard rely on official/trusted third-party public LCD/RPC servers? +## Why does Pocket Dashboard rely on official/trusted third-party public LCD/RPC servers? There are two primary reasons for this choice: - - Trust: In a decentralized system, it is crucial to avoid relying solely on a single entity. By utilizing official/trusted third-party public LCD/RPC servers, Ping Dashboard ensures that the data is sourced from a network of trusted participants. - - Limited Resources: As Ping Dashboard plans to list hundreds of Cosmos-based blockchains in the future, it is impractical for the Ping team to operate validators or full nodes for all of them. Leveraging trusted third-party servers allows for more efficient resource allocation. + - Trust: In a decentralized system, it is crucial to avoid relying solely on a single entity. By utilizing official/trusted third-party public LCD/RPC servers, Pocket Dashboard ensures that the data is sourced from a network of trusted participants. + - Limited Resources: As Pocket Dashboard plans to list hundreds of Cosmos-based blockchains in the future, it is impractical for the Pocket team to operate validators or full nodes for all of them. Leveraging trusted third-party servers allows for more efficient resource allocation. ## Donation diff --git a/auto-imports.d.ts b/auto-imports.d.ts index 1150b743e0..00a564c980 100644 --- a/auto-imports.d.ts +++ b/auto-imports.d.ts @@ -3,41 +3,21 @@ export {} declare global { const EffectScope: typeof import('vue')['EffectScope'] const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate'] - const asyncComputed: typeof import('@vueuse/core')['asyncComputed'] - const autoResetRef: typeof import('@vueuse/core')['autoResetRef'] const computed: typeof import('vue')['computed'] - const computedAsync: typeof import('@vueuse/core')['computedAsync'] - const computedEager: typeof import('@vueuse/core')['computedEager'] - const computedInject: typeof import('@vueuse/core')['computedInject'] - const computedWithControl: typeof import('@vueuse/core')['computedWithControl'] - const controlledComputed: typeof import('@vueuse/core')['controlledComputed'] - const controlledRef: typeof import('@vueuse/core')['controlledRef'] const createApp: typeof import('vue')['createApp'] - const createEventHook: typeof import('@vueuse/core')['createEventHook'] const createGenericProjection: typeof import('@vueuse/math')['createGenericProjection'] - const createGlobalState: typeof import('@vueuse/core')['createGlobalState'] - const createInjectionState: typeof import('@vueuse/core')['createInjectionState'] const createPinia: typeof import('pinia')['createPinia'] const createProjection: typeof import('@vueuse/math')['createProjection'] - const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn'] - const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable'] - const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn'] const customRef: typeof import('vue')['customRef'] - const debouncedRef: typeof import('@vueuse/core')['debouncedRef'] - const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch'] const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] const defineComponent: typeof import('vue')['defineComponent'] const defineStore: typeof import('pinia')['defineStore'] - const eagerComputed: typeof import('@vueuse/core')['eagerComputed'] const effectScope: typeof import('vue')['effectScope'] - const extendRef: typeof import('@vueuse/core')['extendRef'] const getActivePinia: typeof import('pinia')['getActivePinia'] const getCurrentInstance: typeof import('vue')['getCurrentInstance'] const getCurrentScope: typeof import('vue')['getCurrentScope'] const h: typeof import('vue')['h'] - const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch'] const inject: typeof import('vue')['inject'] - const isDefined: typeof import('@vueuse/core')['isDefined'] const isProxy: typeof import('vue')['isProxy'] const isReactive: typeof import('vue')['isReactive'] const isReadonly: typeof import('vue')['isReadonly'] @@ -45,7 +25,6 @@ declare global { const logicAnd: typeof import('@vueuse/math')['logicAnd'] const logicNot: typeof import('@vueuse/math')['logicNot'] const logicOr: typeof import('@vueuse/math')['logicOr'] - const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable'] const mapActions: typeof import('pinia')['mapActions'] const mapGetters: typeof import('pinia')['mapGetters'] const mapState: typeof import('pinia')['mapState'] @@ -59,246 +38,57 @@ declare global { const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate'] const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] - const onClickOutside: typeof import('@vueuse/core')['onClickOutside'] const onDeactivated: typeof import('vue')['onDeactivated'] const onErrorCaptured: typeof import('vue')['onErrorCaptured'] - const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke'] - const onLongPress: typeof import('@vueuse/core')['onLongPress'] const onMounted: typeof import('vue')['onMounted'] const onRenderTracked: typeof import('vue')['onRenderTracked'] const onRenderTriggered: typeof import('vue')['onRenderTriggered'] const onScopeDispose: typeof import('vue')['onScopeDispose'] const onServerPrefetch: typeof import('vue')['onServerPrefetch'] - const onStartTyping: typeof import('@vueuse/core')['onStartTyping'] const onUnmounted: typeof import('vue')['onUnmounted'] const onUpdated: typeof import('vue')['onUpdated'] - const pausableWatch: typeof import('@vueuse/core')['pausableWatch'] const provide: typeof import('vue')['provide'] - const reactify: typeof import('@vueuse/core')['reactify'] - const reactifyObject: typeof import('@vueuse/core')['reactifyObject'] const reactive: typeof import('vue')['reactive'] - const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed'] - const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit'] - const reactivePick: typeof import('@vueuse/core')['reactivePick'] const readonly: typeof import('vue')['readonly'] const ref: typeof import('vue')['ref'] - const refAutoReset: typeof import('@vueuse/core')['refAutoReset'] - const refDebounced: typeof import('@vueuse/core')['refDebounced'] - const refDefault: typeof import('@vueuse/core')['refDefault'] - const refThrottled: typeof import('@vueuse/core')['refThrottled'] - const refWithControl: typeof import('@vueuse/core')['refWithControl'] const resolveComponent: typeof import('vue')['resolveComponent'] const resolveDirective: typeof import('vue')['resolveDirective'] - const resolveRef: typeof import('@vueuse/core')['resolveRef'] - const resolveUnref: typeof import('@vueuse/core')['resolveUnref'] const setActivePinia: typeof import('pinia')['setActivePinia'] const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix'] const shallowReactive: typeof import('vue')['shallowReactive'] const shallowReadonly: typeof import('vue')['shallowReadonly'] const shallowRef: typeof import('vue')['shallowRef'] const storeToRefs: typeof import('pinia')['storeToRefs'] - const syncRef: typeof import('@vueuse/core')['syncRef'] - const syncRefs: typeof import('@vueuse/core')['syncRefs'] - const templateRef: typeof import('@vueuse/core')['templateRef'] - const throttledRef: typeof import('@vueuse/core')['throttledRef'] - const throttledWatch: typeof import('@vueuse/core')['throttledWatch'] const toRaw: typeof import('vue')['toRaw'] - const toReactive: typeof import('@vueuse/core')['toReactive'] const toRef: typeof import('vue')['toRef'] const toRefs: typeof import('vue')['toRefs'] const triggerRef: typeof import('vue')['triggerRef'] - const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount'] - const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount'] - const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted'] - const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose'] - const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted'] const unref: typeof import('vue')['unref'] - const unrefElement: typeof import('@vueuse/core')['unrefElement'] - const until: typeof import('@vueuse/core')['until'] const useAbs: typeof import('@vueuse/math')['useAbs'] - const useActiveElement: typeof import('@vueuse/core')['useActiveElement'] - const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery'] - const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter'] - const useArrayFind: typeof import('@vueuse/core')['useArrayFind'] - const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex'] - const useArrayFindLast: typeof import('@vueuse/core')['useArrayFindLast'] - const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin'] - const useArrayMap: typeof import('@vueuse/core')['useArrayMap'] - const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce'] - const useArraySome: typeof import('@vueuse/core')['useArraySome'] - const useArrayUnique: typeof import('@vueuse/core')['useArrayUnique'] - const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue'] - const useAsyncState: typeof import('@vueuse/core')['useAsyncState'] const useAttrs: typeof import('vue')['useAttrs'] const useAverage: typeof import('@vueuse/math')['useAverage'] - const useBase64: typeof import('@vueuse/core')['useBase64'] - const useBattery: typeof import('@vueuse/core')['useBattery'] - const useBluetooth: typeof import('@vueuse/core')['useBluetooth'] - const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints'] - const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel'] - const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation'] - const useCached: typeof import('@vueuse/core')['useCached'] const useCeil: typeof import('@vueuse/math')['useCeil'] const useClamp: typeof import('@vueuse/math')['useClamp'] - const useClipboard: typeof import('@vueuse/core')['useClipboard'] - const useCloned: typeof import('@vueuse/core')['useCloned'] - const useColorMode: typeof import('@vueuse/core')['useColorMode'] - const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog'] - const useCounter: typeof import('@vueuse/core')['useCounter'] const useCssModule: typeof import('vue')['useCssModule'] - const useCssVar: typeof import('@vueuse/core')['useCssVar'] const useCssVars: typeof import('vue')['useCssVars'] - const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement'] - const useCycleList: typeof import('@vueuse/core')['useCycleList'] - const useDark: typeof import('@vueuse/core')['useDark'] - const useDateFormat: typeof import('@vueuse/core')['useDateFormat'] - const useDebounce: typeof import('@vueuse/core')['useDebounce'] - const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn'] - const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory'] - const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion'] - const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation'] - const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio'] - const useDevicesList: typeof import('@vueuse/core')['useDevicesList'] - const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia'] - const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility'] - const useDraggable: typeof import('@vueuse/core')['useDraggable'] - const useDropZone: typeof import('@vueuse/core')['useDropZone'] - const useElementBounding: typeof import('@vueuse/core')['useElementBounding'] - const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint'] - const useElementHover: typeof import('@vueuse/core')['useElementHover'] - const useElementSize: typeof import('@vueuse/core')['useElementSize'] - const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility'] - const useEventBus: typeof import('@vueuse/core')['useEventBus'] - const useEventListener: typeof import('@vueuse/core')['useEventListener'] - const useEventSource: typeof import('@vueuse/core')['useEventSource'] - const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper'] - const useFavicon: typeof import('@vueuse/core')['useFavicon'] - const useFetch: typeof import('@vueuse/core')['useFetch'] - const useFileDialog: typeof import('@vueuse/core')['useFileDialog'] - const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess'] const useFloor: typeof import('@vueuse/math')['useFloor'] - const useFocus: typeof import('@vueuse/core')['useFocus'] - const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin'] - const useFps: typeof import('@vueuse/core')['useFps'] - const useFullscreen: typeof import('@vueuse/core')['useFullscreen'] - const useGamepad: typeof import('@vueuse/core')['useGamepad'] - const useGeolocation: typeof import('@vueuse/core')['useGeolocation'] const useI18n: typeof import('vue-i18n')['useI18n'] - const useIdle: typeof import('@vueuse/core')['useIdle'] - const useImage: typeof import('@vueuse/core')['useImage'] - const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll'] - const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver'] - const useInterval: typeof import('@vueuse/core')['useInterval'] - const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn'] - const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier'] - const useLastChanged: typeof import('@vueuse/core')['useLastChanged'] const useLink: typeof import('vue-router')['useLink'] - const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage'] - const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys'] - const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory'] const useMath: typeof import('@vueuse/math')['useMath'] const useMax: typeof import('@vueuse/math')['useMax'] - const useMediaControls: typeof import('@vueuse/core')['useMediaControls'] - const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery'] - const useMemoize: typeof import('@vueuse/core')['useMemoize'] - const useMemory: typeof import('@vueuse/core')['useMemory'] const useMin: typeof import('@vueuse/math')['useMin'] - const useMounted: typeof import('@vueuse/core')['useMounted'] - const useMouse: typeof import('@vueuse/core')['useMouse'] - const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement'] - const useMousePressed: typeof import('@vueuse/core')['useMousePressed'] - const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver'] - const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage'] - const useNetwork: typeof import('@vueuse/core')['useNetwork'] - const useNow: typeof import('@vueuse/core')['useNow'] - const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl'] - const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination'] - const useOnline: typeof import('@vueuse/core')['useOnline'] - const usePageLeave: typeof import('@vueuse/core')['usePageLeave'] - const useParallax: typeof import('@vueuse/core')['useParallax'] - const usePermission: typeof import('@vueuse/core')['usePermission'] - const usePointer: typeof import('@vueuse/core')['usePointer'] - const usePointerLock: typeof import('@vueuse/core')['usePointerLock'] - const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe'] const usePrecision: typeof import('@vueuse/math')['usePrecision'] - const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme'] - const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast'] - const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark'] - const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages'] - const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion'] - const usePrevious: typeof import('@vueuse/core')['usePrevious'] const useProjection: typeof import('@vueuse/math')['useProjection'] - const useRafFn: typeof import('@vueuse/core')['useRafFn'] - const useRefHistory: typeof import('@vueuse/core')['useRefHistory'] - const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver'] const useRound: typeof import('@vueuse/math')['useRound'] const useRoute: typeof import('vue-router')['useRoute'] const useRouter: typeof import('vue-router')['useRouter'] - const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation'] - const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea'] - const useScriptTag: typeof import('@vueuse/core')['useScriptTag'] - const useScroll: typeof import('@vueuse/core')['useScroll'] - const useScrollLock: typeof import('@vueuse/core')['useScrollLock'] - const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage'] - const useShare: typeof import('@vueuse/core')['useShare'] const useSlots: typeof import('vue')['useSlots'] - const useSorted: typeof import('@vueuse/core')['useSorted'] - const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition'] - const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis'] - const useStepper: typeof import('@vueuse/core')['useStepper'] - const useStorage: typeof import('@vueuse/core')['useStorage'] - const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync'] - const useStyleTag: typeof import('@vueuse/core')['useStyleTag'] const useSum: typeof import('@vueuse/math')['useSum'] - const useSupported: typeof import('@vueuse/core')['useSupported'] - const useSwipe: typeof import('@vueuse/core')['useSwipe'] - const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList'] - const useTextDirection: typeof import('@vueuse/core')['useTextDirection'] - const useTextSelection: typeof import('@vueuse/core')['useTextSelection'] - const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize'] - const useThrottle: typeof import('@vueuse/core')['useThrottle'] - const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn'] - const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory'] - const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo'] - const useTimeout: typeof import('@vueuse/core')['useTimeout'] - const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn'] - const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll'] - const useTimestamp: typeof import('@vueuse/core')['useTimestamp'] - const useTitle: typeof import('@vueuse/core')['useTitle'] - const useToFixed: typeof import('@vueuse/math')['useToFixed'] - const useToNumber: typeof import('@vueuse/core')['useToNumber'] - const useToString: typeof import('@vueuse/core')['useToString'] - const useToggle: typeof import('@vueuse/core')['useToggle'] - const useTransition: typeof import('@vueuse/core')['useTransition'] const useTrunc: typeof import('@vueuse/math')['useTrunc'] - const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams'] - const useUserMedia: typeof import('@vueuse/core')['useUserMedia'] - const useVModel: typeof import('@vueuse/core')['useVModel'] - const useVModels: typeof import('@vueuse/core')['useVModels'] - const useVibrate: typeof import('@vueuse/core')['useVibrate'] - const useVirtualList: typeof import('@vueuse/core')['useVirtualList'] - const useWakeLock: typeof import('@vueuse/core')['useWakeLock'] - const useWebNotification: typeof import('@vueuse/core')['useWebNotification'] - const useWebSocket: typeof import('@vueuse/core')['useWebSocket'] - const useWebWorker: typeof import('@vueuse/core')['useWebWorker'] - const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn'] - const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus'] - const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll'] - const useWindowSize: typeof import('@vueuse/core')['useWindowSize'] const watch: typeof import('vue')['watch'] - const watchArray: typeof import('@vueuse/core')['watchArray'] - const watchAtMost: typeof import('@vueuse/core')['watchAtMost'] - const watchDebounced: typeof import('@vueuse/core')['watchDebounced'] const watchEffect: typeof import('vue')['watchEffect'] - const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable'] - const watchOnce: typeof import('@vueuse/core')['watchOnce'] - const watchPausable: typeof import('@vueuse/core')['watchPausable'] const watchPostEffect: typeof import('vue')['watchPostEffect'] const watchSyncEffect: typeof import('vue')['watchSyncEffect'] - const watchThrottled: typeof import('@vueuse/core')['watchThrottled'] - const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable'] - const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter'] - const whenever: typeof import('@vueuse/core')['whenever'] } // for vue template auto import import { UnwrapRef } from 'vue' @@ -306,41 +96,21 @@ declare module 'vue' { interface ComponentCustomProperties { readonly EffectScope: UnwrapRef readonly acceptHMRUpdate: UnwrapRef - readonly asyncComputed: UnwrapRef - readonly autoResetRef: UnwrapRef readonly computed: UnwrapRef - readonly computedAsync: UnwrapRef - readonly computedEager: UnwrapRef - readonly computedInject: UnwrapRef - readonly computedWithControl: UnwrapRef - readonly controlledComputed: UnwrapRef - readonly controlledRef: UnwrapRef readonly createApp: UnwrapRef - readonly createEventHook: UnwrapRef readonly createGenericProjection: UnwrapRef - readonly createGlobalState: UnwrapRef - readonly createInjectionState: UnwrapRef readonly createPinia: UnwrapRef readonly createProjection: UnwrapRef - readonly createReactiveFn: UnwrapRef - readonly createSharedComposable: UnwrapRef - readonly createUnrefFn: UnwrapRef readonly customRef: UnwrapRef - readonly debouncedRef: UnwrapRef - readonly debouncedWatch: UnwrapRef readonly defineAsyncComponent: UnwrapRef readonly defineComponent: UnwrapRef readonly defineStore: UnwrapRef - readonly eagerComputed: UnwrapRef readonly effectScope: UnwrapRef - readonly extendRef: UnwrapRef readonly getActivePinia: UnwrapRef readonly getCurrentInstance: UnwrapRef readonly getCurrentScope: UnwrapRef readonly h: UnwrapRef - readonly ignorableWatch: UnwrapRef readonly inject: UnwrapRef - readonly isDefined: UnwrapRef readonly isProxy: UnwrapRef readonly isReactive: UnwrapRef readonly isReadonly: UnwrapRef @@ -348,7 +118,6 @@ declare module 'vue' { readonly logicAnd: UnwrapRef readonly logicNot: UnwrapRef readonly logicOr: UnwrapRef - readonly makeDestructurable: UnwrapRef readonly mapActions: UnwrapRef readonly mapGetters: UnwrapRef readonly mapState: UnwrapRef @@ -362,245 +131,56 @@ declare module 'vue' { readonly onBeforeRouteUpdate: UnwrapRef readonly onBeforeUnmount: UnwrapRef readonly onBeforeUpdate: UnwrapRef - readonly onClickOutside: UnwrapRef readonly onDeactivated: UnwrapRef readonly onErrorCaptured: UnwrapRef - readonly onKeyStroke: UnwrapRef - readonly onLongPress: UnwrapRef readonly onMounted: UnwrapRef readonly onRenderTracked: UnwrapRef readonly onRenderTriggered: UnwrapRef readonly onScopeDispose: UnwrapRef readonly onServerPrefetch: UnwrapRef - readonly onStartTyping: UnwrapRef readonly onUnmounted: UnwrapRef readonly onUpdated: UnwrapRef - readonly pausableWatch: UnwrapRef readonly provide: UnwrapRef - readonly reactify: UnwrapRef - readonly reactifyObject: UnwrapRef readonly reactive: UnwrapRef - readonly reactiveComputed: UnwrapRef - readonly reactiveOmit: UnwrapRef - readonly reactivePick: UnwrapRef readonly readonly: UnwrapRef readonly ref: UnwrapRef - readonly refAutoReset: UnwrapRef - readonly refDebounced: UnwrapRef - readonly refDefault: UnwrapRef - readonly refThrottled: UnwrapRef - readonly refWithControl: UnwrapRef readonly resolveComponent: UnwrapRef readonly resolveDirective: UnwrapRef - readonly resolveRef: UnwrapRef - readonly resolveUnref: UnwrapRef readonly setActivePinia: UnwrapRef readonly setMapStoreSuffix: UnwrapRef readonly shallowReactive: UnwrapRef readonly shallowReadonly: UnwrapRef readonly shallowRef: UnwrapRef readonly storeToRefs: UnwrapRef - readonly syncRef: UnwrapRef - readonly syncRefs: UnwrapRef - readonly templateRef: UnwrapRef - readonly throttledRef: UnwrapRef - readonly throttledWatch: UnwrapRef readonly toRaw: UnwrapRef - readonly toReactive: UnwrapRef readonly toRef: UnwrapRef readonly toRefs: UnwrapRef readonly triggerRef: UnwrapRef - readonly tryOnBeforeMount: UnwrapRef - readonly tryOnBeforeUnmount: UnwrapRef - readonly tryOnMounted: UnwrapRef - readonly tryOnScopeDispose: UnwrapRef - readonly tryOnUnmounted: UnwrapRef readonly unref: UnwrapRef - readonly unrefElement: UnwrapRef - readonly until: UnwrapRef readonly useAbs: UnwrapRef - readonly useActiveElement: UnwrapRef - readonly useArrayEvery: UnwrapRef - readonly useArrayFilter: UnwrapRef - readonly useArrayFind: UnwrapRef - readonly useArrayFindIndex: UnwrapRef - readonly useArrayFindLast: UnwrapRef - readonly useArrayJoin: UnwrapRef - readonly useArrayMap: UnwrapRef - readonly useArrayReduce: UnwrapRef - readonly useArraySome: UnwrapRef - readonly useArrayUnique: UnwrapRef - readonly useAsyncQueue: UnwrapRef - readonly useAsyncState: UnwrapRef readonly useAttrs: UnwrapRef readonly useAverage: UnwrapRef - readonly useBase64: UnwrapRef - readonly useBattery: UnwrapRef - readonly useBluetooth: UnwrapRef - readonly useBreakpoints: UnwrapRef - readonly useBroadcastChannel: UnwrapRef - readonly useBrowserLocation: UnwrapRef - readonly useCached: UnwrapRef readonly useCeil: UnwrapRef readonly useClamp: UnwrapRef - readonly useClipboard: UnwrapRef - readonly useCloned: UnwrapRef - readonly useColorMode: UnwrapRef - readonly useConfirmDialog: UnwrapRef - readonly useCounter: UnwrapRef readonly useCssModule: UnwrapRef - readonly useCssVar: UnwrapRef readonly useCssVars: UnwrapRef - readonly useCurrentElement: UnwrapRef - readonly useCycleList: UnwrapRef - readonly useDark: UnwrapRef - readonly useDateFormat: UnwrapRef - readonly useDebounce: UnwrapRef - readonly useDebounceFn: UnwrapRef - readonly useDebouncedRefHistory: UnwrapRef - readonly useDeviceMotion: UnwrapRef - readonly useDeviceOrientation: UnwrapRef - readonly useDevicePixelRatio: UnwrapRef - readonly useDevicesList: UnwrapRef - readonly useDisplayMedia: UnwrapRef - readonly useDocumentVisibility: UnwrapRef - readonly useDraggable: UnwrapRef - readonly useDropZone: UnwrapRef - readonly useElementBounding: UnwrapRef - readonly useElementByPoint: UnwrapRef - readonly useElementHover: UnwrapRef - readonly useElementSize: UnwrapRef - readonly useElementVisibility: UnwrapRef - readonly useEventBus: UnwrapRef - readonly useEventListener: UnwrapRef - readonly useEventSource: UnwrapRef - readonly useEyeDropper: UnwrapRef - readonly useFavicon: UnwrapRef - readonly useFetch: UnwrapRef - readonly useFileDialog: UnwrapRef - readonly useFileSystemAccess: UnwrapRef readonly useFloor: UnwrapRef - readonly useFocus: UnwrapRef - readonly useFocusWithin: UnwrapRef - readonly useFps: UnwrapRef - readonly useFullscreen: UnwrapRef - readonly useGamepad: UnwrapRef - readonly useGeolocation: UnwrapRef readonly useI18n: UnwrapRef - readonly useIdle: UnwrapRef - readonly useImage: UnwrapRef - readonly useInfiniteScroll: UnwrapRef - readonly useIntersectionObserver: UnwrapRef - readonly useInterval: UnwrapRef - readonly useIntervalFn: UnwrapRef - readonly useKeyModifier: UnwrapRef - readonly useLastChanged: UnwrapRef readonly useLink: UnwrapRef - readonly useLocalStorage: UnwrapRef - readonly useMagicKeys: UnwrapRef - readonly useManualRefHistory: UnwrapRef readonly useMath: UnwrapRef readonly useMax: UnwrapRef - readonly useMediaControls: UnwrapRef - readonly useMediaQuery: UnwrapRef - readonly useMemoize: UnwrapRef - readonly useMemory: UnwrapRef readonly useMin: UnwrapRef - readonly useMounted: UnwrapRef - readonly useMouse: UnwrapRef - readonly useMouseInElement: UnwrapRef - readonly useMousePressed: UnwrapRef - readonly useMutationObserver: UnwrapRef - readonly useNavigatorLanguage: UnwrapRef - readonly useNetwork: UnwrapRef - readonly useNow: UnwrapRef - readonly useObjectUrl: UnwrapRef - readonly useOffsetPagination: UnwrapRef - readonly useOnline: UnwrapRef - readonly usePageLeave: UnwrapRef - readonly useParallax: UnwrapRef - readonly usePermission: UnwrapRef - readonly usePointer: UnwrapRef - readonly usePointerLock: UnwrapRef - readonly usePointerSwipe: UnwrapRef readonly usePrecision: UnwrapRef - readonly usePreferredColorScheme: UnwrapRef - readonly usePreferredContrast: UnwrapRef - readonly usePreferredDark: UnwrapRef - readonly usePreferredLanguages: UnwrapRef - readonly usePreferredReducedMotion: UnwrapRef - readonly usePrevious: UnwrapRef readonly useProjection: UnwrapRef - readonly useRafFn: UnwrapRef - readonly useRefHistory: UnwrapRef - readonly useResizeObserver: UnwrapRef readonly useRound: UnwrapRef readonly useRoute: UnwrapRef readonly useRouter: UnwrapRef - readonly useScreenOrientation: UnwrapRef - readonly useScreenSafeArea: UnwrapRef - readonly useScriptTag: UnwrapRef - readonly useScroll: UnwrapRef - readonly useScrollLock: UnwrapRef - readonly useSessionStorage: UnwrapRef - readonly useShare: UnwrapRef readonly useSlots: UnwrapRef - readonly useSorted: UnwrapRef - readonly useSpeechRecognition: UnwrapRef - readonly useSpeechSynthesis: UnwrapRef - readonly useStepper: UnwrapRef - readonly useStorage: UnwrapRef - readonly useStorageAsync: UnwrapRef - readonly useStyleTag: UnwrapRef readonly useSum: UnwrapRef - readonly useSupported: UnwrapRef - readonly useSwipe: UnwrapRef - readonly useTemplateRefsList: UnwrapRef - readonly useTextDirection: UnwrapRef - readonly useTextSelection: UnwrapRef - readonly useTextareaAutosize: UnwrapRef - readonly useThrottle: UnwrapRef - readonly useThrottleFn: UnwrapRef - readonly useThrottledRefHistory: UnwrapRef - readonly useTimeAgo: UnwrapRef - readonly useTimeout: UnwrapRef - readonly useTimeoutFn: UnwrapRef - readonly useTimeoutPoll: UnwrapRef - readonly useTimestamp: UnwrapRef - readonly useTitle: UnwrapRef - readonly useToFixed: UnwrapRef - readonly useToNumber: UnwrapRef - readonly useToString: UnwrapRef - readonly useToggle: UnwrapRef - readonly useTransition: UnwrapRef readonly useTrunc: UnwrapRef - readonly useUrlSearchParams: UnwrapRef - readonly useUserMedia: UnwrapRef - readonly useVModel: UnwrapRef - readonly useVModels: UnwrapRef - readonly useVibrate: UnwrapRef - readonly useVirtualList: UnwrapRef - readonly useWakeLock: UnwrapRef - readonly useWebNotification: UnwrapRef - readonly useWebSocket: UnwrapRef - readonly useWebWorker: UnwrapRef - readonly useWebWorkerFn: UnwrapRef - readonly useWindowFocus: UnwrapRef - readonly useWindowScroll: UnwrapRef - readonly useWindowSize: UnwrapRef readonly watch: UnwrapRef - readonly watchArray: UnwrapRef - readonly watchAtMost: UnwrapRef - readonly watchDebounced: UnwrapRef readonly watchEffect: UnwrapRef - readonly watchIgnorable: UnwrapRef - readonly watchOnce: UnwrapRef - readonly watchPausable: UnwrapRef readonly watchPostEffect: UnwrapRef readonly watchSyncEffect: UnwrapRef - readonly watchThrottled: UnwrapRef - readonly watchTriggerable: UnwrapRef - readonly watchWithFilter: UnwrapRef - readonly whenever: UnwrapRef } } diff --git a/chains/README.md b/chains/README.md index 7b651cfadd..73f0b58608 100644 --- a/chains/README.md +++ b/chains/README.md @@ -14,13 +14,13 @@ "api": [ { "address": "https://cosmos.api.ping.pub", - "provider": "Ping" + "provider": "Pocket" } ], "rpc": [ { "address": "https://cosmos.api.ping.pub", - "provider": "Ping" + "provider": "Pocket" } ], "sdk_version": "0.42.6", diff --git a/chains/mainnet/cosmos.json b/chains/mainnet/cosmos.json deleted file mode 100644 index 2bd610640d..0000000000 --- a/chains/mainnet/cosmos.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "chain_name": "cosmos", - "registry_name": "cosmoshub", - "api": [ - {"provider": "notional", "address": "https://api-cosmoshub-ia.cosmosia.notional.ventures"}, - {"provider": "blockapsis", "address": "https://lcd-cosmoshub.blockapsis.com:443"}, - {"provider": "WhisperNode🤐", "address": "https://lcd-cosmoshub.whispernode.com:443"}, - {"provider": "pupmos", "address": "https://api-cosmoshub.pupmos.network"}, - {"provider": "publicnode", "address": "https://cosmos-rest.publicnode.com"}, - {"provider": "staketab", "address": "https://cosmos-rest.staketab.org"}, - {"provider": "nodestake", "address": "https://api.cosmos.nodestake.top"}, - {"provider": "Golden Ratio Staking", "address": "https://rest-cosmoshub.goldenratiostaking.net"} - ], - "rpc": [ - {"provider": "icycro", "address": "https://cosmos-rpc.icycro.org"}, - {"provider": "dragonstake", "address": "https://rpc.cosmos.dragonstake.io"}, - {"provider": "Golden Ratio Staking", "address": "https://rpc-cosmoshub.goldenratiostaking.net"} - ], - "sdk_version": "0.45.1", - "coin_type": "118", - "min_tx_fee": "800", - "addr_prefix": "cosmos", - "logo": "/logos/cosmos.svg", - "assets": [{ - "base": "uatom", - "symbol": "ATOM", - "exponent": "6", - "coingecko_id": "cosmos", - "logo": "/logos/cosmos.svg" - }] -} diff --git a/chains/mainnet/neutron.json b/chains/mainnet/neutron.json deleted file mode 100644 index 864e67f10b..0000000000 --- a/chains/mainnet/neutron.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "chain_name": "neutron", - "api": [ - {"provider": "Polkachu", "address": "https://neutron-api.polkachu.com"}, - {"provider": "NodeStake", "address": "https://api.neutron.nodestake.top"}, - {"provider": "Allnodes", "address": "https://neutron-rest.publicnode.com"} - ], - "rpc": [ - {"provider": "Polkachu", "address": "https://neutron-rpc.polkachu.com"}, - {"provider": "NodeStake", "address": "https://rpc.neutron.nodestake.top"}, - {"provider": "Allnodes", "address": "https://neutron-rpc.publicnode.com:443"} - ], - "provider_chain": { - "api": ["https://api-cosmoshub-ia.cosmosia.notional.ventures"] - }, - "features": ["dashboard", "blocks", "ibc", "cosmwasm", "uptime", "parameters", "state-sync", "consensus", "supply", "widget"], - "sdk_version": "0.45.1", - "coin_type": "118", - "min_tx_fee": "8000", - "assets": [{ - "base": "untrn", - "symbol": "NTRN", - "exponent": "6", - "coingecko_id": "neutron", - "logo": "/logos/neutron.svg" - }], - "addr_prefix": "neutron", - "theme_color": "#161723", - "logo": "/logos/neutron.svg" -} diff --git a/chains/mainnet/nolus.json b/chains/mainnet/nolus.json deleted file mode 100644 index 81bdd48470..0000000000 --- a/chains/mainnet/nolus.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "chain_name": "nolus", - "coingecko": "nolus", - "api": [ - {"provider": "Nolus", "address": "https://pirin-cl.nolus.network:1317"}, - {"provider": "LavenderFive", "address": "https://nolus-api.lavenderfive.com:443"}, - {"provider": "Allnodes", "address": "https://nolus-rest.publicnode.com"} - ], - "rpc": [ - {"provider": "Nolus", "address": "https://pirin-cl.nolus.network:26657"}, - {"provider": "LavenderFive", "address": "https://nolus-rpc.lavenderfive.com:443"}, - {"provider": "Allnodes", "address": "https://nolus-rpc.publicnode.com:443"} - ], - "snapshot_provider": "", - "sdk_version": "v0.47.6", - "coin_type": "118", - "min_tx_fee": "0", - "addr_prefix": "nolus", - "logo": "/logos/nolus.svg", - "assets": [{ - "base": "unls", - "symbol": "NLS", - "exponent": "6", - "coingecko_id": "nolus", - "logo": "/logos/nolus.svg" - }] -} diff --git a/chains/mainnet/osmosis.json b/chains/mainnet/osmosis.json deleted file mode 100644 index 35669f39a9..0000000000 --- a/chains/mainnet/osmosis.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "chain_name": "osmosis", - "coingecko": "osmosis", - "api": ["https://lcd.osmosis.zone","https://api-osmosis-ia.cosmosia.notional.ventures", "https://osmosis-api.polkachu.com", "https://lcd-osmosis.blockapsis.com"], - "rpc": ["https://rpc.osmosis.zone", "https://rpc-osmosis-ia.cosmosia.notional.ventures:443", "https://osmosis-rpc.polkachu.com:443", "https://osmosis.validator.network:443", "https://rpc-osmosis.blockapsis.com:443"], - "snapshot_provider": "", - "sdk_version": "0.46.1", - "coin_type": "118", - "min_tx_fee": "800", - "addr_prefix": "osmo", - "logo": "/logos/osmosis.jpg", - "theme_color": "#812cd6", - "assets": [{ - "base": "uosmo", - "symbol": "OSMO", - "exponent": "6", - "coingecko_id": "osmosis", - "logo": "/logos/osmosis.jpg" - },{ - "base": "uion", - "symbol": "ION", - "exponent": "6", - "coingecko_id": "ion", - "logo": "/logos/osmosis.jpg" - },{ - "base": "usomm", - "symbol": "SOMM", - "exponent": "6", - "coingecko_id": "somm", - "logo": "" - }] -} diff --git a/chains/mainnet/poktroll-mainnet.json b/chains/mainnet/poktroll-mainnet.json new file mode 100644 index 0000000000..351f94eb3e --- /dev/null +++ b/chains/mainnet/poktroll-mainnet.json @@ -0,0 +1,23 @@ +{ + "chain_name": "poktroll-mainnet", + "registry_name": "Poktroll-Mainnet", + "api": [ + {"provider": "Poktroll-Mainnet", "address": "https://shannon-grove-api.mainnet.poktroll.com"} + ], + "rpc": [ + {"provider": "Poktroll-Mainnet", "address": "https://shannon-grove-rpc.mainnet.poktroll.com"} + ], + "features": ["dashboard", "blocks", "tx", "applications", "suppliers", "gateways", "services", "validators", "parameters"], + "sdk_version": "0.45.1", + "coin_type": "118", + "min_tx_fee": "800", + "addr_prefix": "pokt", + "logo": "https://assets-global.website-files.com/651fe0a9a906d151784935f8/65c62e2727ed4e265bc9911a_universal-logo.png", + "assets": [{ + "base": "upokt", + "symbol": "pokt", + "exponent": "6", + "coingecko_id": "pocket-network", + "logo": "https://assets-global.website-files.com/651fe0a9a906d151784935f8/65c62e2727ed4e265bc9911a_universal-logo.png" + }] +} diff --git a/chains/mainnet/poktroll.json b/chains/mainnet/poktroll.json new file mode 100644 index 0000000000..53aaeff005 --- /dev/null +++ b/chains/mainnet/poktroll.json @@ -0,0 +1,23 @@ +{ + "chain_name": "poktroll", + "registry_name": "Poktroll-Testnet", + "api": [ + {"provider": "Poktroll-Testnet", "address": "https://shannon-testnet-grove-api.alpha.poktroll.com"} + ], + "rpc": [ + {"provider": "Poktroll-Testnet", "address": "https://shannon-testnet-grove-rpc.alpha.poktroll.com"} + ], + "features": ["dashboard", "blocks", "tx", "applications", "suppliers", "gateways", "services", "validators", "parameters"], + "sdk_version": "0.45.1", + "coin_type": "118", + "min_tx_fee": "800", + "addr_prefix": "pokt", + "logo": "https://assets-global.website-files.com/651fe0a9a906d151784935f8/65c62e2727ed4e265bc9911a_universal-logo.png", + "assets": [{ + "base": "upokt", + "symbol": "pokt", + "exponent": "6", + "coingecko_id": "pocket-network", + "logo": "https://assets-global.website-files.com/651fe0a9a906d151784935f8/65c62e2727ed4e265bc9911a_universal-logo.png" + }] +} diff --git a/chains/testnet/crossfi.json b/chains/testnet/crossfi.json deleted file mode 100644 index 5414f3f202..0000000000 --- a/chains/testnet/crossfi.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "chain_name": "crossfi-testnet-1", - "api": ["https://crossfi-testnet-api.forpeaky.xyz"], - "rpc": ["https://crossfi-testnet-rpc.forpeaky.xyz"], - "coingecko": "", - "snapshot_provider": "", - "sdk_version": "0.47.1", - "coin_type": "118", - "min_tx_fee": "500", - "addr_prefix": "crossfi" - } \ No newline at end of file diff --git a/chains/testnet/pocket-alpha.json b/chains/testnet/pocket-alpha.json new file mode 100644 index 0000000000..fc7b05c3ed --- /dev/null +++ b/chains/testnet/pocket-alpha.json @@ -0,0 +1,24 @@ +{ + "chain_name": "pocket-alpha", + "registry_name": "Alpha", + "api": [ + {"provider": "Poktroll-Testnet-Alpha", "address": "https://shannon-testnet-grove-api.alpha.poktroll.com"} + ], + "rpc": [ + {"provider": "Poktroll-Testnet-Alpha", "address": "https://shannon-testnet-grove-rpc.alpha.poktroll.com"} + ], + "transaction_service": "pocket-testnet-alpha", + "features": ["dashboard", "blocks", "tx", "applications", "suppliers", "gateways", "services", "validators", "parameters"], + "sdk_version": "0.45.1", + "coin_type": "118", + "min_tx_fee": "800", + "addr_prefix": "pokt", + "logo": "https://assets-global.website-files.com/651fe0a9a906d151784935f8/65c62e2727ed4e265bc9911a_universal-logo.png", + "assets": [{ + "base": "upokt", + "symbol": "pokt", + "exponent": "6", + "coingecko_id": "pocket-network", + "logo": "https://assets-global.website-files.com/651fe0a9a906d151784935f8/65c62e2727ed4e265bc9911a_universal-logo.png" + }] +} diff --git a/chains/testnet/pocket-beta.json b/chains/testnet/pocket-beta.json new file mode 100644 index 0000000000..36bfe93238 --- /dev/null +++ b/chains/testnet/pocket-beta.json @@ -0,0 +1,24 @@ +{ + "chain_name": "pocket-beta", + "registry_name": "Beta", + "api": [ + {"provider": "Pocket-Testnet-Beta", "address": "https://shannon-testnet-grove-api.beta.poktroll.com"} + ], + "rpc": [ + {"provider": "Pocket-Testnet-Beta", "address": "https://shannon-testnet-grove-rpc.beta.poktroll.com"} + ], + "transaction_service": "pocket-testnet-beta", + "features": ["dashboard", "blocks", "tx", "applications", "suppliers", "gateways", "services", "validators", "parameters"], + "sdk_version": "0.45.1", + "coin_type": "118", + "min_tx_fee": "800", + "addr_prefix": "pokt", + "logo": "https://assets-global.website-files.com/651fe0a9a906d151784935f8/65c62e2727ed4e265bc9911a_universal-logo.png", + "assets": [{ + "base": "upokt", + "symbol": "pokt", + "exponent": "6", + "coingecko_id": "pocket-network", + "logo": "https://assets-global.website-files.com/651fe0a9a906d151784935f8/65c62e2727ed4e265bc9911a_universal-logo.png" + }] +} diff --git a/chains/testnet/pocket-mainnet.json b/chains/testnet/pocket-mainnet.json new file mode 100644 index 0000000000..b887b4fe1a --- /dev/null +++ b/chains/testnet/pocket-mainnet.json @@ -0,0 +1,24 @@ +{ + "chain_name": "pocket-mainnet", + "registry_name": "Mainnet", + "api": [ + {"provider": "Pocket-Mainnet", "address": "https://shannon-grove-api.mainnet.poktroll.com"} + ], + "rpc": [ + {"provider": "Pocket-Mainnet", "address": "https://shannon-grove-rpc.mainnet.poktroll.com"} + ], + "transaction_service": "pocket-mainnet", + "features": ["dashboard", "blocks", "tx", "applications", "suppliers", "gateways", "services", "validators", "parameters"], + "sdk_version": "0.45.1", + "coin_type": "118", + "min_tx_fee": "800", + "addr_prefix": "pokt", + "logo": "https://assets-global.website-files.com/651fe0a9a906d151784935f8/65c62e2727ed4e265bc9911a_universal-logo.png", + "assets": [{ + "base": "upokt", + "symbol": "pokt", + "exponent": "6", + "coingecko_id": "pocket-network", + "logo": "https://assets-global.website-files.com/651fe0a9a906d151784935f8/65c62e2727ed4e265bc9911a_universal-logo.png" + }] +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000..475ae21a20 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,35 @@ +version: '3.8' + +services: + pocket-poktroll-explorer: + build: + context: . + dockerfile: Dockerfile + restart: unless-stopped + ports: + - 5173:5173 + networks: + - default + - pocket_indexer_network + # Extending from server's docker-compose.yml for backend services + # redis: + # extends: + # file: ./server/docker-compose.yml + # service: redis + # networks: + # - default + + # pocket-poktroll-explorer-backend: + # extends: + # file: ./server/docker-compose.yml + # service: api + # networks: + # - default + +networks: + default: + pocket_indexer_network: + external: true + +volumes: + redis-data: \ No newline at end of file diff --git a/docker-daemon-config.json b/docker-daemon-config.json new file mode 100644 index 0000000000..a8c18a2cf2 --- /dev/null +++ b/docker-daemon-config.json @@ -0,0 +1 @@ +{"log-driver": "json-file", "log-opts": {"max-size": "10m", "max-file": "3", "compress": "true"}} diff --git a/index.html b/index.html index 3c1c9f9468..6b75795755 100644 --- a/index.html +++ b/index.html @@ -4,15 +4,17 @@ - Ping Dashboard - Cosmos Blockchain Explorer And Web Wallet - - + Poktroll Explorer Dashboard - Poktroll Blockchain Explorer And Web Wallet + + + +
diff --git a/installation.md b/installation.md index a214058a8c..6d99567c6c 100644 --- a/installation.md +++ b/installation.md @@ -40,7 +40,7 @@ cp -r ./dist/* docker run -d -p 8088:80 ping.pub/dashboard ``` -# Enable LCD for Ping.pub (do this on the config for your chain) +# Enable LCD for Pocket.pub (do this on the config for your chain) 1. Set `enable = true` in `./config/app.toml` ``` diff --git a/package.json b/package.json index b710aba237..6934d6f759 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "ping.pub", - "version": "3.0.0", + "name": "pocket-explorer", + "version": "2.0.0", "private": true, "target": "", "scripts": { @@ -13,6 +13,7 @@ }, "dependencies": { "@chenfengyuan/vue-countdown": "2", + "@cosmjs/amino": "^0.32.3", "@cosmjs/crypto": "^0.32.3", "@cosmjs/encoding": "^0.32.3", "@cosmjs/stargate": "^0.32.3", @@ -23,8 +24,10 @@ "@osmonauts/lcd": "^0.8.0", "@personaxyz/ad-sdk": "0.0.21", "@ping-pub/chain-registry-client": "^0.0.25", + "@seald-io/nedb": "^4.0.4", "@vitejs/plugin-vue-jsx": "^3.0.0", - "@vueuse/core": "^9.12.0", + "@vueuse/components": "^10.11.0", + "@vueuse/core": "^10.11.0", "@vueuse/integrations": "^10.1.2", "@vueuse/math": "^9.12.0", "apexcharts": "^3.37.1", @@ -32,9 +35,11 @@ "axios": "^1.3.2", "buffer": "^6.0.3", "build": "^0.1.4", + "cron": "^3.1.7", "cross-fetch": "^3.1.5", "daisyui": "^3.1.0", "dayjs": "^1.11.7", + "express": "^4.19.2", "lazy-load-vue3": "^1.3.0", "long": "^5.2.1", "md-editor-v3": "^2.8.1", diff --git a/public/favicon.ico b/public/favicon.ico deleted file mode 100644 index ef242a96ba..0000000000 Binary files a/public/favicon.ico and /dev/null differ diff --git a/public/loader.css b/public/loader.css index c3a9a13462..26c75195e8 100644 --- a/public/loader.css +++ b/public/loader.css @@ -15,7 +15,7 @@ block-size: 55px; border-radius: 50%; inline-size: 55px; - inset-block-start: calc(40% + 35px); + inset-block-start: calc(40% + 40px); inset-inline-start: calc(50% - 27.5px); } diff --git a/server/.env.example b/server/.env.example new file mode 100644 index 0000000000..390218feea --- /dev/null +++ b/server/.env.example @@ -0,0 +1,11 @@ +# RPC Endpoints - comma-separated list of RPC_NAME=RPC_ENDPOINT pairs +RPC_ENDPOINTS=shannon=https://shannon-testnet-grove-api.beta.poktroll.com,mainnet=https://rpc.pokt.network + +# Redis configuration +REDIS_URL=redis://127.0.0.1:6379 +REDIS_PASSWORD= +REDIS_DB=0 + +# Worker configuration +WORKER_CONCURRENCY=2 +HISTORICAL_BATCH_SIZE=50 \ No newline at end of file diff --git a/server/DISK-MANAGEMENT-README.md b/server/DISK-MANAGEMENT-README.md new file mode 100644 index 0000000000..9bfdd9e678 --- /dev/null +++ b/server/DISK-MANAGEMENT-README.md @@ -0,0 +1,134 @@ +# Disk Space Management for Explorer Server + +This document provides guidance on managing disk space for the Explorer server, focusing on Docker log rotation and cleanup strategies. + +## Quick Start + +For immediate disk space relief: + +```bash +# Run on the server +sudo ./disk-cleanup.sh +``` + +For long-term log management: + +```bash +# Set up log rotation and scheduled cleanup +sudo ./setup-log-rotation.sh +``` + +## Available Scripts + +1. **docker-cleanup.sh** + - Cleans up Docker resources and logs + - Can be run manually or via cron + - Keeps logs for no more than 7 days + +2. **disk-cleanup.sh** + - Comprehensive disk cleanup (Docker + system) + - Should be run with sudo + - More aggressive than docker-cleanup.sh + +3. **setup-log-rotation.sh** + - Sets up system-wide Docker log rotation + - Installs cleanup scripts + - Configures weekly cron job + +## Docker Compose Log Configuration + +The `docker-compose.yml` has been updated to include log rotation for each service: + +```yaml +logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + compress: "true" +``` + +This limits each container's logs to 30MB total (3 files × 10MB) and compresses older logs. + +## System-wide Docker Log Configuration + +The setup script creates `/etc/docker/daemon.json` with log rotation settings: + +```json +{ + "log-driver": "json-file", + "log-opts": { + "max-size": "10m", + "max-file": "3", + "compress": "true" + } +} +``` + +## Monitoring Disk Usage + +To monitor disk usage: + +```bash +# Overall disk usage +df -h + +# Docker disk usage +docker system df + +# Detailed Docker disk usage +docker system df -v + +# Find large files +sudo find / -type f -size +100M -exec ls -lh {} \; | sort -k5 -h +``` + +## Schedule Regular Cleanup + +By default, the setup script configures a weekly cleanup job. To change this: + +```bash +# Edit the root crontab +sudo crontab -e +``` + +Example cron configurations: + +1. **Weekly cleanup** (Sunday at 1 AM): + ``` + 0 1 * * 0 /usr/local/bin/docker-cleanup.sh >> /var/log/docker-cleanup.log 2>&1 + ``` + +2. **Daily cleanup** (at 2 AM): + ``` + 0 2 * * * /usr/local/bin/docker-cleanup.sh >> /var/log/docker-cleanup.log 2>&1 + ``` + +3. **Monthly comprehensive cleanup** (1st day at 3 AM): + ``` + 0 3 1 * * /path/to/disk-cleanup.sh >> /var/log/disk-cleanup.log 2>&1 + ``` + +## Applying Changes + +After modifying Docker log settings: + +1. Restart Docker daemon: + ```bash + sudo systemctl restart docker + ``` + +2. Restart containers: + ```bash + cd /path/to/explorer/server + docker-compose down + docker-compose up -d + ``` + +## Best Practices + +1. **Regular Monitoring**: Check disk usage weekly +2. **Retention Policy**: Review log retention needs; 7 days is recommended +3. **Application Logs**: Consider redirecting application logs to files and rotate them +4. **Backup**: Back up important data before running cleanup scripts +5. **Test First**: Test cleanup scripts in non-production environment \ No newline at end of file diff --git a/server/DOCKER-CLEANUP-README.md b/server/DOCKER-CLEANUP-README.md new file mode 100644 index 0000000000..0d06d81510 --- /dev/null +++ b/server/DOCKER-CLEANUP-README.md @@ -0,0 +1,81 @@ +# Docker Log Cleanup + +This document explains how to set up regular Docker log cleanup to prevent disk space issues. + +## Log Rotation in Docker Compose + +The `docker-compose.yml` file includes log rotation settings for each service: + +```yaml +logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + compress: "true" +``` + +These settings: +- Limit log files to 10MB each +- Keep only 3 rotated log files +- Compress older log files + +## Docker System-wide Settings + +For Docker daemon system-wide settings, create or modify `/etc/docker/daemon.json`: + +```json +{ + "log-driver": "json-file", + "log-opts": { + "max-size": "10m", + "max-file": "3", + "compress": "true" + } +} +``` + +After modifying this file, restart Docker: + +```bash +sudo systemctl restart docker +``` + +## Cleanup Script + +The `docker-cleanup.sh` script provides additional cleanup by: +1. Pruning unused Docker resources +2. Removing exited containers +3. Truncating active container logs +4. Cleaning up log files older than 7 days + +### Setting up Automated Cleanup + +To run the cleanup script weekly, add a cron job: + +1. Edit the crontab: + ```bash + sudo crontab -e + ``` + +2. Add the following line to run the script every Sunday at 1 AM: + ``` + 0 1 * * 0 /absolute/path/to/explorer/server/docker-cleanup.sh >> /var/log/docker-cleanup.log 2>&1 + ``` + +3. Save and exit. + +### Manual Run + +To run the cleanup script manually: + +```bash +sudo ./docker-cleanup.sh +``` + +## Important Notes + +- Docker logs are stored in `/var/lib/docker/containers/` on Linux systems +- The cleanup script assumes access to this location +- For Kubernetes environments, consider using a different approach +- Always test the script in a non-production environment first \ No newline at end of file diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000000..f6a7442506 --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,32 @@ +# Use the official Node.js image as base +FROM node:18 + +# Set the working directory inside the container +WORKDIR /usr/src/app + +# Copy package.json and package-lock.json to the working directory +COPY package*.json ./ + +# Install dependencies with legacy-peer-deps flag to bypass dependency conflicts +RUN npm install --legacy-peer-deps + +# Copy the rest of the application code to the working directory +COPY . . + +# Set Node.js environment to production +ENV NODE_ENV=production + +# Increase Node.js memory limit for larger datasets +ENV NODE_OPTIONS="--max-old-space-size=8192" + +# Change ownership of the working directory to the 'node' user +RUN chown -R node:node . + +# Switch to the 'node' user for security +USER node + +# Expose the port your app runs on +EXPOSE 3005 + +# Command to run the server +CMD ["node", "index.js"] diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000000..f38bcae6a3 --- /dev/null +++ b/server/README.md @@ -0,0 +1,136 @@ +# Block Explorer Server + +High-performance blockchain explorer server that indexes and serves transaction data from multiple RPC endpoints. The server uses Redis for fast data access and worker threads for parallel processing. + +## Features + +- Multi-chain support: Fetch transaction data from multiple blockchain RPC endpoints +- High-performance Redis storage: Fast reads and writes using Redis data structures +- Parallel processing: Worker threads for efficient data fetching and processing +- Scalable architecture: Independent workers for each RPC endpoint +- Efficient API endpoints: Chain-specific transaction querying + +## Architecture Overview + +This server uses a multi-layered architecture: + +1. **Data Storage**: Redis for high-performance, in-memory data storage +2. **Worker Pool**: Multi-threaded workers to fetch and process blockchain data +3. **Service Layer**: Business logic for transaction retrieval and processing +4. **API Layer**: RESTful endpoints for querying transaction data + +### Redis Data Schema + +- `tx:{chainName}:{txHash}` - Hash storing transaction details +- `chain:{chainName}:latest_height` - String storing latest processed block height +- `chain:{chainName}:max_height` - String storing most recent chain height +- `chain:{chainName}:txs` - Sorted set of transaction hashes by height +- `chain:{chainName}:blocks` - Set of blocks that have been processed + +## Installation + +### Prerequisites + +- Node.js 18+ +- Redis 6+ (or use Docker) + +### Environment Variables + +Create a `.env` file in the root directory with the following variables: + +``` +# RPC Endpoints - comma-separated list of RPC_NAME=RPC_ENDPOINT pairs +RPC_ENDPOINTS=shannon=https://shannon-testnet-grove-api.beta.poktroll.com,mainnet=https://rpc.pokt.network + +# Redis configuration +REDIS_URL=redis://127.0.0.1:6379 +REDIS_PASSWORD= +REDIS_DB=0 + +# Worker configuration +WORKER_CONCURRENCY=2 +HISTORICAL_BATCH_SIZE=50 +``` + +### Docker Setup (Recommended) + +The easiest way to run the server is with Docker: + +```bash +# Start Redis and the API server +docker-compose up -d + +# View logs +docker-compose logs -f +``` + +### Manual Setup + +```bash +# Install dependencies +npm install + +# Start Redis (install separately or use Docker) +docker run -d -p 6379:6379 redis:7-alpine + +# Start the server +npm start +``` + +## API Endpoints + +| Endpoint | Method | Description | Query Parameters | +|----------|--------|-------------|-----------------| +| `/api/v1/transactions` | GET | Get paginated transactions | `page`, `limit`, `chain` | +| `/api/v1/transactions/count` | GET | Get transaction count or historical data | `chain` | +| `/api/v1/transactions/:transaction_id` | GET | Get transaction by ID | `chain` | +| `/api/v1/chains` | GET | Get available chains | None | +| `/api/v1/chains/stats` | GET | Get chain statistics | None | + +### Example API Calls + +```bash +# Get transactions from a specific chain (paginated) +curl http://localhost:3005/api/v1/transactions?chain=shannon&page=1&limit=10 + +# Get transaction count for all chains +curl http://localhost:3005/api/v1/transactions/count + +# Get transaction count for a specific chain (returns historical data) +curl http://localhost:3005/api/v1/transactions/count?chain=shannon + +# Get a specific transaction +curl http://localhost:3005/api/v1/transactions/{TRANSACTION_HASH} + +# Get available chains +curl http://localhost:3005/api/v1/chains + +# Get chain statistics +curl http://localhost:3005/api/v1/chains/stats +``` + +## Performance Considerations + +- Redis provides in-memory storage for fast access to transaction data +- Worker threads allow parallel processing of blockchain data +- Sorted sets in Redis enable efficient pagination and range queries +- Data is organized by chain name for rapid chain-specific queries + +## Maintenance and Scaling + +### Monitoring + +Monitor Redis memory usage as the transaction database grows. Consider implementing: + +- TTL (Time-To-Live) for older transactions if historical data isn't critical +- Redis persistence configuration for data durability +- Redis cluster for horizontal scaling + +### Adding New Chains + +To add a new chain, simply add it to the `RPC_ENDPOINTS` environment variable with the format: +``` +RPC_ENDPOINTS=chain1=url1,chain2=url2,... +``` + +The system will automatically create workers for each new chain. \ No newline at end of file diff --git a/server/config/redis.js b/server/config/redis.js new file mode 100644 index 0000000000..85f912f56c --- /dev/null +++ b/server/config/redis.js @@ -0,0 +1,89 @@ +const Redis = require('ioredis'); +const dotenv = require('dotenv'); + +dotenv.config(); + +// Redis connection configuration with improved settings +const redisOptions = { + host: process.env.REDIS_URL ? new URL(process.env.REDIS_URL).hostname : '127.0.0.1', + port: process.env.REDIS_URL ? parseInt(new URL(process.env.REDIS_URL).port || '6379', 10) : 6379, + password: process.env.REDIS_PASSWORD || undefined, + db: parseInt(process.env.REDIS_DB || '0', 10), + + // Connection management + connectTimeout: 10000, // Connection timeout in ms + disconnectTimeout: 2000, // Disconnect timeout + keepAlive: 30000, // Keep-alive packet interval + commandTimeout: 5000, // Default command timeout + + // Reconnection strategy + retryStrategy: (times) => { + // Exponential backoff with a cap + const delay = Math.min(Math.exp(times) * 50, 10000); + console.log(`Redis connection retry attempt ${times} in ${delay}ms`); + return delay; + }, + + // Error handling + maxRetriesPerRequest: 3, + enableAutoPipelining: true, // Auto pipeline commands for better performance + enableOfflineQueue: true, // Queue commands when Redis is offline + + // Connection pool settings (if using cluster) + maxLoadingRetryTime: 10000, // Max time to retry during loading + enableReadyCheck: true // Check if Redis is ready before use +}; + +// Create Redis client +const redisClient = new Redis(redisOptions); + +// Redis event listeners with better error handling +redisClient.on('connect', () => { + console.log('Connected to Redis server'); +}); + +redisClient.on('ready', () => { + console.log('Redis client is ready'); +}); + +redisClient.on('error', (err) => { + console.error('Redis connection error:', err); +}); + +redisClient.on('close', () => { + console.log('Redis connection closed'); +}); + +redisClient.on('reconnecting', (time) => { + console.log(`Redis client reconnecting in ${time}ms`); +}); + +redisClient.on('end', () => { + console.log('Redis connection ended'); +}); + +// Helper function to create a Redis client with the same options +// This should be used when a new dedicated connection is needed +const createRedisClient = () => { + const client = new Redis(redisOptions); + + client.on('error', (err) => { + console.error('Redis client error:', err); + }); + + return client; +}; + +// Transaction data structure schema in Redis: +// +// Keys: +// tx:{chainName}:{txHash} - Hash storing transaction details +// chain:{chainName}:latest_height - String storing latest block height +// chain:{chainName}:txs - Sorted set of transaction hashes by height +// chain:{chainName}:blocks - Set of blocks that have been processed +// +// Set expiration on transaction data if needed for memory management +// The sorted set gives us pagination capability and range queries + +module.exports = redisClient; +module.exports.createRedisClient = createRedisClient; \ No newline at end of file diff --git a/server/config/rpc.js b/server/config/rpc.js new file mode 100644 index 0000000000..3104e96bba --- /dev/null +++ b/server/config/rpc.js @@ -0,0 +1,52 @@ +const dotenv = require('dotenv'); + +dotenv.config(); + +/** + * Parse RPC endpoints from environment variables + * Format: RPC_ENDPOINTS=name1=url1,name2=url2 + * + * @returns {Array<{name: string, url: string}>} Array of RPC configs + */ +function parseRpcEndpoints() { + const rpcEndpointsStr = process.env.RPC_ENDPOINTS || ''; + + if (!rpcEndpointsStr) { + console.warn('No RPC endpoints configured in environment variables'); + return []; + } + + try { + const endpoints = rpcEndpointsStr.split(',') + .map(endpoint => { + const [name, url] = endpoint.split('='); + if (!name || !url) { + console.warn(`Invalid RPC endpoint format: ${endpoint}`); + return null; + } + return { name: name.trim(), url: url.trim() }; + }) + .filter(Boolean); + + if (endpoints.length === 0) { + console.warn('No valid RPC endpoints found in configuration'); + } else { + console.log(`Loaded ${endpoints.length} RPC endpoints: ${endpoints.map(e => e.name).join(', ')}`); + } + + return endpoints; + } catch (error) { + console.error('Error parsing RPC endpoints:', error); + return []; + } +} + +// Fallback to single RPC for backward compatibility +function getSingleRpcEndpoint() { + return process.env.rpc_endpoint || 'https://shannon-testnet-grove-api.beta.poktroll.com'; +} + +module.exports = { + getRpcEndpoints: parseRpcEndpoints, + getSingleRpcEndpoint +}; \ No newline at end of file diff --git a/server/datastore/pokt.db b/server/datastore/pokt.db new file mode 100644 index 0000000000..e69de29bb2 diff --git a/server/disk-cleanup.sh b/server/disk-cleanup.sh new file mode 100755 index 0000000000..19ac7b2441 --- /dev/null +++ b/server/disk-cleanup.sh @@ -0,0 +1,81 @@ +#!/bin/bash + +# Comprehensive disk cleanup script for Docker environments +# Run this script with sudo to clean up disk space + +echo "Starting comprehensive disk cleanup - $(date)" + +# Check if running as root +if [ "$EUID" -ne 0 ]; then + echo "Please run as root or with sudo" + exit 1 +fi + +# 1. Docker cleanup +echo "Cleaning up Docker resources..." + +# Stop unused containers (optional - uncomment if needed) +# docker ps -q -f status=exited | xargs -r docker rm + +# Remove unused Docker images, containers, networks, and volumes +echo "Pruning Docker system..." +docker system prune -af --volumes + +# Remove all build cache +echo "Removing Docker build cache..." +docker builder prune -af + +# Clean up Docker container logs directly +echo "Cleaning up container logs..." +CONTAINERS=$(docker ps -q) +for CONTAINER in $CONTAINERS; do + CONTAINER_NAME=$(docker inspect --format '{{.Name}}' $CONTAINER | sed 's/\///') + LOG_PATH=$(docker inspect --format='{{.LogPath}}' $CONTAINER) + + if [ -f "$LOG_PATH" ]; then + echo "Truncating logs for container: $CONTAINER_NAME" + truncate -s 0 "$LOG_PATH" || echo "Failed to truncate $LOG_PATH" + fi +done + +# Clean up Docker log files older than 7 days +echo "Removing Docker log files older than 7 days..." +find /var/lib/docker/containers/ -name "*.log" -type f -mtime +7 -delete 2>/dev/null || echo "No old Docker log files found or permission denied" + +# 2. System cleanup +echo "Cleaning up system files..." + +# Clear apt cache if exists +if command -v apt-get &> /dev/null; then + echo "Cleaning apt cache..." + apt-get clean + apt-get autoremove -y +fi + +# Clear journal logs +if command -v journalctl &> /dev/null; then + echo "Cleaning journal logs..." + journalctl --vacuum-time=7d +fi + +# Clean temp files +echo "Cleaning temp files..." +rm -rf /tmp/* /var/tmp/* 2>/dev/null || echo "Some temp files could not be removed" + +# Clean old log files +echo "Cleaning old log files..." +find /var/log -type f -name "*.gz" -mtime +7 -delete 2>/dev/null +find /var/log -type f -name "*.old" -mtime +7 -delete 2>/dev/null +find /var/log -type f -name "*.log.*" -mtime +7 -delete 2>/dev/null + +# Rotate and truncate logs +if command -v logrotate &> /dev/null; then + echo "Forcing log rotation..." + logrotate -f /etc/logrotate.conf +fi + +# 3. Report disk usage +echo "Disk usage after cleanup:" +df -h + +echo "Comprehensive disk cleanup completed - $(date)" \ No newline at end of file diff --git a/server/docker-cleanup.sh b/server/docker-cleanup.sh new file mode 100755 index 0000000000..ea8c49c0ed --- /dev/null +++ b/server/docker-cleanup.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +# Script to clean up Docker logs and free disk space +# Recommended to run this weekly via cron + +echo "Starting Docker cleanup process - $(date)" + +# Step 1: Prune unused Docker resources +echo "Pruning unused Docker resources..." +docker system prune -f + +# Step 2: Remove exited containers +echo "Removing exited containers..." +docker ps -a -q --filter "status=exited" | xargs -r docker rm + +# Step 3: Find and clean up logs for containers directly +echo "Cleaning up Docker container logs..." +CONTAINERS=$(docker ps -q) +for CONTAINER in $CONTAINERS; do + CONTAINER_NAME=$(docker inspect --format '{{.Name}}' $CONTAINER | sed 's/\///') + echo "Truncating logs for container: $CONTAINER_NAME" + truncate -s 0 $(docker inspect --format='{{.LogPath}}' $CONTAINER) 2>/dev/null || true +done + +# Step 4: Clean up old Docker log files from the system +echo "Cleaning up Docker log files older than 7 days..." +find /var/lib/docker/containers/ -name "*.log" -mtime +7 -delete 2>/dev/null || true + +echo "Docker cleanup completed - $(date)" \ No newline at end of file diff --git a/server/docker-compose.yml b/server/docker-compose.yml new file mode 100644 index 0000000000..c9937480da --- /dev/null +++ b/server/docker-compose.yml @@ -0,0 +1,68 @@ +version: '3.8' + +services: + redis: + image: redis:7-alpine + ports: + - "127.0.0.1:6379:6379" # Only bind to localhost + volumes: + - redis-data:/data + command: > + redis-server + --appendonly yes + --save 900 1 + --save 300 10 + --save 60 10000 + --maxmemory 2gb + --maxmemory-policy volatile-lru + --loglevel warning + --tcp-keepalive 300 + --lazyfree-lazy-eviction yes + --replicaof no one + restart: unless-stopped + healthcheck: + test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-changeme}", "ping"] + interval: 10s + timeout: 5s + retries: 3 + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + compress: "true" + networks: + - default + + api: + build: + context: . + dockerfile: Dockerfile + ports: + - "${PORT:-3005}:3005" + env_file: + - .env + environment: + - RPC_ENDPOINTS=${RPC_ENDPOINTS} + - REDIS_URL=redis://:${REDIS_PASSWORD}@redis:6379 + - PORT=${PORT:-3005} + - REDIS_PASSWORD=${REDIS_PASSWORD} + - REDIS_DB=${REDIS_DB:-0} + - WORKER_CONCURRENCY=${WORKER_CONCURRENCY:-2} + - HISTORICAL_BATCH_SIZE=${HISTORICAL_BATCH_SIZE:-50} + - HISTORY_CACHE_TTL=${HISTORY_CACHE_TTL:-3600} # Cache TTL for historical data (1 hour default) + depends_on: + redis: + condition: service_healthy + restart: unless-stopped + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + compress: "true" + networks: + - default + +volumes: + redis-data: \ No newline at end of file diff --git a/server/index.js b/server/index.js new file mode 100644 index 0000000000..b1a5a549aa --- /dev/null +++ b/server/index.js @@ -0,0 +1,115 @@ +const express = require('express'); +const bodyParser = require('body-parser'); +const cors = require('cors'); +const dotenv = require('dotenv'); +const path = require('path'); +const workerPool = require('./services/worker'); +const transactionService = require('./services/transactionService'); + +// Load environment variables +dotenv.config(); + +const app = express(); +const PORT = process.env.PORT || 3005; + +// Middleware +app.use(bodyParser.json()); +app.use(cors()); + +// API endpoints +app.get('/api/v1/transactions', async (req, res) => { + try { + const { page, limit, chain } = req.query; + const transactions = await transactionService.getTransactions({ + page, + limit, + chain + }); + res.json(transactions); + } catch (error) { + console.error('Error fetching transactions:', error); + res.status(500).json({ error: error.message }); + } +}); + +app.get('/api/v1/transactions/count', async (req, res) => { + try { + const { chain } = req.query; + const count = await transactionService.getTransactionCount(chain); + res.json(count); + } catch (error) { + console.error('Error fetching transaction count:', error); + res.status(500).json({ error: error.message }); + } +}); + +app.get('/api/v1/transactions/:transaction_id', async (req, res) => { + try { + const { transaction_id } = req.params; + const { chain } = req.query; + const transaction = await transactionService.getTransactionById(transaction_id, chain); + + if (!transaction.data) { + return res.status(404).json({ error: 'Transaction not found' }); + } + + res.json(transaction); + } catch (error) { + console.error('Error fetching transaction:', error); + res.status(500).json({ error: error.message }); + } +}); + +// New endpoint to get chain statistics +app.get('/api/v1/chains/stats', async (req, res) => { + try { + const stats = await transactionService.getChainStats(); + res.json(stats); + } catch (error) { + console.error('Error fetching chain statistics:', error); + res.status(500).json({ error: error.message }); + } +}); + +// New endpoint to get available chains +app.get('/api/v1/chains', (req, res) => { + try { + const chains = transactionService.getAvailableChains(); + res.json({ data: chains }); + } catch (error) { + console.error('Error fetching chains:', error); + res.status(500).json({ error: error.message }); + } +}); + +// Start the server and initialize the worker pool +const startServer = async () => { + try { + // Initialize worker pool + await workerPool.initialize(); + + // Start the HTTP server + app.listen(PORT, () => { + console.log(`Server is running on http://localhost:${PORT}`); + }); + + // Handle graceful shutdown + process.on('SIGINT', async () => { + console.log('Shutting down server...'); + await workerPool.shutdown(); + process.exit(0); + }); + + process.on('SIGTERM', async () => { + console.log('Shutting down server...'); + await workerPool.shutdown(); + process.exit(0); + }); + } catch (error) { + console.error('Failed to start server:', error); + process.exit(1); + } +}; + +// Start the server +startServer(); \ No newline at end of file diff --git a/server/package-lock.json b/server/package-lock.json new file mode 100644 index 0000000000..82bd77169f --- /dev/null +++ b/server/package-lock.json @@ -0,0 +1,2297 @@ +{ + "name": "explorer-indexer", + "version": "1.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "explorer-indexer", + "version": "1.0.1", + "dependencies": { + "@cosmjs/crypto": "^0.32.2", + "@cosmjs/encoding": "^0.32.2", + "@cosmjs/proto-signing": "^0.32.2", + "@cosmjs/stargate": "^0.32.2", + "@seald-io/nedb": "^4.0.2", + "body-parser": "^1.20.2", + "cors": "^2.8.5", + "cron": "^3.1.6", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "ioredis": "^5.3.2", + "node-worker-threads-pool": "^1.5.1", + "winston": "^3.11.0" + }, + "devDependencies": { + "nodemon": "^3.0.2" + } + }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@confio/ics23": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@confio/ics23/-/ics23-0.6.8.tgz", + "integrity": "sha512-wB6uo+3A50m0sW/EWcU64xpV/8wShZ6bMTa7pF8eYsTrSkQA7oLUIJcs/wb8g4y2Oyq701BaGiO6n/ak5WXO1w==", + "deprecated": "Unmaintained. The codebase for this package was moved to https://github.com/cosmos/ics23 but then the JS implementation was removed in https://github.com/cosmos/ics23/pull/353. Please consult the maintainers of https://github.com/cosmos for further assistance.", + "dependencies": { + "@noble/hashes": "^1.0.0", + "protobufjs": "^6.8.8" + } + }, + "node_modules/@cosmjs/amino": { + "version": "0.32.4", + "resolved": "https://registry.npmjs.org/@cosmjs/amino/-/amino-0.32.4.tgz", + "integrity": "sha512-zKYOt6hPy8obIFtLie/xtygCkH9ZROiQ12UHfKsOkWaZfPQUvVbtgmu6R4Kn1tFLI/SRkw7eqhaogmW/3NYu/Q==", + "dependencies": { + "@cosmjs/crypto": "^0.32.4", + "@cosmjs/encoding": "^0.32.4", + "@cosmjs/math": "^0.32.4", + "@cosmjs/utils": "^0.32.4" + } + }, + "node_modules/@cosmjs/crypto": { + "version": "0.32.4", + "resolved": "https://registry.npmjs.org/@cosmjs/crypto/-/crypto-0.32.4.tgz", + "integrity": "sha512-zicjGU051LF1V9v7bp8p7ovq+VyC91xlaHdsFOTo2oVry3KQikp8L/81RkXmUIT8FxMwdx1T7DmFwVQikcSDIw==", + "dependencies": { + "@cosmjs/encoding": "^0.32.4", + "@cosmjs/math": "^0.32.4", + "@cosmjs/utils": "^0.32.4", + "@noble/hashes": "^1", + "bn.js": "^5.2.0", + "elliptic": "^6.5.4", + "libsodium-wrappers-sumo": "^0.7.11" + } + }, + "node_modules/@cosmjs/encoding": { + "version": "0.32.4", + "resolved": "https://registry.npmjs.org/@cosmjs/encoding/-/encoding-0.32.4.tgz", + "integrity": "sha512-tjvaEy6ZGxJchiizzTn7HVRiyTg1i4CObRRaTRPknm5EalE13SV+TCHq38gIDfyUeden4fCuaBVEdBR5+ti7Hw==", + "dependencies": { + "base64-js": "^1.3.0", + "bech32": "^1.1.4", + "readonly-date": "^1.0.0" + } + }, + "node_modules/@cosmjs/json-rpc": { + "version": "0.32.4", + "resolved": "https://registry.npmjs.org/@cosmjs/json-rpc/-/json-rpc-0.32.4.tgz", + "integrity": "sha512-/jt4mBl7nYzfJ2J/VJ+r19c92mUKF0Lt0JxM3MXEJl7wlwW5haHAWtzRujHkyYMXOwIR+gBqT2S0vntXVBRyhQ==", + "dependencies": { + "@cosmjs/stream": "^0.32.4", + "xstream": "^11.14.0" + } + }, + "node_modules/@cosmjs/math": { + "version": "0.32.4", + "resolved": "https://registry.npmjs.org/@cosmjs/math/-/math-0.32.4.tgz", + "integrity": "sha512-++dqq2TJkoB8zsPVYCvrt88oJWsy1vMOuSOKcdlnXuOA/ASheTJuYy4+oZlTQ3Fr8eALDLGGPhJI02W2HyAQaw==", + "dependencies": { + "bn.js": "^5.2.0" + } + }, + "node_modules/@cosmjs/proto-signing": { + "version": "0.32.4", + "resolved": "https://registry.npmjs.org/@cosmjs/proto-signing/-/proto-signing-0.32.4.tgz", + "integrity": "sha512-QdyQDbezvdRI4xxSlyM1rSVBO2st5sqtbEIl3IX03uJ7YiZIQHyv6vaHVf1V4mapusCqguiHJzm4N4gsFdLBbQ==", + "dependencies": { + "@cosmjs/amino": "^0.32.4", + "@cosmjs/crypto": "^0.32.4", + "@cosmjs/encoding": "^0.32.4", + "@cosmjs/math": "^0.32.4", + "@cosmjs/utils": "^0.32.4", + "cosmjs-types": "^0.9.0" + } + }, + "node_modules/@cosmjs/socket": { + "version": "0.32.4", + "resolved": "https://registry.npmjs.org/@cosmjs/socket/-/socket-0.32.4.tgz", + "integrity": "sha512-davcyYziBhkzfXQTu1l5NrpDYv0K9GekZCC9apBRvL1dvMc9F/ygM7iemHjUA+z8tJkxKxrt/YPjJ6XNHzLrkw==", + "dependencies": { + "@cosmjs/stream": "^0.32.4", + "isomorphic-ws": "^4.0.1", + "ws": "^7", + "xstream": "^11.14.0" + } + }, + "node_modules/@cosmjs/stargate": { + "version": "0.32.4", + "resolved": "https://registry.npmjs.org/@cosmjs/stargate/-/stargate-0.32.4.tgz", + "integrity": "sha512-usj08LxBSsPRq9sbpCeVdyLx2guEcOHfJS9mHGCLCXpdAPEIEQEtWLDpEUc0LEhWOx6+k/ChXTc5NpFkdrtGUQ==", + "dependencies": { + "@confio/ics23": "^0.6.8", + "@cosmjs/amino": "^0.32.4", + "@cosmjs/encoding": "^0.32.4", + "@cosmjs/math": "^0.32.4", + "@cosmjs/proto-signing": "^0.32.4", + "@cosmjs/stream": "^0.32.4", + "@cosmjs/tendermint-rpc": "^0.32.4", + "@cosmjs/utils": "^0.32.4", + "cosmjs-types": "^0.9.0", + "xstream": "^11.14.0" + } + }, + "node_modules/@cosmjs/stream": { + "version": "0.32.4", + "resolved": "https://registry.npmjs.org/@cosmjs/stream/-/stream-0.32.4.tgz", + "integrity": "sha512-Gih++NYHEiP+oyD4jNEUxU9antoC0pFSg+33Hpp0JlHwH0wXhtD3OOKnzSfDB7OIoEbrzLJUpEjOgpCp5Z+W3A==", + "dependencies": { + "xstream": "^11.14.0" + } + }, + "node_modules/@cosmjs/tendermint-rpc": { + "version": "0.32.4", + "resolved": "https://registry.npmjs.org/@cosmjs/tendermint-rpc/-/tendermint-rpc-0.32.4.tgz", + "integrity": "sha512-MWvUUno+4bCb/LmlMIErLypXxy7ckUuzEmpufYYYd9wgbdCXaTaO08SZzyFM5PI8UJ/0S2AmUrgWhldlbxO8mw==", + "dependencies": { + "@cosmjs/crypto": "^0.32.4", + "@cosmjs/encoding": "^0.32.4", + "@cosmjs/json-rpc": "^0.32.4", + "@cosmjs/math": "^0.32.4", + "@cosmjs/socket": "^0.32.4", + "@cosmjs/stream": "^0.32.4", + "@cosmjs/utils": "^0.32.4", + "axios": "^1.6.0", + "readonly-date": "^1.0.0", + "xstream": "^11.14.0" + } + }, + "node_modules/@cosmjs/utils": { + "version": "0.32.4", + "resolved": "https://registry.npmjs.org/@cosmjs/utils/-/utils-0.32.4.tgz", + "integrity": "sha512-D1Yc+Zy8oL/hkUkFUL/bwxvuDBzRGpc4cF7/SkdhxX4iHpSLgdOuTt1mhCh9+kl6NQREy9t7SYZ6xeW5gFe60w==" + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@ioredis/commands": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==" + }, + "node_modules/@noble/hashes": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz", + "integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, + "node_modules/@seald-io/binary-search-tree": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@seald-io/binary-search-tree/-/binary-search-tree-1.0.3.tgz", + "integrity": "sha512-qv3jnwoakeax2razYaMsGI/luWdliBLHTdC6jU55hQt1hcFqzauH/HsBollQ7IR4ySTtYhT+xyHoijpA16C+tA==" + }, + "node_modules/@seald-io/nedb": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@seald-io/nedb/-/nedb-4.1.1.tgz", + "integrity": "sha512-u7fVfzKQ/3ZaIOnYQONf2lPZtGUeQtMPjfcaQkCw/GZv5dzn20qKW6sfN0NkVbr0ksJMlWcFXNGcXYsQSb1a1g==", + "dependencies": { + "@seald-io/binary-search-tree": "^1.0.3", + "localforage": "^1.9.0", + "util": "^0.12.4" + } + }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" + }, + "node_modules/@types/luxon": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz", + "integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==" + }, + "node_modules/@types/node": { + "version": "22.14.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.0.tgz", + "integrity": "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axios": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", + "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bech32": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", + "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bn.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cosmjs-types": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/cosmjs-types/-/cosmjs-types-0.9.0.tgz", + "integrity": "sha512-MN/yUe6mkJwHnCFfsNPeCfXVhyxHYW6c/xDUzrSbBycYzw++XvWDMJArXp2pLdgD6FQ8DW79vkPjeNKVrXaHeQ==" + }, + "node_modules/cron": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/cron/-/cron-3.5.0.tgz", + "integrity": "sha512-0eYZqCnapmxYcV06uktql93wNWdlTmmBFP2iYz+JPVcQqlyFYcn1lFuIk4R54pkOmE7mcldTAPZv6X5XA4Q46A==", + "dependencies": { + "@types/luxon": "~3.4.0", + "luxon": "~3.5.0" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/elliptic": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/elliptic/node_modules/bn.js": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==" + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ioredis": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.6.0.tgz", + "integrity": "sha512-tBZlIIWbndeWBWCXWZiqtOF/yxf6yZX3tAlTJ7nfo5jhd6dctNxF7QnYlZLZ1a0o0pDoen7CgZqO+zjNaFbJAg==", + "dependencies": { + "@ioredis/commands": "^1.1.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/ioredis/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/ioredis/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isomorphic-ws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz", + "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" + }, + "node_modules/libsodium-sumo": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/libsodium-sumo/-/libsodium-sumo-0.7.15.tgz", + "integrity": "sha512-5tPmqPmq8T8Nikpm1Nqj0hBHvsLFCXvdhBFV7SGOitQPZAA6jso8XoL0r4L7vmfKXr486fiQInvErHtEvizFMw==" + }, + "node_modules/libsodium-wrappers-sumo": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.7.15.tgz", + "integrity": "sha512-aSWY8wKDZh5TC7rMvEdTHoyppVq/1dTSAeAR7H6pzd6QRT3vQWcT5pGwCotLcpPEOLXX6VvqihSPkpEhYAjANA==", + "dependencies": { + "libsodium-sumo": "^0.7.15" + } + }, + "node_modules/lie": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", + "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/localforage": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", + "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", + "dependencies": { + "lie": "3.1.1" + } + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" + }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/logform/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, + "node_modules/luxon": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", + "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-worker-threads-pool": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/node-worker-threads-pool/-/node-worker-threads-pool-1.5.1.tgz", + "integrity": "sha512-7TXAhpMm+jO4MfESxYLtMGSnJWv+itdNHMdaFmeZuPXxwFGU90mtEB42BciUULXOUAxYBfXILAuvrSG3rQZ7mw==" + }, + "node_modules/nodemon": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz", + "integrity": "sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/protobufjs": { + "version": "6.11.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz", + "integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": ">=13.7.0", + "long": "^4.0.0" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readonly-date": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/readonly-date/-/readonly-date-1.0.0.tgz", + "integrity": "sha512-tMKIV7hlk0h4mO3JTmmVuIlJVXjKk3Sep9Bf5OH0O+758ruuVkUy2J9SttDLm91IEX/WHlXPSpxMGjPj4beMIQ==" + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "engines": { + "node": "*" + } + }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==" + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/symbol-observable": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-2.0.3.tgz", + "integrity": "sha512-sQV7phh2WCYAn81oAkakC5qjq2Ml0g8ozqz03wOGnx9dDlG1de6yrF+0RAzSJD8fPUow3PTSMf2SAbOGxb93BA==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/winston": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xstream": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/xstream/-/xstream-11.14.0.tgz", + "integrity": "sha512-1bLb+kKKtKPbgTK6i/BaoAn03g47PpFstlbe1BA+y3pNS/LfvcaghS5BFf9+EE1J+KwSQsEpfJvFN5GqFtiNmw==", + "dependencies": { + "globalthis": "^1.0.1", + "symbol-observable": "^2.0.3" + } + } + } +} diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000000..c88579b8a3 --- /dev/null +++ b/server/package.json @@ -0,0 +1,29 @@ +{ + "name": "explorer-indexer", + "author": "Muhammad Ehsan (syedehsans1)", + "version": "1.0.1", + "description": "Block explorer server with Redis and worker threads", + "main": "index.js", + "scripts": { + "start": "node index.js", + "dev": "nodemon index.js" + }, + "dependencies": { + "@cosmjs/crypto": "^0.32.2", + "@cosmjs/encoding": "^0.32.2", + "@cosmjs/proto-signing": "^0.32.2", + "@cosmjs/stargate": "^0.32.2", + "@seald-io/nedb": "^4.0.2", + "body-parser": "^1.20.2", + "cors": "^2.8.5", + "cron": "^3.1.6", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "ioredis": "^5.3.2", + "node-worker-threads-pool": "^1.5.1", + "winston": "^3.11.0" + }, + "devDependencies": { + "nodemon": "^3.0.2" + } +} \ No newline at end of file diff --git a/server/redis-reset.sh b/server/redis-reset.sh new file mode 100644 index 0000000000..2891dd1dbc --- /dev/null +++ b/server/redis-reset.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +# Script to reset Redis data and remove stale configuration +echo "WARNING: This script will delete all Redis data. Press Ctrl+C to cancel or Enter to continue." +read + +# Stop containers +echo "Stopping containers..." +docker-compose down + +# Remove Redis data volume +echo "Removing Redis data volume..." +docker volume rm explorer_server_redis-data + +# Create fresh Redis data volume +echo "Creating fresh Redis data volume..." +docker volume create explorer_server_redis-data + +# Start containers +echo "Starting containers..." +docker-compose up -d + +# Wait for Redis to start +echo "Waiting for Redis to start..." +sleep 10 + +# Check Redis status +echo "Checking Redis status..." +docker-compose exec redis redis-cli -a ${REDIS_PASSWORD:-changeme} ping + +echo "Redis reset complete. Check logs for any errors:" +echo "docker-compose logs redis" \ No newline at end of file diff --git a/server/services/transactionService.js b/server/services/transactionService.js new file mode 100644 index 0000000000..4ac1030416 --- /dev/null +++ b/server/services/transactionService.js @@ -0,0 +1,469 @@ +const redis = require('../config/redis'); +const { getRpcEndpoints } = require('../config/rpc'); + +/** + * Transaction service that provides methods to interact with stored transaction data + */ +class TransactionService { + constructor() { + this.rpcEndpoints = getRpcEndpoints(); + } + + /** + * Get all available chain names + * @returns {Array} Array of chain names + */ + getAvailableChains() { + return this.rpcEndpoints.map(rpc => rpc.name); + } + + /** + * Get a paginated list of transactions for a specific chain + * + * @param {Object} options Query options + * @param {string} options.chain Chain name (defaults to first available chain) + * @param {number} options.page Page number (1-based) + * @param {number} options.limit Items per page + * @returns {Promise} Transactions and pagination metadata + */ + async getTransactions({ chain, page = 1, limit = 10 } = {}) { + // Use the first available chain if none specified + const chainName = chain || (this.rpcEndpoints.length > 0 ? this.rpcEndpoints[0].name : null); + + if (!chainName) { + throw new Error('No chain name provided and no default chains available'); + } + + // Convert to numbers + const pageNum = parseInt(page, 10); + const limitNum = parseInt(limit, 10); + + try { + // Calculate range for zrevrange + const start = (pageNum - 1) * limitNum; + const end = start + limitNum - 1; + + // Get total count + const totalCount = await redis.zcard(`chain:${chainName}:txs`); + + if (totalCount === 0) { + return { + data: [], + meta: { + total: 0, + page: pageNum, + limit: limitNum, + totalPages: 0, + } + }; + } + + // Get transaction hashes for the page (sorted by height in descending order) + const txHashes = await redis.zrevrange(`chain:${chainName}:txs`, start, end); + + // Get transaction details + const transactions = []; + for (const txHash of txHashes) { + const txData = await redis.hgetall(`tx:${chainName}:${txHash}`); + + if (txData && Object.keys(txData).length > 0) { + // Parse JSON fields + try { + // Transform fields + const transformedTxData = { ...txData }; + + // Parse JSON fields + if (txData.messages) { + try { + transformedTxData.messages = JSON.parse(txData.messages); + } catch (e) { + console.error(`Error parsing messages for tx ${txHash}:`, e); + transformedTxData.messages = []; + } + } + + if (txData.fee) { + try { + transformedTxData.fee = JSON.parse(txData.fee); + } catch (e) { + console.error(`Error parsing fee for tx ${txHash}:`, e); + transformedTxData.fee = {}; + } + } + + // Convert numeric fields if needed + if (txData.height) { + transformedTxData.height = parseInt(txData.height, 10); + } + + if (txData.status) { + transformedTxData.status = parseInt(txData.status, 10); + } + + transactions.push({ + ...transformedTxData, + chain: chainName + }); + } catch (err) { + console.error(`Error processing transaction data for ${txHash}:`, err); + } + } + } + + return { + data: transactions, + meta: { + total: totalCount, + page: pageNum, + limit: limitNum, + totalPages: Math.ceil(totalCount / limitNum), + } + }; + } catch (error) { + console.error(`Error getting transactions for chain ${chainName}:`, error); + throw new Error(`Failed to retrieve transactions: ${error.message}`); + } + } + + /** + * Get a transaction by its hash for a specific chain + * + * @param {string} transactionId Transaction hash + * @param {string} chain Chain name (optional, will search all chains if not provided) + * @returns {Promise} Transaction data + */ + async getTransactionById(transactionId, chain) { + try { + if (chain) { + // If chain is specified, look only in that chain + const txData = await redis.hgetall(`tx:${chain}:${transactionId}`); + + if (Object.keys(txData).length === 0) { + return { data: null }; + } + + // Transform the transaction data + const transformedTxData = { ...txData }; + + // Parse JSON fields + if (txData.messages) { + try { + transformedTxData.messages = JSON.parse(txData.messages); + } catch (e) { + console.error(`Error parsing messages for tx ${transactionId}:`, e); + transformedTxData.messages = []; + } + } + + if (txData.fee) { + try { + transformedTxData.fee = JSON.parse(txData.fee); + } catch (e) { + console.error(`Error parsing fee for tx ${transactionId}:`, e); + transformedTxData.fee = {}; + } + } + + // Convert numeric fields if needed + if (txData.height) { + transformedTxData.height = parseInt(txData.height, 10); + } + + if (txData.status) { + transformedTxData.status = parseInt(txData.status, 10); + } + + return { + data: { + ...transformedTxData, + chain + } + }; + } else { + // If no chain specified, search across all chains + const chains = this.getAvailableChains(); + + for (const chainName of chains) { + const txData = await redis.hgetall(`tx:${chainName}:${transactionId}`); + + if (Object.keys(txData).length > 0) { + // Transform the transaction data + const transformedTxData = { ...txData }; + + // Parse JSON fields + if (txData.messages) { + try { + transformedTxData.messages = JSON.parse(txData.messages); + } catch (e) { + console.error(`Error parsing messages for tx ${transactionId}:`, e); + transformedTxData.messages = []; + } + } + + if (txData.fee) { + try { + transformedTxData.fee = JSON.parse(txData.fee); + } catch (e) { + console.error(`Error parsing fee for tx ${transactionId}:`, e); + transformedTxData.fee = {}; + } + } + + // Convert numeric fields if needed + if (txData.height) { + transformedTxData.height = parseInt(txData.height, 10); + } + + if (txData.status) { + transformedTxData.status = parseInt(txData.status, 10); + } + + return { + data: { + ...transformedTxData, + chain: chainName + } + }; + } + } + + return { data: null }; + } + } catch (error) { + console.error(`Error getting transaction ${transactionId}:`, error); + throw new Error(`Failed to retrieve transaction: ${error.message}`); + } + } + + /** + * Get transaction count for a specific chain + * + * @param {string} chain Chain name (optional) + * @returns {Promise} Count data + */ + async getTransactionCount(chain) { + try { + if (chain) { + // Generate historical data for the specific chain over the last 30 days + const now = new Date(); + const days = 30; + const labels = []; + const counts = []; + + // Generate the date labels + for (let i = days - 1; i >= 0; i--) { + const date = new Date(now); + date.setDate(date.getDate() - i); + labels.push(date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })); + } + + // Get the current total count + const totalCount = await redis.zcard(`chain:${chain}:txs`); + + // For now, generate a reasonable distribution of counts based on the total + // In a real implementation, we would query historical data from Redis + + // Create a more realistic distribution - more recent days have more transactions + let remainingCount = totalCount; + const dailyCounts = []; + + for (let i = 0; i < days; i++) { + // Allocate a percentage of remaining transactions to each day + // More recent days get a higher percentage + let factor; + if (i < days / 3) { + // Older third: distribute 15% of remaining + factor = 0.15 / (days / 3); + } else if (i < 2 * (days / 3)) { + // Middle third: distribute 25% of remaining + factor = 0.25 / (days / 3); + } else { + // Recent third: distribute 60% of remaining + factor = 0.60 / (days / 3); + } + + const dayCount = Math.round(remainingCount * factor); + dailyCounts.push(dayCount); + remainingCount -= dayCount; + } + + // Ensure all transactions are accounted for due to rounding + if (remainingCount > 0) { + dailyCounts[dailyCounts.length - 1] += remainingCount; + } + + // Add counts in reverse order (oldest to newest) + for (let i = 0; i < days; i++) { + counts.push(dailyCounts[i]); + } + + return { + data: { + labels, + counts, + total: totalCount + } + }; + } else { + // Get counts for all chains + const chains = this.getAvailableChains(); + let totalCount = 0; + + for (const chainName of chains) { + const count = await redis.zcard(`chain:${chainName}:txs`); + totalCount += count; + } + + return { data: totalCount }; + } + } catch (error) { + console.error('Error getting transaction count:', error); + throw new Error(`Failed to get transaction count: ${error.message}`); + } + } + + /** + * Get statistics for all chains + * + * @returns {Promise} Chain statistics + */ + async getChainStats() { + try { + const chains = this.getAvailableChains(); + const stats = {}; + + for (const chainName of chains) { + const txCount = await redis.zcard(`chain:${chainName}:txs`); + const latestHeight = await redis.get(`chain:${chainName}:max_height`) || 0; + const processedHeight = await redis.get(`chain:${chainName}:latest_height`) || 0; + + stats[chainName] = { + txCount, + latestHeight: parseInt(latestHeight, 10), + processedHeight: parseInt(processedHeight, 10), + progress: latestHeight > 0 + ? ((parseInt(processedHeight, 10) / parseInt(latestHeight, 10)) * 100).toFixed(2) + '%' + : '0%' + }; + } + + return { data: stats }; + } catch (error) { + console.error('Error getting chain stats:', error); + throw new Error(`Failed to get chain statistics: ${error.message}`); + } + } + + /** + * Get historical transaction counts by day for a specific chain + * + * @param {Object} options Query options + * @param {string} options.chain Chain name (defaults to first available chain) + * @param {number} options.days Number of days to return data for (default: 30) + * @returns {Promise} Historical transaction counts by day + */ + async getHistoricalTransactionCounts({ chain, days = 30 } = {}) { + // Use the first available chain if none specified + const chainName = chain || (this.rpcEndpoints.length > 0 ? this.rpcEndpoints[0].name : null); + + if (!chainName) { + throw new Error('No chain name provided and no default chains available'); + } + + // Convert to number + const daysNum = parseInt(days, 10); + + try { + // Check if we have cached data first + const cacheKey = `history:${chainName}:${daysNum}`; + const cachedData = await redis.get(cacheKey); + + if (cachedData) { + try { + const parsedData = JSON.parse(cachedData); + console.log(`Using cached historical data for ${chainName} (${daysNum} days)`); + return parsedData; + } catch (err) { + console.error('Error parsing cached history data:', err); + // Continue with regenerating the data + } + } + + // Get all transactions for the chain + const txHashes = await redis.zrange(`chain:${chainName}:txs`, 0, -1, 'WITHSCORES'); + + // Initialize counts for each day + const now = new Date(); + const countsMap = new Map(); + const labels = []; + + // Generate date labels for the period + for (let i = daysNum - 1; i >= 0; i--) { + const date = new Date(now); + date.setDate(date.getDate() - i); + const dateKey = date.toISOString().split('T')[0]; // YYYY-MM-DD format + countsMap.set(dateKey, 0); + // Format date as "MMM D" (e.g., "Feb 3") + labels.push(date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })); + } + + // Process transactions in batches + for (let i = 0; i < txHashes.length; i += 2) { + const hash = txHashes[i]; + const blockHeight = parseInt(txHashes[i+1], 10); + + // Get the block to retrieve the timestamp + // First try directly getting the timestamp if it's stored + let timestamp = await redis.hget(`tx:${chainName}:${hash}`, 'timestamp'); + + // If no timestamp directly on tx, try getting from block + if (!timestamp) { + const blockKey = `block:${chainName}:${blockHeight}`; + const blockData = await redis.hget(blockKey, 'timestamp'); + if (blockData) { + timestamp = blockData; + } + } + + // Only count if we have a timestamp + if (timestamp) { + const txDate = new Date(timestamp); + const dateKey = txDate.toISOString().split('T')[0]; + + // Only count if within our range + if (countsMap.has(dateKey)) { + countsMap.set(dateKey, countsMap.get(dateKey) + 1); + } + } + } + + // Convert to array for chart + const counts = []; + for (let i = daysNum - 1; i >= 0; i--) { + const date = new Date(now); + date.setDate(date.getDate() - i); + const dateKey = date.toISOString().split('T')[0]; + counts.push(countsMap.get(dateKey) || 0); + } + + const result = { + data: { + labels, + counts + } + }; + + // Cache the result + const cacheTTL = parseInt(process.env.HISTORY_CACHE_TTL || 3600, 10); // Default 1 hour + await redis.set(cacheKey, JSON.stringify(result), 'EX', cacheTTL); + console.log(`Cached historical data for ${chainName} (${daysNum} days) with TTL of ${cacheTTL} seconds`); + + return result; + } catch (error) { + console.error(`Error getting historical transaction counts for chain ${chainName}:`, error); + throw new Error(`Failed to retrieve historical transaction counts: ${error.message}`); + } + } +} + +module.exports = new TransactionService(); \ No newline at end of file diff --git a/server/services/transactionWorker.js b/server/services/transactionWorker.js new file mode 100644 index 0000000000..a173e3d62e --- /dev/null +++ b/server/services/transactionWorker.js @@ -0,0 +1,378 @@ +const { parentPort, workerData } = require('worker_threads'); +const { sha256 } = require('@cosmjs/crypto'); +const { fromBase64, toHex, toBase64 } = require('@cosmjs/encoding'); +const { decodeTxRaw } = require('@cosmjs/proto-signing'); +const Redis = require('ioredis'); +const dotenv = require('dotenv'); + +dotenv.config(); + +// Worker data from parent thread +const { rpcName, rpcUrl, batchSize } = workerData; + +// Create a dedicated Redis connection for this worker with improved settings +const redisOptions = { + host: process.env.REDIS_URL ? new URL(process.env.REDIS_URL).hostname : '127.0.0.1', + port: process.env.REDIS_URL ? parseInt(new URL(process.env.REDIS_URL).port || '6379', 10) : 6379, + password: process.env.REDIS_PASSWORD || undefined, + db: parseInt(process.env.REDIS_DB || '0', 10), + connectTimeout: 10000, + maxRetriesPerRequest: 3, + retryStrategy: (times) => { + const delay = Math.min(Math.exp(times) * 50, 10000); + return delay; + } +}; + +const redis = new Redis(redisOptions); + +// Constants for transaction data management +const TX_EXPIRE_TIME = 60 * 60 * 24 * 30; // 30 days in seconds + +// Log handler to send logs back to main thread +function log(message) { + parentPort.postMessage({ type: 'log', data: message }); +} + +function report_error(message) { + parentPort.postMessage({ type: 'error', data: message }); +} + +function status(data) { + parentPort.postMessage({ type: 'status', data }); +} + +// Helper function to handle BigInt serialization +function safeJsonStringify(obj) { + return JSON.stringify(obj, (_, value) => + typeof value === 'bigint' ? value.toString() : value + ); +} + +// Hash transaction function +function hashTx(raw) { + return toHex(sha256(raw)).toUpperCase(); +} + +// RPC fetch functions +async function fetchLatestBlock() { + try { + const lbRes = await fetch(`${rpcUrl}/cosmos/base/tendermint/v1beta1/blocks/latest`); + if (!lbRes.ok) { + throw new Error(`Failed to fetch latest block: ${lbRes.status} ${lbRes.statusText}`); + } + const latestBlock = await lbRes.json(); + return latestBlock; + } catch (error) { + throw new Error(`Error fetching latest block: ${error.message}`); + } +} + +async function fetchTx(hash) { + try { + const lbRes = await fetch(`${rpcUrl}/cosmos/tx/v1beta1/txs/${hash}`); + if (!lbRes.ok) { + throw new Error(`Failed to fetch transaction: ${lbRes.status} ${lbRes.statusText}`); + } + const tx = await lbRes.json(); + return tx; + } catch (error) { + throw new Error(`Error fetching transaction ${hash}: ${error.message}`); + } +} + +async function fetchTxByBlock(height) { + try { + const lbRes = await fetch(`${rpcUrl}/cosmos/base/tendermint/v1beta1/blocks/${height}`); + if (!lbRes.ok) { + throw new Error(`Failed to fetch block: ${lbRes.status} ${lbRes.statusText}`); + } + const blockDetails = await lbRes.json(); + return blockDetails; + } catch (error) { + throw new Error(`Error fetching block ${height}: ${error.message}`); + } +} + +// Process and store transactions using Redis +async function processTransactions(blockData, height) { + const pipeline = redis.pipeline(); + let processedCount = 0; + let newCount = 0; + + try { + // Add this block to the processed blocks set + pipeline.sadd(`chain:${rpcName}:blocks`, height); + + // Process each transaction in the block + for (const tx of blockData.block.data.txs || []) { + processedCount++; + const txHash = hashTx(fromBase64(tx)); + + // Check if this transaction is already in Redis + const exists = await redis.exists(`tx:${rpcName}:${txHash}`); + if (exists) { + log(`Transaction ${txHash} already exists in Redis, skipping`); + continue; + } + + // Fetch detailed transaction data + const txRes = await fetchTx(txHash); + + // Parse transaction data + const decodedTx = decodeTxRaw(fromBase64(tx)); + const txData = { + status: txRes.tx_response.code, + timestamp: txRes.tx_response.timestamp, + messages: safeJsonStringify(decodedTx.body.messages), + fee: safeJsonStringify(decodedTx.authInfo.fee), + hash: txHash, + height: height.toString() // Ensure height is stored as string + }; + + // Store transaction data in Redis + // Use a hash to store transaction details + Object.entries(txData).forEach(([key, value]) => { + pipeline.hset(`tx:${rpcName}:${txHash}`, key, value); + }); + + // Set expiration for transaction data to manage Redis memory + pipeline.expire(`tx:${rpcName}:${txHash}`, TX_EXPIRE_TIME); + + // Add to the sorted set of transactions by height for fast range queries + pipeline.zadd(`chain:${rpcName}:txs`, height, txHash); + + newCount++; + } + + if (newCount > 0) { + // Update latest processed block height if this is the newest block we've seen + pipeline.set(`chain:${rpcName}:latest_height`, height); + log(`Processed block ${height}: ${newCount} new transactions out of ${processedCount} total`); + } + + // Execute the pipeline + await pipeline.exec(); + return newCount; + } catch (error) { + report_error(`Error processing transactions for block ${height}: ${error.message}`); + return 0; + } +} + +// Function to fetch historical blocks from newest to oldest +async function fetchHistoricalBlocks() { + try { + // Get the current latest block height from chain + const latestBlock = await fetchLatestBlock(); + const latestHeight = parseInt(latestBlock.block.header.height, 10); + + // Store the latest block height + await redis.set(`chain:${rpcName}:max_height`, latestHeight); + + // Find the last processed block height, or start from the latest if none found + let lastProcessedHeight = parseInt(await redis.get(`chain:${rpcName}:latest_height`) || '0', 10); + + // If we already have processed blocks, start from there minus a small overlap + // to handle any reorgs + let startHeight = lastProcessedHeight > 0 ? Math.max(1, lastProcessedHeight - 5) : latestHeight; + + // Process blocks from newest to oldest with a batch approach + log(`Starting historical processing from height ${startHeight} down to 1`); + + // Start from the determined height + let currentHeight = startHeight; + + // Only process historical blocks if there's a significant number to process + // (Otherwise let the monitor process them) + if (currentHeight <= 10) { + log(`No significant historical blocks to process for chain ${rpcName}, skipping historical processing`); + return; + } + + while (currentHeight > 0) { + const batchEnd = Math.max(1, currentHeight - batchSize + 1); + let batchProcessed = 0; + + // Process blocks in this batch + for (let height = currentHeight; height >= batchEnd; height--) { + // Check if we already processed this block + const blockProcessed = await redis.sismember(`chain:${rpcName}:blocks`, height); + if (blockProcessed) { + log(`Block ${height} already processed, skipping`); + continue; + } + + try { + const blockData = await fetchTxByBlock(height); + const txCount = await processTransactions(blockData, height); + batchProcessed += txCount; + + // Update latest_height if this is a newer block than what we've seen + // This helps keep the monitoring in sync + if (height > lastProcessedHeight) { + lastProcessedHeight = height; + await redis.set(`chain:${rpcName}:latest_height`, height); + } + + // Small delay to avoid overwhelming the RPC + await new Promise(resolve => setTimeout(resolve, 100)); + } catch (error) { + report_error(`Failed to process block ${height}: ${error.message}`); + } + } + + // Update status + status({ + chain: rpcName, + currentHeight: batchEnd, + latestHeight, + progress: ((latestHeight - batchEnd) / latestHeight * 100).toFixed(2) + '%', + batchProcessed + }); + + // Move to the next batch + currentHeight = batchEnd - 1; + + // Take a break between batches to avoid overwhelming the node + await new Promise(resolve => setTimeout(resolve, 1000)); + } + + log(`Historical processing completed for chain ${rpcName}`); + } catch (error) { + report_error(`Historical processing error: ${error.message}`); + } +} + +// Function to monitor for new blocks +async function monitorNewBlocks() { + log(`Starting new block monitor for chain ${rpcName}`); + + try { + // Get the latest height from the chain + const latestBlock = await fetchLatestBlock(); + const currentChainHeight = parseInt(latestBlock.block.header.height, 10); + + // Store the current chain height + await redis.set(`chain:${rpcName}:max_height`, currentChainHeight); + log(`Set max_height for ${rpcName} to ${currentChainHeight}`); + + // Get the last processed height from Redis + let lastProcessedHeight = parseInt(await redis.get(`chain:${rpcName}:latest_height`) || '0', 10); + log(`Block monitor initialized: Chain height=${currentChainHeight}, Last processed=${lastProcessedHeight}`); + + // Monitor interval + setInterval(async () => { + try { + // Always get fresh value from Redis to ensure we don't miss blocks + // between monitor intervals or if multiple workers are running + lastProcessedHeight = parseInt(await redis.get(`chain:${rpcName}:latest_height`) || '0', 10); + + const latestBlock = await fetchLatestBlock(); + const currentHeight = parseInt(latestBlock.block.header.height, 10); + + // Update max_height to reflect the current chain tip + await redis.set(`chain:${rpcName}:max_height`, currentHeight); + + // Check if there are new blocks to process + if (currentHeight > lastProcessedHeight) { + const blockGap = currentHeight - lastProcessedHeight; + + // If there's a large gap, log it but still process in batches + if (blockGap > 100) { + log(`Large block gap detected: ${blockGap} blocks between ${lastProcessedHeight} and ${currentHeight}`); + } else { + log(`New block(s) detected: ${currentHeight} (previous: ${lastProcessedHeight}, gap: ${blockGap})`); + } + + // Define a reasonable batch size for processing + const MONITOR_BATCH_SIZE = 10; + + // Process blocks in manageable batches to avoid timeouts + for (let batchStart = lastProcessedHeight + 1; batchStart <= currentHeight; batchStart += MONITOR_BATCH_SIZE) { + const batchEnd = Math.min(batchStart + MONITOR_BATCH_SIZE - 1, currentHeight); + log(`Processing blocks ${batchStart} to ${batchEnd}...`); + + // Process each block in the batch + for (let height = batchStart; height <= batchEnd; height++) { + try { + const blockData = await fetchTxByBlock(height); + await processTransactions(blockData, height); + + // Update after each successful block processing + lastProcessedHeight = height; + await redis.set(`chain:${rpcName}:latest_height`, height); + } catch (error) { + report_error(`Failed to process new block ${height}: ${error.message}`); + } + } + + // Report status after each batch + status({ + chain: rpcName, + currentHeight: batchEnd, + latestHeight: currentHeight, + progress: ((batchEnd / currentHeight) * 100).toFixed(2) + '%', + monitoring: true + }); + } + } + } catch (error) { + report_error(`Error monitoring for new blocks: ${error.message}`); + } + }, 10000); // Check every 10 seconds + } catch (error) { + report_error(`Failed to initialize block monitor: ${error.message}`); + } +} + +// Main worker function +async function runWorker() { + log(`Starting worker for chain ${rpcName} with RPC endpoint ${rpcUrl}`); + + try { + // First get the current chain tip + const latestBlock = await fetchLatestBlock(); + const latestHeight = parseInt(latestBlock.block.header.height, 10); + + // Update max_height + await redis.set(`chain:${rpcName}:max_height`, latestHeight); + log(`Chain ${rpcName} current height: ${latestHeight}`); + + // Check if we have a latest_height in Redis, indicating some blocks were already processed + const processedHeight = await redis.get(`chain:${rpcName}:latest_height`); + + if (processedHeight) { + log(`Found existing processed data for chain ${rpcName}, last processed height: ${processedHeight}`); + } else { + log(`No existing data found for chain ${rpcName}, will start historical processing`); + } + + // Start the monitoring for new blocks first + monitorNewBlocks(); + + // Then process historical blocks + await fetchHistoricalBlocks(); + } catch (error) { + report_error(`Worker initialization error: ${error.message}`); + } +} + +// Handle errors and cleanup +process.on('unhandledRejection', (reason) => { + report_error(`Unhandled Promise Rejection: ${reason}`); +}); + +redis.on('error', (err) => { + report_error(`Redis error: ${err.message}`); +}); + +// Clean up on exit +process.on('SIGINT', async () => { + log('Worker received SIGINT, shutting down...'); + await redis.quit(); + process.exit(0); +}); + +// Start the worker +runWorker().catch(err => report_error(`Worker failed to start: ${err.message}`)); \ No newline at end of file diff --git a/server/services/worker.js b/server/services/worker.js new file mode 100644 index 0000000000..5444d109f8 --- /dev/null +++ b/server/services/worker.js @@ -0,0 +1,204 @@ +const { Worker } = require('worker_threads'); +const path = require('path'); +const redis = require('../config/redis'); +const { getRpcEndpoints } = require('../config/rpc'); + +// Worker pool for parallel processing +class TransactionWorkerPool { + constructor() { + this.workers = new Map(); + this.rpcEndpoints = getRpcEndpoints(); + this.concurrency = parseInt(process.env.WORKER_CONCURRENCY || '2', 2); + this.batchSize = parseInt(process.env.HISTORICAL_BATCH_SIZE || '50', 10); + this.healthCheckInterval = null; + } + + /** + * Initialize workers for all configured RPC endpoints + */ + async initialize() { + if (this.rpcEndpoints.length === 0) { + console.error('No RPC endpoints configured, cannot start workers'); + return; + } + + // First, check Redis connection + try { + await this.checkRedisHealth(); + } catch (error) { + console.error('Redis health check failed during initialization:', error); + throw new Error('Failed to connect to Redis. Please check Redis configuration and ensure it is running.'); + } + + for (const rpc of this.rpcEndpoints) { + try { + // Create fresh workers for each RPC endpoint + const newWorker = new Worker(path.join(__dirname, 'transactionWorker.js'), { + workerData: { + rpcName: rpc.name, + rpcUrl: rpc.url, + batchSize: this.batchSize + } + }); + + newWorker.on('message', (message) => { + if (message.type === 'log') { + console.log(`[Worker ${rpc.name}]`, message.data); + } else if (message.type === 'error') { + console.error(`[Worker ${rpc.name}]`, message.data); + } else if (message.type === 'status') { + console.log(`[Worker ${rpc.name}] Status:`, message.data); + } + }); + + newWorker.on('error', (err) => { + console.error(`Worker for ${rpc.name} encountered an error:`, err); + this.restartWorker(rpc.name, rpc.url); + }); + + newWorker.on('exit', (code) => { + if (code !== 0) { + console.error(`Worker for ${rpc.name} exited with code ${code}`); + this.restartWorker(rpc.name, rpc.url); + } + }); + + this.workers.set(rpc.name, newWorker); + console.log(`Started worker for RPC endpoint ${rpc.name} (${rpc.url})`); + } catch (error) { + console.error(`Failed to start worker for RPC endpoint ${rpc.name}:`, error); + } + } + + // Start periodic health checks + this.startHealthChecks(); + } + + /** + * Restart a worker that has crashed or exited + */ + async restartWorker(rpcName, rpcUrl) { + console.log(`Restarting worker for RPC endpoint ${rpcName}...`); + + // Remove the old worker reference + if (this.workers.has(rpcName)) { + this.workers.delete(rpcName); + } + + // Wait before restarting to avoid rapid restart cycles + await new Promise(resolve => setTimeout(resolve, 5000)); + + try { + const newWorker = new Worker(path.join(__dirname, 'transactionWorker.js'), { + workerData: { + rpcName, + rpcUrl, + batchSize: this.batchSize + } + }); + + newWorker.on('message', (message) => { + if (message.type === 'log') { + console.log(`[Worker ${rpcName}]`, message.data); + } else if (message.type === 'error') { + console.error(`[Worker ${rpcName}]`, message.data); + } + }); + + newWorker.on('error', (err) => { + console.error(`Worker for ${rpcName} encountered an error:`, err); + this.restartWorker(rpcName, rpcUrl); + }); + + newWorker.on('exit', (code) => { + if (code !== 0) { + console.error(`Worker for ${rpcName} exited with code ${code}`); + this.restartWorker(rpcName, rpcUrl); + } + }); + + this.workers.set(rpcName, newWorker); + console.log(`Restarted worker for RPC endpoint ${rpcName}`); + } catch (error) { + console.error(`Failed to restart worker for RPC endpoint ${rpcName}:`, error); + // Try again after a longer delay + setTimeout(() => this.restartWorker(rpcName, rpcUrl), 10000); + } + } + + /** + * Check Redis health and connection + */ + async checkRedisHealth() { + try { + const ping = await redis.ping(); + if (ping !== 'PONG') { + throw new Error('Redis ping did not return PONG'); + } + + // Check memory usage + const info = await redis.info('memory'); + console.log('Redis memory info:', info); + + return true; + } catch (error) { + console.error('Redis health check failed:', error); + return false; + } + } + + /** + * Start periodic health checks for Redis + */ + startHealthChecks() { + // Clear any existing interval + if (this.healthCheckInterval) { + clearInterval(this.healthCheckInterval); + } + + // Run health check every 5 minutes + this.healthCheckInterval = setInterval(async () => { + console.log('Running Redis health check...'); + + const isHealthy = await this.checkRedisHealth(); + if (!isHealthy) { + console.error('Redis health check failed, attempting to recover...'); + // Try to flush some data if necessary + try { + // Check if we need to trim some data (optional) + const txsSize = await redis.zcard('chain:*:txs'); + console.log(`Current transactions in Redis: ${txsSize}`); + + // Log the status but don't take action automatically + } catch (error) { + console.error('Failed to check Redis data size:', error); + } + } + }, 5 * 60 * 1000); // 5 minutes + } + + /** + * Shutdown all workers + */ + async shutdown() { + console.log('Shutting down all transaction workers...'); + + // Clear health check interval + if (this.healthCheckInterval) { + clearInterval(this.healthCheckInterval); + this.healthCheckInterval = null; + } + + const promises = []; + for (const [name, worker] of this.workers.entries()) { + console.log(`Terminating worker for ${name}...`); + promises.push(worker.terminate()); + } + + await Promise.all(promises); + this.workers.clear(); + console.log('All transaction workers have been terminated'); + } +} + +module.exports = new TransactionWorkerPool(); \ No newline at end of file diff --git a/server/setup-log-rotation.sh b/server/setup-log-rotation.sh new file mode 100755 index 0000000000..3d13c4fb77 --- /dev/null +++ b/server/setup-log-rotation.sh @@ -0,0 +1,77 @@ +#!/bin/bash + +# Setup script for Docker log rotation +# Run this on the server to set up log rotation + +# Exit on any error +set -e + +echo "Setting up Docker log rotation..." + +# Step 1: Create daemon.json for Docker +echo "Creating system-wide Docker log configuration..." +DAEMON_CONFIG='{ + "log-driver": "json-file", + "log-opts": { + "max-size": "10m", + "max-file": "3", + "compress": "true" + } +}' + +# Check if we have sudo access +if command -v sudo &> /dev/null; then + echo "$DAEMON_CONFIG" | sudo tee /etc/docker/daemon.json > /dev/null + echo "Docker daemon configuration created at /etc/docker/daemon.json" + echo "You'll need to restart Docker for these changes to take effect:" + echo " sudo systemctl restart docker" +else + echo "No sudo access detected. Please manually create /etc/docker/daemon.json with the following content:" + echo "$DAEMON_CONFIG" +fi + +# Step 2: Copy cleanup script to appropriate location +echo "Installing Docker cleanup script..." +CLEANUP_SCRIPT_PATH="/usr/local/bin/docker-cleanup.sh" + +if command -v sudo &> /dev/null; then + sudo cp docker-cleanup.sh $CLEANUP_SCRIPT_PATH + sudo chmod +x $CLEANUP_SCRIPT_PATH + echo "Cleanup script installed to $CLEANUP_SCRIPT_PATH" +else + echo "No sudo access detected. Please manually copy docker-cleanup.sh to a suitable location" + echo "and make it executable with: chmod +x docker-cleanup.sh" +fi + +# Step 3: Set up cron job for weekly cleanup +echo "Setting up weekly cron job for Docker cleanup..." +CRON_JOB="0 1 * * 0 $CLEANUP_SCRIPT_PATH >> /var/log/docker-cleanup.log 2>&1" + +if command -v sudo &> /dev/null; then + # Check if crontab exists for root + if sudo crontab -l &> /dev/null; then + # Append to existing crontab + (sudo crontab -l; echo "$CRON_JOB") | sudo crontab - + else + # Create new crontab + echo "$CRON_JOB" | sudo crontab - + fi + echo "Cron job set up to run every Sunday at 1 AM" +else + echo "No sudo access detected. Please manually add the following cron job:" + echo "$CRON_JOB" + echo "You can do this by running: sudo crontab -e" +fi + +# Step 4: Verify docker-compose.yml has logging configuration +echo "Checking docker-compose.yml for logging configuration..." +if grep -q "logging:" docker-compose.yml; then + echo "Logging configuration already exists in docker-compose.yml" +else + echo "WARNING: No logging configuration found in docker-compose.yml" + echo "Please ensure you've added the logging configuration to each service in docker-compose.yml" +fi + +echo "Log rotation setup completed!" +echo "Run 'docker-compose down && docker-compose up -d' to apply changes to running containers" +echo "Note: You may need to restart Docker for system-wide changes to take effect" \ No newline at end of file diff --git a/src/App.vue b/src/App.vue index 00768e06b9..ced4d40558 100644 --- a/src/App.vue +++ b/src/App.vue @@ -2,9 +2,13 @@ import { themeChange } from 'theme-change'; import { onMounted } from 'vue'; import TxDialog from './components/TxDialog.vue'; - +import { useRouter } from 'vue-router'; +const router = useRouter(); onMounted(() => { themeChange(false); + //overrriding default route here + if (window.location.pathname.length == 1) + router.push(window.location.href.includes('beta') ? 'pocket-beta' : 'pocket-mainnet') }); diff --git a/src/components/CardParameter.vue b/src/components/CardParameter.vue index d157a5fd7c..598570cea6 100644 --- a/src/components/CardParameter.vue +++ b/src/components/CardParameter.vue @@ -29,7 +29,7 @@ function calculateValue(value: any) { function formatTitle(v: string) { if(!v) return "" - return v.replace(/_/g, " ") + return v.replace(/_/g, " ").replace('bonded', 'Staked').replace('unbonding', 'Unstaking').replace('unbonded', 'Unstaked') }