mirror of
https://github.com/EvernodeXRPL/sashimono.git
synced 2026-07-27 09:00:32 +00:00
Message board instance life time record keeping (#57)
This commit is contained in:
@@ -77,9 +77,10 @@ target_precompile_headers(sagent PUBLIC src/pchheader.hpp)
|
||||
# Add target to generate the installer setup.
|
||||
add_custom_target(installer
|
||||
COMMAND mkdir -p ./build/sashimono-installer
|
||||
COMMAND bash -c "cp -r ./build/{sagent,sashi,hpfs,user-install.sh,user-uninstall.sh,contract_template,mb-xrpl} ./build/sashimono-installer/"
|
||||
COMMAND bash -c "cp -r ./build/{sagent,sashi,hpfs,user-install.sh,user-uninstall.sh,contract_template} ./build/sashimono-installer/"
|
||||
COMMAND bash -c "cp -r ./installer/{docker-install.sh,registry-install.sh,registry-uninstall.sh,sashimono-install.sh,sashimono-uninstall.sh} ./build/sashimono-installer/"
|
||||
COMMAND bash -c "cp -r ./dependencies/{user-cgcreate.sh,libblake3.so} ./build/sashimono-installer/"
|
||||
COMMAND bash -c "cp -r ./mb-xrpl/dist/mb-xrpl ./build/sashimono-installer/"
|
||||
COMMAND tar cfz ./build/sashimono-installer.tar.gz --directory=./build/ sashimono-installer
|
||||
COMMAND rm -r ./build/sashimono-installer
|
||||
)
|
||||
|
||||
@@ -2,16 +2,13 @@ const fs = require('fs');
|
||||
const readLine = require('readline');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const fetch = require('node-fetch');
|
||||
const xrpl = require('../../mb-xrpl/ripple-handler');
|
||||
const XrplAccount = xrpl.XrplAccount;
|
||||
const RippleAPIWarpper = xrpl.RippleAPIWarpper;
|
||||
const { XrplAccount, RippleAPIWarpper, Events, MemoFormats, MemoTypes } = require('../../mb-xrpl/lib/ripple-handler');
|
||||
|
||||
const RIPPLE_SERVER = 'wss://hooks-testnet.xrpl-labs.com';
|
||||
const FAUSET_URL = 'https://hooks-testnet.xrpl-labs.com/newcreds';
|
||||
|
||||
const OWNER_PUBKEY = 'ed5cb83404120ac759609819591ef839b7d222c84f1f08b3012f490586159d2b50'
|
||||
const CONFIG_PATH = 'user.cfg';
|
||||
const REDEEM_FEE = 1;
|
||||
|
||||
// Test Hook
|
||||
// rwQ7ECXhkF1ZF6qFHH4y7sc1y3ZnXgf6Rh
|
||||
@@ -64,7 +61,7 @@ class TestUser {
|
||||
this.xrplAcc = new XrplAccount(this.ripplAPI.api, this.cfg.xrpl.address, this.cfg.xrpl.secret);
|
||||
this.evernodeXrplAcc = new XrplAccount(this.ripplAPI.api, this.cfg.xrpl.hookAddress);
|
||||
|
||||
this.evernodeXrplAcc.events.on(xrpl.Events.PAYMENT, (data, error) => {
|
||||
this.evernodeXrplAcc.events.on(Events.PAYMENT, (data, error) => {
|
||||
if (data) {
|
||||
// Check whether issued currency
|
||||
const isXrp = (typeof data.Amount !== "object");
|
||||
@@ -79,8 +76,8 @@ class TestUser {
|
||||
data: m.Memo.MemoData ? hexToASCII(m.Memo.MemoData) : null
|
||||
};
|
||||
});
|
||||
const instanceRef = deserialized.filter(m => m.data && m.type === xrpl.MemoTypes.INST_CRET_REF && m.format === xrpl.MemoFormats.BINARY);
|
||||
const instanceInfo = deserialized.filter(m => m.data && m.type === xrpl.MemoTypes.INST_CRET_RESP && m.format === xrpl.MemoFormats.BINARY);
|
||||
const instanceRef = deserialized.filter(m => m.data && m.type === MemoTypes.REDEEM_REF && m.format === MemoFormats.BINARY);
|
||||
const instanceInfo = deserialized.filter(m => m.data && m.type === MemoTypes.REDEEM_RESP && m.format === MemoFormats.BINARY);
|
||||
|
||||
if (instanceRef && instanceRef.length && instanceInfo && instanceInfo.length) {
|
||||
const ref = instanceRef[0].data;
|
||||
@@ -109,7 +106,7 @@ class TestUser {
|
||||
|
||||
// Subscribe to transactions when api is reconnected.
|
||||
// Because API will be automatically reconnected if it's disconnected.
|
||||
this.ripplAPI.events.on(xrpl.Events.RECONNECTED, (e) => {
|
||||
this.ripplAPI.events.on(Events.RECONNECTED, (e) => {
|
||||
this.evernodeXrplAcc.subscribe();
|
||||
});
|
||||
|
||||
@@ -122,7 +119,7 @@ class TestUser {
|
||||
}
|
||||
|
||||
async inputPump() {
|
||||
const inp = await this.askForInput('');
|
||||
const inp = await this.askForInput('Enter command');
|
||||
if (inp && inp.length > 0) {
|
||||
switch (inp) {
|
||||
case 'create':
|
||||
@@ -139,7 +136,7 @@ class TestUser {
|
||||
|
||||
askForInput(label, defaultValue) {
|
||||
return new Promise(resolve => {
|
||||
this.rl.question(label ? `${label}? ` : '', (input) => {
|
||||
this.rl.question(label ? `${label} : ` : '', (input) => {
|
||||
resolve(input && input.length > 0 ? input : defaultValue);
|
||||
})
|
||||
})
|
||||
@@ -333,6 +330,7 @@ class TestUser {
|
||||
}
|
||||
|
||||
async createInstance() {
|
||||
const tokenCount = await this.askForInput(`${this.cfg.xrpl.hostToken} amount (default:1)`, 1);
|
||||
const contractId = await this.askForInput('Contract ID (default:uuidv4)', uuidv4());
|
||||
const image = await this.askForInput('Image: 1=ubuntu(default) | 2=nodejs', "1");
|
||||
if (image != "1" && image != "2") {
|
||||
@@ -355,14 +353,15 @@ class TestUser {
|
||||
|
||||
const memoData = JSON.stringify(data);
|
||||
const res = await this.xrplAcc.makePayment(this.cfg.xrpl.hookAddress,
|
||||
REDEEM_FEE,
|
||||
+tokenCount,
|
||||
this.cfg.xrpl.hostToken,
|
||||
this.cfg.xrpl.hostAddress,
|
||||
[{ type: xrpl.MemoTypes.INST_CRET, format: xrpl.MemoFormats.BINARY, data: memoData }]);
|
||||
[{ type: MemoTypes.REDEEM, format: MemoFormats.BINARY, data: memoData }]);
|
||||
|
||||
if (res) {
|
||||
console.log("Transaction succeed, wait for the instance creation...");
|
||||
return new Promise(resolve => {
|
||||
this.promises[res] = resolve
|
||||
this.promises[res.txHash] = resolve;
|
||||
});
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -183,7 +183,7 @@ if [ "$quiet" != "-q" ]; then
|
||||
Group=root
|
||||
Type=simple
|
||||
WorkingDirectory=$mb_xrpl_dir
|
||||
ExecStart=node $mb_xrpl_dir $xrpl_server_url
|
||||
ExecStart=node $mb_xrpl_dir $xrpl_server_url --enable-logging
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
[Install]
|
||||
|
||||
5
mb-xrpl/.gitignore
vendored
5
mb-xrpl/.gitignore
vendored
@@ -1,2 +1,5 @@
|
||||
node_modules
|
||||
mb-xrpl.cfg
|
||||
dist
|
||||
log
|
||||
mb-xrpl.cfg
|
||||
mb-xrpl.sqlite
|
||||
31
mb-xrpl/lib/logger.js
Normal file
31
mb-xrpl/lib/logger.js
Normal file
@@ -0,0 +1,31 @@
|
||||
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(/-/, ''); // Delete the dashes.
|
||||
|
||||
return `${date} [${logType}] ${text}\n`;
|
||||
}
|
||||
|
||||
exports.init = (logPath) => {
|
||||
const dirname = path.dirname(logPath);
|
||||
if (!fs.existsSync(dirname))
|
||||
fs.mkdirSync(dirname, { recursive: true });
|
||||
|
||||
// Formating logs and printing.
|
||||
const ws = fs.createWriteStream(logPath, { flags: 'a' });
|
||||
console.log = function () {
|
||||
const text = formatText(util.format.apply(this, arguments));
|
||||
ws.write(text);
|
||||
process.stdout.write(text);
|
||||
};
|
||||
console.error = function () {
|
||||
const text = formatText(util.format.apply(this, arguments), 'err');
|
||||
ws.write(text);
|
||||
process.stderr.write(text);
|
||||
};
|
||||
}
|
||||
@@ -6,10 +6,10 @@ const CONNECTION_RETRY_INTERVAL = 1000;
|
||||
const maxLedgerOffset = 10;
|
||||
|
||||
const MemoTypes = {
|
||||
INST_CRET: 'evndInstCreate',
|
||||
INST_CRET_REF: 'evndInstCreateRef',
|
||||
INST_CRET_RESP: 'evndInstCreateResp',
|
||||
HOST_REG: 'evndHostReg'
|
||||
REDEEM: 'evnRedeem',
|
||||
REDEEM_REF: 'evnRedeemRef',
|
||||
REDEEM_RESP: 'evnRedeemResp',
|
||||
HOST_REG: 'evnHostReg'
|
||||
}
|
||||
|
||||
const MemoFormats = {
|
||||
@@ -20,6 +20,7 @@ const MemoFormats = {
|
||||
|
||||
const Events = {
|
||||
RECONNECTED: 'reconnected',
|
||||
LEDGER: 'ledger',
|
||||
PAYMENT: 'payment'
|
||||
}
|
||||
|
||||
@@ -68,6 +69,9 @@ class RippleAPIWarpper {
|
||||
}
|
||||
catch (e) { console.error(e); };
|
||||
});
|
||||
this.api.on('ledger', (ledger) => {
|
||||
this.events.emit(Events.LEDGER, ledger);
|
||||
});
|
||||
}
|
||||
|
||||
async connect() {
|
||||
@@ -92,6 +96,10 @@ class RippleAPIWarpper {
|
||||
this.connectionRetryCount = 0;
|
||||
throw `Max connection retry count reached for ${this.rippleServer}. Try again later.`;
|
||||
}
|
||||
|
||||
async getLedgerVersion() {
|
||||
return (await this.api.getLedgerVersion());
|
||||
}
|
||||
}
|
||||
|
||||
class XrplAccount {
|
||||
@@ -143,7 +151,7 @@ class XrplAccount {
|
||||
|
||||
await this.api.submit(signed.signedTransaction);
|
||||
const verified = await this.verifyTransaction(signed.id, ledger, maxLedger);
|
||||
return verified ? signed.id : false;
|
||||
return verified ? verified : false;
|
||||
}
|
||||
|
||||
async createTrustline(currency, issuer, limit, memos = null) {
|
||||
@@ -189,7 +197,7 @@ class XrplAccount {
|
||||
await this.api.submit(signed.signedTransaction);
|
||||
console.log("Submitted trust line.");
|
||||
const verified = await this.verifyTransaction(signed.id, ledger, maxLedger);
|
||||
verified ? resolve(signed.id) : resolve(false);
|
||||
verified ? resolve(verified) : resolve(false);
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -206,8 +214,7 @@ class XrplAccount {
|
||||
console.log(data.outcome.result);
|
||||
if (data.outcome.result !== 'tesSUCCESS')
|
||||
console.log("Transaction verification failed. Result: " + data.outcome.result);
|
||||
|
||||
resolve(data.outcome.result === 'tesSUCCESS');
|
||||
resolve(data.outcome.result === 'tesSUCCESS' ? { txHash: data.id, ledgerVersion: data.outcome.ledgerVersion } : false);
|
||||
}).catch(error => {
|
||||
// If transaction not in latest validated ledger, try again until max ledger is hit.
|
||||
if (error instanceof this.api.errors.PendingLedgerVersionError || error instanceof this.api.errors.NotFoundError) {
|
||||
153
mb-xrpl/lib/sqlite-handler.js
Normal file
153
mb-xrpl/lib/sqlite-handler.js
Normal file
@@ -0,0 +1,153 @@
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
|
||||
const DataTypes = {
|
||||
TEXT: 'TEXT',
|
||||
INTEGER: 'INTEGER',
|
||||
NULL: 'NULL'
|
||||
}
|
||||
|
||||
class SqliteDatabase {
|
||||
constructor(dbFile) {
|
||||
this.dbFile = dbFile;
|
||||
}
|
||||
|
||||
open() {
|
||||
this.db = new sqlite3.Database(this.dbFile);
|
||||
}
|
||||
|
||||
close() {
|
||||
this.db.close();
|
||||
this.db = null;
|
||||
}
|
||||
|
||||
async createTableIfNotExists(tableName, columnInfo) {
|
||||
if (!this.db)
|
||||
throw 'Database connection is not open.';
|
||||
|
||||
const columns = columnInfo.map(c => {
|
||||
let info = `${c.name} ${c.type}`;
|
||||
if (c.default)
|
||||
info += ` DEFAULT ${c.default}`;
|
||||
if (c.unique)
|
||||
info += ' UNIQUE';
|
||||
if (c.primary)
|
||||
info += ' PRIMARY KEY';
|
||||
if (c.notNull)
|
||||
info += ' NOT NULL';
|
||||
return info;
|
||||
}).join(', ');
|
||||
|
||||
const query = `CREATE TABLE IF NOT EXISTS ${tableName}(${columns})`;
|
||||
await this.runQuery(query);
|
||||
}
|
||||
|
||||
getValues(tableName, filter = null) {
|
||||
if (!this.db)
|
||||
throw 'Database connection is not open.';
|
||||
|
||||
let values = [];
|
||||
let filterStr = '1 AND '
|
||||
if (filter) {
|
||||
const columnNames = Object.keys(filter);
|
||||
for (const columnName of columnNames) {
|
||||
filterStr += `${columnName} = ? AND `;
|
||||
values.push(filter[columnName] ? filter[columnName] : 'NULL');
|
||||
}
|
||||
}
|
||||
filterStr = filterStr.slice(0, -5);
|
||||
|
||||
const query = `SELECT * FROM ${tableName}` + (filterStr ? ` WHERE ${filterStr};` : ';');
|
||||
return new Promise((resolve, reject) => {
|
||||
let rows = [];
|
||||
this.db.each(query, values, function (err, row) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
rows.push(row);
|
||||
}, function (err, count) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(rows);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async insertValue(tableName, value) {
|
||||
return (await this.insertValues(tableName, [value]));
|
||||
}
|
||||
|
||||
async updateValue(tableName, value, filter = null) {
|
||||
if (!this.db)
|
||||
throw 'Database connection is not open.';
|
||||
|
||||
let columnNames = Object.keys(value);
|
||||
|
||||
let valueStr = '';
|
||||
let values = [];
|
||||
for (const columnName of columnNames) {
|
||||
valueStr += `${columnName} = ?,`;
|
||||
values.push(value[columnName] ? value[columnName] : 'NULL');
|
||||
}
|
||||
valueStr = valueStr.slice(0, -1);
|
||||
|
||||
let filterStr = '1 AND '
|
||||
if (filter) {
|
||||
columnNames = Object.keys(filter);
|
||||
for (const columnName of columnNames) {
|
||||
filterStr += `${columnName} = ? AND `;
|
||||
values.push(filter[columnName] ? filter[columnName] : 'NULL');
|
||||
}
|
||||
}
|
||||
filterStr = filterStr.slice(0, -5);
|
||||
|
||||
const query = `UPDATE ${tableName} SET ${valueStr} WHERE ${filterStr};`;
|
||||
return (await this.runQuery(query, values));
|
||||
}
|
||||
|
||||
async insertValues(tableName, values) {
|
||||
if (!this.db)
|
||||
throw 'Database connection is not open.';
|
||||
|
||||
if (values.length) {
|
||||
const columnNames = Object.keys(values[0]);
|
||||
|
||||
let rowValueStr = '';
|
||||
let rowValues = [];
|
||||
for (const val of values) {
|
||||
rowValueStr += '(';
|
||||
for (const columnName of columnNames) {
|
||||
rowValueStr += ('?,');
|
||||
rowValues.push(val[columnName] ? val[columnName] : 'NULL');
|
||||
}
|
||||
rowValueStr = rowValueStr.slice(0, -1) + '),';
|
||||
}
|
||||
rowValueStr = rowValueStr.slice(0, -1);
|
||||
|
||||
const query = `INSERT INTO ${tableName}(${columnNames.join(', ')}) VALUES ${rowValueStr}`;
|
||||
return (await this.runQuery(query, rowValues));
|
||||
}
|
||||
}
|
||||
|
||||
runQuery(query, params = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.run(query, params ? params : [], function (err) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve({ lastId: this.lastID, changes: this.changes });
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
SqliteDatabase,
|
||||
DataTypes
|
||||
}
|
||||
@@ -1,14 +1,24 @@
|
||||
const fs = require('fs');
|
||||
const { execSync } = require("child_process");
|
||||
const xrpl = require('./ripple-handler');
|
||||
const XrplAccount = xrpl.XrplAccount;
|
||||
const RippleAPIWarpper = xrpl.RippleAPIWarpper;
|
||||
const { exec } = require("child_process");
|
||||
const logger = require('./lib/logger');
|
||||
const { XrplAccount, RippleAPIWarpper, Events, MemoFormats, MemoTypes } = require('./lib/ripple-handler');
|
||||
const { SqliteDatabase, DataTypes } = require('./lib/sqlite-handler');
|
||||
|
||||
const CONFIG_PATH = 'mb-xrpl.cfg';
|
||||
const DB_PATH = 'mb-xrpl.sqlite';
|
||||
const DB_TABLE_NAME = 'redeem_ops';
|
||||
const EVR_CUR_CODE = 'EVR';
|
||||
const EVR_LIMIT = 99999999;
|
||||
const REG_FEE = 5;
|
||||
const RES_FEE = 0.000001;
|
||||
const LEDGERS_PER_MOMENT = 72;
|
||||
|
||||
const RedeemStatus = {
|
||||
REDEEMING: 'Redeeming',
|
||||
REDEEMED: 'Redeemed',
|
||||
FAILED: 'Failed',
|
||||
EXPIRED: 'Expired'
|
||||
}
|
||||
|
||||
const SASHI_CLI_PATH_DEV = "../build/sashi";
|
||||
const SASHI_CLI_PATH_PROD = "/usr/bin/sashi";
|
||||
@@ -22,33 +32,63 @@ const hexToASCII = (hex) => {
|
||||
}
|
||||
|
||||
class MessageBoard {
|
||||
constructor(configPath, sashiCliPath, rippleServer) {
|
||||
constructor(configPath, dbPath, sashiCliPath, rippleServer) {
|
||||
this.configPath = configPath;
|
||||
this.redeemTable = DB_TABLE_NAME;
|
||||
this.expiryList = [];
|
||||
|
||||
if (!fs.existsSync(this.configPath))
|
||||
throw `${this.configPath} does not exist.`;
|
||||
else if (!fs.existsSync(sashiCliPath))
|
||||
throw `Sashi CLI does not exist in ${sashiCliPath}.`;
|
||||
|
||||
this.readConfig();
|
||||
this.sashiCli = new SashiCLI(sashiCliPath);
|
||||
this.ripplAPI = new RippleAPIWarpper(rippleServer);
|
||||
this.db = new SqliteDatabase(dbPath);
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.readConfig();
|
||||
|
||||
if (!this.cfg.xrpl.address || !this.cfg.xrpl.secret || !this.cfg.xrpl.token || !this.cfg.xrpl.hookAddress)
|
||||
throw "Required cfg fields cannot be empty.";
|
||||
|
||||
try { await this.ripplAPI.connect(); }
|
||||
catch (e) { throw e; }
|
||||
|
||||
this.db.open();
|
||||
await this.createRedeemTable();
|
||||
|
||||
const redeems = await this.getRedeemedRecords();
|
||||
for (const redeem of redeems)
|
||||
this.addToExpiryList(redeem.tx_hash, redeem.container_name, this.getExpiryLedger(redeem.created_on_ledger, redeem.h_token_amount));
|
||||
|
||||
this.db.close();
|
||||
|
||||
// Check for instance expiry.
|
||||
this.ripplAPI.events.on(Events.LEDGER, async (e) => {
|
||||
const expired = this.expiryList.filter(x => x.expiryLedger <= e.ledgerVersion);
|
||||
if (expired && expired.length) {
|
||||
this.expiryList = this.expiryList.filter(x => x.expiryLedger > e.ledgerVersion);
|
||||
|
||||
this.db.open();
|
||||
for (const x of expired) {
|
||||
console.log(`Moments exceeded. Destroying ${x.containerName}`);
|
||||
await this.sashiCli.destroyInstance(x.containerName);
|
||||
await this.updateRedeemStatus(x.txHash, RedeemStatus.EXPIRED);
|
||||
console.log(`Destroyed ${x.containerName}`);
|
||||
}
|
||||
this.db.close();
|
||||
}
|
||||
});
|
||||
|
||||
this.xrplAcc = new XrplAccount(this.ripplAPI.api, this.cfg.xrpl.address, this.cfg.xrpl.secret);
|
||||
|
||||
// Create trustline with evernode account.
|
||||
if (!this.cfg.xrpl.regTrustHash) {
|
||||
const res = await this.xrplAcc.createTrustline(EVR_CUR_CODE, this.cfg.xrpl.hookAddress, EVR_LIMIT);
|
||||
if (res) {
|
||||
this.cfg.xrpl.regTrustHash = res;
|
||||
this.cfg.xrpl.regTrustHash = res.txHash;
|
||||
this.persistConfig();
|
||||
console.log(`Created ${EVR_CUR_CODE} trustline with evernode account.`)
|
||||
}
|
||||
@@ -62,14 +102,14 @@ class MessageBoard {
|
||||
// REG_FEE,
|
||||
// EVR_CUR_CODE,
|
||||
// this.cfg.xrpl.address,
|
||||
// [{ type: xrpl.MemoTypes.HOST_REG, format: xrpl.MemoFormats.TEXT, data: memoData }]);
|
||||
// [{ type: MemoTypes.HOST_REG, format: MemoFormats.TEXT, data: memoData }]);
|
||||
const res = await this.xrplAcc.makePayment(this.cfg.xrpl.hookAddress,
|
||||
REG_FEE,
|
||||
"XRP",
|
||||
null,
|
||||
[{ type: xrpl.MemoTypes.HOST_REG, format: xrpl.MemoFormats.TEXT, data: memoData }]);
|
||||
[{ type: MemoTypes.HOST_REG, format: MemoFormats.TEXT, data: memoData }]);
|
||||
if (res) {
|
||||
this.cfg.xrpl.regFeeHash = res;
|
||||
this.cfg.xrpl.regFeeHash = res.txHash;
|
||||
this.persistConfig();
|
||||
console.log('Registration payment made for evernode account.')
|
||||
}
|
||||
@@ -77,7 +117,7 @@ class MessageBoard {
|
||||
|
||||
this.evernodeXrplAcc = new XrplAccount(this.ripplAPI.api, this.cfg.xrpl.hookAddress);
|
||||
|
||||
this.evernodeXrplAcc.events.on(xrpl.Events.PAYMENT, (data, error) => {
|
||||
this.evernodeXrplAcc.events.on(Events.PAYMENT, async (data, error) => {
|
||||
if (data) {
|
||||
// Check whether issued currency
|
||||
const isIssuedCurrency = (typeof data.Amount === "object");
|
||||
@@ -87,36 +127,61 @@ class MessageBoard {
|
||||
const token = data.Amount.currency;
|
||||
const issuer = data.Amount.issuer;
|
||||
const amount = parseInt(data.Amount.value);
|
||||
const isInstruction = (token === this.cfg.xrpl.token && issuer === this.cfg.xrpl.address);
|
||||
if (isInstruction) {
|
||||
const isRedeem = (token === this.cfg.xrpl.token && issuer === this.cfg.xrpl.address);
|
||||
if (isRedeem) {
|
||||
const memos = data.Memos;
|
||||
const txHash = data.hash;
|
||||
const txAccount = data.Account;
|
||||
const deserialized = memos.map(m => {
|
||||
return {
|
||||
type: m.Memo.MemoType ? hexToASCII(m.Memo.MemoType) : null,
|
||||
format: m.Memo.MemoFormat ? hexToASCII(m.Memo.MemoFormat) : null,
|
||||
data: m.Memo.MemoData ? hexToASCII(m.Memo.MemoData) : null
|
||||
};
|
||||
}).filter(m => m.data && m.type === xrpl.MemoTypes.INST_CRET && m.format === xrpl.MemoFormats.BINARY);
|
||||
const txHash = data.hash;
|
||||
const txAccount = data.Account;
|
||||
}).filter(m => m.data && m.type === MemoTypes.REDEEM && m.format === MemoFormats.BINARY);
|
||||
|
||||
this.db.open();
|
||||
for (let instance of deserialized) {
|
||||
let res;
|
||||
|
||||
let createRes;
|
||||
let hasError = false;
|
||||
try {
|
||||
res = this.sashiCli.createInstance(JSON.parse(instance.data));
|
||||
console.log(`Received redeem from ${txAccount}`)
|
||||
await this.createRedeemRecord(txHash, txAccount, amount);
|
||||
createRes = await this.sashiCli.createInstance(JSON.parse(instance.data));
|
||||
console.log(`Instance created for ${txAccount}`)
|
||||
}
|
||||
catch (e) {
|
||||
res = e;
|
||||
console.error(e)
|
||||
hasError = true;
|
||||
console.error(e);
|
||||
createRes = {
|
||||
code: 'REDEEM_ERR',
|
||||
message: 'Error occured while redeeming.'
|
||||
}
|
||||
}
|
||||
this.xrplAcc.makePayment(this.cfg.xrpl.hookAddress,
|
||||
RES_FEE,
|
||||
"XRP",
|
||||
null,
|
||||
[{ type: xrpl.MemoTypes.INST_CRET_REF, format: xrpl.MemoFormats.BINARY, data: txHash },
|
||||
{ type: xrpl.MemoTypes.INST_CRET_RESP, format: xrpl.MemoFormats.BINARY, data: res }])
|
||||
.then(res => console.log(res)).catch(console.error);
|
||||
|
||||
try {
|
||||
const data = await this.xrplAcc.makePayment(this.cfg.xrpl.hookAddress,
|
||||
RES_FEE,
|
||||
"XRP",
|
||||
null,
|
||||
[{ type: MemoTypes.REDEEM_REF, format: MemoFormats.BINARY, data: txHash },
|
||||
{ type: MemoTypes.REDEEM_RESP, format: MemoFormats.BINARY, data: createRes }]);
|
||||
|
||||
if (!hasError) {
|
||||
this.addToExpiryList(txHash, createRes.content.name, this.getExpiryLedger(data.ledgerVersion, amount));
|
||||
await this.updateRedeemedRecord(txHash, createRes.content.name, data.ledgerVersion);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
hasError = true;
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
if (hasError)
|
||||
await this.updateRedeemStatus(txHash, RedeemStatus.FAILED);
|
||||
}
|
||||
this.db.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,11 +193,62 @@ class MessageBoard {
|
||||
|
||||
// Subscribe to transactions when api is reconnected.
|
||||
// Because API will be automatically reconnected if it's disconnected.
|
||||
this.ripplAPI.events.on(xrpl.Events.RECONNECTED, (e) => {
|
||||
this.ripplAPI.events.on(Events.RECONNECTED, (e) => {
|
||||
this.evernodeXrplAcc.subscribe();
|
||||
});
|
||||
}
|
||||
|
||||
addToExpiryList(txHash, containerName, expiryLedger) {
|
||||
this.expiryList.push({
|
||||
txHash: txHash,
|
||||
containerName: containerName,
|
||||
expiryLedger: expiryLedger,
|
||||
});
|
||||
}
|
||||
|
||||
async createRedeemTable() {
|
||||
// Create table if not exists.
|
||||
await this.db.createTableIfNotExists(this.redeemTable, [
|
||||
{ name: 'timestamp', type: DataTypes.INTEGER, notNull: true },
|
||||
{ name: 'tx_hash', type: DataTypes.TEXT, primary: true, notNull: true },
|
||||
{ name: 'user_xrp_address', type: DataTypes.TEXT, notNull: true },
|
||||
{ name: 'h_token_amount', type: DataTypes.INTEGER, notNull: true },
|
||||
{ name: 'container_name', type: DataTypes.TEXT },
|
||||
{ name: 'created_on_ledger', type: DataTypes.INTEGER },
|
||||
{ name: 'status', type: DataTypes.TEXT, notNull: true }
|
||||
]);
|
||||
}
|
||||
|
||||
async getRedeemedRecords() {
|
||||
return (await this.db.getValues(this.redeemTable, { status: RedeemStatus.REDEEMED }));
|
||||
}
|
||||
|
||||
async createRedeemRecord(txHash, txUserAddress, txAmount) {
|
||||
await this.db.insertValue(this.redeemTable, {
|
||||
timestamp: Date.now(),
|
||||
tx_hash: txHash,
|
||||
user_xrp_address: txUserAddress,
|
||||
h_token_amount: txAmount,
|
||||
status: RedeemStatus.REDEEMING
|
||||
});
|
||||
}
|
||||
|
||||
async updateRedeemedRecord(txHash, containerName, ledgerVersion) {
|
||||
await this.db.updateValue(this.redeemTable, {
|
||||
container_name: containerName,
|
||||
created_on_ledger: ledgerVersion,
|
||||
status: RedeemStatus.REDEEMED
|
||||
}, { tx_hash: txHash });
|
||||
}
|
||||
|
||||
async updateRedeemStatus(txHash, status) {
|
||||
await this.db.updateValue(this.redeemTable, { status: status }, { tx_hash: txHash });
|
||||
}
|
||||
|
||||
getExpiryLedger(createdOnLedger, moments) {
|
||||
return createdOnLedger + (moments * LEDGERS_PER_MOMENT);
|
||||
}
|
||||
|
||||
readConfig() {
|
||||
this.cfg = JSON.parse(fs.readFileSync(this.configPath).toString());
|
||||
}
|
||||
@@ -147,41 +263,81 @@ class SashiCLI {
|
||||
this.cliPath = cliPath;
|
||||
}
|
||||
|
||||
createInstance(msg) {
|
||||
if (!msg.type)
|
||||
msg.type = 'create';
|
||||
async createInstance(requirements) {
|
||||
if (!requirements.type)
|
||||
requirements.type = 'create';
|
||||
|
||||
let output = execSync(`${this.cliPath} json -m '${JSON.stringify(msg)}'`, { stdio: 'pipe' });
|
||||
let message = Buffer.from(output).toString();
|
||||
message = JSON.parse(message.substring(0, message.length - 1)); // Skipping the \n from the result.
|
||||
if (message.content && typeof message.content == 'string' && message.content.endsWith("error"))
|
||||
throw message;
|
||||
const res = await this.execSashiCli(requirements);
|
||||
if (res.content && typeof res.content == 'string' && res.content.endsWith("error"))
|
||||
throw res;
|
||||
|
||||
return message;
|
||||
return res;
|
||||
}
|
||||
|
||||
async destroyInstance(containerName) {
|
||||
const msg = {
|
||||
type: 'destroy',
|
||||
container_name: containerName
|
||||
};
|
||||
const res = await this.execSashiCli(msg);
|
||||
if (res.content && typeof res.content == 'string' && res.content.endsWith("error"))
|
||||
throw res;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
execSashiCli(msg) {
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(`${this.cliPath} json -m '${JSON.stringify(msg)}'`, { stdio: 'pipe' }, (err, stdout, stderr) => {
|
||||
if (err || stderr) {
|
||||
reject(err || stderr);
|
||||
return;
|
||||
}
|
||||
|
||||
let message = Buffer.from(stdout).toString();
|
||||
resolve(JSON.parse(message.substring(0, message.length - 1))); // Skipping the \n from the result.
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
checkStatus() {
|
||||
const output = execSync(`${this.cliPath} status`, { stdio: 'pipe' });
|
||||
let message = Buffer.from(output).toString();
|
||||
message = message.substring(0, message.length - 1); // Skipping the \n from the result.
|
||||
console.log(`Sashi CLI : ${message}`);
|
||||
return message;
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(`${this.cliPath} status`, { stdio: 'pipe' }, (err, stdout, stderr) => {
|
||||
if (err || stderr) {
|
||||
reject(err || stderr);
|
||||
return;
|
||||
}
|
||||
|
||||
let message = Buffer.from(stdout).toString();
|
||||
message = message.substring(0, message.length - 1); // Skipping the \n from the result.
|
||||
console.log(`Sashi CLI : ${message}`);
|
||||
resolve(message);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// Read Ripple Server Url.
|
||||
const args = process.argv;
|
||||
|
||||
// This is used for logging purposes.
|
||||
// Logs are formatted with the timestamp and a log file will be created inside log directory.
|
||||
if (args.includes('--enable-logging'))
|
||||
logger.init('log/mb-xrpl.log');
|
||||
|
||||
if (args.length < 3)
|
||||
throw "Arguments mismatch.\n Usage: node mb-xrpl rippleServer";
|
||||
throw "Arguments mismatch.\n Usage: node mb-xrpl <ripple server url>";
|
||||
|
||||
let sashiCliPath = SASHI_CLI_PATH_PROD;
|
||||
// Use sashi CLI in the build folder for dev environment.
|
||||
if (args.length == 4 && args[3] == 'dev')
|
||||
if (args.includes('--dev'))
|
||||
sashiCliPath = SASHI_CLI_PATH_DEV;
|
||||
|
||||
console.log('Starting the xrpl message board' + (args[3] == '--dev' ? ' (in dev mode)' : ''));
|
||||
|
||||
// Read Ripple Server Url.
|
||||
const rippleServer = args[2];
|
||||
const mb = new MessageBoard(CONFIG_PATH, sashiCliPath, rippleServer);
|
||||
const mb = new MessageBoard(CONFIG_PATH, DB_PATH, sashiCliPath, rippleServer);
|
||||
await mb.init();
|
||||
}
|
||||
|
||||
|
||||
919
mb-xrpl/package-lock.json
generated
919
mb-xrpl/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"name": "mb-xrpl",
|
||||
"scripts": {
|
||||
"build": "ncc build mb-xrpl.js -o ../build/mb-xrpl"
|
||||
"build": "ncc build mb-xrpl.js -o dist/mb-xrpl"
|
||||
},
|
||||
"dependencies": {
|
||||
"ripple-lib": "^1.9.8"
|
||||
"ripple-lib": "^1.9.8",
|
||||
"sqlite3": "^5.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ fi
|
||||
shopt -s expand_aliases
|
||||
alias sshskp='ssh -o StrictHostKeychecking=no'
|
||||
if [ "$sshpass" != "" ] && [ "$sshpass" != "null" ]; then
|
||||
alias sshskp='sshpass -p $sshpass ssh -o StrictHostKeychecking=no'
|
||||
alias sshskp="sshpass -p $sshpass ssh -o StrictHostKeychecking=no"
|
||||
fi
|
||||
|
||||
function updateconfig() {
|
||||
|
||||
Reference in New Issue
Block a user