mirror of
https://github.com/EvernodeXRPL/sashimono.git
synced 2026-08-21 02:10:51 +00:00
Include reputationd as a sub module (#390)
This commit is contained in:
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -2,3 +2,6 @@
|
||||
path = evernode-bootstrap-contract
|
||||
url = https://github.com/HotPocketDev/evernode-bootstrap-contract.git
|
||||
branch = release
|
||||
[submodule "reputationd"]
|
||||
path = reputationd
|
||||
url = https://github.com/EvernodeXRPL/reputationd
|
||||
|
||||
1
reputationd
Submodule
1
reputationd
Submodule
Submodule reputationd added at ddc00f398f
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"commonjs": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 13
|
||||
},
|
||||
"rules": {
|
||||
"no-async-promise-executor": "off"
|
||||
},
|
||||
"globals": {
|
||||
"__dirname": true
|
||||
}
|
||||
}
|
||||
5
reputationd/.gitignore
vendored
5
reputationd/.gitignore
vendored
@@ -1,5 +0,0 @@
|
||||
node_modules
|
||||
dist
|
||||
log
|
||||
reputationd.cfg
|
||||
secret.cfg
|
||||
@@ -1,114 +0,0 @@
|
||||
const process = require('process');
|
||||
// Uncaught Exception Handling.
|
||||
process.on('uncaughtException', (err) => {
|
||||
process.removeAllListeners('uncaughtException');
|
||||
process.removeAllListeners('unhandledRejection');
|
||||
console.error('Unhandled exception occurred:', err?.message);
|
||||
console.error('Stack trace:', err?.stack);
|
||||
console.log("REPUTATIOND_EXITED");
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Unhandled Rejection Handling.
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
process.removeAllListeners('unhandledRejection');
|
||||
process.removeAllListeners('uncaughtException');
|
||||
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
||||
console.log("REPUTATIOND_EXITED");
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
const logger = require('./lib/logger');
|
||||
const { appenv } = require('./lib/appenv');
|
||||
const { Setup } = require('./lib/setup');
|
||||
const { ReputationD } = require('./lib/reputationd');
|
||||
|
||||
async function main() {
|
||||
|
||||
if (process.argv[2] === 'version') {
|
||||
console.log(appenv.REPUTATIOND_VERSION);
|
||||
}
|
||||
|
||||
if (process.argv.length >= 3) {
|
||||
try {
|
||||
if (process.argv.length >= 5 && process.argv[2] === 'new') {
|
||||
const accountAddress = process.argv[3];
|
||||
const accountSecretPath = process.argv[4];
|
||||
const setup = new Setup();
|
||||
setup.newConfig(accountAddress, accountSecretPath);
|
||||
}
|
||||
else if (process.argv.length >= 4 && process.argv[2] === 'wait-for-funds') {
|
||||
await new Setup().waitForFunds(process.argv[3], parseFloat(process.argv[4]));
|
||||
}
|
||||
else if (process.argv.length >= 2 && process.argv[2] === 'prepare') {
|
||||
await new Setup().prepareReputationAccount();
|
||||
} else if (process.argv.length >= 4 && process.argv[2] === 'update-config') {
|
||||
// TODO: Remove this in 0.8.4.
|
||||
}
|
||||
else if (process.argv.length >= 3 && process.argv[2] === 'upgrade') {
|
||||
await new Setup().upgrade();
|
||||
}
|
||||
else if (process.argv.length === 3 && process.argv[2] === 'repinfo') {
|
||||
await new Setup().repInfo();
|
||||
}
|
||||
else if (process.argv[2] === 'help') {
|
||||
console.log(`Usage:
|
||||
node index.js - Run message board.
|
||||
node index.js version - Print version.
|
||||
node index.js new [address] [secretPath] - Create new config files.
|
||||
node index.js wait-for-funds [currencyType] [expectedBalance] - Wait until the funds are received.
|
||||
node index.js upgrade [governorAddress] - Upgrade message board data.
|
||||
node index.js help - Print help.`);
|
||||
}
|
||||
else {
|
||||
throw "Invalid args.";
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
process.removeAllListeners('uncaughtException');
|
||||
process.removeAllListeners('unhandledRejection');
|
||||
|
||||
// If error is a RippledError show internal error message, Otherwise show err.
|
||||
console.log(err?.data?.error_message || err);
|
||||
console.log("REPUTATIOND_EXITED");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
else {
|
||||
try {
|
||||
// Logs are formatted with the timestamp and a log file will be created inside log directory.
|
||||
logger.init(appenv.LOG_PATH, appenv.FILE_LOG_ENABLED);
|
||||
|
||||
console.log('Starting the Evernode Xahau reputationd.' + (appenv.IS_DEV_MODE ? ' (in dev mode)' : '') + ' --- patch applied ---');
|
||||
console.log('Data dir: ' + appenv.DATA_DIR);
|
||||
console.log('Using message board config: ' + appenv.MB_XRPL_CONFIG_PATH);
|
||||
|
||||
const rep = new ReputationD(appenv.CONFIG_PATH, appenv.MB_XRPL_CONFIG_PATH, appenv.INSTANCE_IMAGE);
|
||||
await rep.init();
|
||||
}
|
||||
catch (err) {
|
||||
process.removeAllListeners('uncaughtException');
|
||||
process.removeAllListeners('unhandledRejection');
|
||||
|
||||
// If error is a RippledError show internal error message, Otherwise show err.
|
||||
console.log(err?.data?.error_message || err);
|
||||
console.log("Evernode ReputationD exiting with error.");
|
||||
console.log("REPUTATIOND_EXITED");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().then(() => {
|
||||
process.removeAllListeners('uncaughtException');
|
||||
process.removeAllListeners('unhandledRejection');
|
||||
|
||||
console.log("REPUTATIOND_SUCCESS");
|
||||
}).catch((e) => {
|
||||
process.removeAllListeners('uncaughtException');
|
||||
process.removeAllListeners('unhandledRejection');
|
||||
|
||||
console.error(e);
|
||||
console.log("REPUTATIOND_EXITED");
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,24 +0,0 @@
|
||||
const process = require('process');
|
||||
const path = require('path');
|
||||
|
||||
let appenv = {
|
||||
IS_DEV_MODE: process.env.REPUTATIOND_DEV === "1",
|
||||
FILE_LOG_ENABLED: process.env.REPUTATIOND_FILE_LOG === "1",
|
||||
DATA_DIR: process.env.REPUTATIOND_DATA_DIR || __dirname,
|
||||
INSTANCE_IMAGE: 'evernodedev/reputation:hp.latest-ubt.20.04',
|
||||
}
|
||||
|
||||
appenv = {
|
||||
...appenv,
|
||||
CONFIG_PATH: appenv.DATA_DIR + '/reputationd.cfg',
|
||||
LOG_PATH: appenv.DATA_DIR + '/log/reputationd.log',
|
||||
REPUTATIOND_VERSION: '0.8.3',
|
||||
REPUTATIOND_SCHEDULER_INTERVAL_SECONDS: 2,
|
||||
MB_XRPL_CONFIG_PATH: path.join(appenv.DATA_DIR, '../') + "mb-xrpl/mb-xrpl.cfg",
|
||||
}
|
||||
|
||||
Object.freeze(appenv);
|
||||
|
||||
module.exports = {
|
||||
appenv
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
const fs = require('fs');
|
||||
|
||||
const COMMON_CONFIG_KEYS = ['network', 'governorAddress', 'rippledServer'];
|
||||
|
||||
class ConfigHelper {
|
||||
static readConfig(configPath, mbXrplConfigPath = null, readSecret = false) {
|
||||
if (!fs.existsSync(configPath))
|
||||
throw `Config file does not exist at ${configPath}`;
|
||||
|
||||
let config = JSON.parse(fs.readFileSync(configPath).toString());
|
||||
|
||||
if (readSecret) {
|
||||
if (!fs.existsSync(config.xrpl.secretPath))
|
||||
throw `Secret config file does not exist at ${config.xrpl.secretPath}`;
|
||||
|
||||
const secretCfg = JSON.parse(fs.readFileSync(config.xrpl.secretPath).toString());
|
||||
config.xrpl = { ...config.xrpl, ...secretCfg.xrpl };
|
||||
}
|
||||
|
||||
if (mbXrplConfigPath && fs.existsSync(mbXrplConfigPath)) {
|
||||
let mbXrplConfig = JSON.parse(fs.readFileSync(mbXrplConfigPath).toString());
|
||||
|
||||
if (readSecret) {
|
||||
if (!fs.existsSync(mbXrplConfig.xrpl.secretPath))
|
||||
throw `Secret config file does not exist at ${mbXrplConfig.xrpl.secretPath}`;
|
||||
|
||||
const mbXrplSecretCfg = JSON.parse(fs.readFileSync(mbXrplConfig.xrpl.secretPath).toString());
|
||||
mbXrplConfig.xrpl = { ...mbXrplConfig.xrpl, ...mbXrplSecretCfg.xrpl }
|
||||
}
|
||||
|
||||
for (const e of Object.entries(mbXrplConfig.xrpl)) {
|
||||
const key = COMMON_CONFIG_KEYS.includes(e[0]) ? e[0] : `host${e[0].charAt(0).toUpperCase()}${e[0].slice(1)}`;
|
||||
if (!(key in config.xrpl))
|
||||
config.xrpl[key] = e[1];
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
static writeConfig(config, configPath) {
|
||||
let publicCfg = JSON.parse(JSON.stringify(config)); // Make a copy. So, referenced object won't get changed.
|
||||
if ('secret' in publicCfg.xrpl)
|
||||
delete publicCfg.xrpl.secret;
|
||||
if ('network' in publicCfg.xrpl)
|
||||
delete publicCfg.xrpl.network;
|
||||
if ('governorAddress' in publicCfg.xrpl)
|
||||
delete publicCfg.xrpl.governorAddress;
|
||||
if ('rippledServer' in publicCfg.xrpl)
|
||||
delete publicCfg.xrpl.rippledServer;
|
||||
// Remove host related props.
|
||||
for (const e of Object.entries(publicCfg.xrpl)) {
|
||||
if (e[0].startsWith('host'))
|
||||
delete publicCfg.xrpl[e[0]];
|
||||
}
|
||||
fs.writeFileSync(configPath, JSON.stringify(publicCfg, null, 2), { mode: 0o644 }); // Set file permission so only current user can read/write and others can read.
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ConfigHelper
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
|
||||
const bson = require('bson');
|
||||
const HotPocket = require('hotpocket-js-client');
|
||||
const { CommonHelper } = require('./util-helper');
|
||||
|
||||
const DEFAULT_TIMEOUT = 120000;
|
||||
const INPUT_PROTOCOLS = HotPocket.protocols;
|
||||
|
||||
class ContractInstanceManager {
|
||||
#ip;
|
||||
#userPort;
|
||||
#userPrivateKey;
|
||||
#hpClient;
|
||||
|
||||
constructor(options = {}) {
|
||||
this.#ip = options.ip;
|
||||
this.#userPort = options.userPort;
|
||||
this.#userPrivateKey = options.userPrivateKey;
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (!this.#ip)
|
||||
throw "Instance IP is missing!";
|
||||
else if (!this.#userPort)
|
||||
throw "Instance user port is missing!";
|
||||
else if (!this.#userPrivateKey)
|
||||
throw "Instance user private key is missing!";
|
||||
|
||||
const userKeys = await CommonHelper.generateKeys(this.#userPrivateKey, 'binary');
|
||||
console.log('My public key is: ' + Buffer.from(userKeys.publicKey).toString('hex'));
|
||||
|
||||
const server = `wss://${this.#ip}:${this.#userPort}`;
|
||||
this.#hpClient = await HotPocket.createClient([server], userKeys, {
|
||||
protocol: HotPocket.protocols.bson
|
||||
});
|
||||
|
||||
// Establish HotPocket connection.
|
||||
if (!await this.#hpClient.connect())
|
||||
throw `${server} connection failed.`;
|
||||
}
|
||||
|
||||
async terminate() {
|
||||
if (this.#hpClient)
|
||||
await this.#hpClient.close()
|
||||
}
|
||||
|
||||
async sendContractInput(input, timeoutMs = DEFAULT_TIMEOUT, protocol = HotPocket.protocols.bson) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
|
||||
const inputTimer = setTimeout(() => {
|
||||
clearTimeout(inputTimer);
|
||||
this.#hpClient.clear(HotPocket.events.contractOutput);
|
||||
reject("Input timeout.");
|
||||
}, timeoutMs);
|
||||
|
||||
const failure = (e) => {
|
||||
clearTimeout(inputTimer);
|
||||
this.#hpClient.clear(HotPocket.events.contractOutput);
|
||||
reject(e);
|
||||
}
|
||||
const success = (result) => {
|
||||
clearTimeout(inputTimer);
|
||||
resolve(result);
|
||||
}
|
||||
|
||||
// This will get fired when contract sends an output.
|
||||
this.#hpClient.on(HotPocket.events.contractOutput, (r) => {
|
||||
|
||||
r.outputs.forEach(output => {
|
||||
let result;
|
||||
try {
|
||||
result = protocol === INPUT_PROTOCOLS.bson ? bson.deserialize(output) : JSON.parse(output);
|
||||
}
|
||||
catch (e) {
|
||||
failure(e);
|
||||
}
|
||||
if (result?.type == `${input.type}Result`) {
|
||||
if (result.status == "ok")
|
||||
success(result.message);
|
||||
else
|
||||
failure(`Input failed. reason: ${result.message}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const res = await this.#hpClient.submitContractInput(protocol === INPUT_PROTOCOLS.bson ? bson.serialize(input) : JSON.stringify(input));
|
||||
|
||||
const submission = await res.submissionStatus;
|
||||
if (submission.status != "accepted")
|
||||
failure("Input submission failed. reason: " + submission.reason);
|
||||
});
|
||||
}
|
||||
|
||||
async sendContractReadRequest(input, protocol = HotPocket.protocols.bson) {
|
||||
const output = await this.#hpClient.submitContractReadRequest(protocol === INPUT_PROTOCOLS.bson ? bson.serialize(input) : JSON.stringify(input));
|
||||
const result = protocol === INPUT_PROTOCOLS.bson ? bson.deserialize(output) : JSON.parse(output);
|
||||
return result?.message;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ContractInstanceManager,
|
||||
INPUT_PROTOCOLS
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
|
||||
const { CommonHelper } = require('./util-helper');
|
||||
const WebSocket = require('ws');
|
||||
|
||||
const DEFAULT_TIMEOUT = 120000;
|
||||
|
||||
class LobbyManager {
|
||||
#ip;
|
||||
#userPort;
|
||||
#userPrivateKey;
|
||||
#userKeys;
|
||||
#wsClient;
|
||||
|
||||
constructor(options = {}) {
|
||||
this.#ip = options.ip;
|
||||
this.#userPort = options.userPort;
|
||||
this.#userPrivateKey = options.userPrivateKey;
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (!this.#ip)
|
||||
throw "Instance IP is missing!";
|
||||
else if (!this.#userPort)
|
||||
throw "Instance user port is missing!";
|
||||
else if (!this.#userPrivateKey)
|
||||
throw "Instance user private key is missing!";
|
||||
|
||||
this.#userKeys = await CommonHelper.generateKeys(this.#userPrivateKey, 'binary');
|
||||
console.log('My public key is: ' + Buffer.from(this.#userKeys.publicKey).toString('hex'));
|
||||
|
||||
const server = `wss://${this.#ip}:${this.#userPort}`;
|
||||
this.#wsClient = new WebSocket(server, {
|
||||
rejectUnauthorized: false
|
||||
});
|
||||
}
|
||||
|
||||
terminate() {
|
||||
if (this.#wsClient)
|
||||
this.#wsClient.close()
|
||||
}
|
||||
|
||||
#handleMessage(message) {
|
||||
var message = JSON.parse(message);
|
||||
switch (message.type) {
|
||||
case 'upgrade':
|
||||
if (message.status === 'SUCCESS')
|
||||
return true;
|
||||
else
|
||||
throw message.data ?? 'UNKNOWN_ERROR';
|
||||
default:
|
||||
throw 'UNHANDLED_MESSAGE';
|
||||
}
|
||||
}
|
||||
|
||||
async upgradeContract(unl, peers, timeoutMs = DEFAULT_TIMEOUT) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const inputTimer = setTimeout(() => {
|
||||
clearTimeout(inputTimer);
|
||||
reject("Input timeout.");
|
||||
}, timeoutMs);
|
||||
|
||||
const failure = (e) => {
|
||||
clearTimeout(inputTimer);
|
||||
reject(e);
|
||||
}
|
||||
|
||||
const success = (result) => {
|
||||
clearTimeout(inputTimer);
|
||||
resolve(result);
|
||||
}
|
||||
|
||||
if (!this.#wsClient)
|
||||
failure('Web socket connection is not initiated');
|
||||
|
||||
try {
|
||||
// This will get fired when contract sends an output.
|
||||
this.#wsClient.on('message', (data) => {
|
||||
console.log('Received from server:', data.toString());
|
||||
try {
|
||||
const res = this.#handleMessage(data);
|
||||
if (res)
|
||||
success('CONTRACT_UPGRADED');
|
||||
else
|
||||
throw 'UNKNOWN_ERROR'
|
||||
}
|
||||
catch (e) {
|
||||
failure(e);
|
||||
}
|
||||
});
|
||||
|
||||
this.#wsClient.on('open', () => {
|
||||
console.log('Connection opened. Sending upgrade request...');
|
||||
try {
|
||||
this.#wsClient.send(JSON.stringify({
|
||||
type: 'upgrade',
|
||||
user: this.#userKeys.publicKey,
|
||||
data: {
|
||||
unl: unl,
|
||||
peers: peers
|
||||
}
|
||||
}));
|
||||
}
|
||||
catch (e) {
|
||||
failure(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
failure(e);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
LobbyManager
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const util = require('util');
|
||||
|
||||
const formatText = (text, logType = 'dbg') => {
|
||||
const date = new Date().toISOString().
|
||||
replace(/T/, ' '). // Replace T with a space.
|
||||
replace(/\..+/, ''). // Delete the dot and everything after.
|
||||
replace(/-/g, ''); // Delete the dashes.
|
||||
|
||||
return `${date} [${logType}] ${text}\n`;
|
||||
}
|
||||
|
||||
exports.init = (logPath, fileLogEnabled) => {
|
||||
|
||||
let flog = null;
|
||||
|
||||
if (fileLogEnabled) {
|
||||
const dirname = path.dirname(logPath);
|
||||
if (!fs.existsSync(dirname))
|
||||
fs.mkdirSync(dirname, { recursive: true });
|
||||
|
||||
flog = fs.createWriteStream(logPath, { flags: 'a' });
|
||||
}
|
||||
|
||||
console.log = function () {
|
||||
const text = formatText(util.format.apply(this, arguments));
|
||||
process.stdout.write(text);
|
||||
|
||||
if (flog)
|
||||
flog.write(text);
|
||||
};
|
||||
console.error = function () {
|
||||
const text = formatText(util.format.apply(this, arguments), 'err');
|
||||
process.stderr.write(text);
|
||||
|
||||
if (flog)
|
||||
flog.write(text);
|
||||
};
|
||||
}
|
||||
@@ -1,652 +0,0 @@
|
||||
const evernode = require('evernode-js-client');
|
||||
const crypto = require('crypto');
|
||||
const uuid = require('uuid');
|
||||
const { appenv } = require('./appenv');
|
||||
const { ConfigHelper } = require('./config-helper');
|
||||
const { CommonHelper } = require('./util-helper');
|
||||
const { ContractInstanceManager, INPUT_PROTOCOLS } = require('./contract-instance-manager');
|
||||
const { LobbyManager } = require('./lobby-manager');
|
||||
|
||||
const ContractStatus = {
|
||||
Created: 1,
|
||||
Updated: 2,
|
||||
Deployed: 3
|
||||
}
|
||||
|
||||
class ReputationD {
|
||||
#concurrencyQueue = {
|
||||
processing: false,
|
||||
queue: []
|
||||
};
|
||||
#applyFeeUpliftment = false;
|
||||
#reputationRetryDelay = 300000; // 5 mins
|
||||
#reputationRetryCount = 3;
|
||||
#feeUpliftment = 0;
|
||||
#preparationTimeQuota = 0.9; // Percentage of moment size.
|
||||
#reputationRegTimeQuota = 0.2; // Percentage of (1 - preparationTimeQuota) for reputation registration.
|
||||
#lobbyTimeQuota = 0.8; // Percentage of (1 - reputationRegTimeQuota) for reputation contract deployment.
|
||||
#universeSize = 64;
|
||||
#readScoreCmd = 'read_scores';
|
||||
|
||||
#configPath;
|
||||
#mbXrplConfigPath;
|
||||
#instanceImage;
|
||||
|
||||
constructor(configPath, mbXrplConfigPath, instanceImage) {
|
||||
this.#configPath = configPath;
|
||||
this.#mbXrplConfigPath = mbXrplConfigPath;
|
||||
this.#instanceImage = instanceImage;
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.#readConfig();
|
||||
if (!this.cfg.version || !this.cfg.xrpl.address || !this.cfg.xrpl.secret)
|
||||
throw "Required cfg fields cannot be empty.";
|
||||
|
||||
await evernode.Defaults.useNetwork(this.cfg.xrpl.network || appenv.NETWORK);
|
||||
|
||||
if (this.cfg.xrpl.governorAddress)
|
||||
evernode.Defaults.set({
|
||||
governorAddress: this.cfg.xrpl.governorAddress
|
||||
});
|
||||
|
||||
if (this.cfg.xrpl.rippledServer)
|
||||
evernode.Defaults.set({
|
||||
rippledServer: this.cfg.xrpl.rippledServer
|
||||
});
|
||||
|
||||
if (this.cfg.xrpl.fallbackRippledServers && this.cfg.xrpl.fallbackRippledServers.length)
|
||||
evernode.Defaults.set({
|
||||
fallbackRippledServers: this.cfg.xrpl.fallbackRippledServers
|
||||
});
|
||||
|
||||
this.xrplApi = new evernode.XrplApi();
|
||||
evernode.Defaults.set({
|
||||
xrplApi: this.xrplApi
|
||||
})
|
||||
await this.xrplApi.connect();
|
||||
|
||||
this.hostClient = new evernode.HostClient(this.cfg.xrpl.hostAddress, this.cfg.xrpl.hostSecret);
|
||||
await this.#connectHost();
|
||||
|
||||
console.log("Using,");
|
||||
console.log("\tGovernor account " + this.cfg.xrpl.governorAddress);
|
||||
console.log("\tReputation account " + this.hostClient.config.reputationAddress);
|
||||
console.log("Using xahaud " + this.cfg.xrpl.rippledServer);
|
||||
|
||||
// Get last heartbeat moment from the host info.
|
||||
let hostInfo = await this.hostClient.getRegistration();
|
||||
if (!hostInfo)
|
||||
throw "Host is not registered.";
|
||||
|
||||
this.reputationClient = await evernode.HookClientFactory.create(evernode.HookTypes.reputation, { config: this.hostClient.config });
|
||||
|
||||
await this.#connectReputation({ skipConfigs: true });
|
||||
|
||||
const repInfo = await this.hostClient.getReputationInfo();
|
||||
// Last registered moment n means reputation is sent in n-1 moment.
|
||||
this.lastReputationMoment = repInfo ? (repInfo.lastRegisteredMoment - 1) : 0;
|
||||
|
||||
this.xrplApi.on(evernode.XrplApiEvents.DISCONNECTED, async (e) => {
|
||||
console.log(`Exiting due to server disconnect (code ${e})...`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
|
||||
this.xrplApi.on(evernode.XrplApiEvents.SERVER_DESYNCED, async (e) => {
|
||||
console.log(`Exiting due to server desync condition...`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
this.xrplApi.on(evernode.XrplApiEvents.LEDGER, async (e) => {
|
||||
this.lastValidatedLedgerIndex = e.ledger_index;
|
||||
this.lastLedgerTime = evernode.UtilHelpers.getCurrentUnixTime('milli');
|
||||
});
|
||||
|
||||
// Start queue processor job.
|
||||
this.#startReputationClockScheduler();
|
||||
|
||||
// Schedule reputation jobs.
|
||||
this.#startReputationSendScheduler();
|
||||
|
||||
// Schedule reputation contract jobs.
|
||||
this.#startReputationContractScheduler();
|
||||
}
|
||||
|
||||
#prepareHostClientFunctionOptions() {
|
||||
let options = {}
|
||||
if (this.#applyFeeUpliftment) {
|
||||
options.transactionOptions = { feeUplift: this.#feeUpliftment }
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
// Try to acquire the lease update lock.
|
||||
async #acquireConcurrencyQueue() {
|
||||
await new Promise(async resolve => {
|
||||
while (this.#concurrencyQueue.processing) {
|
||||
await new Promise(resolveSleep => {
|
||||
setTimeout(resolveSleep, 1000);
|
||||
})
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
this.#concurrencyQueue.processing = true;
|
||||
}
|
||||
|
||||
// Release the lease update lock.
|
||||
async #releaseConcurrencyQueue() {
|
||||
this.#concurrencyQueue.processing = false;
|
||||
}
|
||||
|
||||
async #queueAction(action, maxAttempts = 5, delay = 0) {
|
||||
await this.#acquireConcurrencyQueue();
|
||||
|
||||
this.#concurrencyQueue.queue.push({
|
||||
callback: action,
|
||||
submissionRefs: {},
|
||||
attempts: 0,
|
||||
maxAttempts: maxAttempts,
|
||||
delay: delay
|
||||
});
|
||||
|
||||
await this.#releaseConcurrencyQueue();
|
||||
}
|
||||
|
||||
async #processConcurrencyQueue() {
|
||||
await this.#acquireConcurrencyQueue();
|
||||
|
||||
let toKeep = [];
|
||||
for (let action of this.#concurrencyQueue.queue) {
|
||||
try {
|
||||
await action.callback(action.submissionRefs);
|
||||
this.#applyFeeUpliftment = false;
|
||||
this.#feeUpliftment = 0;
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
if (action.attempts < action.maxAttempts) {
|
||||
action.attempts++;
|
||||
console.log(`Retry attempt ${action.attempts}`);
|
||||
if (this.cfg.xrpl.affordableExtraFee > 0 && e.status === "TOOK_LONG") {
|
||||
this.#applyFeeUpliftment = true;
|
||||
this.#feeUpliftment = Math.floor((this.cfg.xrpl.affordableExtraFee * action.attempts) / action.maxAttempts);
|
||||
}
|
||||
if (action.delay > 0) {
|
||||
new Promise((resolve) => {
|
||||
const checkFlagInterval = setInterval(() => {
|
||||
if (!this.#concurrencyQueue.processing) {
|
||||
this.#concurrencyQueue.queue.push(action);
|
||||
clearInterval(checkFlagInterval);
|
||||
resolve();
|
||||
}
|
||||
}, action.delay);
|
||||
});
|
||||
} else
|
||||
toKeep.push(action);
|
||||
}
|
||||
else {
|
||||
console.error('Max retry attempts reached. Abandoned.');
|
||||
}
|
||||
}
|
||||
}
|
||||
this.#concurrencyQueue.queue = toKeep;
|
||||
|
||||
await this.#releaseConcurrencyQueue();
|
||||
}
|
||||
|
||||
// Connect the host and trying to reconnect in the event of account not found error.
|
||||
// Account not found error can be because of a network reset. (Dev and test nets)
|
||||
async #connect(client, options = null) {
|
||||
let attempts = 0;
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
try {
|
||||
attempts++;
|
||||
const ret = options ? await client.connect(options) : await client.connect();
|
||||
if (ret)
|
||||
break;
|
||||
} catch (error) {
|
||||
if (error?.data?.error === 'actNotFound') {
|
||||
let delaySec;
|
||||
// The maximum delay will be 5 minutes.
|
||||
if (attempts > 150) {
|
||||
delaySec = 300;
|
||||
} else {
|
||||
delaySec = 2 * attempts;
|
||||
}
|
||||
console.log(`Network reset detected. Attempt ${attempts} failed. Retrying in ${delaySec}s...`);
|
||||
await new Promise(resolve => setTimeout(resolve, delaySec * 1000));
|
||||
} else
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async #connectHost() {
|
||||
await this.#connect(this.hostClient, { reputationAddress: this.cfg.xrpl.address, reputationSecret: this.cfg.xrpl.secret });
|
||||
}
|
||||
|
||||
async #connectReputation(options = {}) {
|
||||
await this.#connect(this.reputationClient, options);
|
||||
}
|
||||
|
||||
async #startReputationClockScheduler() {
|
||||
const timeout = appenv.REPUTATIOND_SCHEDULER_INTERVAL_SECONDS * 1000; // Seconds to millisecs.
|
||||
|
||||
const scheduler = async () => {
|
||||
await this.#processConcurrencyQueue();
|
||||
setTimeout(async () => {
|
||||
await scheduler();
|
||||
}, timeout);
|
||||
};
|
||||
|
||||
setTimeout(async () => {
|
||||
await scheduler();
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
async #startReputationSendScheduler() {
|
||||
const momentSize = this.hostClient.config.momentSize;
|
||||
|
||||
const timeout = momentSize * 1000; // Converting seconds to milliseconds.
|
||||
|
||||
const scheduler = async () => {
|
||||
setTimeout(async () => {
|
||||
await scheduler();
|
||||
}, timeout);
|
||||
await this.#sendReputations();
|
||||
};
|
||||
|
||||
let startTimeout = 0;
|
||||
const momentStartTimestamp = await this.hostClient.getMomentStartIndex();
|
||||
const currentTimestamp = evernode.UtilHelpers.getCurrentUnixTime();
|
||||
const currentMoment = await this.hostClient.getMoment();
|
||||
|
||||
// Set time relative to current passed time.
|
||||
const timeQuota = momentSize * (1 - this.#preparationTimeQuota);
|
||||
const upperBound = Math.floor(momentStartTimestamp + momentSize - (timeQuota * (1 - this.#reputationRegTimeQuota)));
|
||||
const lowerBound = Math.floor(momentStartTimestamp + momentSize - timeQuota);
|
||||
if (currentTimestamp < lowerBound || currentTimestamp >= upperBound)
|
||||
startTimeout = Math.floor(lowerBound + (Math.random() * ((upperBound - lowerBound) / 2)) - currentTimestamp) * 1000 // Converting seconds to milliseconds.
|
||||
|
||||
// If already registered for this moment, Schedule for next moment.
|
||||
if (startTimeout < 0 || this.lastReputationMoment === currentMoment)
|
||||
startTimeout += (momentSize * 1000);
|
||||
|
||||
console.log(`Reputation sender scheduled to start in ${startTimeout} milliseconds.`);
|
||||
|
||||
setTimeout(async () => {
|
||||
await scheduler();
|
||||
}, startTimeout);
|
||||
}
|
||||
|
||||
async #startReputationContractScheduler() {
|
||||
const momentSize = this.hostClient.config.momentSize;
|
||||
|
||||
const timeout = momentSize * 1000; // Converting seconds to milliseconds.
|
||||
|
||||
const scheduler = async () => {
|
||||
setTimeout(async () => {
|
||||
await scheduler();
|
||||
}, timeout);
|
||||
await this.#createReputationContract();
|
||||
};
|
||||
|
||||
let startTimeout = 0;
|
||||
const momentStartTimestamp = await this.hostClient.getMomentStartIndex();
|
||||
const currentTimestamp = evernode.UtilHelpers.getCurrentUnixTime();
|
||||
const currentMoment = await this.hostClient.getMoment();
|
||||
|
||||
const timeQuota = momentSize * (1 - this.#preparationTimeQuota);
|
||||
const upperBound = Math.floor(momentStartTimestamp + momentSize);
|
||||
const lowerBound = Math.floor(momentStartTimestamp + momentSize - (timeQuota * (1 - this.#reputationRegTimeQuota)));
|
||||
|
||||
if (currentTimestamp < lowerBound)
|
||||
startTimeout = Math.floor(lowerBound - currentTimestamp) * 1000;
|
||||
|
||||
// If deploy widow has passed or, not registered for next moment, Schedule for next moment.
|
||||
if (currentTimestamp > upperBound || (startTimeout === 0 && this.lastReputationMoment !== currentMoment))
|
||||
startTimeout = Math.floor(lowerBound + momentSize - currentTimestamp) * 1000;
|
||||
|
||||
// If zero, We are in the deploy window. Try to deploy now and schedule the next in start of next moments window.
|
||||
if (startTimeout === 0) {
|
||||
console.log(`Reputation contract creation will be done now since we are in the window.`);
|
||||
setTimeout(async () => {
|
||||
await this.#createReputationContract();
|
||||
}, 0);
|
||||
startTimeout = Math.floor(lowerBound + momentSize - currentTimestamp) * 1000;
|
||||
console.log(`Next reputation contract creation scheduled to start in ${startTimeout} milliseconds.`);
|
||||
}
|
||||
else {
|
||||
console.log(`Reputation contract creation scheduled to start in ${startTimeout} milliseconds.`);
|
||||
}
|
||||
|
||||
setTimeout(async () => {
|
||||
await scheduler();
|
||||
}, startTimeout);
|
||||
}
|
||||
|
||||
async #getUniverseInfo(moment) {
|
||||
if (!this.hostClient.reputationAcc)
|
||||
return null;
|
||||
|
||||
const orderInfo = await this.reputationClient.getReputationOrderByAddress(this.hostClient.reputationAcc.address, moment);
|
||||
|
||||
if (!orderInfo || !('orderedId' in orderInfo))
|
||||
return null;
|
||||
|
||||
return {
|
||||
universeIndex: Math.floor(orderInfo.orderedId / this.#universeSize)
|
||||
};
|
||||
}
|
||||
|
||||
async #getInstancesInUniverse(universeIndex, moment) {
|
||||
const minOrderedId = universeIndex * this.#universeSize;
|
||||
return (await Promise.all(Array.from({ length: this.#universeSize }, (_, i) => i + minOrderedId).map(async (orderedId) => {
|
||||
const repInfo = await this.reputationClient.getReputationContractInfoByOrderedId(orderedId, moment);
|
||||
if (!repInfo)
|
||||
return null;
|
||||
|
||||
return repInfo.contract;
|
||||
}))).filter(i => i);
|
||||
}
|
||||
|
||||
// Find the universe id and generate contract id.
|
||||
#generateContractId(universeIndex) {
|
||||
const buf = Buffer.alloc(4, 0);
|
||||
buf.writeUint32LE(universeIndex);
|
||||
|
||||
// Generate a hash from the seed
|
||||
const hash = crypto.createHash('sha1').update(buf.toString('hex')).digest('hex');
|
||||
// Use a portion of the hash to generate a random UUID
|
||||
const id = uuid.v4({
|
||||
random: Buffer.from(hash, 'hex')
|
||||
});
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
async #upgradeContract(instanceIp, instanceUserPort, userPrivateKey, unl, peers) {
|
||||
let lobbyMgr;
|
||||
try {
|
||||
lobbyMgr = new LobbyManager({
|
||||
ip: instanceIp,
|
||||
userPort: instanceUserPort,
|
||||
userPrivateKey: userPrivateKey
|
||||
});
|
||||
|
||||
await lobbyMgr.init();
|
||||
await lobbyMgr.upgradeContract(unl, peers);
|
||||
|
||||
if (lobbyMgr)
|
||||
lobbyMgr.terminate();
|
||||
console.log(`Contract bundle uploaded!`);
|
||||
} catch (e) {
|
||||
if (lobbyMgr)
|
||||
lobbyMgr.terminate();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// Create and setup reputation contract.
|
||||
async #createReputationContract() {
|
||||
const scheduledMoment = await this.hostClient.getMoment();
|
||||
|
||||
await this.#queueAction(async (submissionRefs) => {
|
||||
const curMoment = await this.reputationClient.getMoment();
|
||||
|
||||
const universeInfo = await this.#getUniverseInfo(curMoment + 1);
|
||||
|
||||
if (!universeInfo) {
|
||||
console.log(`Skipping reputation contract preparation since there's no universe info for the moment ${curMoment + 1}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (scheduledMoment != curMoment) {
|
||||
console.log(`Skipping since scheduled moment has passed. Scheduled in ${scheduledMoment}, Current moment ${curMoment}.`);
|
||||
return;
|
||||
}
|
||||
else if (this.lastReputationMoment !== curMoment) {
|
||||
console.log(`Skipping reputation contract preparation since not registered for the moment ${curMoment + 1}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Preparing reputation contract for the Moment ${curMoment + 1}...`);
|
||||
|
||||
let createdMoment = this.cfg.contractInstance?.created_moment ?? -1;
|
||||
let acquireSentMoment = this.cfg.contractInstance?.transaction ? (this.cfg.contractInstance?.acquire_sent_moment ?? -1) : -1;
|
||||
|
||||
if (curMoment > createdMoment) {
|
||||
const tenantClient = new evernode.TenantClient(this.hostClient.reputationAcc.address, this.hostClient.reputationAcc.secret);
|
||||
await tenantClient.connect();
|
||||
|
||||
submissionRefs.refs ??= [{}, {}];
|
||||
// Check again wether the transaction is validated before retry.
|
||||
const txHash1 = submissionRefs?.refs[0]?.submissionResult?.result?.tx_json?.hash;
|
||||
let retry = true;
|
||||
if (txHash1) {
|
||||
const txResponse = await tenantClient.xrplApi.getTransactionValidatedResults(txHash1);
|
||||
if (txResponse && txResponse.code === "tesSUCCESS") {
|
||||
console.log('Transaction is validated and success, Retry skipped!');
|
||||
retry = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (retry) {
|
||||
await tenantClient.prepareAccount({ submissionRef: submissionRefs?.refs[0], ...this.#prepareHostClientFunctionOptions() });
|
||||
}
|
||||
|
||||
// Check again wether the transaction is validated before retry.
|
||||
const txHash2 = submissionRefs?.refs[1]?.submissionResult?.result?.tx_json?.hash;
|
||||
retry = true;
|
||||
if (txHash2) {
|
||||
const txResponse = await tenantClient.xrplApi.getTransactionValidatedResults(txHash2);
|
||||
if (txResponse && txResponse.code === "tesSUCCESS") {
|
||||
console.log('Transaction is validated and success, Retry skipped!')
|
||||
retry = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (retry) {
|
||||
if (curMoment > createdMoment ||
|
||||
curMoment > acquireSentMoment) {
|
||||
console.log(`Acquiring the reputation contract instance...`);
|
||||
|
||||
const ownerKeys = await CommonHelper.generateKeys();
|
||||
const contractId = this.#generateContractId(universeInfo.universeIndex);
|
||||
|
||||
let requirement = {
|
||||
owner_pubkey: ownerKeys.publicKey,
|
||||
contract_id: contractId,
|
||||
image: this.#instanceImage,
|
||||
config: {}
|
||||
};
|
||||
|
||||
// Update the registry with the active instance count.
|
||||
const transaction = await tenantClient.acquireLeaseSubmit(this.hostClient.xrplAcc.address, requirement, { submissionRef: submissionRefs?.refs[1], ...this.#prepareHostClientFunctionOptions() });
|
||||
if (!transaction)
|
||||
throw 'Error on acquire submit';
|
||||
|
||||
acquireSentMoment = await this.reputationClient.getMoment();
|
||||
|
||||
this.cfg.contractInstance = {
|
||||
transaction: transaction,
|
||||
acquire_sent_moment: acquireSentMoment,
|
||||
owner_privatekey: ownerKeys.privateKey,
|
||||
status: ContractStatus.AcquireSent
|
||||
};
|
||||
this.#persistConfig();
|
||||
}
|
||||
|
||||
const result = await tenantClient.watchAcquireResponse(this.cfg.contractInstance.transaction);
|
||||
createdMoment = await this.reputationClient.getMoment();
|
||||
|
||||
// Assign ip to domain and outbound_ip for instance created from old sashimono version.
|
||||
if ('ip' in result.instance) {
|
||||
result.instance.domain = result.instance.ip;
|
||||
delete result.instance.ip;
|
||||
}
|
||||
|
||||
console.log('Reputation contract created in instance', result.instance);
|
||||
|
||||
this.cfg.contractInstance = {
|
||||
...result.instance,
|
||||
created_moment: createdMoment,
|
||||
owner_privatekey: this.cfg.contractInstance.owner_privatekey,
|
||||
status: ContractStatus.Created
|
||||
};
|
||||
this.#persistConfig();
|
||||
}
|
||||
|
||||
await tenantClient.disconnect();
|
||||
}
|
||||
else {
|
||||
console.log(`Skipping acquire since there is already created instance for the moment ${curMoment + 1}.`);
|
||||
}
|
||||
|
||||
if (curMoment === createdMoment) {
|
||||
|
||||
if (this.cfg.contractInstance.status === ContractStatus.Created) {
|
||||
// Set reputation contract info in domain.
|
||||
console.log(`Updating host reputation domain info...`);
|
||||
await this.hostClient.setReputationContractInfo(this.cfg.contractInstance.peer_port, this.cfg.contractInstance.pubkey, curMoment + 1);
|
||||
console.log(`Updated host reputation domain info.`);
|
||||
|
||||
// Mark as updated.
|
||||
this.cfg.contractInstance.status = ContractStatus.Updated;
|
||||
this.#persistConfig();
|
||||
}
|
||||
|
||||
if (this.cfg.contractInstance.status === ContractStatus.Updated) {
|
||||
// Wait for some time to let others to prepare.
|
||||
const momentSize = this.hostClient.config.momentSize;
|
||||
const momentStartTimestamp = await this.hostClient.getMomentStartIndex();
|
||||
const currentTimestamp = evernode.UtilHelpers.getCurrentUnixTime();
|
||||
|
||||
const timeQuota = momentSize * (1 - this.#preparationTimeQuota);
|
||||
const upperBound = Math.floor(momentStartTimestamp + momentSize);
|
||||
const lowerBound = Math.floor(momentStartTimestamp + momentSize - (timeQuota * (1 - this.#reputationRegTimeQuota)));
|
||||
const startTimestamp = Math.floor(lowerBound + ((upperBound - lowerBound) * this.#lobbyTimeQuota));
|
||||
|
||||
let startTimeout = 0;
|
||||
if (startTimestamp > currentTimestamp)
|
||||
startTimeout = (startTimestamp - currentTimestamp) * 1000;
|
||||
|
||||
console.log(`Waiting ${startTimeout} milliseconds until other hosts are ready.`);
|
||||
await new Promise((resolve) => setTimeout(resolve, startTimeout));
|
||||
|
||||
const instances = await this.#getInstancesInUniverse(universeInfo.universeIndex, curMoment + 1);
|
||||
const unl = instances.map(p => `${p.pubkey}`);
|
||||
const peers = instances.map(p => `${p.domain}:${p.peerPort}`);
|
||||
|
||||
console.log(`Upgrading the reputation contract instance.`);
|
||||
await this.#upgradeContract(this.cfg.contractInstance.domain, this.cfg.contractInstance.user_port, this.cfg.contractInstance.owner_privatekey, unl, peers);
|
||||
console.log(`Reputation contract instance upgraded.`);
|
||||
|
||||
// Mark as deployed.
|
||||
this.cfg.contractInstance.status = ContractStatus.Deployed;
|
||||
this.#persistConfig();
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.log(`Skipping deploy since instance is not created in the moment ${curMoment}.`)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async #getScores() {
|
||||
if (!this.cfg.contractInstance?.domain || !this.cfg.contractInstance?.user_port)
|
||||
return null;
|
||||
|
||||
let instanceMgr;
|
||||
try {
|
||||
instanceMgr = new ContractInstanceManager({
|
||||
ip: this.cfg.contractInstance.domain,
|
||||
userPort: this.cfg.contractInstance.user_port,
|
||||
userPrivateKey: this.cfg.contractInstance.owner_privatekey
|
||||
});
|
||||
|
||||
await instanceMgr.init();
|
||||
const res = await instanceMgr.sendContractReadRequest({ command: this.#readScoreCmd }, INPUT_PROTOCOLS.json);
|
||||
return res;
|
||||
} catch (e) {
|
||||
console.error('Error occurred while reading the scores:', e);
|
||||
return null;
|
||||
}
|
||||
finally {
|
||||
if (instanceMgr)
|
||||
await instanceMgr.terminate();
|
||||
}
|
||||
}
|
||||
|
||||
// Reputation sender.
|
||||
async #sendReputations() {
|
||||
const scheduledMoment = await this.hostClient.getMoment();
|
||||
|
||||
await this.#queueAction(async (submissionRefs) => {
|
||||
// Skip if host is not registered.
|
||||
const hostInfo = await this.hostClient.getRegistration();
|
||||
if (!hostInfo.active) {
|
||||
console.log(`Skipping reputation sender since host is not active.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentMoment = await this.hostClient.getMoment();
|
||||
|
||||
if (scheduledMoment == currentMoment) {
|
||||
// Sending reputations every moment.
|
||||
if (this.lastReputationMoment === 0 || currentMoment !== this.lastReputationMoment) {
|
||||
submissionRefs.refs ??= [{}];
|
||||
// Check again wether the transaction is validated before retry.
|
||||
const txHash = submissionRefs?.refs[0]?.submissionResult?.result?.tx_json?.hash;
|
||||
if (txHash) {
|
||||
const txResponse = await this.hostClient.xrplApi.getTransactionValidatedResults(txHash);
|
||||
if (txResponse && txResponse.code === "tesSUCCESS") {
|
||||
console.log('Transaction is validated and success, Retry skipped!')
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let scores = null;
|
||||
const createdMoment = this.cfg.contractInstance?.created_moment ?? -2;
|
||||
if (currentMoment === (createdMoment + 1))
|
||||
scores = await this.#getScores();
|
||||
|
||||
console.log(`Reporting reputations at Moment ${currentMoment} ${scores ? 'With scores' : 'Without scores'}...`);
|
||||
|
||||
try {
|
||||
await this.hostClient.sendReputations(scores, { submissionRef: submissionRefs?.refs[0], ...this.#prepareHostClientFunctionOptions() });
|
||||
this.lastReputationMoment = await this.hostClient.getMoment();
|
||||
}
|
||||
catch (err) {
|
||||
if (err.code === 'tecHOOK_REJECTED') {
|
||||
console.log("Reputation rejected by the hook.");
|
||||
}
|
||||
else {
|
||||
console.log("Reputation tx error", err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.log(`Skipping reputation sender since scheduled moment has passed. Scheduled in ${scheduledMoment}, Current moment ${curMoment}.`);
|
||||
}
|
||||
|
||||
}, this.#reputationRetryCount, this.#reputationRetryDelay);
|
||||
}
|
||||
|
||||
#readConfig() {
|
||||
this.cfg = ConfigHelper.readConfig(this.#configPath, this.#mbXrplConfigPath, true);
|
||||
}
|
||||
|
||||
#persistConfig() {
|
||||
ConfigHelper.writeConfig(this.cfg, this.#configPath);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ReputationD
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
const { appenv } = require('./appenv');
|
||||
const evernode = require('evernode-js-client');
|
||||
const { ConfigHelper } = require('./config-helper');
|
||||
|
||||
async function setEvernodeDefaults(network, governorAddress, rippledServer, fallbackRippledServers) {
|
||||
await evernode.Defaults.useNetwork(network || appenv.NETWORK);
|
||||
|
||||
if (governorAddress)
|
||||
evernode.Defaults.set({
|
||||
governorAddress: governorAddress
|
||||
});
|
||||
|
||||
if (rippledServer)
|
||||
evernode.Defaults.set({
|
||||
rippledServer: rippledServer
|
||||
});
|
||||
|
||||
if (fallbackRippledServers && fallbackRippledServers.length)
|
||||
evernode.Defaults.set({
|
||||
fallbackRippledServers: fallbackRippledServers
|
||||
});
|
||||
}
|
||||
|
||||
const MAX_TX_RETRY_ATTEMPTS = 10;
|
||||
|
||||
class Setup {
|
||||
|
||||
#getConfig(readSecret = true, includeMbConfig = true) {
|
||||
return ConfigHelper.readConfig(appenv.CONFIG_PATH, includeMbConfig ? appenv.MB_XRPL_CONFIG_PATH : null, readSecret);
|
||||
}
|
||||
|
||||
#saveConfig(cfg) {
|
||||
ConfigHelper.writeConfig(cfg, appenv.CONFIG_PATH);
|
||||
}
|
||||
|
||||
newConfig(address = "", secretPath = "") {
|
||||
const baseConfig = {
|
||||
version: appenv.REPUTATIOND_VERSION,
|
||||
xrpl: {
|
||||
address: address,
|
||||
secretPath: secretPath
|
||||
},
|
||||
contractInstance: {}
|
||||
};
|
||||
|
||||
this.#saveConfig(baseConfig);
|
||||
}
|
||||
|
||||
async prepareReputationAccount() {
|
||||
|
||||
const config = this.#getConfig();
|
||||
const acc = config.xrpl;
|
||||
await setEvernodeDefaults(acc.network, acc.governorAddress, acc.rippledServer, acc.fallbackRippledServers);
|
||||
|
||||
// Prepare host account.
|
||||
const hostClient = new evernode.HostClient(acc.hostAddress, acc.hostSecret);
|
||||
await hostClient.connect();
|
||||
|
||||
// Update the Defaults with "xrplApi" of the client.
|
||||
evernode.Defaults.set({
|
||||
xrplApi: hostClient.xrplApi
|
||||
});
|
||||
|
||||
try {
|
||||
console.log(`Preparing reputation account:${acc.address} | Reputation Hook:${hostClient.config.reputationAddress}`);
|
||||
await hostClient.prepareReputationAccount(acc.address, acc.secret, { retryOptions: { maxRetryAttempts: MAX_TX_RETRY_ATTEMPTS, feeUplift: Math.floor(acc.affordableExtraFee / MAX_TX_RETRY_ATTEMPTS) } });
|
||||
await hostClient.disconnect();
|
||||
}
|
||||
catch (e) {
|
||||
await hostClient.disconnect();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async waitForFunds(currencyType, expectedBalance, waitPeriod = 120) {
|
||||
|
||||
const config = this.#getConfig(false);
|
||||
const acc = config.xrpl;
|
||||
await setEvernodeDefaults(acc.network, acc.governorAddress, acc.rippledServer, acc.fallbackRippledServers);
|
||||
|
||||
// Prepare host account.
|
||||
const hostClient = new evernode.HostClient(acc.hostAddress);
|
||||
|
||||
// Update the Defaults with "xrplApi" of the client.
|
||||
evernode.Defaults.set({
|
||||
xrplApi: hostClient.xrplApi
|
||||
});
|
||||
|
||||
try {
|
||||
let attempts = 0;
|
||||
let balance = 0;
|
||||
while (attempts >= 0) {
|
||||
try {
|
||||
// In order to handle the account not found issue via catch block.
|
||||
await hostClient.connect();
|
||||
|
||||
// Prepare reputation account.
|
||||
const reputationAcc = new evernode.XrplAccount(acc.address);
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
if (currencyType === 'NATIVE')
|
||||
balance = Number((await reputationAcc.getInfo()).Balance) / 1000000;
|
||||
else {
|
||||
const lines = await reputationAcc.getTrustLines(evernode.EvernodeConstants.EVR, hostClient.config.evrIssuerAddress);
|
||||
balance = lines.length > 0 ? Number(lines[0].balance) : 0;
|
||||
}
|
||||
|
||||
if (balance < expectedBalance) {
|
||||
if (++attempts <= waitPeriod)
|
||||
continue;
|
||||
|
||||
await hostClient.disconnect();
|
||||
throw "NOT_ENOUGH_FUNDS";
|
||||
}
|
||||
|
||||
break;
|
||||
} catch (err) {
|
||||
if (err.data?.error === 'actNotFound' && ++attempts <= waitPeriod) {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
continue;
|
||||
}
|
||||
await hostClient.disconnect();
|
||||
throw (err.data?.error === 'actNotFound' || err === 'NOT_ENOUGH_FUNDS') ? "Funds not received within timeout." : "Error occurred in account balance check.";
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`${balance} ${currencyType == 'NATIVE' ? 'XAH' : 'EVR'} balance is there in your host account.`);
|
||||
await hostClient.disconnect();
|
||||
}
|
||||
catch (e) {
|
||||
await hostClient.disconnect();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// Upgrades existing message board data to the new version.
|
||||
async upgrade() {
|
||||
|
||||
// Do a simple version change in the config.
|
||||
const cfg = this.#getConfig(false, false);
|
||||
cfg.version = appenv.REPUTATIOND_VERSION;
|
||||
|
||||
this.#saveConfig(cfg);
|
||||
|
||||
await Promise.resolve(); // async placeholder.
|
||||
}
|
||||
|
||||
async repInfo() {
|
||||
const acc = this.#getConfig(false).xrpl;
|
||||
await setEvernodeDefaults(acc.network, acc.governorAddress, acc.rippledServer, acc.fallbackRippledServers);
|
||||
|
||||
const hostClient = new evernode.HostClient(acc.hostAddress);
|
||||
await hostClient.connect();
|
||||
|
||||
// Update the Defaults with "xrplApi" of the client.
|
||||
evernode.Defaults.set({
|
||||
xrplApi: hostClient.xrplApi
|
||||
});
|
||||
|
||||
try {
|
||||
const repInfo = await hostClient.getReputationInfo();
|
||||
const config = hostClient.config;
|
||||
await hostClient.disconnect();
|
||||
const moment = await hostClient.getMoment();
|
||||
|
||||
if (!repInfo) {
|
||||
console.log('You don\'t have reputation info yet.\n Make sure you have opted-in. If opted-in, wait until your host reporting for first reputation.');
|
||||
return;
|
||||
}
|
||||
else if (!repInfo.moment) {
|
||||
repInfo.moment = moment;
|
||||
}
|
||||
|
||||
const repClient = await evernode.HookClientFactory.create(evernode.HookTypes.reputation, { config: config });
|
||||
await repClient.connect({ skipConfigs: true });
|
||||
|
||||
const globalInfo = await repClient.getReputationInfo();
|
||||
|
||||
await repClient.disconnect();
|
||||
|
||||
console.log(JSON.stringify({ ...repInfo, reportedHostCount: globalInfo?.count ?? 0 }, null, 2));
|
||||
}
|
||||
catch (e) {
|
||||
await hostClient.disconnect();
|
||||
throw e;
|
||||
}
|
||||
finally {
|
||||
await evernode.Defaults.values.xrplApi.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Setup
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
const HotPocket = require('hotpocket-js-client');
|
||||
|
||||
class CommonHelper {
|
||||
static async generateKeys(privateKey = null, format = 'hex') {
|
||||
const keys = await HotPocket.generateKeys(privateKey);
|
||||
return format === 'hex' ? {
|
||||
privateKey: Buffer.from(keys.privateKey).toString('hex'),
|
||||
publicKey: Buffer.from(keys.publicKey).toString('hex')
|
||||
} : keys;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
CommonHelper
|
||||
}
|
||||
2771
reputationd/package-lock.json
generated
2771
reputationd/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"name": "reputationd",
|
||||
"scripts": {
|
||||
"lint": "./node_modules/.bin/eslint ./app.js",
|
||||
"build": "npm run lint && ncc build app.js --minify -o dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"archiver": "5.3.1",
|
||||
"evernode-js-client": "0.6.52",
|
||||
"hotpocket-js-client": "0.5.6",
|
||||
"uuid": "9.0.1",
|
||||
"ws": "8.17.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "8.3.0"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user