mirror of
https://github.com/XRPLF/rippled.git
synced 2025-12-06 17:27:55 +00:00
Merge branch 'master' of github.com:jedmccaleb/NewCoin
This commit is contained in:
17
js/amount.js
17
js/amount.js
@@ -1,4 +1,4 @@
|
||||
// Represent Newcoin amounts and currencies.
|
||||
// Represent Ripple amounts and currencies.
|
||||
// - Numbers in hex are big-endian.
|
||||
|
||||
var utils = require('./utils.js');
|
||||
@@ -14,7 +14,7 @@ var UInt160 = function () {
|
||||
};
|
||||
|
||||
UInt160.from_json = function (j) {
|
||||
return (new UInt160()).parse_json(j);
|
||||
return (new UInt160()).parse_json(j in accounts ? accounts[j].account : j);
|
||||
};
|
||||
|
||||
UInt160.prototype.clone = function() {
|
||||
@@ -140,6 +140,12 @@ Currency.prototype.to_human = function() {
|
||||
return this.value ? this.value : "XNS";
|
||||
};
|
||||
|
||||
var accounts = {};
|
||||
|
||||
var setAccounts = function (accounts_new) {
|
||||
accounts = accounts_new;
|
||||
};
|
||||
|
||||
var Amount = function () {
|
||||
// Json format:
|
||||
// integer : XNS
|
||||
@@ -394,9 +400,10 @@ Amount.prototype.parse_json = function(j) {
|
||||
return this;
|
||||
};
|
||||
|
||||
exports.Amount = Amount;
|
||||
exports.Currency = Currency;
|
||||
exports.UInt160 = UInt160;
|
||||
exports.setAccounts = setAccounts;
|
||||
exports.Amount = Amount;
|
||||
exports.Currency = Currency;
|
||||
exports.UInt160 = UInt160;
|
||||
|
||||
exports.consts = {
|
||||
'address_xns' : "rrrrrrrrrrrrrrrrrrrrrhoLvTp",
|
||||
|
||||
551
js/remote.js
551
js/remote.js
@@ -26,13 +26,17 @@ var Amount = amount.Amount;
|
||||
// 'remoteUnexpected'
|
||||
// 'remoteDisconnected'
|
||||
var Request = function (remote, command) {
|
||||
var self = this;
|
||||
|
||||
this.message = {
|
||||
'command' : command,
|
||||
'id' : undefined,
|
||||
};
|
||||
this.remote = remote;
|
||||
|
||||
this.on('request', this.request_default);
|
||||
this.on('request', function () {
|
||||
self.request_default();
|
||||
});
|
||||
};
|
||||
|
||||
Request.prototype = new EventEmitter;
|
||||
@@ -44,6 +48,12 @@ Request.prototype.on = function (e, c) {
|
||||
return this;
|
||||
};
|
||||
|
||||
Request.prototype.once = function (e, c) {
|
||||
EventEmitter.prototype.once.call(this, e, c);
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
// Send the request to a remote.
|
||||
Request.prototype.request = function (remote) {
|
||||
this.emit('request', remote);
|
||||
@@ -94,6 +104,19 @@ Request.prototype.transaction = function (t) {
|
||||
return this;
|
||||
};
|
||||
|
||||
//
|
||||
// Remote - access to a remote Ripple server via websocket.
|
||||
//
|
||||
// Events:
|
||||
// 'connectted'
|
||||
// 'disconnected'
|
||||
// 'state':
|
||||
// - 'online' : connectted and subscribed
|
||||
// - 'offline' : not subscribed or not connectted.
|
||||
// 'ledger_closed': A good indicate of ready to serve.
|
||||
// 'subscribed' : This indicates stand-alone is available.
|
||||
//
|
||||
|
||||
// --> trusted: truthy, if remote is trusted
|
||||
var Remote = function (trusted, websocket_ip, websocket_port, config, trace) {
|
||||
this.trusted = trusted;
|
||||
@@ -105,6 +128,11 @@ var Remote = function (trusted, websocket_ip, websocket_port, config, trace) {
|
||||
this.ledger_closed = undefined;
|
||||
this.ledger_current_index = undefined;
|
||||
this.stand_alone = undefined;
|
||||
this.online_target = false;
|
||||
this.online_state = 'closed'; // 'open', 'closed', 'connecting', 'closing'
|
||||
this.state = 'offline'; // 'online', 'offline'
|
||||
this.retry_timer = undefined;
|
||||
this.retry = undefined;
|
||||
|
||||
// Cache information for accounts.
|
||||
this.accounts = {
|
||||
@@ -159,198 +187,254 @@ var fees = {
|
||||
'offer' : Amount.from_json("100"),
|
||||
};
|
||||
|
||||
Remote.prototype.connect_helper = function () {
|
||||
// Set the emited state: 'online' or 'offline'
|
||||
Remote.prototype._set_state = function (state) {
|
||||
if (this.trace) console.log("remote: set_state: %s", state);
|
||||
|
||||
if (this.state !== state) {
|
||||
this.state = state;
|
||||
|
||||
this.emit('state', state);
|
||||
switch (state) {
|
||||
case 'online':
|
||||
this.online_state = 'open';
|
||||
this.emit('connected');
|
||||
break;
|
||||
|
||||
case 'offline':
|
||||
this.online_state = 'closed';
|
||||
this.emit('disconnected');
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Set the target online state. Defaults to false.
|
||||
Remote.prototype.connect = function (online) {
|
||||
var target = undefined === online || online;
|
||||
|
||||
if (this.online_target != target) {
|
||||
this.online_target = target;
|
||||
|
||||
// If we were in a stable state, go dynamic.
|
||||
switch (this.online_state) {
|
||||
case 'open':
|
||||
if (!target) this._connect_stop();
|
||||
break;
|
||||
|
||||
case 'closed':
|
||||
if (target) this._connect_retry();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
// Stop from open state.
|
||||
Remote.prototype._connect_stop = function () {
|
||||
delete this.ws.onerror;
|
||||
delete this.ws.onclose;
|
||||
|
||||
this.ws.terminate();
|
||||
delete this.ws;
|
||||
|
||||
this._set_state('offline');
|
||||
};
|
||||
|
||||
// Implictly we are not connected.
|
||||
Remote.prototype._connect_retry = function () {
|
||||
var self = this;
|
||||
|
||||
if (!self.online_target) {
|
||||
// Do not continue trying to connect.
|
||||
this._set_state('offline');
|
||||
}
|
||||
else if ('connecting' !== this.online_state) {
|
||||
// New to connecting state.
|
||||
this.online_state = 'connecting';
|
||||
this.retry = 0;
|
||||
|
||||
this._connect_start();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Delay and retry.
|
||||
this.retry += 1;
|
||||
this.retry_timer = setTimeout(function () {
|
||||
if (self.trace) console.log("remote: retry");
|
||||
|
||||
if (self.online_target) {
|
||||
self._connect_start();
|
||||
}
|
||||
else {
|
||||
self._connect_retry();
|
||||
}
|
||||
}, this.retry < 40 ? 1000/20 : 1000); // 20 times per second for 2 seconds then once per second.
|
||||
}
|
||||
};
|
||||
|
||||
Remote.prototype._connect_start = function () {
|
||||
// Note: as a browser client can't make encrypted connections to random ips
|
||||
// with self-signed certs as the user must have pre-approved the self-signed certs.
|
||||
|
||||
var self = this;
|
||||
var url = util.format("ws://%s:%s", this.websocket_ip, this.websocket_port);
|
||||
|
||||
if (this.trace) console.log("remote: connect: %s", this.url);
|
||||
if (this.trace) console.log("remote: connect: %s", url);
|
||||
|
||||
var ws = this.ws = new WebSocket(this.url);;
|
||||
var ws = this.ws = new WebSocket(url);
|
||||
|
||||
ws.response = {};
|
||||
|
||||
ws.onopen = function () {
|
||||
if (self.trace) console.log("remote: onopen: %s", ws.readyState);
|
||||
if (self.trace) console.log("remote: onopen: %s: online_target=%s", ws.readyState, self.online_target);
|
||||
|
||||
ws.onclose = undefined;
|
||||
ws.onerror = undefined;
|
||||
|
||||
clearTimeout(self.connect_timer); delete self.connect_timer;
|
||||
clearTimeout(self.retry_timer); delete self.retry_timer;
|
||||
ws.onerror = function () {
|
||||
if (self.trace) console.log("remote: onerror: %s", ws.readyState);
|
||||
|
||||
self.done(ws.readyState);
|
||||
delete ws.onclose;
|
||||
|
||||
self._connect_retry();
|
||||
};
|
||||
|
||||
ws.onclose = function () {
|
||||
if (self.trace) console.log("remote: onclose: %s", ws.readyState);
|
||||
|
||||
delete ws.onerror;
|
||||
|
||||
self._connect_retry();
|
||||
};
|
||||
|
||||
if (self.online_target) {
|
||||
self._set_state('online');
|
||||
|
||||
// Note, we could get disconnected before tis go through.
|
||||
self._server_subscribe(); // Automatically subscribe.
|
||||
}
|
||||
else {
|
||||
self._connect_stop();
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = function () {
|
||||
if (self.trace) console.log("remote: onerror: %s", ws.readyState);
|
||||
|
||||
delete ws.onclose;
|
||||
|
||||
ws.onclose = undefined;
|
||||
|
||||
if (self.expire) {
|
||||
if (self.trace) console.log("remote: was expired");
|
||||
|
||||
ws.onerror = undefined;
|
||||
self.done(ws.readyState);
|
||||
|
||||
} else {
|
||||
// Delay and retry.
|
||||
|
||||
clearTimeout(self.retry_timer);
|
||||
self.retry_timer = setTimeout(function () {
|
||||
if (self.trace) console.log("remote: retry");
|
||||
|
||||
self.connect_helper();
|
||||
}, 50); // Retry rate 50ms.
|
||||
}
|
||||
self._connect_retry();
|
||||
};
|
||||
|
||||
// Covers failure to open.
|
||||
|
||||
// Failure to open.
|
||||
ws.onclose = function () {
|
||||
if (self.trace) console.log("remote: onclose: %s", ws.readyState);
|
||||
|
||||
ws.onerror = undefined;
|
||||
delete ws.onerror;
|
||||
|
||||
clearTimeout(self.retry_timer);
|
||||
delete self.retry_timer;
|
||||
|
||||
self.done(ws.readyState);
|
||||
self._connect_retry();
|
||||
};
|
||||
|
||||
// Node's ws module doesn't pass arguments to onmessage.
|
||||
ws.on('message', function (json, flags) {
|
||||
var message = JSON.parse(json);
|
||||
var unexpected = false;
|
||||
var request;
|
||||
|
||||
if ('object' !== typeof message) {
|
||||
unexpected = true;
|
||||
}
|
||||
else {
|
||||
switch (message.type) {
|
||||
case 'response':
|
||||
{
|
||||
request = ws.response[message.id];
|
||||
|
||||
if (!request) {
|
||||
unexpected = true;
|
||||
}
|
||||
else if ('success' === message.result) {
|
||||
if (self.trace) console.log("message: %s", json);
|
||||
|
||||
request.emit('success', message);
|
||||
}
|
||||
else if (message.error) {
|
||||
if (self.trace) console.log("message: %s", json);
|
||||
|
||||
request.emit('error', {
|
||||
'error' : 'remoteError',
|
||||
'error_message' : 'Remote reported an error.',
|
||||
'remote' : message,
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ledgerClosed':
|
||||
// XXX If not trusted, need to verify we consider ledger closed.
|
||||
// XXX Also need to consider a slow server or out of order response.
|
||||
// XXX Be more defensive fields could be missing or of wrong type.
|
||||
// YYY Might want to do some cache management.
|
||||
|
||||
self.ledger_closed = message.ledger_closed;
|
||||
self.ledger_current_index = message.ledger_closed_index + 1;
|
||||
|
||||
self.emit('ledger_closed', self.ledger_closed, self.ledger_closed_index);
|
||||
break;
|
||||
|
||||
default:
|
||||
unexpected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!unexpected) {
|
||||
}
|
||||
// Unexpected response from remote.
|
||||
// XXX This isn't so robust. Hard fails should probably only happen in a debugging scenairo.
|
||||
else if (self.trusted) {
|
||||
// Remote is trusted, report an error.
|
||||
console.log("unexpected message from trusted remote: %s", json);
|
||||
|
||||
(request || self).emit('error', {
|
||||
'error' : 'remoteUnexpected',
|
||||
'error_message' : 'Unexpected response from remote.'
|
||||
});
|
||||
}
|
||||
else {
|
||||
// Treat as a disconnect.
|
||||
if (self.trace) console.log("unexpected message from untrusted remote: %s", json);
|
||||
|
||||
// XXX All pending request need this treatment and need to actionally disconnect.
|
||||
(request || self).emit('error', {
|
||||
'error' : 'remoteDisconnected',
|
||||
'error_message' : 'Remote disconnected.'
|
||||
});
|
||||
}
|
||||
});
|
||||
self._connect_message(ws, json, flags);
|
||||
});
|
||||
};
|
||||
|
||||
// Target state is connectted.
|
||||
// XXX Get rid of 'done' use event model.
|
||||
// done(readyState):
|
||||
// --> readyState: OPEN, CLOSED
|
||||
Remote.prototype.connect = function (done, timeout) {
|
||||
var self = this;
|
||||
|
||||
this.url = util.format("ws://%s:%s", this.websocket_ip, this.websocket_port);
|
||||
this.done = done;
|
||||
|
||||
if (timeout) {
|
||||
if (this.trace) console.log("remote: expire: false");
|
||||
|
||||
this.expire = false;
|
||||
// It is possible for messages to be dispatched after the connection is closed.
|
||||
Remote.prototype._connect_message = function (ws, json, flags) {
|
||||
var message = JSON.parse(json);
|
||||
var unexpected = false;
|
||||
var request;
|
||||
|
||||
this.connect_timer = setTimeout(function () {
|
||||
if (self.trace) console.log("remote: expire: timeout");
|
||||
|
||||
delete self.connect_timer;
|
||||
self.expire = true;
|
||||
}, timeout);
|
||||
|
||||
} else {
|
||||
if (this.trace) console.log("remote: expire: false");
|
||||
this.expire = true;
|
||||
if ('object' !== typeof message) {
|
||||
unexpected = true;
|
||||
}
|
||||
|
||||
this.connect_helper();
|
||||
};
|
||||
else {
|
||||
switch (message.type) {
|
||||
case 'response':
|
||||
{
|
||||
request = ws.response[message.id];
|
||||
|
||||
// Target stated is disconnected.
|
||||
// Note: if exiting or other side is going away, don't need to disconnect.
|
||||
Remote.prototype.disconnect = function (done) {
|
||||
var self = this;
|
||||
var ws = this.ws;
|
||||
if (!request) {
|
||||
unexpected = true;
|
||||
}
|
||||
else if ('success' === message.result) {
|
||||
if (this.trace) console.log("message: %s", json);
|
||||
|
||||
if (this.trace) console.log("remote: disconnect");
|
||||
|
||||
ws.onclose = function () {
|
||||
if (self.trace) console.log("remote: onclose: %s", ws.readyState);
|
||||
done(ws.readyState);
|
||||
};
|
||||
request.emit('success', message);
|
||||
}
|
||||
else if (message.error) {
|
||||
if (this.trace) console.log("message: %s", json);
|
||||
|
||||
// ws package has a hard coded 30 second timeout.
|
||||
ws.close();
|
||||
request.emit('error', {
|
||||
'error' : 'remoteError',
|
||||
'error_message' : 'Remote reported an error.',
|
||||
'remote' : message,
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ledgerClosed':
|
||||
// XXX If not trusted, need to verify we consider ledger closed.
|
||||
// XXX Also need to consider a slow server or out of order response.
|
||||
// XXX Be more defensive fields could be missing or of wrong type.
|
||||
// YYY Might want to do some cache management.
|
||||
|
||||
this.ledger_closed = message.ledger_closed;
|
||||
this.ledger_current_index = message.ledger_closed_index + 1;
|
||||
|
||||
this.emit('ledger_closed', message.ledger_closed, message.ledger_closed_index);
|
||||
break;
|
||||
|
||||
default:
|
||||
unexpected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!unexpected) {
|
||||
}
|
||||
// Unexpected response from remote.
|
||||
// XXX This isn't so robust. Hard fails should probably only happen in a debugging scenairo.
|
||||
else if (this.trusted) {
|
||||
// Remote is trusted, report an error.
|
||||
console.log("unexpected message from trusted remote: %s", json);
|
||||
|
||||
(request || this).emit('error', {
|
||||
'error' : 'remoteUnexpected',
|
||||
'error_message' : 'Unexpected response from remote.'
|
||||
});
|
||||
}
|
||||
else {
|
||||
// Treat as a disconnect.
|
||||
if (this.trace) console.log("unexpected message from untrusted remote: %s", json);
|
||||
|
||||
// XXX All pending request need this treatment and need to actionally disconnect.
|
||||
(request || this).emit('error', {
|
||||
'error' : 'remoteDisconnected',
|
||||
'error_message' : 'Remote disconnected.'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Send a request.
|
||||
// <-> request: what to send, consumed.
|
||||
Remote.prototype.request = function (request) {
|
||||
this.ws.response[request.message.id = this.id] = request;
|
||||
|
||||
this.id += 1; // Advance id.
|
||||
|
||||
if (this.trace) console.log("remote: request: %s", JSON.stringify(request.message));
|
||||
|
||||
this.ws.send(JSON.stringify(request.message));
|
||||
if (this.ws) {
|
||||
// Only bother if we are still connected.
|
||||
|
||||
this.ws.response[request.message.id = this.id] = request;
|
||||
|
||||
this.id += 1; // Advance id.
|
||||
|
||||
if (this.trace) console.log("remote: request: %s", JSON.stringify(request.message));
|
||||
|
||||
this.ws.send(JSON.stringify(request.message));
|
||||
}
|
||||
else {
|
||||
if (this.trace) console.log("remote: request: DROPPING: %s", JSON.stringify(request.message));
|
||||
}
|
||||
};
|
||||
|
||||
Remote.prototype.request_ledger_closed = function () {
|
||||
@@ -415,7 +499,7 @@ Remote.prototype.request_ledger_entry = function (type) {
|
||||
// This type not cached.
|
||||
}
|
||||
|
||||
this.request_default(remote);
|
||||
this.request_default();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -455,19 +539,28 @@ Remote.prototype.submit = function (transaction) {
|
||||
}
|
||||
|
||||
if (!transaction.transaction.Sequence) {
|
||||
var cache_request = this.account_cache(transaction.transaction.Account);
|
||||
|
||||
cache_request.on('success_account_cache', function () {
|
||||
// Look in the last closed ledger.
|
||||
this.account_seq_cache(transaction.transaction.Account, false)
|
||||
.on('success_account_seq_cache', function () {
|
||||
// Try again.
|
||||
self.submit(transaction);
|
||||
});
|
||||
})
|
||||
.on('error', function (message) {
|
||||
// XXX Maybe be smarter about this. Don't want to trust an untrusted server for this seq number.
|
||||
|
||||
cache_request.on('error', function (message) {
|
||||
// Forward errors.
|
||||
transaction.emit('error', message);
|
||||
});
|
||||
|
||||
cache_request.request();
|
||||
// Look in the current ledger.
|
||||
self.account_seq_cache(transaction.transaction.Account, 'CURRENT')
|
||||
.on('success_account_seq_cache', function () {
|
||||
// Try again.
|
||||
self.submit(transaction);
|
||||
})
|
||||
.on('error', function (message) {
|
||||
// Forward errors.
|
||||
transaction.emit('error', message);
|
||||
})
|
||||
.request();
|
||||
})
|
||||
.request();
|
||||
}
|
||||
else {
|
||||
var submit_request = new Request(this, 'submit');
|
||||
@@ -479,10 +572,6 @@ Remote.prototype.submit = function (transaction) {
|
||||
submit_request.on('success', function (message) { transaction.emit('success', message); });
|
||||
submit_request.on('error', function (message) { transaction.emit('error', message); });
|
||||
|
||||
// XXX If transaction has a 'final' event listeners, register transaction to listen to final results.
|
||||
// XXX Final messages only happen if a transaction makes it into a ledger.
|
||||
// XXX A transaction may be "lost" or even resubmitted in this case.
|
||||
// XXX For when ledger closes, can look up transaction meta data.
|
||||
submit_request.request();
|
||||
}
|
||||
}
|
||||
@@ -495,20 +584,23 @@ Remote.prototype.submit = function (transaction) {
|
||||
// Subscribe to a server to get 'ledger_closed' events.
|
||||
// 'subscribed' : This command was successful.
|
||||
// 'ledger_closed : ledger_closed and ledger_current_index are updated.
|
||||
Remote.prototype.server_subscribe = function () {
|
||||
Remote.prototype._server_subscribe = function () {
|
||||
var self = this;
|
||||
|
||||
var request = new Request(this, 'server_subscribe');
|
||||
|
||||
request.
|
||||
on('success', function (message) {
|
||||
self.ledger_closed = message.ledger_closed;
|
||||
self.ledger_current_index = message.ledger_current_index;
|
||||
self.stand_alone = !!message.stand_alone;
|
||||
|
||||
self.emit('subscribed');
|
||||
if (message.ledger_closed && message.ledger_current_index) {
|
||||
self.ledger_closed = message.ledger_closed;
|
||||
self.ledger_current_index = message.ledger_current_index;
|
||||
|
||||
self.emit('ledger_closed', self.ledger_closed, self.ledger_current_index-1);
|
||||
self.emit('ledger_closed', self.ledger_closed, self.ledger_current_index-1);
|
||||
}
|
||||
|
||||
self.emit('subscribed');
|
||||
})
|
||||
.request();
|
||||
|
||||
@@ -552,16 +644,13 @@ Remote.prototype.account_seq = function (account, advance) {
|
||||
}
|
||||
|
||||
// Return a request to refresh accounts[account].seq.
|
||||
Remote.prototype.account_cache = function (account) {
|
||||
Remote.prototype.account_seq_cache = function (account, current) {
|
||||
var self = this;
|
||||
var request = this.request_ledger_entry('account_root')
|
||||
var request = this.request_ledger_entry('account_root');
|
||||
|
||||
// Only care about a closed ledger.
|
||||
// YYY Might be more advanced and work with a changing current ledger.
|
||||
request.ledger(this.ledger_closed); // XXX Requires active server_subscribe
|
||||
request.account_root(account);
|
||||
|
||||
request.on('success', function (message) {
|
||||
request
|
||||
.account_root(account)
|
||||
.on('success', function (message) {
|
||||
var seq = message.node.Sequence;
|
||||
|
||||
if (!self.accounts[account])
|
||||
@@ -570,10 +659,18 @@ Remote.prototype.account_cache = function (account) {
|
||||
self.accounts[account].seq = seq;
|
||||
|
||||
// If the caller also waits for 'success', they might run before this.
|
||||
request.emit('success_account_cache');
|
||||
request.emit('success_account_seq_cache');
|
||||
});
|
||||
|
||||
return request;
|
||||
if (current)
|
||||
{
|
||||
request.ledger_index(this.ledger_current_index);
|
||||
}
|
||||
else {
|
||||
request.ledger(this.ledger_closed);
|
||||
}
|
||||
|
||||
return request;
|
||||
};
|
||||
|
||||
// Mark an account's root node as dirty.
|
||||
@@ -588,7 +685,14 @@ Remote.prototype.transaction = function () {
|
||||
//
|
||||
// Transactions
|
||||
//
|
||||
// Transaction events:
|
||||
// Construction:
|
||||
// remote.transaction() // Build a transaction object.
|
||||
// .offer_create(...) // Set major parameters.
|
||||
// .flags() // Set optional parameters.
|
||||
// .on() // Register for events.
|
||||
// .submit(); // Send to network.
|
||||
//
|
||||
// Events:
|
||||
// 'success' : Transaction submitted without error.
|
||||
// 'error' : Error submitting transaction.
|
||||
// 'proposed: Advisory proposed status transaction.
|
||||
@@ -599,21 +703,23 @@ Remote.prototype.transaction = function () {
|
||||
// - malformed error: local server thought it was malformed.
|
||||
// - The client should only trust this when talking to a trusted server.
|
||||
// 'final' : Final status of transaction.
|
||||
// - Only expect a final from honest clients after a tesSUCCESS or ter*.
|
||||
// - Only expect a final from dishonest servers after a tesSUCCESS or ter*.
|
||||
// 'lost' : Gave up looking for on ledger_closed.
|
||||
// 'pending' : Transaction was not found on ledger_closed.
|
||||
// 'state' : Follow the state of a transaction.
|
||||
// 'clientSubmitted' - Sent to remote
|
||||
// |- 'remoteError' - Remote rejected transaction.
|
||||
// \- 'clientProposed' - Remote provisionally accepted transaction.
|
||||
// |- 'clientMissing' - Transaction has not appeared in ledger as expected.
|
||||
// | |- 'clientLost' - No longer monitoring missing transaction.
|
||||
// 'client_submitted' - Sent to remote
|
||||
// |- 'remoteError' - Remote rejected transaction.
|
||||
// \- 'client_proposed' - Remote provisionally accepted transaction.
|
||||
// |- 'client_missing' - Transaction has not appeared in ledger as expected.
|
||||
// | |\- 'client_lost' - No longer monitoring missing transaction.
|
||||
// |/
|
||||
// |- 'tesSUCCESS' - Transaction in ledger as expected.
|
||||
// |- 'ter...' - Transaction failed.
|
||||
// |- 'tep...' - Transaction partially succeeded.
|
||||
// |- 'tesSUCCESS' - Transaction in ledger as expected.
|
||||
// |- 'ter...' - Transaction failed.
|
||||
// \- 'tep...' - Transaction partially succeeded.
|
||||
//
|
||||
// Notes:
|
||||
// - All transactions including locally errors and malformed errors may be
|
||||
// forwarded.
|
||||
// - All transactions including those with local and malformed errors may be
|
||||
// forwarded anyway.
|
||||
// - A malicous server can:
|
||||
// - give any proposed result.
|
||||
// - it may declare something correct as incorrect or something correct as incorrect.
|
||||
@@ -645,7 +751,7 @@ var Transaction = function (remote) {
|
||||
if (message.engine_result) {
|
||||
self.hash = message.transaction.hash;
|
||||
|
||||
self.set_state('clientProposed');
|
||||
self.set_state('client_proposed');
|
||||
|
||||
self.emit('proposed', {
|
||||
'result' : message.engine_result,
|
||||
@@ -734,8 +840,8 @@ Transaction.prototype.submit = function () {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.listeners('final').length) {
|
||||
// There are listeners for 'final' arrange to emit it.
|
||||
if (this.listeners('final').length || this.listeners('lost').length || this.listeners('pending').length) {
|
||||
// There are listeners for 'final', 'lost', or 'pending' arrange to emit them.
|
||||
|
||||
this.submit_index = this.remote.ledger_current_index;
|
||||
|
||||
@@ -745,22 +851,23 @@ Transaction.prototype.submit = function () {
|
||||
// XXX make sure self.hash is available.
|
||||
self.remote.request_transaction_entry(self.hash, ledger_closed)
|
||||
.on('success', function (message) {
|
||||
// XXX Fake results for now.
|
||||
if (!message.metadata.result)
|
||||
message.metadata.result = 'tesSUCCESS';
|
||||
|
||||
self.set_state(message.metadata.result); // XXX Untested.
|
||||
self.set_state(message.metadata.TransactionResult);
|
||||
self.emit('final', message);
|
||||
})
|
||||
.on('error', function (message) {
|
||||
if ('remoteError' === message.error
|
||||
&& 'transactionNotFound' === message.remote.error) {
|
||||
if (self.submit_index + SUBMIT_LOST < ledger_closed_index) {
|
||||
self.set_state('clientLost'); // Gave up.
|
||||
self.set_state('client_lost'); // Gave up.
|
||||
self.emit('lost');
|
||||
stop = true;
|
||||
}
|
||||
else if (self.submit_index + SUBMIT_MISSING < ledger_closed_index) {
|
||||
self.set_state('clientMissing'); // We don't know what happened to transaction, still might find.
|
||||
self.set_state('client_missing'); // We don't know what happened to transaction, still might find.
|
||||
self.emit('pending');
|
||||
}
|
||||
else {
|
||||
self.emit('pending');
|
||||
}
|
||||
}
|
||||
// XXX Could log other unexpectedness.
|
||||
@@ -776,7 +883,7 @@ Transaction.prototype.submit = function () {
|
||||
this.remote.on('ledger_closed', on_ledger_closed);
|
||||
}
|
||||
|
||||
this.set_state('clientSubmitted');
|
||||
this.set_state('client_submitted');
|
||||
|
||||
this.remote.submit(this);
|
||||
|
||||
@@ -832,18 +939,6 @@ Transaction.prototype.flags = function (flags) {
|
||||
//
|
||||
// Transactions
|
||||
//
|
||||
// Construction:
|
||||
// remote.transaction() // Build a transaction object.
|
||||
// .offer_create(...) // Set major parameters.
|
||||
// .flags() // Set optional parameters.
|
||||
// .on() // Register for events.
|
||||
// .submit(); // Send to network.
|
||||
//
|
||||
// Events:
|
||||
// 'success' // Transaction was successfully submitted: hash, proposed TER
|
||||
// 'error' // Error submitting transaction.
|
||||
// 'closed' // Result from closed ledger: TER
|
||||
//
|
||||
|
||||
// Allow config account defaults to be used.
|
||||
Transaction.prototype.account_default = function (account) {
|
||||
@@ -883,7 +978,7 @@ Transaction.prototype.payment = function (src, dst, deliver_amount) {
|
||||
return this;
|
||||
}
|
||||
|
||||
Remote.prototype.ripple_line_set = function (src, limit, quaility_in, quality_out) {
|
||||
Transaction.prototype.ripple_line_set = function (src, limit, quality_in, quality_out) {
|
||||
this.secret = this.account_secret(src);
|
||||
this.transaction.TransactionType = 'CreditSet';
|
||||
this.transaction.Account = this.account_default(src);
|
||||
@@ -892,11 +987,11 @@ Remote.prototype.ripple_line_set = function (src, limit, quaility_in, quality_ou
|
||||
if (undefined !== limit)
|
||||
this.transaction.LimitAmount = limit.to_json();
|
||||
|
||||
if (quaility_in)
|
||||
this.transaction.QualityIn = quaility_in;
|
||||
if (quality_in)
|
||||
this.transaction.QualityIn = quality_in;
|
||||
|
||||
if (quaility_out)
|
||||
this.transaction.QualityOut = quaility_out;
|
||||
if (quality_out)
|
||||
this.transaction.QualityOut = quality_out;
|
||||
|
||||
// XXX Throw an error if nothing is set.
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
#include "SerializedTypes.h"
|
||||
#include "utils.h"
|
||||
|
||||
SETUP_LOG();
|
||||
|
||||
uint64 STAmount::uRateOne = STAmount::getRate(STAmount(1), STAmount(1));
|
||||
|
||||
// --> sCurrency: "", "XNS", or three letter ISO code.
|
||||
@@ -63,15 +65,21 @@ STAmount::STAmount(SField::ref n, const Json::Value& v)
|
||||
|
||||
if (v.isObject())
|
||||
{
|
||||
value = v["value"];
|
||||
currency = v["currency"];
|
||||
issuer = v["issuer"];
|
||||
cLog(lsTRACE)
|
||||
<< boost::str(boost::format("value='%s', currency='%s', issuer='%s'")
|
||||
% v["value"].asString()
|
||||
% v["currency"].asString()
|
||||
% v["issuer"].asString());
|
||||
|
||||
value = v["value"];
|
||||
currency = v["currency"];
|
||||
issuer = v["issuer"];
|
||||
}
|
||||
else if (v.isArray())
|
||||
{
|
||||
value = v.get(Json::UInt(0), 0);
|
||||
currency = v.get(Json::UInt(1), Json::nullValue);
|
||||
issuer = v.get(Json::UInt(2), Json::nullValue);
|
||||
value = v.get(Json::UInt(0), 0);
|
||||
currency = v.get(Json::UInt(1), Json::nullValue);
|
||||
issuer = v.get(Json::UInt(2), Json::nullValue);
|
||||
}
|
||||
else if (v.isString())
|
||||
{
|
||||
@@ -93,6 +101,31 @@ STAmount::STAmount(SField::ref n, const Json::Value& v)
|
||||
|
||||
mIsNative = !currency.isString() || currency.asString().empty() || (currency.asString() == SYSTEM_CURRENCY_CODE);
|
||||
|
||||
if (!mIsNative) {
|
||||
if (!currencyFromString(mCurrency, currency.asString()))
|
||||
throw std::runtime_error("invalid currency");
|
||||
|
||||
if (!issuer.isString())
|
||||
throw std::runtime_error("invalid issuer");
|
||||
|
||||
if (issuer.size() == (160/4))
|
||||
{
|
||||
mIssuer.SetHex(issuer.asString());
|
||||
}
|
||||
else
|
||||
{
|
||||
NewcoinAddress is;
|
||||
|
||||
if(!is.setAccountID(issuer.asString()))
|
||||
throw std::runtime_error("invalid issuer");
|
||||
|
||||
mIssuer = is.getAccountID();
|
||||
}
|
||||
|
||||
if (mIssuer.isZero())
|
||||
throw std::runtime_error("invalid issuer");
|
||||
}
|
||||
|
||||
if (value.isInt())
|
||||
{
|
||||
if (value.asInt() >= 0)
|
||||
@@ -102,9 +135,15 @@ STAmount::STAmount(SField::ref n, const Json::Value& v)
|
||||
mValue = -value.asInt();
|
||||
mIsNegative = true;
|
||||
}
|
||||
|
||||
canonicalize();
|
||||
}
|
||||
else if (value.isUInt())
|
||||
{
|
||||
mValue = v.asUInt();
|
||||
|
||||
canonicalize();
|
||||
}
|
||||
else if (value.isString())
|
||||
{
|
||||
if (mIsNative)
|
||||
@@ -117,35 +156,18 @@ STAmount::STAmount(SField::ref n, const Json::Value& v)
|
||||
mValue = -val;
|
||||
mIsNegative = true;
|
||||
}
|
||||
|
||||
canonicalize();
|
||||
}
|
||||
else
|
||||
{
|
||||
setValue(value.asString());
|
||||
}
|
||||
}
|
||||
else
|
||||
throw std::runtime_error("invalid amount type");
|
||||
|
||||
if (mIsNative)
|
||||
return;
|
||||
|
||||
if (!currencyFromString(mCurrency, currency.asString()))
|
||||
throw std::runtime_error("invalid currency");
|
||||
|
||||
if (!issuer.isString())
|
||||
throw std::runtime_error("invalid issuer");
|
||||
|
||||
if (issuer.size() == (160/4))
|
||||
mIssuer.SetHex(issuer.asString());
|
||||
else
|
||||
{
|
||||
NewcoinAddress is;
|
||||
if(!is.setAccountID(issuer.asString()))
|
||||
throw std::runtime_error("invalid issuer");
|
||||
mIssuer = is.getAccountID();
|
||||
}
|
||||
if (mIssuer.isZero())
|
||||
throw std::runtime_error("invalid issuer");
|
||||
|
||||
canonicalize();
|
||||
cLog(lsTRACE) << "Parsed: " << this->getJson(0);
|
||||
}
|
||||
|
||||
std::string STAmount::createHumanCurrency(const uint160& uCurrency)
|
||||
@@ -196,7 +218,7 @@ std::string STAmount::createHumanCurrency(const uint160& uCurrency)
|
||||
|
||||
// Assumes trusted input.
|
||||
bool STAmount::setValue(const std::string& sAmount)
|
||||
{ // Note: mIsNative must be set already!
|
||||
{ // Note: mIsNative and mCurrency must be set already!
|
||||
uint64 uValue;
|
||||
int iOffset;
|
||||
size_t uDecimal = sAmount.find_first_of(mIsNative ? "^" : ".");
|
||||
|
||||
@@ -92,13 +92,15 @@ Ledger::Ledger(bool /* dummy */, Ledger& prevLedger) :
|
||||
Ledger::Ledger(const std::vector<unsigned char>& rawLedger) :
|
||||
mClosed(false), mValidHash(false), mAccepted(false), mImmutable(true)
|
||||
{
|
||||
setRaw(Serializer(rawLedger));
|
||||
Serializer s(rawLedger);
|
||||
setRaw(s);
|
||||
}
|
||||
|
||||
Ledger::Ledger(const std::string& rawLedger) :
|
||||
mClosed(false), mValidHash(false), mAccepted(false), mImmutable(true)
|
||||
{
|
||||
setRaw(Serializer(rawLedger));
|
||||
Serializer s(rawLedger);
|
||||
setRaw(s);
|
||||
}
|
||||
|
||||
void Ledger::updateHash()
|
||||
@@ -118,7 +120,7 @@ void Ledger::updateHash()
|
||||
mValidHash = true;
|
||||
}
|
||||
|
||||
void Ledger::setRaw(const Serializer &s)
|
||||
void Ledger::setRaw(Serializer &s)
|
||||
{
|
||||
SerializerIterator sit(s);
|
||||
mLedgerSeq = sit.get32();
|
||||
@@ -311,7 +313,7 @@ bool Ledger::getTransaction(const uint256& txID, Transaction::pointer& txn, Tran
|
||||
}
|
||||
else if (type == SHAMapTreeNode::tnTRANSACTION_MD)
|
||||
{ // in tree with metadata
|
||||
SerializerIterator it(item->getData());
|
||||
SerializerIterator it(item->peekSerializer());
|
||||
txn = theApp->getMasterTransaction().fetch(txID, false);
|
||||
if (!txn)
|
||||
txn = Transaction::sharedTransaction(it.getVL(), true);
|
||||
|
||||
@@ -112,7 +112,7 @@ public:
|
||||
|
||||
// ledger signature operations
|
||||
void addRaw(Serializer &s) const;
|
||||
void setRaw(const Serializer& s);
|
||||
void setRaw(Serializer& s);
|
||||
|
||||
uint256 getHash();
|
||||
const uint256& getParentHash() const { return mParentHash; }
|
||||
|
||||
@@ -22,10 +22,10 @@ LedgerProposal::LedgerProposal(const uint256& pLgr, uint32 seq, const uint256& t
|
||||
|
||||
LedgerProposal::LedgerProposal(const NewcoinAddress& naSeed, const uint256& prevLgr,
|
||||
const uint256& position, uint32 closeTime) :
|
||||
mPreviousLedger(prevLgr), mCurrentHash(position), mCloseTime(closeTime), mProposeSeq(0)
|
||||
mPreviousLedger(prevLgr), mCurrentHash(position), mCloseTime(closeTime), mProposeSeq(0),
|
||||
mPublicKey(NewcoinAddress::createNodePublic(naSeed)),
|
||||
mPrivateKey(NewcoinAddress::createNodePrivate(naSeed))
|
||||
{
|
||||
mPublicKey = NewcoinAddress::createNodePublic(naSeed);
|
||||
mPrivateKey = NewcoinAddress::createNodePrivate(naSeed);
|
||||
mPeerID = mPublicKey.getNodeID();
|
||||
mTime = boost::posix_time::second_clock::universal_time();
|
||||
}
|
||||
|
||||
69
src/Log.cpp
69
src/Log.cpp
@@ -4,6 +4,7 @@
|
||||
#include <fstream>
|
||||
|
||||
#include <boost/date_time/posix_time/posix_time.hpp>
|
||||
#include <boost/algorithm/string.hpp>
|
||||
|
||||
boost::recursive_mutex Log::sLock;
|
||||
|
||||
@@ -15,6 +16,28 @@ uint32 Log::logRotateCounter = 0;
|
||||
|
||||
LogPartition* LogPartition::headLog = NULL;
|
||||
|
||||
LogPartition::LogPartition(const char *name) : mNextLog(headLog), mMinSeverity(lsWARNING)
|
||||
{
|
||||
const char *ptr = strrchr(name, '/');
|
||||
mName = (ptr == NULL) ? name : (ptr + 1);
|
||||
|
||||
size_t p = mName.find(".cpp");
|
||||
if (p != std::string::npos)
|
||||
mName.erase(mName.begin() + p, mName.end());
|
||||
|
||||
headLog = this;
|
||||
}
|
||||
|
||||
std::vector< std::pair<std::string, std::string> > LogPartition::getSeverities()
|
||||
{
|
||||
std::vector< std::pair<std::string, std::string> > sevs;
|
||||
|
||||
for (LogPartition *l = headLog; l != NULL; l = l->mNextLog)
|
||||
sevs.push_back(std::make_pair(l->mName, Log::severityToString(l->mMinSeverity)));
|
||||
|
||||
return sevs;
|
||||
}
|
||||
|
||||
Log::~Log()
|
||||
{
|
||||
std::string logMsg = boost::posix_time::to_simple_string(boost::posix_time::second_clock::universal_time());
|
||||
@@ -26,6 +49,7 @@ Log::~Log()
|
||||
case lsWARNING: logMsg += " WARN "; break;
|
||||
case lsERROR: logMsg += " EROR "; break;
|
||||
case lsFATAL: logMsg += " FATL "; break;
|
||||
case lsINVALID: assert(false); return;
|
||||
}
|
||||
logMsg += oss.str();
|
||||
boost::recursive_mutex::scoped_lock sl(sLock);
|
||||
@@ -84,6 +108,44 @@ void Log::setMinSeverity(LogSeverity s)
|
||||
LogPartition::setSeverity(s);
|
||||
}
|
||||
|
||||
LogSeverity Log::getMinSeverity()
|
||||
{
|
||||
boost::recursive_mutex::scoped_lock sl(sLock);
|
||||
return sMinSeverity;
|
||||
}
|
||||
|
||||
std::string Log::severityToString(LogSeverity s)
|
||||
{
|
||||
switch (s)
|
||||
{
|
||||
case lsTRACE: return "Trace";
|
||||
case lsDEBUG: return "Debug";
|
||||
case lsINFO: return "Info";
|
||||
case lsWARNING: return "Warning";
|
||||
case lsERROR: return "Error";
|
||||
case lsFATAL: return "Fatal";
|
||||
default: assert(false); return "Unknown";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
LogSeverity Log::stringToSeverity(const std::string& s)
|
||||
{
|
||||
if (boost::iequals(s, "trace"))
|
||||
return lsTRACE;
|
||||
if (boost::iequals(s, "debug"))
|
||||
return lsDEBUG;
|
||||
if (boost::iequals(s, "info") || boost::iequals(s, "information"))
|
||||
return lsINFO;
|
||||
if (boost::iequals(s, "warn") || boost::iequals(s, "warning") || boost::iequals(s, "warnings"))
|
||||
return lsWARNING;
|
||||
if (boost::iequals(s, "error") || boost::iequals(s, "errors"))
|
||||
return lsERROR;
|
||||
if (boost::iequals(s, "fatal") || boost::iequals(s, "fatals"))
|
||||
return lsFATAL;
|
||||
return lsINVALID;
|
||||
}
|
||||
|
||||
void Log::setLogFile(boost::filesystem::path path)
|
||||
{
|
||||
std::ofstream* newStream = new std::ofstream(path.c_str(), std::fstream::app);
|
||||
@@ -103,14 +165,15 @@ void Log::setLogFile(boost::filesystem::path path)
|
||||
pathToLog = new boost::filesystem::path(path);
|
||||
}
|
||||
|
||||
void LogPartition::setSeverity(const char *partition, LogSeverity severity)
|
||||
bool LogPartition::setSeverity(const std::string& partition, LogSeverity severity)
|
||||
{
|
||||
for (LogPartition *p = headLog; p != NULL; p = p->mNextLog)
|
||||
if (p->mName == partition)
|
||||
if (boost::iequals(p->mName, partition))
|
||||
{
|
||||
p->mMinSeverity = severity;
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void LogPartition::setSeverity(LogSeverity severity)
|
||||
|
||||
17
src/Log.h
17
src/Log.h
@@ -28,6 +28,7 @@
|
||||
|
||||
enum LogSeverity
|
||||
{
|
||||
lsINVALID = -1, // used to indicate an invalid severity
|
||||
lsTRACE = 0, // Very low-level progress information, details inside an operation
|
||||
lsDEBUG = 1, // Function-level progress information, operations
|
||||
lsINFO = 2, // Server-level progress information, major operations
|
||||
@@ -46,20 +47,16 @@ protected:
|
||||
std::string mName;
|
||||
|
||||
public:
|
||||
LogPartition(const char *name) : mNextLog(headLog), mMinSeverity(lsWARNING)
|
||||
{
|
||||
const char *ptr = strrchr(name, '/');
|
||||
mName = (ptr == NULL) ? name : ptr;
|
||||
headLog = this;
|
||||
}
|
||||
LogPartition(const char *name);
|
||||
|
||||
bool doLog(enum LogSeverity s)
|
||||
bool doLog(LogSeverity s)
|
||||
{
|
||||
return s >= mMinSeverity;
|
||||
}
|
||||
|
||||
static void setSeverity(const char *partition, LogSeverity severity);
|
||||
static bool setSeverity(const std::string& partition, LogSeverity severity);
|
||||
static void setSeverity(LogSeverity severity);
|
||||
static std::vector< std::pair<std::string, std::string> > getSeverities();
|
||||
};
|
||||
|
||||
class Log
|
||||
@@ -95,6 +92,10 @@ public:
|
||||
return oss;
|
||||
}
|
||||
|
||||
static std::string severityToString(LogSeverity);
|
||||
static LogSeverity stringToSeverity(const std::string&);
|
||||
|
||||
static LogSeverity getMinSeverity();
|
||||
static void setMinSeverity(LogSeverity);
|
||||
static void setLogFile(boost::filesystem::path);
|
||||
static std::string rotateLog(void);
|
||||
|
||||
@@ -935,6 +935,10 @@ void NetworkOPs::pubAccountInfo(const NewcoinAddress& naAccountID, const Json::V
|
||||
|
||||
void NetworkOPs::pubLedger(Ledger::ref lpAccepted)
|
||||
{
|
||||
// Don't publish to clients ledgers we don't trust.
|
||||
if (NetworkOPs::omDISCONNECTED == getOperatingMode())
|
||||
return;
|
||||
|
||||
{
|
||||
boost::interprocess::sharable_lock<boost::interprocess::interprocess_upgradable_mutex> sl(mMonitorLock);
|
||||
|
||||
|
||||
@@ -1875,8 +1875,8 @@ Json::Value RPCServer::doSend(const Json::Value& params)
|
||||
if (asDst) {
|
||||
// Destination exists, ordinary send.
|
||||
|
||||
STPathSet spsPaths;
|
||||
uint160 srcCurrencyID;
|
||||
STPathSet spsPaths;
|
||||
uint160 srcCurrencyID;
|
||||
|
||||
if (!saSrcAmountMax.isNative() || !saDstAmount.isNative())
|
||||
{
|
||||
@@ -2610,6 +2610,49 @@ Json::Value RPCServer::doLogin(const Json::Value& params)
|
||||
}
|
||||
}
|
||||
|
||||
Json::Value RPCServer::doLogSeverity(const Json::Value& params)
|
||||
{
|
||||
if (params.size() == 0)
|
||||
{ // get log severities
|
||||
Json::Value ret = Json::objectValue;
|
||||
|
||||
ret["base"] = Log::severityToString(Log::getMinSeverity());
|
||||
|
||||
std::vector< std::pair<std::string, std::string> > logTable = LogPartition::getSeverities();
|
||||
for (std::vector< std::pair<std::string, std::string> >::iterator it = logTable.begin();
|
||||
it != logTable.end(); ++it)
|
||||
ret[it->first] = it->second;
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (params.size() == 1)
|
||||
{ // set base log severity
|
||||
LogSeverity sv = Log::stringToSeverity(params[0u].asString());
|
||||
if (sv == lsINVALID)
|
||||
{
|
||||
Log(lsWARNING) << "Unable to parse severity: " << params[0u].asString();
|
||||
return RPCError(rpcINVALID_PARAMS);
|
||||
}
|
||||
Log::setMinSeverity(sv);
|
||||
return RPCError(rpcSUCCESS);
|
||||
}
|
||||
|
||||
if (params.size() == 2)
|
||||
{ // set partition severity
|
||||
LogSeverity sv = Log::stringToSeverity(params[1u].asString());
|
||||
if (sv == lsINVALID)
|
||||
return RPCError(rpcINVALID_PARAMS);
|
||||
if (params[2u].asString() == "base")
|
||||
Log::setMinSeverity(sv);
|
||||
else if (!LogPartition::setSeverity(params[0u].asString(), sv))
|
||||
return RPCError(rpcINVALID_PARAMS);
|
||||
return RPCError(rpcSUCCESS);
|
||||
}
|
||||
|
||||
assert(false);
|
||||
return RPCError(rpcINVALID_PARAMS);
|
||||
}
|
||||
|
||||
Json::Value RPCServer::doLogRotate(const Json::Value& params)
|
||||
{
|
||||
return Log::rotateLog();
|
||||
@@ -2641,7 +2684,8 @@ Json::Value RPCServer::doCommand(const std::string& command, Json::Value& params
|
||||
{ "data_fetch", &RPCServer::doDataFetch, 1, 1, true },
|
||||
{ "data_store", &RPCServer::doDataStore, 2, 2, true },
|
||||
{ "ledger", &RPCServer::doLedger, 0, 2, false, optNetwork },
|
||||
{ "logrotate", &RPCServer::doLogRotate, 0, 0, true },
|
||||
{ "logrotate", &RPCServer::doLogRotate, 0, 0, true },
|
||||
{ "logseverity", &RPCServer::doLogSeverity, 0, 2, true },
|
||||
{ "nickname_info", &RPCServer::doNicknameInfo, 1, 1, false, optCurrent },
|
||||
{ "nickname_set", &RPCServer::doNicknameSet, 2, 3, false, optCurrent },
|
||||
{ "offer_create", &RPCServer::doOfferCreate, 9, 10, false, optCurrent },
|
||||
|
||||
@@ -160,6 +160,7 @@ private:
|
||||
Json::Value doServerInfo(const Json::Value& params);
|
||||
Json::Value doSessionClose(const Json::Value& params);
|
||||
Json::Value doSessionOpen(const Json::Value& params);
|
||||
Json::Value doLogSeverity(const Json::Value& params);
|
||||
Json::Value doStop(const Json::Value& params);
|
||||
Json::Value doTransitSet(const Json::Value& params);
|
||||
Json::Value doTx(const Json::Value& params);
|
||||
|
||||
@@ -704,10 +704,7 @@ void SHAMapItem::dump()
|
||||
SHAMapTreeNode::pointer SHAMap::fetchNodeExternal(const SHAMapNode& id, const uint256& hash)
|
||||
{
|
||||
if (!theApp->running())
|
||||
{
|
||||
cLog(lsTRACE) << "Trying to fetch external node with application not running";
|
||||
throw SHAMapMissingNode(mType, id, hash);
|
||||
}
|
||||
|
||||
HashedObject::pointer obj(theApp->getHashedObjectStore().retrieve(hash));
|
||||
if (!obj)
|
||||
@@ -817,6 +814,38 @@ SHAMapTreeNode::pointer SHAMap::getNode(const SHAMapNode& nodeID)
|
||||
return node;
|
||||
}
|
||||
|
||||
bool SHAMap::getPath(const uint256& index, std::vector< std::vector<unsigned char> >& nodes, SHANodeFormat format)
|
||||
{
|
||||
// Return the path of nodes to the specified index in the specified format
|
||||
// Return value: true = node present, false = node not present
|
||||
|
||||
boost::recursive_mutex::scoped_lock sl(mLock);
|
||||
SHAMapTreeNode* inNode = root.get();
|
||||
|
||||
while (!inNode->isLeaf())
|
||||
{
|
||||
Serializer s;
|
||||
inNode->addRaw(s, format);
|
||||
nodes.push_back(s.peekData());
|
||||
|
||||
int branch = inNode->selectBranch(index);
|
||||
if (inNode->isEmptyBranch(branch)) // paths leads to empty branch
|
||||
return false;
|
||||
inNode = getNodePointer(inNode->getChildNodeID(branch), inNode->getChildHash(branch));
|
||||
if (!inNode)
|
||||
throw SHAMapMissingNode(mType, inNode->getChildNodeID(branch), inNode->getChildHash(branch), index);
|
||||
}
|
||||
|
||||
if (inNode->getTag() != index) // path leads to different leaf
|
||||
return false;
|
||||
|
||||
// path lead to the requested leaf
|
||||
Serializer s;
|
||||
inNode->addRaw(s, format);
|
||||
nodes.push_back(s.peekData());
|
||||
return true;
|
||||
}
|
||||
|
||||
void SHAMap::dump(bool hash)
|
||||
{
|
||||
#if 0
|
||||
|
||||
@@ -98,8 +98,7 @@ public:
|
||||
std::vector<unsigned char> getData() const { return mData.getData(); }
|
||||
const std::vector<unsigned char>& peekData() const { return mData.peekData(); }
|
||||
Serializer& peekSerializer() { return mData; }
|
||||
void addRaw(Serializer &s) { s.addRaw(mData); }
|
||||
void addRaw(std::vector<unsigned char>& s) { s.insert(s.end(), mData.begin(), mData.end()); }
|
||||
void addRaw(std::vector<unsigned char>& s) const { s.insert(s.end(), mData.begin(), mData.end()); }
|
||||
|
||||
void updateData(const std::vector<unsigned char>& data) { mData=data; }
|
||||
|
||||
@@ -126,6 +125,7 @@ enum SHANodeFormat
|
||||
{
|
||||
snfPREFIX = 1, // Form that hashes to its official hash
|
||||
snfWIRE = 2, // Compressed form used on the wire
|
||||
snfHASH = 3, // just the hash
|
||||
};
|
||||
|
||||
enum SHAMapType
|
||||
@@ -405,6 +405,8 @@ public:
|
||||
|
||||
void walkMap(std::vector<SHAMapMissingNode>& missingNodes, int maxMissing);
|
||||
|
||||
bool getPath(const uint256& index, std::vector< std::vector<unsigned char> >& nodes, SHANodeFormat format);
|
||||
|
||||
bool deepCompare(SHAMap& other);
|
||||
virtual void dump(bool withHashes = false);
|
||||
};
|
||||
|
||||
@@ -259,7 +259,7 @@ SHAMapTreeNode::SHAMapTreeNode(const SHAMapNode& id, const std::vector<unsigned
|
||||
}
|
||||
}
|
||||
|
||||
if (format == snfPREFIX)
|
||||
else if (format == snfPREFIX)
|
||||
{
|
||||
if (rawNode.size() < 4)
|
||||
{
|
||||
@@ -316,6 +316,12 @@ SHAMapTreeNode::SHAMapTreeNode(const SHAMapNode& id, const std::vector<unsigned
|
||||
}
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
assert(false);
|
||||
throw std::runtime_error("Unknown format");
|
||||
}
|
||||
|
||||
updateHash();
|
||||
}
|
||||
|
||||
@@ -333,7 +339,16 @@ bool SHAMapTreeNode::updateHash()
|
||||
break;
|
||||
}
|
||||
if(!empty)
|
||||
{
|
||||
nh = Serializer::getPrefixHash(sHP_InnerNode, reinterpret_cast<unsigned char *>(mHashes), sizeof(mHashes));
|
||||
#ifdef DEBUG
|
||||
Serializer s;
|
||||
s.add32(sHP_InnerNode);
|
||||
for(int i = 0; i < 16; ++i)
|
||||
s.add256(mHashes[i]);
|
||||
assert(nh == s.getSHA512Half());
|
||||
#endif
|
||||
}
|
||||
}
|
||||
else if (mType == tnTRANSACTION_NM)
|
||||
{
|
||||
@@ -366,11 +381,15 @@ bool SHAMapTreeNode::updateHash()
|
||||
|
||||
void SHAMapTreeNode::addRaw(Serializer& s, SHANodeFormat format)
|
||||
{
|
||||
assert((format == snfPREFIX) || (format == snfWIRE));
|
||||
assert((format == snfPREFIX) || (format == snfWIRE) || (format == snfHASH));
|
||||
if (mType == tnERROR)
|
||||
throw std::runtime_error("invalid I node type");
|
||||
|
||||
if (mType == tnINNER)
|
||||
if (format == snfHASH)
|
||||
{
|
||||
s.add256(getNodeHash());
|
||||
}
|
||||
else if (mType == tnINNER)
|
||||
{
|
||||
assert(!isEmpty());
|
||||
if (format == snfPREFIX)
|
||||
@@ -404,12 +423,12 @@ void SHAMapTreeNode::addRaw(Serializer& s, SHANodeFormat format)
|
||||
if (format == snfPREFIX)
|
||||
{
|
||||
s.add32(sHP_LeafNode);
|
||||
mItem->addRaw(s);
|
||||
s.addRaw(mItem->peekData());
|
||||
s.add256(mItem->getTag());
|
||||
}
|
||||
else
|
||||
{
|
||||
mItem->addRaw(s);
|
||||
s.addRaw(mItem->peekData());
|
||||
s.add256(mItem->getTag());
|
||||
s.add8(1);
|
||||
}
|
||||
@@ -419,11 +438,11 @@ void SHAMapTreeNode::addRaw(Serializer& s, SHANodeFormat format)
|
||||
if (format == snfPREFIX)
|
||||
{
|
||||
s.add32(sHP_TransactionID);
|
||||
mItem->addRaw(s);
|
||||
s.addRaw(mItem->peekData());
|
||||
}
|
||||
else
|
||||
{
|
||||
mItem->addRaw(s);
|
||||
s.addRaw(mItem->peekData());
|
||||
s.add8(0);
|
||||
}
|
||||
}
|
||||
@@ -432,12 +451,12 @@ void SHAMapTreeNode::addRaw(Serializer& s, SHANodeFormat format)
|
||||
if (format == snfPREFIX)
|
||||
{
|
||||
s.add32(sHP_TransactionNode);
|
||||
mItem->addRaw(s);
|
||||
s.addRaw(mItem->peekData());
|
||||
s.add256(mItem->getTag());
|
||||
}
|
||||
else
|
||||
{
|
||||
mItem->addRaw(s);
|
||||
s.addRaw(mItem->peekData());
|
||||
s.add256(mItem->getTag());
|
||||
s.add8(4);
|
||||
}
|
||||
|
||||
@@ -57,10 +57,7 @@ class Uint160Data : public Data
|
||||
{
|
||||
uint160 mValue;
|
||||
public:
|
||||
Uint160Data(uint160 value)
|
||||
{
|
||||
mValue=value;
|
||||
}
|
||||
Uint160Data(uint160 value) : mValue(value) { ; }
|
||||
bool isUint160(){ return(true); }
|
||||
uint160 getUint160(){ return(mValue); }
|
||||
};
|
||||
|
||||
@@ -21,7 +21,7 @@ SerializedLedgerEntry::SerializedLedgerEntry(SerializerIterator& sit, const uint
|
||||
SerializedLedgerEntry::SerializedLedgerEntry(const Serializer& s, const uint256& index)
|
||||
: STObject(sfLedgerEntry), mIndex(index)
|
||||
{
|
||||
SerializerIterator sit(s);
|
||||
SerializerIterator sit(const_cast<Serializer&>(s)); // we know 's' isn't going away
|
||||
set(sit);
|
||||
|
||||
uint16 type = getFieldU16(sfLedgerEntryType);
|
||||
|
||||
@@ -143,7 +143,9 @@ protected:
|
||||
int mPos;
|
||||
|
||||
public:
|
||||
SerializerIterator(const Serializer& s) : mSerializer(s), mPos(0) { ; }
|
||||
|
||||
// Reference is not const because we don't want to bind to a temporary
|
||||
SerializerIterator(Serializer& s) : mSerializer(s), mPos(0) { ; }
|
||||
|
||||
void reset(void) { mPos = 0; }
|
||||
void setPos(int p) { mPos = p; }
|
||||
|
||||
@@ -60,10 +60,8 @@ Transaction::Transaction(
|
||||
uint32 uSeq,
|
||||
const STAmount& saFee,
|
||||
uint32 uSourceTag) :
|
||||
mStatus(NEW), mResult(temUNCERTAIN)
|
||||
mAccountFrom(naSourceAccount), mFromPubKey(naPublicKey), mStatus(NEW), mResult(temUNCERTAIN)
|
||||
{
|
||||
mAccountFrom = naSourceAccount;
|
||||
mFromPubKey = naPublicKey;
|
||||
assert(mFromPubKey.isValid());
|
||||
|
||||
mTransaction = boost::make_shared<SerializedTransaction>(ttKind);
|
||||
|
||||
@@ -121,3 +121,4 @@ std::string transToken(TER terCode);
|
||||
std::string transHuman(TER terCode);
|
||||
|
||||
#endif
|
||||
// vim:ts=4
|
||||
|
||||
@@ -28,7 +28,7 @@ protected:
|
||||
|
||||
public:
|
||||
TransactionMetaSet() : mLedger(0), mResult(255) { ; }
|
||||
TransactionMetaSet(const uint256& txID, uint32 ledger) : mTransactionID(txID), mLedger(ledger) { ; }
|
||||
TransactionMetaSet(const uint256& txID, uint32 ledger) : mTransactionID(txID), mLedger(ledger), mResult(255) { ; }
|
||||
TransactionMetaSet(const uint256& txID, uint32 ledger, const std::vector<unsigned char>&);
|
||||
|
||||
void init(const uint256& transactionID, uint32 ledger);
|
||||
|
||||
@@ -801,6 +801,9 @@ void WSConnection::doLedgerEntry(Json::Value& jvResult, const Json::Value& jvReq
|
||||
}
|
||||
}
|
||||
|
||||
// The objective is to allow the client to know the server's status. The only thing that show the server is fully operating is the
|
||||
// stream of ledger_closeds. Therefore, that is all that is provided. A client can drop servers that do not provide recent
|
||||
// ledger_closeds.
|
||||
void WSConnection::doServerSubscribe(Json::Value& jvResult, const Json::Value& jvRequest)
|
||||
{
|
||||
if (!mNetwork.subLedger(this))
|
||||
@@ -812,10 +815,10 @@ void WSConnection::doServerSubscribe(Json::Value& jvResult, const Json::Value& j
|
||||
if (theConfig.RUN_STANDALONE)
|
||||
jvResult["stand_alone"] = 1;
|
||||
|
||||
// XXX Make sure these values are available before returning them.
|
||||
// XXX return connected status.
|
||||
jvResult["ledger_closed"] = mNetwork.getClosedLedger().ToString();
|
||||
jvResult["ledger_current_index"] = mNetwork.getCurrentLedgerID();
|
||||
if (NetworkOPs::omDISCONNECTED != mNetwork.getOperatingMode()) {
|
||||
jvResult["ledger_closed"] = mNetwork.getClosedLedger().ToString();
|
||||
jvResult["ledger_current_index"] = mNetwork.getCurrentLedgerID();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@ exports.servers = {
|
||||
'rpc_port' : 5005,
|
||||
'websocket_ip' : "127.0.0.1",
|
||||
'websocket_port' : 6005,
|
||||
'validation_seed' : "shhDFVsmS2GSu5vUyZSPXYfj1r79h",
|
||||
'validators' : "n9L8LZZCwsdXzKUN9zoVxs4YznYXZ9hEhsQZY7aVpxtFaSceiyDZ beta"
|
||||
// 'validation_seed' : "shhDFVsmS2GSu5vUyZSPXYfj1r79h",
|
||||
// 'validators' : "n9L8LZZCwsdXzKUN9zoVxs4YznYXZ9hEhsQZY7aVpxtFaSceiyDZ beta"
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ var Amount = amount.Amount;
|
||||
var fastTearDown = true;
|
||||
|
||||
// How long to wait for server to start.
|
||||
var serverDelay = 1500;
|
||||
var serverDelay = 1500; // XXX Not implemented.
|
||||
|
||||
buster.testRunner.timeout = 5000;
|
||||
|
||||
@@ -23,32 +23,22 @@ buster.testCase("Remote functions", {
|
||||
|
||||
alpha = remote.remoteConfig(config, "alpha");
|
||||
|
||||
alpha.connect(function (stat) {
|
||||
buster.assert(1 == stat); // OPEN
|
||||
done();
|
||||
}, serverDelay);
|
||||
alpha
|
||||
.once('ledger_closed', done)
|
||||
.connect();
|
||||
});
|
||||
},
|
||||
|
||||
'tearDown' :
|
||||
function (done) {
|
||||
if (fastTearDown) {
|
||||
// Fast tearDown
|
||||
server.stop("alpha", function (e) {
|
||||
buster.refute(e);
|
||||
done();
|
||||
});
|
||||
}
|
||||
else {
|
||||
alpha.disconnect(function (stat) {
|
||||
buster.assert(3 == stat); // CLOSED
|
||||
|
||||
alpha
|
||||
.on('disconnected', function () {
|
||||
server.stop("alpha", function (e) {
|
||||
buster.refute(e);
|
||||
done();
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
.connect(false);
|
||||
},
|
||||
|
||||
'request_ledger_current' :
|
||||
@@ -223,6 +213,46 @@ buster.testCase("Remote functions", {
|
||||
})
|
||||
.submit();
|
||||
},
|
||||
|
||||
"create account final" :
|
||||
function (done) {
|
||||
var got_proposed;
|
||||
var got_success;
|
||||
|
||||
alpha.transaction()
|
||||
.payment('root', 'alice', Amount.from_json("10000"))
|
||||
.flags('CreateAccount')
|
||||
.on('success', function (r) {
|
||||
console.log("create_account: %s", JSON.stringify(r));
|
||||
|
||||
got_success = true;
|
||||
})
|
||||
.on('error', function (m) {
|
||||
console.log("error: %s", m);
|
||||
|
||||
buster.assert(false);
|
||||
})
|
||||
.on('final', function (m) {
|
||||
console.log("final: %s", JSON.stringify(m));
|
||||
|
||||
buster.assert(got_success && got_proposed);
|
||||
done();
|
||||
})
|
||||
.on('proposed', function (m) {
|
||||
console.log("proposed: %s", JSON.stringify(m));
|
||||
|
||||
// buster.assert.equals(m.result, 'terNO_DST');
|
||||
buster.assert.equals(m.result, 'tesSUCCESS');
|
||||
|
||||
got_proposed = true;
|
||||
|
||||
alpha.ledger_accept();
|
||||
})
|
||||
.on('status', function (s) {
|
||||
console.log("status: %s", JSON.stringify(s));
|
||||
})
|
||||
.submit();
|
||||
},
|
||||
});
|
||||
|
||||
// vim:sw=2:sts=2:ts=8
|
||||
|
||||
99
test/send-test.js
Normal file
99
test/send-test.js
Normal file
@@ -0,0 +1,99 @@
|
||||
var buster = require("buster");
|
||||
|
||||
var config = require("./config.js");
|
||||
var server = require("./server.js");
|
||||
var amount = require("../js/amount.js");
|
||||
var remote = require("../js/remote.js");
|
||||
|
||||
var Amount = amount.Amount;
|
||||
|
||||
// How long to wait for server to start.
|
||||
var serverDelay = 1500;
|
||||
|
||||
buster.testRunner.timeout = 5000;
|
||||
|
||||
buster.testCase("Sending", {
|
||||
'setUp' :
|
||||
function (done) {
|
||||
server.start("alpha",
|
||||
function (e) {
|
||||
buster.refute(e);
|
||||
|
||||
alpha = remote.remoteConfig(config, "alpha");
|
||||
|
||||
alpha
|
||||
.once('ledger_closed', done)
|
||||
.connect();
|
||||
});
|
||||
},
|
||||
|
||||
'tearDown' :
|
||||
function (done) {
|
||||
alpha
|
||||
.on('disconnected', function () {
|
||||
server.stop("alpha", function (e) {
|
||||
buster.refute(e);
|
||||
done();
|
||||
});
|
||||
})
|
||||
.connect(false);
|
||||
},
|
||||
|
||||
"send to non-existant account without create." :
|
||||
function (done) {
|
||||
var got_proposed;
|
||||
var ledgers = 20;
|
||||
|
||||
alpha.transaction()
|
||||
.payment('root', 'alice', Amount.from_json("10000"))
|
||||
.on('success', function (r) {
|
||||
// Transaction sent.
|
||||
|
||||
console.log("success: %s", JSON.stringify(r));
|
||||
})
|
||||
.on('pending', function() {
|
||||
// Moving ledgers along.
|
||||
console.log("missing: %d", ledgers);
|
||||
|
||||
ledgers -= 1;
|
||||
if (ledgers) {
|
||||
alpha.ledger_accept();
|
||||
}
|
||||
else {
|
||||
buster.assert(false, "Final never received.");
|
||||
done();
|
||||
}
|
||||
})
|
||||
.on('lost', function () {
|
||||
// Transaction did not make it in.
|
||||
console.log("lost");
|
||||
|
||||
buster.assert(true);
|
||||
done();
|
||||
})
|
||||
.on('proposed', function (m) {
|
||||
// Transaction got an error.
|
||||
console.log("proposed: %s", JSON.stringify(m));
|
||||
|
||||
buster.assert.equals(m.result, 'terNO_DST');
|
||||
|
||||
got_proposed = true;
|
||||
|
||||
alpha.ledger_accept(); // Move it along.
|
||||
})
|
||||
.on('final', function (m) {
|
||||
console.log("final: %s", JSON.stringify(m));
|
||||
|
||||
buster.assert(false, "Should not have got a final.");
|
||||
done();
|
||||
})
|
||||
.on('error', function(m) {
|
||||
console.log("error: %s", m);
|
||||
|
||||
buster.assert(false);
|
||||
})
|
||||
.submit();
|
||||
},
|
||||
});
|
||||
|
||||
// vim:sw=2:sts=2:ts=8
|
||||
@@ -19,8 +19,9 @@ var child = require("child_process");
|
||||
var servers = {};
|
||||
|
||||
// Create a server object
|
||||
var Server = function (name) {
|
||||
var Server = function (name, mock) {
|
||||
this.name = name;
|
||||
this.mock = mock;
|
||||
};
|
||||
|
||||
// Return a server's rippled.cfg as string.
|
||||
@@ -92,20 +93,29 @@ Server.prototype.makeBase = function (done) {
|
||||
Server.prototype.start = function (done) {
|
||||
var self = this;
|
||||
|
||||
this.makeBase(function (e) {
|
||||
if (e) {
|
||||
throw e;
|
||||
}
|
||||
else {
|
||||
self.serverSpawnSync();
|
||||
done();
|
||||
}
|
||||
});
|
||||
if (this.mock) {
|
||||
done();
|
||||
}
|
||||
else {
|
||||
this.makeBase(function (e) {
|
||||
if (e) {
|
||||
throw e;
|
||||
}
|
||||
else {
|
||||
self.serverSpawnSync();
|
||||
done();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Stop a standalone server.
|
||||
Server.prototype.stop = function (done) {
|
||||
if (this.child) {
|
||||
if (this.mock) {
|
||||
console.log("server: stop: mock");
|
||||
done();
|
||||
}
|
||||
else if (this.child) {
|
||||
// Update the on exit to invoke done.
|
||||
this.child.on('exit', function (code, signal) {
|
||||
console.log("server: stop: server exited");
|
||||
@@ -121,14 +131,14 @@ Server.prototype.stop = function (done) {
|
||||
};
|
||||
|
||||
// Start the named server.
|
||||
exports.start = function (name, done) {
|
||||
exports.start = function (name, done, mock) {
|
||||
if (servers[name])
|
||||
{
|
||||
console.log("server: start: server already started.");
|
||||
}
|
||||
else
|
||||
{
|
||||
var server = new Server(name);
|
||||
var server = new Server(name, mock);
|
||||
|
||||
servers[name] = server;
|
||||
|
||||
|
||||
@@ -32,14 +32,20 @@ buster.testCase("WebSocket connection", {
|
||||
function (done) {
|
||||
var alpha = remote.remoteConfig(config, "alpha", 'TRACE');
|
||||
|
||||
alpha.connect(function (stat) {
|
||||
buster.assert.equals(stat, 1); // OPEN
|
||||
alpha
|
||||
.on('connected', function () {
|
||||
// OPEN
|
||||
buster.assert(true);
|
||||
|
||||
alpha.disconnect(function (stat) {
|
||||
buster.assert.equals(stat, 3); // CLOSED
|
||||
done();
|
||||
});
|
||||
}, serverDelay);
|
||||
alpha
|
||||
.on('disconnected', function () {
|
||||
// CLOSED
|
||||
buster.assert(true);
|
||||
done();
|
||||
})
|
||||
.connect(false);
|
||||
})
|
||||
.connect();
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user