@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 +73 -0
- package/bin/cli.js +87 -0
- package/package.json +14 -0
- package/src/borsh.js +55 -0
- package/src/registry.js +275 -0
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
|
+
};
|
package/src/registry.js
ADDED
|
@@ -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 };
|