@effectstream/npm-midnight-indexer 0.3.99

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,245 @@
1
+ # Midnight Indexer
2
+
3
+ A Node.js package that downloads and runs the
4
+ [Midnight](https://midnight.network) Indexer. The indexer requires a running
5
+ Midnight Node to function properly.
6
+
7
+ ## Overview
8
+
9
+ This package provides flexible execution options for the Midnight Indexer:
10
+
11
+ - **Docker mode**: Runs the indexer in a Docker container (recommended)
12
+ - **Binary mode**: Downloads and runs the native binary locally (Linux, Windows,
13
+ macOS arm64)
14
+
15
+ ### Platform Support
16
+
17
+ | Platform | Docker | Binary |
18
+ | -------- | ------ | ------ |
19
+ | macOS | ✅ Yes | ✅ Yes |
20
+ | Linux | ✅ Yes | ✅ Yes |
21
+ | Windows | ❌ No | ✅ Yes |
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ npm install npm-midnight-indexer
27
+ ```
28
+
29
+ ## Prerequisites
30
+
31
+ ### For Docker Mode
32
+
33
+ - Docker installed and running
34
+ - `APP__INFRA__SECRET` environment variable (required)
35
+
36
+ ### For Binary Mode
37
+
38
+ - Supported platform (Linux/macOS ARM64)
39
+ - Local Midnight Node and Proof Server running on standard ports. But they can
40
+ be set using env variables
41
+ - `APP__INFRA__SECRET` environment variable (required)
42
+
43
+ ## Usage
44
+
45
+ ### Command Line Options
46
+
47
+ ```bash
48
+ node index.js [options] [args...]
49
+ ```
50
+
51
+ **Options:**
52
+
53
+ - `--docker` - Force Docker execution
54
+ - `--binary` - Force binary execution (available on Linux, Windows, macOS arm64)
55
+ - `--help` - Show help information
56
+
57
+ **Examples:**
58
+
59
+ ```bash
60
+ # Force Docker usage
61
+ APP__INFRA__SECRET=mysecret node index.js --docker
62
+
63
+ # Force binary usage (Linux/Windows/macOS arm64)
64
+ APP__INFRA__SECRET=mysecret node index.js --binary
65
+
66
+ # Interactive mode - prompts for Docker vs binary choice
67
+ APP__INFRA__SECRET=mysecret node index.js
68
+
69
+ # Show help
70
+ node index.js --help
71
+ ```
72
+
73
+ ## Environment Variables
74
+
75
+ The indexer supports several environment variables with different defaults based
76
+ on execution mode:
77
+
78
+ | Variable | Required | Docker Default | Binary Default | Description |
79
+ | ------------------------------- | -------- | -------------------------- | ----------------------- | ------------------------------ |
80
+ | `APP__INFRA__SECRET` | Yes | - | - | Secret key for the application |
81
+ | `LEDGER_NETWORK_ID` | No | `Undeployed` | `Undeployed` | Ledger network identifier |
82
+ | `SUBSTRATE_NODE_WS_URL` | No | `ws://node:9944` | `ws://localhost:9944` | Substrate node WebSocket URL |
83
+ | `FEATURES_WALLET_ENABLED` | No | `true` | `true` | Enable wallet features |
84
+ | `APP__INFRA__PROOF_SERVER__URL` | No | `http://proof-server:6300` | `http://localhost:6300` | Proof server URL |
85
+ | `APP__INFRA__NODE__URL` | No | `ws://node:9944` | `ws://localhost:9944` | Node WebSocket URL |
86
+
87
+ ### Environment Variable Examples
88
+
89
+ ```bash
90
+ # Minimal Docker setup
91
+ APP__INFRA__SECRET=A5F07FCAC914A181EC0998CCCA68312DA5F07FCAC914A181EC0998CCCA68312D \
92
+ node index.js --docker
93
+
94
+ # Custom configuration
95
+ APP__INFRA__SECRET=mysecret \
96
+ LEDGER_NETWORK_ID=CustomNetwork \
97
+ SUBSTRATE_NODE_WS_URL=ws://localhost:9944 \
98
+ APP__INFRA__PROOF_SERVER__URL=http://localhost:6300 \
99
+ node index.js --docker
100
+
101
+ # Binary mode (uses localhost defaults automatically)
102
+ APP__INFRA__SECRET=mysecret node index.js --binary
103
+ ```
104
+
105
+ ## Docker Mode Details
106
+
107
+ When using Docker mode, the indexer:
108
+
109
+ 1. **Pulls the latest image**: `midnightntwrk/indexer-standalone`
110
+ 2. **Maps port 8088**: Container port 8088 → Host port 8088
111
+ 3. **Uses container networking**: Defaults work with Docker Compose setups
112
+ 4. **Auto-cleanup**: Removes existing containers before starting new ones
113
+
114
+ ### Docker Command Generated
115
+
116
+ The package generates Docker commands similar to:
117
+
118
+ ```bash
119
+ docker run --rm --name midnight-local-indexer \
120
+ -p 8088:8088 \
121
+ -e LEDGER_NETWORK_ID=Undeployed \
122
+ -e SUBSTRATE_NODE_WS_URL=ws://node:9944 \
123
+ -e APP__INFRA__SECRET=A5F07FCAC914A181EC0998CCCA68312DA5F07FCAC914A181EC0998CCCA68312D \
124
+ -e FEATURES_WALLET_ENABLED=true \
125
+ -e APP__INFRA__PROOF_SERVER__URL=http://proof-server:6300 \
126
+ -e APP__INFRA__NODE__URL=ws://node:9944 \
127
+ midnightntwrk/indexer-standalone
128
+ ```
129
+
130
+ ## Binary Mode Details
131
+
132
+ When using binary mode, the indexer:
133
+
134
+ 1. **Downloads platform-specific binary** if not already present
135
+ 2. **Uses localhost URLs** for all service connections
136
+ 3. **Runs natively** on the host system
137
+
138
+ ## Path Resolution
139
+
140
+ The indexer relies on two user-supplied file paths and **both are interpreted
141
+ relative to the process’s current working directory (CWD)** when they are not
142
+ absolute:
143
+
144
+ | Configuration location | Purpose | If relative, resolved against |
145
+ | ---------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------- |
146
+ | `CONFIG_FILE` environment variable | Location of the YAML config file | The directory you launch `node index.js …` from (or the Docker `WORKDIR`) |
147
+ | `infra.storage.cnn_url` in YAML | SQLite database used by the standalone build | The CWD at runtime of the indexer process |
148
+
149
+ ### Practical tips
150
+
151
+ 1. Prefer absolute paths when you need to be explicit.
152
+ 2. In **binary mode** this package sets the CWD to the bundled
153
+ `indexer-standalone` folder, so a default `cnn_url: "./indexer.sqlite"` ends
154
+ up next to the binary.
155
+ 3. In **Docker mode** the image’s `WORKDIR` is `/opt/indexer-standalone`.
156
+ Bind-mount volumes accordingly if you want the database file somewhere else.
157
+
158
+ ---
159
+
160
+ ### Supported Binary Platforms
161
+
162
+ - Linux ARM64 (`linux-arm64`)
163
+ - Linux AMD64 (`linux-amd64`)
164
+ - macOS ARM64 (`macos-arm64`)
165
+
166
+ ## Troubleshooting
167
+
168
+ ### Common Issues
169
+
170
+ **"Docker is not installed or not available"**
171
+
172
+ - Install Docker Desktop or Docker Engine
173
+ - Ensure Docker is running
174
+ - Check Docker is accessible from command line: `docker --version`
175
+
176
+ **"APP__INFRA__SECRET environment variable is required"**
177
+
178
+ - Set the secret: `export APP__INFRA__SECRET=your_secret_here`
179
+ - Or pass inline: `APP__INFRA__SECRET=your_secret node index.js --docker`
180
+ - Required for both Docker and binary execution modes
181
+
182
+ **"Binary execution is only supported on macOS ARM64"**
183
+
184
+ - Use `--docker` flag or run without flags to use Docker automatically
185
+ - Install Docker if not already installed
186
+
187
+ **"Failed to start midnight-indexer"**
188
+
189
+ - Check if ports 8088, 6300, 9944 are available
190
+ - Verify Midnight Node is running and accessible
191
+ - Check environment variables are correctly set
192
+
193
+ ### Logs and Debugging
194
+
195
+ The indexer outputs detailed logs including:
196
+
197
+ - Download progress for binaries
198
+ - Docker pull progress
199
+ - Process IDs and status
200
+ - Error messages with suggestions
201
+
202
+ ## Development
203
+
204
+ ### Package Structure
205
+
206
+ ```
207
+ npm-midnight-indexer/
208
+ ├── index.js # Main entry point
209
+ ├── binary.js # Binary download logic
210
+ ├── docker.js # Docker execution logic
211
+ ├── run_midnight_indexer.js # Binary execution logic
212
+ ├── package.json # Package configuration
213
+ └── README.md # This file
214
+ ```
215
+
216
+ ### Testing
217
+
218
+ ```bash
219
+ # Test Docker detection
220
+ node -e "const { checkIfDockerExists } = require('./docker.js'); checkIfDockerExists().then(console.log)"
221
+
222
+ # Test help
223
+ node index.js --help
224
+
225
+ # Test platform detection
226
+ node -e "console.log('Platform:', require('os').platform())"
227
+ ```
228
+
229
+ ## License
230
+
231
+ ISC
232
+
233
+ ## Support
234
+
235
+ For issues related to:
236
+
237
+ - **This package**: Open an issue in the repository
238
+ - **Midnight Protocol**: Visit [midnight.network](https://midnight.network)
239
+ - **Docker**: Check [Docker documentation](https://docs.docker.com/)
240
+
241
+ ## Related Links
242
+
243
+ - [Midnight Network](https://midnight.network)
244
+ - [Docker Hub - Midnight Indexer](https://hub.docker.com/r/midnightntwrk/indexer-standalone)
245
+ - [Node.js](https://nodejs.org/)
package/binary.js ADDED
@@ -0,0 +1,102 @@
1
+ const os = require("os");
2
+ const fs = require("fs");
3
+ const axios = require("axios");
4
+ const extract = require("extract-zip");
5
+ const path = require("path");
6
+
7
+ const CURRENT_BINARY_VERSION = "v2.1.4";
8
+
9
+ /*
10
+ @returns {string} The platform and architecture of the current machine. Example: "linux-amd64"
11
+ */
12
+ function getPlatform() {
13
+ const platform = os.platform();
14
+ const arch = os.arch();
15
+
16
+ if (platform === "darwin") {
17
+ // For macOS, return the full platform-arch combination
18
+ if (arch === "x64") {
19
+ return "macos-amd64"; // Will not be in supportedPlatforms, so will fall back to Docker
20
+ } else {
21
+ return `macos-${arch}`;
22
+ }
23
+ } else {
24
+ // For Linux and other platforms, only arch is needed
25
+ if (arch === "x64") {
26
+ return `${platform}-amd64`;
27
+ }
28
+ return `${platform}-${arch}`;
29
+ }
30
+ }
31
+
32
+ /*
33
+ @returns {string} The URL to download the binary for the current platform.
34
+ */
35
+ function getBinaryUrl() {
36
+ const platform = getPlatform();
37
+ const supportedPlatforms = require("./package.json").supportedPlatforms;
38
+ // Check if platform is supported
39
+ if (!supportedPlatforms.includes(platform)) {
40
+ throw new Error(`Unsupported platform: ${platform}`);
41
+ }
42
+ return `https://paima-midnight.nyc3.cdn.digitaloceanspaces.com/binaries/indexer-standalone-${platform}-${CURRENT_BINARY_VERSION}.zip`;
43
+ }
44
+
45
+ /*
46
+ @returns {Promise<void>} Downloads and saves the binary for the current platform.
47
+ */
48
+ async function downloadAndSaveBinary() {
49
+ const url = getBinaryUrl();
50
+ try {
51
+ console.error(`Downloading... ${url}`);
52
+ const response = await axios.get(url, { responseType: "stream" });
53
+ const writer = fs.createWriteStream(
54
+ path.join(__dirname, "indexer-standalone.zip"),
55
+ );
56
+
57
+ response.data.pipe(writer);
58
+
59
+ return new Promise((resolve, reject) => {
60
+ writer.on("finish", resolve);
61
+ writer.on("error", reject);
62
+ });
63
+ } catch (error) {
64
+ console.error("Error downloading binary:", error);
65
+ throw error;
66
+ }
67
+ }
68
+
69
+ /*
70
+ @returns {Promise<void>} Unzips the binary for the current platform.
71
+ */
72
+ async function unzipBinary() {
73
+ const dir = path.join(__dirname, "indexer-standalone");
74
+ if (!fs.existsSync(dir)) {
75
+ fs.mkdirSync(dir, { recursive: true });
76
+ }
77
+ await extract(path.join(__dirname, "indexer-standalone.zip"), { dir });
78
+ fs.unlinkSync(path.join(__dirname, "indexer-standalone.zip"));
79
+
80
+ const platform = getPlatform();
81
+ const parts = platform.split("-");
82
+ if (parts[0] === "linux") {
83
+ fs.chmodSync(
84
+ path.join(
85
+ __dirname,
86
+ "indexer-standalone",
87
+ `indexer-standalone-${platform}`,
88
+ ),
89
+ 0o755,
90
+ );
91
+ }
92
+ }
93
+
94
+ async function binary() {
95
+ await downloadAndSaveBinary();
96
+ await unzipBinary();
97
+ }
98
+
99
+ module.exports = {
100
+ binary,
101
+ getPlatform,
102
+ };
package/docker.js ADDED
@@ -0,0 +1,164 @@
1
+ const { spawn, exec } = require('child_process');
2
+ const { promisify } = require('util');
3
+ const execAsync = promisify(exec);
4
+
5
+ /**
6
+ * Checks if Docker is installed and available on the system
7
+ * @returns {Promise<boolean>} True if Docker is available, false otherwise
8
+ */
9
+ async function checkIfDockerExists() {
10
+ try {
11
+ await execAsync('docker --version');
12
+ return true;
13
+ } catch (error) {
14
+ return false;
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Pulls the latest midnight indexer Docker image
20
+ * @returns {Promise<void>}
21
+ */
22
+ async function pullDockerImage() {
23
+ const imageName = 'midnightntwrk/indexer-standalone';
24
+
25
+ console.log(`Pulling Docker image: ${imageName}`);
26
+
27
+ return new Promise((resolve, reject) => {
28
+ const childProcess = spawn('docker', ['pull', imageName], {
29
+ stdio: 'inherit'
30
+ });
31
+
32
+ childProcess.on('exit', (code) => {
33
+ if (code === 0) {
34
+ console.log('Docker image pulled successfully');
35
+ resolve();
36
+ } else {
37
+ reject(new Error(`Docker pull failed with exit code ${code}`));
38
+ }
39
+ });
40
+
41
+ childProcess.on('error', (error) => {
42
+ reject(new Error(`Failed to pull Docker image: ${error.message}`));
43
+ });
44
+ });
45
+ }
46
+
47
+ /**
48
+ * Runs the midnight indexer Docker container
49
+ * @param {Object} env - Environment variables to pass to the container
50
+ * @param {Array} args - Arguments to pass to the container
51
+ * @returns {ChildProcess} The spawned Docker process
52
+ */
53
+ function runDockerContainer(env = process.env, args = []) {
54
+ const imageName = 'midnightntwrk/indexer-standalone';
55
+ const containerName = 'midnight-local-indexer';
56
+
57
+ // Stop and remove existing container if it exists
58
+ try {
59
+ require('child_process').execSync(`docker stop ${containerName} 2>/dev/null || true`, { stdio: 'ignore' });
60
+ require('child_process').execSync(`docker rm ${containerName} 2>/dev/null || true`, { stdio: 'ignore' });
61
+ } catch (error) {
62
+ // Ignore errors - container might not exist
63
+ }
64
+
65
+ // Build Docker run command arguments
66
+ const dockerArgs = ['run'];
67
+
68
+ // Add container name (remove existing container if it exists)
69
+ dockerArgs.push('--rm'); // Automatically remove container when it exits
70
+ dockerArgs.push('--name', containerName);
71
+
72
+ // Intelligent port mapping - default to 8088:8088 if not specified
73
+ const defaultPort = '8088:8088';
74
+ let portMapping = defaultPort;
75
+
76
+ // Check if port is specified in args
77
+ const portArgIndex = args.findIndex(arg => arg.includes('--port') || arg.includes('-p'));
78
+ if (portArgIndex !== -1 && args[portArgIndex + 1]) {
79
+ const specifiedPort = args[portArgIndex + 1];
80
+ portMapping = `${specifiedPort}:${specifiedPort}`;
81
+ // Remove port args from the args array as they're handled by Docker
82
+ args.splice(portArgIndex, 2);
83
+ }
84
+
85
+ dockerArgs.push('-p', portMapping);
86
+
87
+ // Add environment variables
88
+ const requiredEnvVars = [
89
+ 'LEDGER_NETWORK_ID',
90
+ 'SUBSTRATE_NODE_WS_URL',
91
+ 'APP__INFRA__SECRET',
92
+ 'FEATURES_WALLET_ENABLED',
93
+ 'APP__INFRA__PROOF_SERVER__URL',
94
+ 'APP__INFRA__NODE__URL'
95
+ ];
96
+
97
+ // Set default values for Docker environment variables (using container network names)
98
+ const envDefaults = {
99
+ 'LEDGER_NETWORK_ID': env.LEDGER_NETWORK_ID || 'Undeployed',
100
+ 'SUBSTRATE_NODE_WS_URL': env.SUBSTRATE_NODE_WS_URL || 'ws://node:9944',
101
+ 'APP__INFRA__SECRET': env.APP__INFRA__SECRET,
102
+ 'FEATURES_WALLET_ENABLED': env.FEATURES_WALLET_ENABLED || 'true',
103
+ 'APP__INFRA__PROOF_SERVER__URL': env.APP__INFRA__PROOF_SERVER__URL || 'http://proof-server:6300',
104
+ 'APP__INFRA__NODE__URL': env.APP__INFRA__NODE__URL || 'ws://node:9944'
105
+ };
106
+
107
+ // Validate that SECRET is provided
108
+ if (!envDefaults['APP__INFRA__SECRET']) {
109
+ throw new Error('APP__INFRA__SECRET environment variable is required');
110
+ }
111
+
112
+ // Add environment variables to Docker command
113
+ Object.entries(envDefaults).forEach(([key, value]) => {
114
+ if (value) {
115
+ dockerArgs.push('-e', `${key}=${value}`);
116
+ }
117
+ });
118
+
119
+ // Add any additional environment variables from env object
120
+ Object.entries(env).forEach(([key, value]) => {
121
+ if (!requiredEnvVars.includes(key) && value) {
122
+ dockerArgs.push('-e', `${key}=${value}`);
123
+ }
124
+ });
125
+
126
+ // Add the image name
127
+ dockerArgs.push(imageName);
128
+
129
+ // Add any remaining arguments
130
+ if (args.length > 0) {
131
+ dockerArgs.push(...args);
132
+ }
133
+
134
+ console.log(`Starting Docker container: docker ${dockerArgs.join(' ')}`);
135
+
136
+ const childProcess = spawn('docker', dockerArgs, {
137
+ stdio: 'inherit'
138
+ });
139
+
140
+ childProcess.on('spawn', () => {
141
+ console.log(`Docker container started with PID: ${childProcess.pid}`);
142
+ console.log(`Container accessible on port: ${portMapping.split(':')[0]}`);
143
+ });
144
+
145
+ childProcess.on('error', (error) => {
146
+ console.error('Failed to start Docker container:', error);
147
+ });
148
+
149
+ childProcess.on('exit', (code, signal) => {
150
+ if (code !== null) {
151
+ console.log(`Docker container exited with code: ${code}`);
152
+ } else {
153
+ console.log(`Docker container terminated by signal: ${signal}`);
154
+ }
155
+ });
156
+
157
+ return childProcess;
158
+ }
159
+
160
+ module.exports = {
161
+ checkIfDockerExists,
162
+ pullDockerImage,
163
+ runDockerContainer
164
+ };
package/index.js ADDED
@@ -0,0 +1,316 @@
1
+ const { binary, getPlatform } = require("./binary");
2
+ const { runMidnightIndexer } = require("./run_midnight_indexer");
3
+ const { checkIfDockerExists, pullDockerImage, runDockerContainer } = require(
4
+ "./docker",
5
+ );
6
+ const fs = require("fs");
7
+ const path = require("path");
8
+ const readline = require("readline");
9
+ const os = require("os");
10
+
11
+ function checkIfBinaryExists() {
12
+ const platform = getPlatform();
13
+ const parts = platform.split("-");
14
+ const binaryName = `indexer-standalone-${platform}`;
15
+ return fs.existsSync(
16
+ path.join(__dirname, "indexer-standalone", binaryName),
17
+ );
18
+ }
19
+
20
+ /**
21
+ * Checks if the current platform supports binary execution
22
+ * @returns {boolean} True if binary execution is supported
23
+ */
24
+ function isBinarySupported() {
25
+ const platform = os.platform();
26
+ const arch = os.arch();
27
+
28
+ // Check if the current platform-arch combination is supported
29
+ const supportedPlatforms = require("./package.json").supportedPlatforms;
30
+
31
+ let platformString;
32
+ if (platform === "darwin") {
33
+ platformString = arch === "x64" ? "macos-amd64" : `macos-${arch}`;
34
+ } else {
35
+ platformString = arch === "x64"
36
+ ? `${platform}-amd64`
37
+ : `${platform}-${arch}`;
38
+ }
39
+
40
+ return supportedPlatforms.includes(platformString);
41
+ }
42
+
43
+ /**
44
+ * Prompts the user to choose between Docker and binary execution
45
+ * @returns {Promise<boolean>} True if user chooses Docker, false for binary
46
+ */
47
+ function promptUserForDockerChoice() {
48
+ const rl = readline.createInterface({
49
+ input: process.stdin,
50
+ output: process.stdout,
51
+ });
52
+
53
+ return new Promise((resolve) => {
54
+ rl.question(
55
+ "Docker is available. Would you like to use Docker instead of downloading the binary? (y/n): ",
56
+ (answer) => {
57
+ rl.close();
58
+ resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
59
+ },
60
+ );
61
+ });
62
+ }
63
+
64
+ /**
65
+ * Shows usage information
66
+ */
67
+ function showUsage() {
68
+ console.log(`
69
+ Usage: node index.js [options] [args...]
70
+
71
+ Options:
72
+ --docker Force use of Docker container
73
+ --binary Force use of binary execution (supports Linux and macOS arm64)
74
+ --help Show this help message
75
+
76
+ Environment Variables:
77
+ APP__INFRA__SECRET Required: Secret key for the application
78
+ LEDGER_NETWORK_ID Optional: Ledger network ID (default: Undeployed)
79
+ SUBSTRATE_NODE_WS_URL Optional: Substrate node WebSocket URL
80
+ (Docker default: ws://node:9944, Binary default: ws://localhost:9944)
81
+ FEATURES_WALLET_ENABLED Optional: Enable wallet features (default: true)
82
+ APP__INFRA__PROOF_SERVER__URL Optional: Proof server URL
83
+ (Docker default: http://proof-server:6300, Binary default: http://localhost:6300)
84
+ APP__INFRA__NODE__URL Optional: Node URL
85
+ (Docker default: ws://node:9944, Binary default: ws://localhost:9944)
86
+
87
+ Note: macOS Intel users will automatically use Docker. macOS arm64 supports both binary and Docker execution.
88
+
89
+ Examples:
90
+ APP__INFRA__SECRET=mysecret node index.js --docker # Use Docker
91
+ APP__INFRA__SECRET=mysecret node index.js --binary # Use binary (if supported)
92
+ APP__INFRA__SECRET=mysecret node index.js # Interactive mode (or auto-Docker on unsupported platforms)
93
+ node index.js --help # Show this help
94
+ `);
95
+ }
96
+
97
+ /**
98
+ * Parses command line arguments to extract flags and remaining args
99
+ * @param {Array} args - Command line arguments
100
+ * @returns {Object} Object containing useDocker, useBinary flags and remaining args
101
+ */
102
+ function parseFlags(args) {
103
+ const flags = {
104
+ useDocker: false,
105
+ useBinary: false,
106
+ showHelp: false,
107
+ remainingArgs: [],
108
+ };
109
+
110
+ for (let i = 0; i < args.length; i++) {
111
+ if (args[i] === "--docker") {
112
+ flags.useDocker = true;
113
+ } else if (args[i] === "--binary") {
114
+ flags.useBinary = true;
115
+ } else if (args[i] === "--help" || args[i] === "-h") {
116
+ flags.showHelp = true;
117
+ } else {
118
+ flags.remainingArgs.push(args[i]);
119
+ }
120
+ }
121
+
122
+ return flags;
123
+ }
124
+
125
+ async function runWithDocker(env, args) {
126
+ console.log("Using Docker to run midnight indexer...");
127
+
128
+ try {
129
+ // Pull the latest image
130
+ await pullDockerImage();
131
+
132
+ // Run the container
133
+ return runDockerContainer(env, args);
134
+ } catch (error) {
135
+ console.error("Failed to run with Docker:", error.message);
136
+ console.log("Falling back to binary execution...");
137
+ return runWithBinary(env, args);
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Sets appropriate defaults for binary execution (localhost-based)
143
+ * @param {Object} env - Original environment variables
144
+ * @returns {Object} Environment variables with localhost defaults
145
+ */
146
+ function setBinaryDefaults(env) {
147
+ const binaryEnv = { ...env };
148
+
149
+ // Set localhost-based defaults for binary execution
150
+ if (!binaryEnv.LEDGER_NETWORK_ID) {
151
+ binaryEnv.LEDGER_NETWORK_ID = "Undeployed";
152
+ }
153
+ if (!binaryEnv.SUBSTRATE_NODE_WS_URL) {
154
+ binaryEnv.SUBSTRATE_NODE_WS_URL = "ws://localhost:9944";
155
+ }
156
+ if (!binaryEnv.FEATURES_WALLET_ENABLED) {
157
+ binaryEnv.FEATURES_WALLET_ENABLED = "true";
158
+ }
159
+ if (!binaryEnv.APP__INFRA__PROOF_SERVER__URL) {
160
+ binaryEnv.APP__INFRA__PROOF_SERVER__URL = "http://localhost:6300";
161
+ }
162
+ if (!binaryEnv.APP__INFRA__NODE__URL) {
163
+ binaryEnv.APP__INFRA__NODE__URL = "ws://localhost:9944";
164
+ }
165
+
166
+ return binaryEnv;
167
+ }
168
+
169
+ async function runWithBinary(env, args) {
170
+ console.log("Using binary to run midnight indexer...");
171
+
172
+ // Check for required environment variable
173
+ if (!env.APP__INFRA__SECRET) {
174
+ console.error("Error: APP__INFRA__SECRET environment variable is required");
175
+ console.log("Please set APP__INFRA__SECRET=<your_secret> and run again");
176
+ process.exit(1);
177
+ }
178
+
179
+ if (!checkIfBinaryExists()) {
180
+ console.log("Binary not found, downloading...");
181
+ await binary();
182
+ }
183
+
184
+ // Set appropriate localhost defaults for binary execution
185
+ const binaryEnv = setBinaryDefaults(env);
186
+
187
+ return runMidnightIndexer(binaryEnv, args);
188
+ }
189
+
190
+ async function main(args) {
191
+ const flags = parseFlags(args);
192
+ const env = process.env;
193
+
194
+ // Show help if requested
195
+ if (flags.showHelp) {
196
+ showUsage();
197
+ process.exit(0);
198
+ }
199
+
200
+ // If both flags are provided, show error
201
+ if (flags.useDocker && flags.useBinary) {
202
+ console.error(
203
+ "Error: Cannot use both --docker and --binary flags simultaneously",
204
+ );
205
+ process.exit(1);
206
+ }
207
+
208
+ // If --docker flag is explicitly provided
209
+ if (flags.useDocker) {
210
+ const dockerAvailable = await checkIfDockerExists();
211
+ if (!dockerAvailable) {
212
+ console.error("Error: Docker is not installed or not available");
213
+ console.log("Please install Docker or use the --binary flag");
214
+ process.exit(1);
215
+ }
216
+
217
+ // Check for required environment variable
218
+ if (!env.APP__INFRA__SECRET) {
219
+ console.error(
220
+ "Error: APP__INFRA__SECRET environment variable is required",
221
+ );
222
+ console.log(
223
+ "Please set APP__INFRA__SECRET=<your_secret> when running with --docker",
224
+ );
225
+ process.exit(1);
226
+ }
227
+
228
+ return runWithDocker(env, flags.remainingArgs);
229
+ }
230
+
231
+ // If --binary flag is explicitly provided
232
+ if (flags.useBinary) {
233
+ if (!isBinarySupported()) {
234
+ console.error(
235
+ "Error: Binary execution is not supported on this platform for: " +
236
+ os.platform() +
237
+ " " +
238
+ os.arch(),
239
+ );
240
+ console.log(
241
+ "Please use --docker flag instead, or run without flags to use Docker automatically",
242
+ );
243
+ process.exit(1);
244
+ }
245
+ return runWithBinary(env, flags.remainingArgs);
246
+ }
247
+
248
+ // No explicit flag provided - determine best execution method
249
+ const dockerAvailable = await checkIfDockerExists();
250
+ const binarySupported = isBinarySupported();
251
+
252
+ // If binary is not supported on this platform, try Docker
253
+ if (!binarySupported) {
254
+ if (!dockerAvailable) {
255
+ console.error(
256
+ "Error: Binary execution is not supported on this platform and Docker is not installed or available. For: " +
257
+ os.platform() +
258
+ " " +
259
+ os.arch(),
260
+ );
261
+ console.log(
262
+ "Please install Docker or ensure your platform is supported for binary execution",
263
+ );
264
+ process.exit(1);
265
+ }
266
+ console.log(
267
+ "Binary execution not supported on this platform - using Docker",
268
+ );
269
+
270
+ // Check for required environment variable
271
+ if (!env.APP__INFRA__SECRET) {
272
+ console.error(
273
+ "Error: APP__INFRA__SECRET environment variable is required",
274
+ );
275
+ console.log("Please set APP__INFRA__SECRET=<your_secret> and run again");
276
+ process.exit(1);
277
+ }
278
+
279
+ return runWithDocker(env, flags.remainingArgs);
280
+ }
281
+
282
+ // On supported platforms, prompt user if Docker is available
283
+ if (dockerAvailable) {
284
+ const useDocker = await promptUserForDockerChoice();
285
+ if (useDocker) {
286
+ // Check for required environment variable
287
+ if (!env.APP__INFRA__SECRET) {
288
+ console.error(
289
+ "Error: APP__INFRA__SECRET environment variable is required",
290
+ );
291
+ console.log(
292
+ "Please set APP__INFRA__SECRET=<your_secret> and run again",
293
+ );
294
+ process.exit(1);
295
+ }
296
+ return runWithDocker(env, flags.remainingArgs);
297
+ }
298
+ }
299
+
300
+ // Default to binary execution (only on supported platforms)
301
+ return runWithBinary(env, flags.remainingArgs);
302
+ }
303
+
304
+ // Handle unhandled promise rejections
305
+ process.on("unhandledRejection", (error) => {
306
+ console.error("Unhandled promise rejection:", error);
307
+ process.exit(1);
308
+ });
309
+
310
+ // Handle SIGINT (Ctrl+C) gracefully
311
+ process.on("SIGINT", () => {
312
+ console.log("\nShutting down gracefully...");
313
+ process.exit(0);
314
+ });
315
+
316
+ main(process.argv.slice(2));
@@ -0,0 +1,44 @@
1
+ run_migrations: true
2
+ network_id: &network_id "Undeployed"
3
+
4
+ chain_indexer_application:
5
+ network_id: *network_id
6
+ blocks_buffer: 60
7
+ save_zswap_state_after: 1000
8
+ caught_up_max_distance: 60
9
+ caught_up_leeway: 30
10
+
11
+ wallet_indexer_application:
12
+ network_id: *network_id
13
+ active_wallets_repeat_delay: "100ms"
14
+ active_wallets_ttl: "30m"
15
+ transaction_batch_size: 10
16
+ # Number of cores by default.
17
+ # parallelism:
18
+
19
+ infra:
20
+ node:
21
+ url: "ws://localhost:9944"
22
+ reconnect_max_delay: "10s" # 10ms, 100ms, 1s, 10s
23
+ reconnect_max_attempts: 30 # Roughly 5m
24
+
25
+ storage:
26
+ cnn_url: "./data/indexer.sqlite"
27
+
28
+ api:
29
+ address: "127.0.0.1"
30
+ port: 8088
31
+ request_body_limit: "1MiB"
32
+ max_complexity: 200
33
+ max_depth: 15
34
+ network_id: *network_id
35
+
36
+ telemetry:
37
+ tracing:
38
+ enabled: false
39
+ service_name: "indexer"
40
+ otlp_exporter_endpoint: "http://localhost:4317"
41
+ metrics:
42
+ enabled: false
43
+ address: "0.0.0.0"
44
+ port: 9000
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@effectstream/npm-midnight-indexer",
3
+ "version": "0.3.99",
4
+ "description": "Downloads and runs the Midnight Indexer. It needs a running Midnight Node",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "bin": {
10
+ "npm-midnight-indexer": "index.js"
11
+ },
12
+ "author": "Paima Studios",
13
+ "license": "ISC",
14
+ "supportedPlatforms": [
15
+ "linux-arm64",
16
+ "linux-amd64",
17
+ "macos-arm64"
18
+ ],
19
+ "dependencies": {
20
+ "axios": "^1.10.0",
21
+ "axios-proxy-builder": "^0.1.2",
22
+ "extract-zip": "^2.0.1",
23
+ "js-yaml": "^4.1.0"
24
+ }
25
+ }
@@ -0,0 +1,160 @@
1
+ const { spawn } = require("child_process");
2
+ const path = require("path");
3
+ const fs = require("fs");
4
+ const yaml = require("js-yaml");
5
+ const { getPlatform } = require("./binary");
6
+ /**
7
+ * Resolves the SQLite database path using the midnight-indexer configuration rules
8
+ * @param {Object} env - Environment variables
9
+ * @param {string} workingDir - The working directory where the indexer runs
10
+ * @returns {string|null} The resolved SQLite database path, or null if not found
11
+ */
12
+ function resolveSqlitePath(env, workingDir) {
13
+ // First check the APP__INFRA__STORAGE__CNN_URL environment variable
14
+ const envCnnUrl = env.APP__INFRA__STORAGE__CNN_URL;
15
+ if (envCnnUrl) {
16
+ console.log(`Found SQLite path from environment variable: ${envCnnUrl}`);
17
+ return envCnnUrl;
18
+ }
19
+
20
+ // Determine config file path using the configuration resolution rules
21
+ let configPath;
22
+ if (env.CONFIG_FILE) {
23
+ configPath = path.isAbsolute(env.CONFIG_FILE)
24
+ ? env.CONFIG_FILE
25
+ : path.resolve(workingDir, env.CONFIG_FILE);
26
+ } else {
27
+ // Fall back to config.yaml in the current working directory
28
+ configPath = path.join(workingDir, "config.yaml");
29
+ }
30
+
31
+ console.log(`Looking for config file at: ${configPath}`);
32
+
33
+ // Check if config file exists
34
+ if (!fs.existsSync(configPath)) {
35
+ console.warn(`Config file not found at: ${configPath}`);
36
+ return null;
37
+ }
38
+
39
+ try {
40
+ // Parse the YAML config file
41
+ const configContent = fs.readFileSync(configPath, "utf8");
42
+ const config = yaml.load(configContent);
43
+
44
+ // Extract the cnn_url from infra.storage
45
+ const cnnUrl = config?.infra?.storage?.cnn_url;
46
+ if (!cnnUrl) {
47
+ console.warn("No cnn_url found in config file under infra.storage");
48
+ return null;
49
+ }
50
+
51
+ console.log(`Found SQLite path from config file: ${cnnUrl}`);
52
+
53
+ // If the path is relative, resolve it against the binary working directory (indexer-standalone)
54
+ if (!path.isAbsolute(cnnUrl)) {
55
+ const resolvedPath = path.resolve(workingDir, cnnUrl);
56
+ console.log(`Resolved relative path to: ${resolvedPath}`);
57
+ return resolvedPath;
58
+ }
59
+
60
+ return cnnUrl;
61
+ } catch (error) {
62
+ console.error(`Failed to parse config file: ${error.message}`);
63
+ return null;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Handles the --clean flag by deleting the SQLite database file
69
+ * @param {Object} env - Environment variables
70
+ * @param {string} workingDir - The working directory where the indexer runs
71
+ */
72
+ function handleCleanFlag(env, workingDir) {
73
+ console.log("Processing --clean flag...");
74
+
75
+ const sqlitePath = resolveSqlitePath(env, workingDir);
76
+
77
+ if (!sqlitePath) {
78
+ console.warn("Could not resolve SQLite database path. Skipping cleanup.");
79
+ return;
80
+ }
81
+
82
+ // Handle sqlite:// URLs and extract the file path
83
+ let filePath = sqlitePath;
84
+ if (sqlitePath.startsWith("sqlite://")) {
85
+ filePath = sqlitePath.replace("sqlite://", "");
86
+ } else if (sqlitePath.startsWith("sqlite:///")) {
87
+ filePath = sqlitePath.replace("sqlite:///", "/");
88
+ }
89
+
90
+ console.log(`Attempting to clean SQLite database at: ${filePath}`);
91
+
92
+ if (fs.existsSync(filePath)) {
93
+ try {
94
+ fs.unlinkSync(filePath);
95
+ console.log(`Successfully deleted SQLite database: ${filePath}`);
96
+ } catch (error) {
97
+ console.error(`Failed to delete SQLite database: ${error.message}`);
98
+ }
99
+ } else {
100
+ console.log(
101
+ `SQLite database does not exist (will be created fresh): ${filePath}`,
102
+ );
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Executes the midnight-indexer binary as a child process
108
+ * @param {Object} env - Environment variables to pass to the child process
109
+ * @param {Array} args - Optional arguments to pass to the binary
110
+ * @returns {ChildProcess} The spawned child process
111
+ */
112
+ function runMidnightIndexer(env = process.env, args = []) {
113
+ const platform = getPlatform();
114
+ const parts = platform.split("-");
115
+ const binaryName = `indexer-standalone-${platform}`;
116
+ const binaryPath = path.join(
117
+ __dirname,
118
+ "indexer-standalone",
119
+ binaryName,
120
+ );
121
+ const workingDir = path.join(__dirname, "indexer-standalone");
122
+
123
+ // Check for --clean flag and handle it
124
+ const cleanFlagIndex = args.indexOf("--clean");
125
+ if (cleanFlagIndex !== -1) {
126
+ handleCleanFlag(env, workingDir);
127
+ // Remove the --clean flag from args since the binary doesn't expect it
128
+ args.splice(cleanFlagIndex, 1);
129
+ }
130
+
131
+ console.log(`Starting midnight-indexer binary at: ${binaryPath}`);
132
+
133
+ const childProcess = spawn(binaryPath, args, {
134
+ env: env,
135
+ stdio: "inherit", // Inherit stdin, stdout, stderr from parent process
136
+ cwd: workingDir, // Run from inside the indexer-standalone directory
137
+ });
138
+
139
+ childProcess.on("spawn", () => {
140
+ console.log(
141
+ `midnight-indexer process spawned with PID: ${childProcess.pid}`,
142
+ );
143
+ });
144
+
145
+ childProcess.on("error", (error) => {
146
+ console.error("Failed to start midnight-indexer:", error);
147
+ });
148
+
149
+ childProcess.on("exit", (code, signal) => {
150
+ if (code !== null) {
151
+ console.log(`midnight-indexer process exited with code: ${code}`);
152
+ } else {
153
+ console.log(`midnight-indexer process terminated by signal: ${signal}`);
154
+ }
155
+ });
156
+
157
+ return childProcess;
158
+ }
159
+
160
+ module.exports = { runMidnightIndexer };