diodejs 0.3.0 → 0.4.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 +56 -5
- package/bindPort.js +405 -28
- package/clientManager.js +435 -0
- package/connection.js +107 -14
- package/examples/RPCTest.js +9 -7
- package/examples/nativeBindTest.js +55 -0
- package/examples/nativeForwardTest.js +80 -0
- package/examples/nativeTcpClientTest.js +56 -0
- package/examples/nativeUdpClientTest.js +40 -0
- package/examples/portForwardTest.js +5 -7
- package/examples/publishAndBind.js +6 -10
- package/examples/publishPortTest.js +4 -6
- package/index.js +2 -1
- package/nativeCrypto.js +321 -0
- package/package.json +1 -1
- package/publishPort.js +514 -52
- package/rpc.js +125 -1
- package/utils.js +23 -0
package/clientManager.js
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
const EventEmitter = require('events');
|
|
2
|
+
const DiodeConnection = require('./connection');
|
|
3
|
+
const DiodeRPC = require('./rpc');
|
|
4
|
+
const logger = require('./logger');
|
|
5
|
+
|
|
6
|
+
const DEFAULT_DIODE_ADDRS = [
|
|
7
|
+
'as1.prenet.diode.io:41046',
|
|
8
|
+
'as2.prenet.diode.io:41046',
|
|
9
|
+
'us1.prenet.diode.io:41046',
|
|
10
|
+
'us2.prenet.diode.io:41046',
|
|
11
|
+
'eu1.prenet.diode.io:41046',
|
|
12
|
+
'eu2.prenet.diode.io:41046',
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
function splitHostPort(input, defaultPort) {
|
|
16
|
+
if (!input || typeof input !== 'string') {
|
|
17
|
+
return { host: '', port: defaultPort };
|
|
18
|
+
}
|
|
19
|
+
const trimmed = input.trim();
|
|
20
|
+
if (!trimmed) {
|
|
21
|
+
return { host: '', port: defaultPort };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (trimmed.startsWith('[')) {
|
|
25
|
+
const idx = trimmed.indexOf(']');
|
|
26
|
+
if (idx !== -1) {
|
|
27
|
+
const host = trimmed.slice(1, idx);
|
|
28
|
+
const rest = trimmed.slice(idx + 1);
|
|
29
|
+
if (rest.startsWith(':')) {
|
|
30
|
+
const port = parseInt(rest.slice(1), 10);
|
|
31
|
+
return { host, port: Number.isFinite(port) && port > 0 ? port : defaultPort };
|
|
32
|
+
}
|
|
33
|
+
return { host, port: defaultPort };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const lastColon = trimmed.lastIndexOf(':');
|
|
38
|
+
if (lastColon > -1 && trimmed.indexOf(':') === lastColon) {
|
|
39
|
+
const host = trimmed.slice(0, lastColon);
|
|
40
|
+
const portStr = trimmed.slice(lastColon + 1);
|
|
41
|
+
if (/^\d+$/.test(portStr)) {
|
|
42
|
+
const port = parseInt(portStr, 10);
|
|
43
|
+
return { host, port: Number.isFinite(port) && port > 0 ? port : defaultPort };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { host: trimmed, port: defaultPort };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function joinHostPort(host, port) {
|
|
51
|
+
if (!host) return '';
|
|
52
|
+
if (host.includes(':') && !host.startsWith('[')) {
|
|
53
|
+
return `[${host}]:${port}`;
|
|
54
|
+
}
|
|
55
|
+
return `${host}:${port}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function normalizeAddress(address) {
|
|
59
|
+
if (!address) return null;
|
|
60
|
+
if (Buffer.isBuffer(address)) return address;
|
|
61
|
+
if (address instanceof Uint8Array) return Buffer.from(address);
|
|
62
|
+
if (typeof address === 'string') {
|
|
63
|
+
const hex = address.toLowerCase().startsWith('0x') ? address.slice(2) : address;
|
|
64
|
+
if (!hex) return Buffer.alloc(0);
|
|
65
|
+
return Buffer.from(hex, 'hex');
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function normalizeServerIdHex(serverId) {
|
|
71
|
+
if (!serverId) return '';
|
|
72
|
+
if (Buffer.isBuffer(serverId) || serverId instanceof Uint8Array) {
|
|
73
|
+
return `0x${Buffer.from(serverId).toString('hex')}`.toLowerCase();
|
|
74
|
+
}
|
|
75
|
+
if (typeof serverId === 'string') {
|
|
76
|
+
return (serverId.startsWith('0x') ? serverId : `0x${serverId}`).toLowerCase();
|
|
77
|
+
}
|
|
78
|
+
return '';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isConnected(connection) {
|
|
82
|
+
return connection && connection.socket && !connection.socket.destroyed;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
class DiodeClientManager extends EventEmitter {
|
|
86
|
+
constructor(options = {}) {
|
|
87
|
+
super();
|
|
88
|
+
|
|
89
|
+
this.keyLocation = options.keyLocation || './db/keys.json';
|
|
90
|
+
this.defaultPort = Number.isFinite(options.port) && options.port > 0 ? options.port : 41046;
|
|
91
|
+
this.deviceCacheTtlMs = Number.isFinite(options.deviceCacheTtlMs)
|
|
92
|
+
? options.deviceCacheTtlMs
|
|
93
|
+
: 30000;
|
|
94
|
+
|
|
95
|
+
this.connections = [];
|
|
96
|
+
this.connectionByHost = new Map();
|
|
97
|
+
this.serverIdToConnection = new Map();
|
|
98
|
+
this.pendingConnections = new Map();
|
|
99
|
+
this.deviceRelayCache = new Map();
|
|
100
|
+
this._rpcByConnection = new Map();
|
|
101
|
+
this._rrIndex = 0;
|
|
102
|
+
|
|
103
|
+
this.initialHosts = this._buildInitialHosts(options);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
_buildInitialHosts(options) {
|
|
107
|
+
if (typeof options.host === 'string' && options.host.trim()) {
|
|
108
|
+
const { host, port } = splitHostPort(options.host, this.defaultPort);
|
|
109
|
+
return host ? [joinHostPort(host, port)] : [];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let hosts = [];
|
|
113
|
+
if (Array.isArray(options.hosts)) {
|
|
114
|
+
hosts = options.hosts;
|
|
115
|
+
} else if (typeof options.hosts === 'string') {
|
|
116
|
+
hosts = options.hosts.split(',').map((entry) => entry.trim());
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (hosts.length === 0) {
|
|
120
|
+
hosts = DEFAULT_DIODE_ADDRS.slice();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const seen = new Set();
|
|
124
|
+
const normalized = [];
|
|
125
|
+
for (const entry of hosts) {
|
|
126
|
+
if (!entry) continue;
|
|
127
|
+
const { host, port } = splitHostPort(entry, this.defaultPort);
|
|
128
|
+
if (!host) continue;
|
|
129
|
+
const key = joinHostPort(host, port);
|
|
130
|
+
const lower = key.toLowerCase();
|
|
131
|
+
if (!seen.has(lower)) {
|
|
132
|
+
seen.add(lower);
|
|
133
|
+
normalized.push(key);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return normalized;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
_getRpcFor(connection) {
|
|
140
|
+
if (!connection) return null;
|
|
141
|
+
let rpc = this._rpcByConnection.get(connection);
|
|
142
|
+
if (!rpc) {
|
|
143
|
+
rpc = connection.RPC || new DiodeRPC(connection);
|
|
144
|
+
this._rpcByConnection.set(connection, rpc);
|
|
145
|
+
}
|
|
146
|
+
return rpc;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
_updateServerIdMapping(connection) {
|
|
150
|
+
if (!connection) return;
|
|
151
|
+
try {
|
|
152
|
+
const serverId = connection.getServerEthereumAddress(true);
|
|
153
|
+
if (serverId) {
|
|
154
|
+
this.serverIdToConnection.set(serverId.toLowerCase(), connection);
|
|
155
|
+
}
|
|
156
|
+
} catch (error) {
|
|
157
|
+
logger.debug(() => `Failed to map server ID for ${connection.host}:${connection.port}: ${error}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
_localAddressHintFor(connection) {
|
|
162
|
+
const connected = this._connectedConnections();
|
|
163
|
+
if (connected.length === 0) {
|
|
164
|
+
return Buffer.alloc(0);
|
|
165
|
+
}
|
|
166
|
+
const primary = connected[0];
|
|
167
|
+
const secondary = connected.length > 1 ? connected[1] : null;
|
|
168
|
+
|
|
169
|
+
let primaryId = '';
|
|
170
|
+
try {
|
|
171
|
+
primaryId = normalizeServerIdHex(primary.getServerEthereumAddress(true));
|
|
172
|
+
} catch (_) {
|
|
173
|
+
primaryId = '';
|
|
174
|
+
}
|
|
175
|
+
if (!primaryId) {
|
|
176
|
+
return Buffer.alloc(0);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (primary === connection) {
|
|
180
|
+
if (!secondary) return Buffer.alloc(0);
|
|
181
|
+
let secondaryId = '';
|
|
182
|
+
try {
|
|
183
|
+
secondaryId = normalizeServerIdHex(secondary.getServerEthereumAddress(true));
|
|
184
|
+
} catch (_) {
|
|
185
|
+
secondaryId = '';
|
|
186
|
+
}
|
|
187
|
+
if (!secondaryId) return Buffer.alloc(0);
|
|
188
|
+
return Buffer.concat([
|
|
189
|
+
Buffer.from([1]),
|
|
190
|
+
Buffer.from(secondaryId.slice(2), 'hex'),
|
|
191
|
+
]);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return Buffer.concat([
|
|
195
|
+
Buffer.from([0]),
|
|
196
|
+
Buffer.from(primaryId.slice(2), 'hex'),
|
|
197
|
+
]);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
_registerConnection(connection, hostKey) {
|
|
201
|
+
connection._managerHostKey = hostKey;
|
|
202
|
+
this.connections.push(connection);
|
|
203
|
+
this.connectionByHost.set(hostKey, connection);
|
|
204
|
+
if (typeof connection.setLocalAddressProvider === 'function') {
|
|
205
|
+
connection.setLocalAddressProvider(() => this._localAddressHintFor(connection));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
connection.on('unsolicited', (message) => {
|
|
209
|
+
this.emit('unsolicited', message, connection);
|
|
210
|
+
});
|
|
211
|
+
connection.on('reconnected', () => {
|
|
212
|
+
this._updateServerIdMapping(connection);
|
|
213
|
+
this.emit('reconnected', connection);
|
|
214
|
+
});
|
|
215
|
+
connection.on('reconnecting', (info) => {
|
|
216
|
+
this.emit('reconnecting', connection, info);
|
|
217
|
+
});
|
|
218
|
+
connection.on('reconnect_failed', () => {
|
|
219
|
+
this.emit('reconnect_failed', connection);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
_unregisterConnection(connection, hostKey) {
|
|
224
|
+
if (!connection) return;
|
|
225
|
+
if (hostKey) {
|
|
226
|
+
this.connectionByHost.delete(hostKey);
|
|
227
|
+
}
|
|
228
|
+
this.connections = this.connections.filter((item) => item !== connection);
|
|
229
|
+
for (const [serverId, conn] of this.serverIdToConnection.entries()) {
|
|
230
|
+
if (conn === connection) {
|
|
231
|
+
this.serverIdToConnection.delete(serverId);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
this._rpcByConnection.delete(connection);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async _ensureConnection(hostEntry) {
|
|
238
|
+
const { host, port } = splitHostPort(hostEntry, this.defaultPort);
|
|
239
|
+
const hostKey = joinHostPort(host, port);
|
|
240
|
+
if (!host) {
|
|
241
|
+
throw new Error(`Invalid host entry: ${hostEntry}`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (this.connectionByHost.has(hostKey)) {
|
|
245
|
+
return this.connectionByHost.get(hostKey);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (this.pendingConnections.has(hostKey)) {
|
|
249
|
+
return this.pendingConnections.get(hostKey);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const connection = new DiodeConnection(host, port, this.keyLocation);
|
|
253
|
+
this._registerConnection(connection, hostKey);
|
|
254
|
+
|
|
255
|
+
const promise = connection.connect()
|
|
256
|
+
.then(() => {
|
|
257
|
+
this._updateServerIdMapping(connection);
|
|
258
|
+
this.emit('connected', connection);
|
|
259
|
+
return connection;
|
|
260
|
+
})
|
|
261
|
+
.catch((error) => {
|
|
262
|
+
this._unregisterConnection(connection, hostKey);
|
|
263
|
+
throw error;
|
|
264
|
+
})
|
|
265
|
+
.finally(() => {
|
|
266
|
+
this.pendingConnections.delete(hostKey);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
this.pendingConnections.set(hostKey, promise);
|
|
270
|
+
return promise;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
_connectedConnections() {
|
|
274
|
+
return this.connections.filter((connection) => isConnected(connection));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
getNearestConnection() {
|
|
278
|
+
const connected = this._connectedConnections();
|
|
279
|
+
if (connected.length === 0) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
this._rrIndex = (this._rrIndex + 1) % connected.length;
|
|
283
|
+
return connected[this._rrIndex];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async connect() {
|
|
287
|
+
if (!this.initialHosts || this.initialHosts.length === 0) {
|
|
288
|
+
throw new Error('No Diode hosts configured');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const results = await Promise.allSettled(
|
|
292
|
+
this.initialHosts.map((host) => this._ensureConnection(host))
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
const success = results.some((result) => result.status === 'fulfilled');
|
|
296
|
+
if (!success) {
|
|
297
|
+
const errorMessages = results
|
|
298
|
+
.filter((result) => result.status === 'rejected')
|
|
299
|
+
.map((result) => result.reason && result.reason.message ? result.reason.message : String(result.reason));
|
|
300
|
+
throw new Error(`Failed to connect to any Diode hosts. ${errorMessages.join('; ')}`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return this;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async getConnectionForDevice(deviceId) {
|
|
307
|
+
const deviceIdBuffer = normalizeAddress(deviceId);
|
|
308
|
+
if (!deviceIdBuffer) {
|
|
309
|
+
throw new Error('Invalid device ID');
|
|
310
|
+
}
|
|
311
|
+
const deviceIdHex = deviceIdBuffer.toString('hex');
|
|
312
|
+
|
|
313
|
+
if (this.deviceCacheTtlMs > 0) {
|
|
314
|
+
const cached = this.deviceRelayCache.get(deviceIdHex);
|
|
315
|
+
if (cached && Date.now() - cached.ts < this.deviceCacheTtlMs) {
|
|
316
|
+
const cachedConn = this.serverIdToConnection.get(cached.serverIdHex) ||
|
|
317
|
+
this.connectionByHost.get(cached.hostKey);
|
|
318
|
+
if (cachedConn && isConnected(cachedConn)) {
|
|
319
|
+
return cachedConn;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const primary = this.getNearestConnection();
|
|
325
|
+
if (!primary) {
|
|
326
|
+
throw new Error('No connected relay available');
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
let ticket = null;
|
|
330
|
+
try {
|
|
331
|
+
ticket = await this._getRpcFor(primary).getObject(deviceIdBuffer);
|
|
332
|
+
} catch (error) {
|
|
333
|
+
logger.warn(() => `Failed to resolve device ticket: ${error}`);
|
|
334
|
+
return primary;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const serverIdHex = normalizeServerIdHex(ticket && (ticket.serverIdHex || ticket.serverId));
|
|
338
|
+
if (!serverIdHex) {
|
|
339
|
+
return primary;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const existing = this.serverIdToConnection.get(serverIdHex);
|
|
343
|
+
if (existing && isConnected(existing)) {
|
|
344
|
+
this.deviceRelayCache.set(deviceIdHex, {
|
|
345
|
+
serverIdHex,
|
|
346
|
+
hostKey: existing._managerHostKey || '',
|
|
347
|
+
ts: Date.now(),
|
|
348
|
+
});
|
|
349
|
+
return existing;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
let nodeInfo = null;
|
|
353
|
+
try {
|
|
354
|
+
const nodeId = Buffer.from(serverIdHex.slice(2), 'hex');
|
|
355
|
+
nodeInfo = await this._getRpcFor(primary).getNode(nodeId);
|
|
356
|
+
} catch (error) {
|
|
357
|
+
logger.warn(() => `Failed to resolve relay node for ${serverIdHex}: ${error}`);
|
|
358
|
+
return primary;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (!nodeInfo || !nodeInfo.host) {
|
|
362
|
+
return primary;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const relayPort = nodeInfo.edgePort || nodeInfo.serverPort;
|
|
366
|
+
if (!relayPort) {
|
|
367
|
+
return primary;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const hostKey = joinHostPort(nodeInfo.host, relayPort);
|
|
371
|
+
let relayConnection;
|
|
372
|
+
try {
|
|
373
|
+
relayConnection = await this._ensureConnection(hostKey);
|
|
374
|
+
} catch (error) {
|
|
375
|
+
logger.warn(() => `Failed to connect to relay ${hostKey}: ${error}`);
|
|
376
|
+
return primary;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
this.deviceRelayCache.set(deviceIdHex, {
|
|
380
|
+
serverIdHex,
|
|
381
|
+
hostKey,
|
|
382
|
+
ts: Date.now(),
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
return relayConnection || primary;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async resolveRelayForDevice(deviceId) {
|
|
389
|
+
const deviceIdBuffer = normalizeAddress(deviceId);
|
|
390
|
+
if (!deviceIdBuffer) {
|
|
391
|
+
throw new Error('Invalid device ID');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const primary = this.getNearestConnection();
|
|
395
|
+
if (!primary) {
|
|
396
|
+
throw new Error('No connected relay available');
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const ticket = await this._getRpcFor(primary).getObject(deviceIdBuffer);
|
|
400
|
+
const serverIdHex = normalizeServerIdHex(ticket && (ticket.serverIdHex || ticket.serverId));
|
|
401
|
+
if (!serverIdHex) {
|
|
402
|
+
throw new Error('Device ticket missing server ID');
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const nodeId = Buffer.from(serverIdHex.slice(2), 'hex');
|
|
406
|
+
const nodeInfo = await this._getRpcFor(primary).getNode(nodeId);
|
|
407
|
+
if (!nodeInfo || !nodeInfo.host) {
|
|
408
|
+
throw new Error('Relay node info missing host');
|
|
409
|
+
}
|
|
410
|
+
const relayPort = nodeInfo.edgePort || nodeInfo.serverPort;
|
|
411
|
+
if (!relayPort) {
|
|
412
|
+
throw new Error('Relay node info missing port');
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return {
|
|
416
|
+
serverId: serverIdHex,
|
|
417
|
+
host: nodeInfo.host,
|
|
418
|
+
port: relayPort,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
getConnections() {
|
|
423
|
+
return this.connections.slice();
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
close() {
|
|
427
|
+
for (const connection of this.connections) {
|
|
428
|
+
try {
|
|
429
|
+
connection.close();
|
|
430
|
+
} catch (_) {}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
module.exports = DiodeClientManager;
|
package/connection.js
CHANGED
|
@@ -3,7 +3,7 @@ const tls = require('tls');
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const { RLP } = require('@ethereumjs/rlp');
|
|
5
5
|
const EventEmitter = require('events');
|
|
6
|
-
const { makeReadable, parseRequestId, parseResponseType, parseReason, generateCert, ensureDirectoryExistence, loadOrGenerateKeyPair } = require('./utils');
|
|
6
|
+
const { makeReadable, parseRequestId, parseResponseType, parseReason, parseUInt, generateCert, ensureDirectoryExistence, loadOrGenerateKeyPair, toBufferView } = require('./utils');
|
|
7
7
|
const { Buffer } = require('buffer'); // Import Buffer
|
|
8
8
|
const asn1 = require('asn1.js');
|
|
9
9
|
const secp256k1 = require('secp256k1');
|
|
@@ -47,6 +47,7 @@ class DiodeConnection extends EventEmitter {
|
|
|
47
47
|
this.connections = new Map(); // For PublishPort
|
|
48
48
|
this.certPem = null;
|
|
49
49
|
this._serverEthAddress = null; // cache after first read
|
|
50
|
+
this.localAddressProvider = null;
|
|
50
51
|
// Load or generate keypair
|
|
51
52
|
this.keyPair = loadOrGenerateKeyPair(this.keyLocation);
|
|
52
53
|
|
|
@@ -82,6 +83,12 @@ class DiodeConnection extends EventEmitter {
|
|
|
82
83
|
|
|
83
84
|
// Log the ticket batching settings
|
|
84
85
|
logger.info(() => `Ticket batching settings - Bytes Threshold: ${this.ticketUpdateThreshold} bytes, Update Interval: ${this.ticketUpdateInterval}ms`);
|
|
86
|
+
|
|
87
|
+
// Handle server ticket requests on the API socket
|
|
88
|
+
this._onUnsolicited = (message) => {
|
|
89
|
+
this._handleUnsolicitedMessage(message);
|
|
90
|
+
};
|
|
91
|
+
this.on('unsolicited', this._onUnsolicited);
|
|
85
92
|
}
|
|
86
93
|
|
|
87
94
|
connect() {
|
|
@@ -106,17 +113,18 @@ class DiodeConnection extends EventEmitter {
|
|
|
106
113
|
};
|
|
107
114
|
|
|
108
115
|
this.socket = tls.connect(this.port, this.host, options, async () => {
|
|
109
|
-
|
|
116
|
+
const relayHost = this.getServerRelayHost();
|
|
117
|
+
const relayPort = (this.socket && this.socket.remotePort) ? this.socket.remotePort : this.port;
|
|
118
|
+
logger.info(() => `Connected to Diode relay ${relayHost}:${relayPort}`);
|
|
110
119
|
// Reset retry counter on successful connection
|
|
111
120
|
this.retryCount = 0;
|
|
112
121
|
// Set keep-alive to prevent connection timeout forever
|
|
113
122
|
this.socket.setKeepAlive(true, 1500);
|
|
114
123
|
this.socket.setNoDelay(true);
|
|
115
124
|
// Cache server address after handshake
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
logger.warn(() => `Failed caching server address: ${e}`);
|
|
125
|
+
const cachedServerAddress = await this._waitForServerEthereumAddress();
|
|
126
|
+
if (cachedServerAddress) {
|
|
127
|
+
this._serverEthAddress = cachedServerAddress;
|
|
120
128
|
}
|
|
121
129
|
// Start periodic ticket updates now that we are fully connected
|
|
122
130
|
this._startTicketUpdateTimer();
|
|
@@ -269,6 +277,12 @@ class DiodeConnection extends EventEmitter {
|
|
|
269
277
|
return this;
|
|
270
278
|
}
|
|
271
279
|
|
|
280
|
+
// Optional provider for LocalAddr ticket hint (Buffer or string)
|
|
281
|
+
setLocalAddressProvider(provider) {
|
|
282
|
+
this.localAddressProvider = typeof provider === 'function' ? provider : null;
|
|
283
|
+
return this;
|
|
284
|
+
}
|
|
285
|
+
|
|
272
286
|
// Update close method to prevent reconnection when intentionally closing
|
|
273
287
|
close() {
|
|
274
288
|
if (this.ticketUpdateTimer) {
|
|
@@ -382,6 +396,34 @@ class DiodeConnection extends EventEmitter {
|
|
|
382
396
|
this.receiveBuffer = this.receiveBuffer.slice(offset);
|
|
383
397
|
}
|
|
384
398
|
|
|
399
|
+
_handleUnsolicitedMessage(message) {
|
|
400
|
+
if (!Array.isArray(message) || message.length < 2) return;
|
|
401
|
+
const messageContent = message[1];
|
|
402
|
+
if (!Array.isArray(messageContent) || messageContent.length < 1) return;
|
|
403
|
+
|
|
404
|
+
const messageTypeRaw = messageContent[0];
|
|
405
|
+
const messageType = toBufferView(messageTypeRaw).toString('utf8');
|
|
406
|
+
|
|
407
|
+
if (messageType === 'ticket_request') {
|
|
408
|
+
const deviceUsageRaw = messageContent[1];
|
|
409
|
+
const deviceUsage = parseUInt(deviceUsageRaw);
|
|
410
|
+
if (typeof deviceUsage === 'number' && deviceUsage > this.totalBytes) {
|
|
411
|
+
this.totalBytes = deviceUsage;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Send a fresh ticket promptly to avoid disconnect
|
|
415
|
+
this.createTicketCommand()
|
|
416
|
+
.then((ticketCommand) => this.sendCommand(ticketCommand))
|
|
417
|
+
.then(() => {
|
|
418
|
+
this.accumulatedBytes = 0;
|
|
419
|
+
this.lastTicketUpdate = Date.now();
|
|
420
|
+
})
|
|
421
|
+
.catch((error) => {
|
|
422
|
+
logger.error(() => `Error handling ticket_request: ${error}`);
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
385
427
|
fixResponse(response) {
|
|
386
428
|
/* response is :
|
|
387
429
|
[
|
|
@@ -474,14 +516,18 @@ class DiodeConnection extends EventEmitter {
|
|
|
474
516
|
}
|
|
475
517
|
}
|
|
476
518
|
|
|
477
|
-
getServerEthereumAddress() {
|
|
519
|
+
getServerEthereumAddress(quiet = false) {
|
|
478
520
|
try {
|
|
479
521
|
if (this._serverEthAddress) {
|
|
480
522
|
return this._serverEthAddress;
|
|
481
523
|
}
|
|
482
524
|
const serverCert = this.socket.getPeerCertificate(true);
|
|
483
525
|
if (!serverCert.raw) {
|
|
484
|
-
|
|
526
|
+
const err = new Error('Failed to get server certificate.');
|
|
527
|
+
if (!quiet) {
|
|
528
|
+
throw err;
|
|
529
|
+
}
|
|
530
|
+
return null;
|
|
485
531
|
}
|
|
486
532
|
|
|
487
533
|
const publicKeyBuffer = Buffer.isBuffer(serverCert.pubkey)
|
|
@@ -495,9 +541,40 @@ class DiodeConnection extends EventEmitter {
|
|
|
495
541
|
this._serverEthAddress = address;
|
|
496
542
|
return this._serverEthAddress;
|
|
497
543
|
} catch (error) {
|
|
498
|
-
|
|
499
|
-
|
|
544
|
+
if (!quiet) {
|
|
545
|
+
logger.error(() => `Error extracting server Ethereum address: ${error}`);
|
|
546
|
+
throw error;
|
|
547
|
+
}
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
getServerRelayHost() {
|
|
553
|
+
if (this.socket && this.socket.remoteAddress) {
|
|
554
|
+
const address = this.socket.remoteAddress;
|
|
555
|
+
if (address.startsWith('::ffff:')) {
|
|
556
|
+
return address.slice(7);
|
|
557
|
+
}
|
|
558
|
+
if (address.includes(':')) {
|
|
559
|
+
return this.host;
|
|
560
|
+
}
|
|
561
|
+
return address;
|
|
562
|
+
}
|
|
563
|
+
return this.host;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
async _waitForServerEthereumAddress(options = {}) {
|
|
567
|
+
const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : 2000;
|
|
568
|
+
const intervalMs = Number.isFinite(options.intervalMs) ? options.intervalMs : 50;
|
|
569
|
+
const start = Date.now();
|
|
570
|
+
let address = this.getServerEthereumAddress(true);
|
|
571
|
+
if (address) return address;
|
|
572
|
+
while (Date.now() - start < timeoutMs) {
|
|
573
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
574
|
+
address = this.getServerEthereumAddress(true);
|
|
575
|
+
if (address) return address;
|
|
500
576
|
}
|
|
577
|
+
return null;
|
|
501
578
|
}
|
|
502
579
|
|
|
503
580
|
// Method to extract private key bytes from keyPair
|
|
@@ -517,8 +594,10 @@ class DiodeConnection extends EventEmitter {
|
|
|
517
594
|
const chainId = 1284;
|
|
518
595
|
const fleetContractBuffer = ethUtil.toBuffer('0x6000000000000000000000000000000000000000'); // 20-byte Buffer
|
|
519
596
|
|
|
520
|
-
|
|
521
|
-
|
|
597
|
+
const localAddressBytes = Buffer.isBuffer(localAddress) || localAddress instanceof Uint8Array
|
|
598
|
+
? toBufferView(localAddress)
|
|
599
|
+
: Buffer.from(localAddress || '', 'utf8');
|
|
600
|
+
const localAddressHash = crypto.createHash('sha256').update(localAddressBytes).digest();
|
|
522
601
|
|
|
523
602
|
// Data to sign
|
|
524
603
|
const dataToSign = [
|
|
@@ -559,7 +638,18 @@ class DiodeConnection extends EventEmitter {
|
|
|
559
638
|
async createTicketCommand() {
|
|
560
639
|
const chainId = 1284;
|
|
561
640
|
const fleetContract = ethUtil.toBuffer('0x6000000000000000000000000000000000000000')
|
|
562
|
-
|
|
641
|
+
let localAddress = '';
|
|
642
|
+
if (typeof this.localAddressProvider === 'function') {
|
|
643
|
+
try {
|
|
644
|
+
localAddress = this.localAddressProvider();
|
|
645
|
+
} catch (error) {
|
|
646
|
+
logger.warn(() => `Failed to get local address hint: ${error}`);
|
|
647
|
+
localAddress = '';
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
if (localAddress === null || localAddress === undefined) {
|
|
651
|
+
localAddress = '';
|
|
652
|
+
}
|
|
563
653
|
|
|
564
654
|
// Increment totalConnections
|
|
565
655
|
this.totalConnections += 1;
|
|
@@ -569,7 +659,10 @@ class DiodeConnection extends EventEmitter {
|
|
|
569
659
|
const totalBytes = this.totalBytes;
|
|
570
660
|
|
|
571
661
|
// Get server Ethereum address as Buffer
|
|
572
|
-
const serverIdBuffer = this.
|
|
662
|
+
const serverIdBuffer = await this._waitForServerEthereumAddress();
|
|
663
|
+
if (!serverIdBuffer) {
|
|
664
|
+
throw new Error('Failed to get server certificate.');
|
|
665
|
+
}
|
|
573
666
|
|
|
574
667
|
// Get epoch
|
|
575
668
|
const epoch = await this.RPC.getEpoch();
|
package/examples/RPCTest.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const { DiodeClientManager, DiodeRPC } = require('../index');
|
|
2
2
|
const { makeReadable } = require('../utils');
|
|
3
3
|
|
|
4
4
|
async function main() {
|
|
5
|
-
const host = 'us2.prenet.diode.io';
|
|
6
|
-
const port = 41046;
|
|
7
5
|
const keyLocation = './db/keys.json';
|
|
8
6
|
|
|
9
|
-
const
|
|
10
|
-
await
|
|
11
|
-
const
|
|
7
|
+
const client = new DiodeClientManager({ keyLocation });
|
|
8
|
+
await client.connect();
|
|
9
|
+
const [connection] = client.getConnections();
|
|
10
|
+
if (!connection) {
|
|
11
|
+
throw new Error('No relay connection available');
|
|
12
|
+
}
|
|
13
|
+
const rpc = connection.RPC || new DiodeRPC(connection);
|
|
12
14
|
|
|
13
15
|
try {
|
|
14
16
|
const address = connection.getEthereumAddress();
|
|
@@ -22,7 +24,7 @@ async function main() {
|
|
|
22
24
|
} catch (error) {
|
|
23
25
|
console.error('RPC Error:', error);
|
|
24
26
|
} finally {
|
|
25
|
-
|
|
27
|
+
client.close();
|
|
26
28
|
}
|
|
27
29
|
}
|
|
28
30
|
|