forked from aspectron/kdx
-
Notifications
You must be signed in to change notification settings - Fork 0
/
rpc.js
313 lines (266 loc) · 10.7 KB
/
rpc.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
#!/usr/bin/env node
const fs = require('fs');
const { Command, Option } = require('commander');
const { RPC } = require('@kaspa/grpc-node');
const pkg = require('./package.json');
const { fstat } = require('fs');
const { colors } = require('@aspectron/colors.ts');
const { Logger, FlowLogger } = require('@aspectron/flow-logger');
const NATS = require('nats');
const jc = NATS.JSONCodec();
const log = new FlowLogger('RPC');
const networks = {
mainnet: { port: 16110 },
testnet: { port: 16210 },
simnet: { port: 16510 },
devnet: { port: 16610 }
};
const program = new Command();
class KaspaInterface {
get options() {
if(!this.options_) {
this.options_ = program.opts();
// Object.entries(this._options).forEach(([k,v])=>{ if(v === undefined) delete this._options[k]; });
}
return this.options_;
}
get network() {
const { options } = this;
let network = 'mainnet';
Object.entries(options).forEach(([k,v])=>{
if(v === undefined)
delete options[k];
else
if(networks[k])
network = k;
});
return network;
}
get rpc() {
if(this.rpc_)
return this.rpc_;
const { network } = this;
const { port } = networks[network]; //Wallet.networkTypes[network];
const { options } = this;
const host = options.server || `127.0.0.1:${port}`;
this.rpc_ = new RPC({ clientConfig:{ host, reconnect : false, verbose : false, disableConnectionCheck : true } });
// this.rpc_.onConnectFailure((reason)=>{
// const msg = `gRPC - no connection to ${Wallet.networkTypes[network].name} at ${host} (${reason})`;
// console.log('');
// console.log(msg);
// });
return this.rpc_;
}
getProto() {
const rpc = new RPC({ clientConfig:{disableConnectionCheck : true } });
rpc.disconnect();
return rpc.client.proto;
}
async main() {
const proto = this.getProto();
const methods = proto.KaspadMessage.type.field
.filter(({name})=>/request/i.test(name));
if(process.argv.includes('--verbose'))
log.enable('verbose');
program
.version(pkg.version,'--version')
.description(`Kaspa gRPC client ${pkg.version}`)
.usage('[options] <gRPC method> [gRPC method options]')
.option('--verbose','display arguments and additional info')
.addOption(new Option('--testnet','use testnet network').hideHelp())
// .option('--testnet','use testnet network').hide()
// .option('--devnet','use devnet network')
// .option('--simnet','use simnet network')
//.option('--server <host>:<port>','use custom gRPC server')
//.option('--subscribe','create a subscription channel')
.option('--file <filename>','read arguments from file')
;
program.addHelpText('after',`
Please run ${'kaspa-rpc help'.yellow} for addition information and examples.
`)
program
.command('run')
.description('call gRPC with raw JSON arguments "run -m <method> -j <json_data>" ')
.option('-m, --method <method>', "rpc request")
.option('-j, --json <json>', "rpc request args as json string, default will be '{}' ")
.action(async (cmd, options) => {
let {json='{}', method=''} = cmd;
//console.log("cmd", cmd)
if(!method){
console.log("Invalid method")
this.rpc.disconnect();
}
//console.log("method, json_data:", method, json_data)
let args = JSON.parse(json)
console.log("method, args:", method, args)
console.log("\nCalling:", method)
console.log("Arguments:\n", JSON.stringify(args, null, " "))
let result = await this.rpc.request(method, args)
.catch(error=>{
console.log("Error:", error)
})
console.log("Result:\n", JSON.stringify(result, null, " "))
this.rpc.disconnect();
});
program
.command('help')
.description('list available gRPC commands and examples')
.action(async (cmd, options) => {
console.log('');
console.log(`Usage: rpc [options] <gRPC method> [gRPC method options]`);
console.log('');
console.log(`Kaspa gRPC client ${pkg.version}`);
console.log('');
console.log('Following gRPC commands are available:');
console.log('');
let padding = methods.map(method=>method.name.replace(/Request$/,'').length).reduce((a,b)=>Math.max(a,b),0);
console.log(` ${"Method".padEnd(padding)} Options`);
console.log(` ${"------".padEnd(padding)} -------`);
methods.forEach(method=>{
const {name, typeName} = method;
const fn = name.replace(/Request$/,'');
let fields = proto[typeName].type.field;
let opts = fields.map(f=>`--${f.name}`);
let descr = ` ${fn.padEnd(padding)} ${opts.length?opts.join(' '):''}`;
console.log(descr);
});
0 && console.log(`
Please note, when supplying JSON formatted arguments, you must escape quotes.
Examples:
Get additional help information on gRPC method:
$ ${`rpc addPeer --help`.yellow}
Get list of UTXOs for an address:
$ ${`rpc --verbose getUtxosByAddresses --addresses=[\\"kaspatest:qru9nrs0mjcrfnl7rpxhhe33l3sxzgrc3ypkvkx57u\\"]`.yellow}
Monitor DAG Blue Score:
$ ${`rpc --subscribe notifyVirtualSelectedParentBlueScoreChanged`.yellow}
Get list of UTXOs for an address (load address list from file):
$ ${`rpc --verbose getUtxosByAddresses --args=file.js`.yellow}
Where file.js can contain: ${`{ addresses : ['kaspatest:qru9nrs0mjcrfnl7rpxhhe33l3sxzgrc3ypkvkx57u'] }`.yellow}
(note that the file uses JavaScript syntax and can contain comments or NodeJS code producing an Object)
`);
});
methods.forEach(method=>{
const {name, typeName} = method;
const fn = name.replace(/Request$/,'');
let fields = proto[typeName].type.field;
let opts = fields.map(f=>`--${f.name}`);
let cmd = program.command(fn, { hidden : true }).description(opts.length?opts.join(' '):'');
fields.forEach(f=>{
cmd.option(`--${f.name} <${f.name}>`, `Request argument ${f.name} of ${f.type}, ${f.defaultValue ? `default value will be ${f.defaultValue}` : `by default this argument is absent`}`)
})
if(fields.length) {
cmd.option(`--proto`,`show proto declaration for ${method}`, ()=>{
fields.forEach(f=>{
console.log(f);
})
})
}
cmd.action(async (cmd, options) => {
let args = {};
fields.forEach(f=>{
if(cmd[f.name] !== undefined){
if(typeof cmd[f.name] == 'boolean')
args[f.name] = 1;
// args[f.name] = cmd[f.name] || 1;
else
args[f.name] = JSON.parse(cmd[f.name]);
}
})
if(this.options.file) {
let file = this.options.file;
if(!fs.existsSync(file)) {
console.log("Unable to locate",file);
process.exit(0);
}
let text = fs.readFileSync(file,'utf8');
try {
args = eval(`(${text})`);
if(typeof args != 'object') {
console.log("Args file must produce an object");
process.exit(0);
}
} catch(ex) {
console.log(ex.toString());
}
}
if(this.options.verbose) {
if(this.options.subscribe)
console.log("\nSubscribing:", name)
else
console.log("\nCalling:", name)
console.log("Arguments:\n", JSON.stringify(args, null, " "))
}
if(this.options.subscribe) {
this.rpc.subscribe(name, args, (data) => {
console.log(data);
})
.catch(error=>{
console.log("Error:", error.toString())
this.rpc.disconnect();
})
} else {
this.rpc.request(name, args)
.then((result)=>{
if(this.options.verbose)
console.log("Result:");
console.log(JSON.stringify(result,null,' '));
this.rpc.disconnect();
})
.catch(error=>{
console.log("Error:", error.toString())
this.rpc.disconnect();
})
}
})
})
program.parse(process.argv);
// console.log('options:',this.options);
//if()
}
async connectNATS(options) {
this.nats = await NATS.connect(options);
(async () => {
log.info(`nats connected to ${this.nats.getServer()}`);
for await (const status of this.nats.status()) {
//console.info(`NATS: ${status.type}: ${status.data}`);
}
})().then();
this.nats.closed().then((err) => {
console.log(`connection closed ${err ? " with error: " + err.message : ""}`);
});
const { info } = this.nats.protocol;
const entries = Object.entries(info);
let padding = entries.map(([k]) => k.length).reduce((a,k) => Math.max(k,a));
entries.forEach(([k,v]) => {
log.verbose(`${k}:`.padStart(padding+1,' '),(v+''));
})
}
async stopNATS() {
if(this.nats) {
await this.nats.drain();
this.nats.close();
delete this.nats;
}
}
async shutdown() {
await this.stopNATS();
this.rpc.close();
}
interrupt() {
return (signal) => {
if(signal)
this.log(`^${signal}...`);
this.shutdown().then(() => {
this.log("shutdown ok");
process.exit();
}, (err) => {
this.log("shutdown failure:", err);
process.exit();
});
}
}
}
(async()=>{
const ki = new KaspaInterface();
await ki.main();
})();