@x1scroll/node-cli 1.0.0

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,73 @@
1
+ # @x1scroll/node-cli
2
+
3
+ CLI for registering X1 validators as storage nodes in the [x1scroll](https://x1scroll.io) network.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @x1scroll/node-cli
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Register your validator as a storage node
14
+
15
+ ```bash
16
+ x1scroll-node register \
17
+ --keypair ~/.config/solana/identity.json \
18
+ --ipfs-peer 12D3KooW... \
19
+ --endpoint https://your-node.com/api/ipfs/upload
20
+ ```
21
+
22
+ ### Update node info
23
+
24
+ ```bash
25
+ x1scroll-node update \
26
+ --keypair ~/.config/solana/identity.json \
27
+ --ipfs-peer 12D3KooW... \
28
+ --endpoint https://your-node.com/api/ipfs/upload
29
+ ```
30
+
31
+ ### Deregister your node
32
+
33
+ ```bash
34
+ x1scroll-node deregister --keypair ~/.config/solana/identity.json
35
+ ```
36
+
37
+ > ⏳ Note: Deregistration starts a 2-day cooldown before it is finalized on-chain.
38
+
39
+ ### Check status
40
+
41
+ ```bash
42
+ x1scroll-node status --keypair ~/.config/solana/identity.json
43
+ ```
44
+
45
+ ### List all registered nodes
46
+
47
+ ```bash
48
+ x1scroll-node list
49
+ ```
50
+
51
+ ## Options
52
+
53
+ All commands accept an optional `--rpc <url>` flag to override the default RPC endpoint (`https://rpc.x1.xyz`).
54
+
55
+ ```bash
56
+ x1scroll-node list --rpc https://my-custom-rpc.example.com
57
+ ```
58
+
59
+ ## Details
60
+
61
+ - **Network:** X1 (SVM-compatible)
62
+ - **Program ID:** `GqzvCjz8nzxWxH39twk4oPfFaHXeyVDty9oJ6F4UcfF5`
63
+ - **Registration fee:** 0.01 XNT
64
+ - **PDA seeds:** `["storage_node", validator_pubkey]`
65
+
66
+ ## Requirements
67
+
68
+ - Node.js >= 16
69
+ - A funded X1 validator identity keypair (at least 0.01 XNT for registration)
70
+
71
+ ## License
72
+
73
+ MIT
package/bin/cli.js ADDED
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { Command } = require('commander');
5
+ const registry = require('../src/registry.js');
6
+
7
+ const program = new Command();
8
+
9
+ program
10
+ .name('x1scroll-node')
11
+ .description('CLI for managing x1scroll storage node registration on the X1 network')
12
+ .version('1.0.0');
13
+
14
+ program
15
+ .command('register')
16
+ .description('Register a validator as a storage node')
17
+ .requiredOption('--keypair <path>', 'Path to validator keypair JSON file')
18
+ .requiredOption('--ipfs-peer <id>', 'IPFS peer ID (e.g. 12D3KooW...)')
19
+ .requiredOption('--endpoint <url>', 'Storage node endpoint URL')
20
+ .option('--rpc <url>', 'RPC endpoint', 'https://rpc.x1.xyz')
21
+ .action(async (opts) => {
22
+ try {
23
+ await registry.register(opts.keypair, opts.ipfsPeer, opts.endpoint, opts.rpc);
24
+ } catch (err) {
25
+ console.error(`❌ ${err.message}`);
26
+ process.exit(1);
27
+ }
28
+ });
29
+
30
+ program
31
+ .command('update')
32
+ .description('Update storage node IPFS peer or endpoint')
33
+ .requiredOption('--keypair <path>', 'Path to validator keypair JSON file')
34
+ .requiredOption('--ipfs-peer <id>', 'New IPFS peer ID')
35
+ .requiredOption('--endpoint <url>', 'New storage node endpoint URL')
36
+ .option('--rpc <url>', 'RPC endpoint', 'https://rpc.x1.xyz')
37
+ .action(async (opts) => {
38
+ try {
39
+ await registry.update(opts.keypair, opts.ipfsPeer, opts.endpoint, opts.rpc);
40
+ } catch (err) {
41
+ console.error(`❌ ${err.message}`);
42
+ process.exit(1);
43
+ }
44
+ });
45
+
46
+ program
47
+ .command('deregister')
48
+ .description('Deregister storage node (sets active=false, 2-day cooldown)')
49
+ .requiredOption('--keypair <path>', 'Path to validator keypair JSON file')
50
+ .option('--rpc <url>', 'RPC endpoint', 'https://rpc.x1.xyz')
51
+ .action(async (opts) => {
52
+ try {
53
+ await registry.deregister(opts.keypair, opts.rpc);
54
+ } catch (err) {
55
+ console.error(`❌ ${err.message}`);
56
+ process.exit(1);
57
+ }
58
+ });
59
+
60
+ program
61
+ .command('status')
62
+ .description('Show registration status for a validator')
63
+ .requiredOption('--keypair <path>', 'Path to validator keypair JSON file')
64
+ .option('--rpc <url>', 'RPC endpoint', 'https://rpc.x1.xyz')
65
+ .action(async (opts) => {
66
+ try {
67
+ await registry.status(opts.keypair, opts.rpc);
68
+ } catch (err) {
69
+ console.error(`❌ ${err.message}`);
70
+ process.exit(1);
71
+ }
72
+ });
73
+
74
+ program
75
+ .command('list')
76
+ .description('List all registered storage nodes')
77
+ .option('--rpc <url>', 'RPC endpoint', 'https://rpc.x1.xyz')
78
+ .action(async (opts) => {
79
+ try {
80
+ await registry.list(opts.rpc);
81
+ } catch (err) {
82
+ console.error(`❌ ${err.message}`);
83
+ process.exit(1);
84
+ }
85
+ });
86
+
87
+ program.parse(process.argv);
package/package.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "@x1scroll/node-cli",
3
+ "version": "1.0.0",
4
+ "description": "CLI for registering X1 validators as storage nodes in the x1scroll network",
5
+ "bin": {
6
+ "x1scroll-node": "./bin/cli.js"
7
+ },
8
+ "main": "./bin/cli.js",
9
+ "license": "MIT",
10
+ "dependencies": {
11
+ "@solana/web3.js": "^1.98.0",
12
+ "commander": "^12.0.0"
13
+ }
14
+ }
package/src/borsh.js ADDED
@@ -0,0 +1,55 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Borsh encoding helpers
5
+ * String: 4-byte LE u32 length prefix + UTF-8 bytes
6
+ */
7
+
8
+ function encodeString(str) {
9
+ const bytes = Buffer.from(str, 'utf8');
10
+ const len = Buffer.alloc(4);
11
+ len.writeUInt32LE(bytes.length, 0);
12
+ return Buffer.concat([len, bytes]);
13
+ }
14
+
15
+ function decodeString(buf, offset) {
16
+ const len = buf.readUInt32LE(offset);
17
+ offset += 4;
18
+ const str = buf.slice(offset, offset + len).toString('utf8');
19
+ return { value: str, offset: offset + len };
20
+ }
21
+
22
+ function encodeBool(val) {
23
+ const b = Buffer.alloc(1);
24
+ b.writeUInt8(val ? 1 : 0, 0);
25
+ return b;
26
+ }
27
+
28
+ function decodeBool(buf, offset) {
29
+ const val = buf.readUInt8(offset) !== 0;
30
+ return { value: val, offset: offset + 1 };
31
+ }
32
+
33
+ function decodeU64(buf, offset) {
34
+ // Read as two 32-bit LE values (low, high)
35
+ const lo = buf.readUInt32LE(offset);
36
+ const hi = buf.readUInt32LE(offset + 4);
37
+ const val = BigInt(hi) * BigInt(0x100000000) + BigInt(lo);
38
+ return { value: val, offset: offset + 8 };
39
+ }
40
+
41
+ function decodeI64(buf, offset) {
42
+ const lo = buf.readUInt32LE(offset);
43
+ const hi = buf.readInt32LE(offset + 4);
44
+ const val = BigInt(hi) * BigInt(0x100000000) + BigInt(lo);
45
+ return { value: val, offset: offset + 8 };
46
+ }
47
+
48
+ module.exports = {
49
+ encodeString,
50
+ decodeString,
51
+ encodeBool,
52
+ decodeBool,
53
+ decodeU64,
54
+ decodeI64,
55
+ };
@@ -0,0 +1,275 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ Connection,
5
+ PublicKey,
6
+ Keypair,
7
+ Transaction,
8
+ TransactionInstruction,
9
+ SystemProgram,
10
+ LAMPORTS_PER_SOL,
11
+ } = require('@solana/web3.js');
12
+ const crypto = require('crypto');
13
+ const fs = require('fs');
14
+ const borsh = require('./borsh.js');
15
+
16
+ const PROGRAM_ID = new PublicKey('GqzvCjz8nzxWxH39twk4oPfFaHXeyVDty9oJ6F4UcfF5');
17
+ const TREASURY = new PublicKey('HYP2VdVk2QNGKMBfWGFZpaFqMoqQkB7Vp5F12eSxCxtf');
18
+ const DEFAULT_RPC = 'https://rpc.x1.xyz';
19
+ const REGISTRATION_FEE = 0.01 * LAMPORTS_PER_SOL; // 0.01 XNT in lamports
20
+
21
+ // Compute 8-byte discriminator: sha256("global:<name>")[0..8]
22
+ function discriminator(name) {
23
+ const hash = crypto.createHash('sha256').update(`global:${name}`).digest();
24
+ return hash.slice(0, 8);
25
+ }
26
+
27
+ const DISC_REGISTER = discriminator('register_storage_node');
28
+ const DISC_UPDATE = discriminator('update_storage_node');
29
+ const DISC_DEREGISTER = discriminator('deregister_storage_node');
30
+ const DISC_STORAGE_NODE = discriminator('storage_node'); // account discriminator
31
+
32
+ function loadKeypair(keypairPath) {
33
+ const resolved = keypairPath.replace(/^~/, process.env.HOME || '');
34
+ if (!fs.existsSync(resolved)) {
35
+ throw new Error(`Keypair file not found: ${keypairPath}`);
36
+ }
37
+ const raw = JSON.parse(fs.readFileSync(resolved, 'utf8'));
38
+ return Keypair.fromSecretKey(Uint8Array.from(raw));
39
+ }
40
+
41
+ async function deriveStorageNodePDA(validatorPubkey) {
42
+ const [pda, bump] = await PublicKey.findProgramAddress(
43
+ [Buffer.from('storage_node'), validatorPubkey.toBuffer()],
44
+ PROGRAM_ID
45
+ );
46
+ return { pda, bump };
47
+ }
48
+
49
+ async function getConnection(rpc) {
50
+ return new Connection(rpc || DEFAULT_RPC, 'confirmed');
51
+ }
52
+
53
+ // Decode StorageNode account data
54
+ function decodeStorageNode(data) {
55
+ // Skip 8-byte discriminator
56
+ let offset = 8;
57
+
58
+ // validator: Pubkey (32 bytes)
59
+ const validator = new PublicKey(data.slice(offset, offset + 32));
60
+ offset += 32;
61
+
62
+ // ipfs_peer_id: String
63
+ const ipfsResult = borsh.decodeString(data, offset);
64
+ const ipfs_peer_id = ipfsResult.value;
65
+ offset = ipfsResult.offset;
66
+
67
+ // endpoint: String
68
+ const endpointResult = borsh.decodeString(data, offset);
69
+ const endpoint = endpointResult.value;
70
+ offset = endpointResult.offset;
71
+
72
+ // active: bool
73
+ const activeResult = borsh.decodeBool(data, offset);
74
+ const active = activeResult.value;
75
+ offset = activeResult.offset;
76
+
77
+ // total_pins: u64
78
+ const pinsResult = borsh.decodeU64(data, offset);
79
+ const total_pins = pinsResult.value;
80
+ offset = pinsResult.offset;
81
+
82
+ // registered_at: i64
83
+ const regResult = borsh.decodeI64(data, offset);
84
+ const registered_at = regResult.value;
85
+ offset = regResult.offset;
86
+
87
+ // last_active: i64
88
+ const lastResult = borsh.decodeI64(data, offset);
89
+ const last_active = lastResult.value;
90
+ offset = lastResult.offset;
91
+
92
+ return { validator, ipfs_peer_id, endpoint, active, total_pins, registered_at, last_active };
93
+ }
94
+
95
+ async function register(keypairPath, ipfsPeer, endpoint, rpc) {
96
+ const connection = await getConnection(rpc);
97
+ const keypair = loadKeypair(keypairPath);
98
+ const { pda } = await deriveStorageNodePDA(keypair.publicKey);
99
+
100
+ // Check if already registered
101
+ const existing = await connection.getAccountInfo(pda);
102
+ if (existing) {
103
+ throw new Error("Already registered. Use 'update' to change settings.");
104
+ }
105
+
106
+ // Check balance
107
+ const balance = await connection.getBalance(keypair.publicKey);
108
+ if (balance < REGISTRATION_FEE + 5000) {
109
+ throw new Error('Need at least 0.01 XNT for registration fee');
110
+ }
111
+
112
+ // Build instruction data
113
+ const ipfsBuf = borsh.encodeString(ipfsPeer);
114
+ const endpointBuf = borsh.encodeString(endpoint);
115
+ const data = Buffer.concat([DISC_REGISTER, ipfsBuf, endpointBuf]);
116
+
117
+ const instruction = new TransactionInstruction({
118
+ programId: PROGRAM_ID,
119
+ keys: [
120
+ { pubkey: keypair.publicKey, isSigner: true, isWritable: true },
121
+ { pubkey: pda, isSigner: false, isWritable: true },
122
+ { pubkey: TREASURY, isSigner: false, isWritable: true },
123
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
124
+ ],
125
+ data,
126
+ });
127
+
128
+ const tx = new Transaction().add(instruction);
129
+ const sig = await connection.sendTransaction(tx, [keypair]);
130
+ await connection.confirmTransaction(sig, 'confirmed');
131
+
132
+ console.log(`✅ Registered`);
133
+ console.log(` Validator : ${keypair.publicKey.toBase58()}`);
134
+ console.log(` PDA : ${pda.toBase58()}`);
135
+ console.log(` TX : ${sig}`);
136
+ }
137
+
138
+ async function update(keypairPath, ipfsPeer, endpoint, rpc) {
139
+ const connection = await getConnection(rpc);
140
+ const keypair = loadKeypair(keypairPath);
141
+ const { pda } = await deriveStorageNodePDA(keypair.publicKey);
142
+
143
+ // Check if registered
144
+ const existing = await connection.getAccountInfo(pda);
145
+ if (!existing) {
146
+ throw new Error("Not registered. Use 'register' first.");
147
+ }
148
+
149
+ const ipfsBuf = borsh.encodeString(ipfsPeer);
150
+ const endpointBuf = borsh.encodeString(endpoint);
151
+ const data = Buffer.concat([DISC_UPDATE, ipfsBuf, endpointBuf]);
152
+
153
+ const instruction = new TransactionInstruction({
154
+ programId: PROGRAM_ID,
155
+ keys: [
156
+ { pubkey: keypair.publicKey, isSigner: true, isWritable: true },
157
+ { pubkey: pda, isSigner: false, isWritable: true },
158
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
159
+ ],
160
+ data,
161
+ });
162
+
163
+ const tx = new Transaction().add(instruction);
164
+ const sig = await connection.sendTransaction(tx, [keypair]);
165
+ await connection.confirmTransaction(sig, 'confirmed');
166
+
167
+ console.log(`✅ Updated`);
168
+ console.log(` Validator : ${keypair.publicKey.toBase58()}`);
169
+ console.log(` PDA : ${pda.toBase58()}`);
170
+ console.log(` TX : ${sig}`);
171
+ }
172
+
173
+ async function deregister(keypairPath, rpc) {
174
+ const connection = await getConnection(rpc);
175
+ const keypair = loadKeypair(keypairPath);
176
+ const { pda } = await deriveStorageNodePDA(keypair.publicKey);
177
+
178
+ // Check if registered
179
+ const existing = await connection.getAccountInfo(pda);
180
+ if (!existing) {
181
+ throw new Error("Not registered. Use 'register' first.");
182
+ }
183
+
184
+ const data = Buffer.from(DISC_DEREGISTER);
185
+
186
+ const instruction = new TransactionInstruction({
187
+ programId: PROGRAM_ID,
188
+ keys: [
189
+ { pubkey: keypair.publicKey, isSigner: true, isWritable: true },
190
+ { pubkey: pda, isSigner: false, isWritable: true },
191
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
192
+ ],
193
+ data,
194
+ });
195
+
196
+ const tx = new Transaction().add(instruction);
197
+ const sig = await connection.sendTransaction(tx, [keypair]);
198
+ await connection.confirmTransaction(sig, 'confirmed');
199
+
200
+ console.log(`✅ Deregister requested`);
201
+ console.log(` Validator : ${keypair.publicKey.toBase58()}`);
202
+ console.log(` TX : ${sig}`);
203
+ console.log(` ⏳ 2-day cooldown before finalized`);
204
+ }
205
+
206
+ async function status(keypairPath, rpc) {
207
+ const connection = await getConnection(rpc);
208
+ const keypair = loadKeypair(keypairPath);
209
+ const { pda } = await deriveStorageNodePDA(keypair.publicKey);
210
+
211
+ const accountInfo = await connection.getAccountInfo(pda);
212
+ if (!accountInfo) {
213
+ console.log('Not registered');
214
+ return;
215
+ }
216
+
217
+ const node = decodeStorageNode(accountInfo.data);
218
+
219
+ const toDate = (ts) => {
220
+ if (ts === 0n) return 'never';
221
+ return new Date(Number(ts) * 1000).toISOString();
222
+ };
223
+
224
+ console.log('📡 Storage Node Status');
225
+ console.log(` Validator : ${node.validator.toBase58()}`);
226
+ console.log(` PDA : ${pda.toBase58()}`);
227
+ console.log(` IPFS Peer : ${node.ipfs_peer_id}`);
228
+ console.log(` Endpoint : ${node.endpoint}`);
229
+ console.log(` Active : ${node.active ? '✅ Yes' : '❌ No'}`);
230
+ console.log(` Total Pins : ${node.total_pins.toString()}`);
231
+ console.log(` Registered : ${toDate(node.registered_at)}`);
232
+ console.log(` Last Active : ${toDate(node.last_active)}`);
233
+ }
234
+
235
+ async function list(rpc) {
236
+ const connection = await getConnection(rpc);
237
+
238
+ const accounts = await connection.getProgramAccounts(PROGRAM_ID, {
239
+ filters: [
240
+ {
241
+ memcmp: {
242
+ offset: 0,
243
+ bytes: Buffer.from(DISC_STORAGE_NODE).toString('base64'),
244
+ encoding: 'base64',
245
+ },
246
+ },
247
+ ],
248
+ });
249
+
250
+ if (accounts.length === 0) {
251
+ console.log('No registered storage nodes found.');
252
+ return;
253
+ }
254
+
255
+ console.log(`\n${'Validator'.padEnd(46)} ${'IPFS Peer'.padEnd(54)} ${'Endpoint'.padEnd(45)} ${'Active'.padEnd(7)} ${'Pins'}`);
256
+ console.log('-'.repeat(165));
257
+
258
+ for (const { pubkey, account } of accounts) {
259
+ try {
260
+ const node = decodeStorageNode(account.data);
261
+ const validator = node.validator.toBase58().padEnd(46);
262
+ const ipfs = (node.ipfs_peer_id || '').substring(0, 52).padEnd(54);
263
+ const ep = (node.endpoint || '').substring(0, 43).padEnd(45);
264
+ const active = (node.active ? '✅' : '❌').padEnd(7);
265
+ const pins = node.total_pins.toString();
266
+ console.log(`${validator} ${ipfs} ${ep} ${active} ${pins}`);
267
+ } catch (e) {
268
+ console.log(`[Could not decode account ${pubkey.toBase58()}]`);
269
+ }
270
+ }
271
+
272
+ console.log(`\nTotal: ${accounts.length} node(s)`);
273
+ }
274
+
275
+ module.exports = { register, update, deregister, status, list };