From 9d0b22cb6a4b92b0a80a045651bc2763f99fb9f0 Mon Sep 17 00:00:00 2001 From: Max Wittal Date: Thu, 26 Sep 2024 21:13:12 +0700 Subject: [PATCH] pool server work --- www/pool-server/account.js | 1 + www/pool-server/config.js | 14 ++- www/pool-server/payout.js | 210 +++++++++++++++++++++++++++++++++++++ www/pool-server/schema.js | 28 ++++- www/pool-server/utils.js | 6 ++ 5 files changed, 253 insertions(+), 6 deletions(-) create mode 100644 www/pool-server/payout.js diff --git a/www/pool-server/account.js b/www/pool-server/account.js index 95f96707c..d7d1a5a14 100644 --- a/www/pool-server/account.js +++ b/www/pool-server/account.js @@ -143,6 +143,7 @@ async function update() await account.save(opt); } + // Distribute rewards and update account stats let res = []; for(const entry of result) { const pool_share = entry.points / total_points; diff --git a/www/pool-server/config.js b/www/pool-server/config.js index e21a11c29..e600add8e 100644 --- a/www/pool-server/config.js +++ b/www/pool-server/config.js @@ -1,19 +1,25 @@ const config = {}; +config.pool_fee = 0.01; // [%] config.mmx_divider = 1e6; config.challenge_delay = 6; // [blocks] -config.block_interval = 10 * 1000; // [ms] -config.max_response_time = 55 * 1000; // [ms] config.partial_expiry = 100; // [blocks] config.account_delay = 24; // [blocks] -config.pool_fee = 0.01; // [%] config.share_window = 8640; // [blocks] +config.block_interval = 10 * 1000; // [ms] +config.max_response_time = 50 * 1000; // [ms] config.share_window_hours = 24; // [hours] config.share_interval = 5 * 60; // [sec] -config.min_difficulty = 1; +config.min_difficulty = 1; // needs to be >= 2 for mainnet config.default_difficulty = 1; +config.space_diff_constant = 10000000000; +config.payout_interval = 8640; // [blocks] +config.payout_tx_expire = 1000; // [blocks] +config.payout_threshold = 5; // [MMX] +config.max_payout_count = 2000; config.server_port = 8080; +config.wallet_index = 0; // for pool wallet (payout) config.node_url = "http://localhost:11380"; config.fee_account = "mmx1e7yktu9vpeyq7hx39cmagzfp2um3kddwjf4tlt8j3kmktwc7fk6qmyc6ns"; config.pool_target = "mmx1uj2dth7r9tcn3vas42f0hzz74dkz8ygv59mpx44n7px7j7yhvv4sfmkf0d"; diff --git a/www/pool-server/payout.js b/www/pool-server/payout.js new file mode 100644 index 000000000..699bbeddf --- /dev/null +++ b/www/pool-server/payout.js @@ -0,0 +1,210 @@ + +const mongoose = require('mongoose'); +const axios = require("axios"); +const dbs = require('./schema.js'); +const utils = require('./utils.js'); +const config = require('./config.js'); + +var db = null; +var sync_height = null; + +var check_lock = false; + +async function check() +{ + if(!sync_height) { + return; + } + const height = sync_height; + + if(check_lock) { + return; + } + check_lock = true; + try { + const list = await dbs.Payout.find({pending: true}); + for(const payout of list) { + let failed = false; + let confirmed = false; + try { + const res = await axios.get(config.node_url + '/wapi/transaction?id=' + payout.txid); + const tx = res.data; + if(tx.confirm && tx.confirm >= config.account_delay) { + if(tx.did_fail) { + failed = true; + console.log("Payout transaction failed with:", tx.message); + } else { + confirmed = true; + } + payout.tx_fee = tx.fee.value; + } + } catch(e) { + if(height - payout.height > config.payout_tx_expire + config.account_delay) { + failed = true; + console.log("Payout transaction expired:", "height", payout.height, "txid", payout.txid); + } + console.log("Failed to check payout transaction:", e.message, "txid", payout.txid); + } + + if(confirmed) { + payout.valid = true; + payout.pending = false; + await payout.save(); + console.log("Payout confirmed:", "height", payout.height, "total_amount", payout.total_amount, "count", payout.count, "txid", payout.txid); + } + else if(failed) { + const conn = await db.startSession(); + try { + await conn.startTransaction(); + const opt = {session: conn}; + + // TODO: rollback balances minus tx fee + + payout.valid = false; + payout.pending = false; + await payout.save(); + console.log("Payout failed:", "height", payout.height, "txid", payout.txid); + } catch(e) { + await conn.abortTransaction(); + throw e; + } finally { + conn.endSession(); + } + } + } + } catch(e) { + console.log("check() failed:", e.message); + } finally { + check_lock = false; + } +} + +async function make_payout(height, outputs, opt) +{ + const options = { + auto_send: false, + expire_at: height + config.payout_tx_expire, + }; + let tx = null; + // TODO: create tx + + let total_amount = 0; + for(const entry of outputs) { + const payout = new dbs.UserPayout({ + account: entry[0], + height: height, + amount: entry[1], + txid: tx.id, + }); + total_amount += payout.amount; + await payout.save(opt); + } + const payout = new dbs.Payout({ + txid: tx.id, + total_amount: total_amount, + amounts: outputs, + count: outputs.length, + time: Date.now(), + height: height, + pending: true, + }); + await payout.save(opt); + return tx; +} + +var payout_lock = false; + +async function payout() +{ + if(!sync_height) { + return; + } + const height = sync_height; + + if(payout_lock) { + return; + } + payout_lock = true; + try { + const pool = await dbs.Pool.findOne({id: "this"}); + if(!pool) { + throw new Error("Pool state not found"); + } + if(pool.last_payout && height - pool.last_payout < config.payout_interval) { + return; + } + const conn = await db.startSession(); + try { + await conn.startTransaction(); + const opt = {session: conn}; + + const list = dbs.Account.find({balance: {$gt: config.payout_threshold}}); + + let total_amount = 0; + let outputs = []; + let tx_list = []; + + for await(const account of list) + { + const amount = Math.floor(account.balance); + account.balance -= amount; + await account.save(opt); + + console.log("Payout triggered for", account.address, "amount", amount, "value", amount / config.mmx_divider, "MMX"); + + total_amount += amount; + outputs.push([account.address, amount]); + + if(outputs.length >= config.max_payouts) { + tx_list.push(await make_payout(outputs, opt)); + outputs = []; + } + } + if(outputs.length) { + tx_list.push(await make_payout(outputs, opt)); + } + pool.last_payout = height; + await pool.save(opt); + + await conn.commitTransaction(); + + // TODO: send all tx + } catch(e) { + await conn.abortTransaction(); + throw e; + } finally { + conn.endSession(); + } + } catch(e) { + console.log("payout() failed:", e.message); + } finally { + payout_lock = false; + } +} + +async function update() +{ + try { + sync_height = await utils.get_synced_height(); + if(!sync_height) { + console.log('Waiting for node sync ...'); + } + } catch(e) { + console.log("Failed to query sync height:", e.message); + } +} + +async function main() +{ + db = await mongoose.connect(config.mongodb_uri); + + await update(); + await payout(); + await check(); + + setInterval(update, 10 * 1000); + setInterval(payout, 15 * 60 * 1000); + setInterval(check, 60 * 1000); +} + +main(); diff --git a/www/pool-server/schema.js b/www/pool-server/schema.js index 6ca176628..d1f16f53a 100644 --- a/www/pool-server/schema.js +++ b/www/pool-server/schema.js @@ -22,7 +22,7 @@ const partial = new mongoose.Schema({ const account = new mongoose.Schema({ address: {type: String, unique: true}, balance: {type: Number, default: 0, index: true}, - total_paid: {type: String, default: '0', index: true}, + total_paid: {type: Number, default: 0, index: true}, difficulty: {type: Number, default: 1, index: true}, pool_share: {type: Number, default: 0, index: true}, points_rate: {type: Number, default: 0, index: true}, @@ -45,16 +45,40 @@ const block = new mongoose.Schema({ valid: {type: Boolean, index: true}, }); +const payout = new mongoose.Schema({ + txid: {type: String, unique: true}, + total_amount: Number, + tx_fee: Number, + amounts: Array, // [[account, amount], ...] + count: Number, + time: Date, + height: {type: Number, index: true}, + pending: {type: Boolean, default: true, index: true}, + valid: {type: Boolean, index: true}, +}); + +const user_payout = new mongoose.Schema({ + account: {type: String, index: true}, + height: {type: Number, index: true}, + amount: Number, + txid: String, +}); + const pool = new mongoose.Schema({ id: {type: String, unique: true}, farmers: {type: Number, default: 0}, points_rate: {type: Number, default: 0}, partial_rate: {type: Number, default: 0}, - partial_errors: {type: Object, minimize: false}, + partial_errors: {type: Object}, last_update: {type: Number, default: 0}, + last_payout: {type: Number, default: 0}, +}, { + minimize: false, }); exports.Partial = mongoose.model('Partial', partial); exports.Account = mongoose.model('Account', account); exports.Block = mongoose.model('Block', block); +exports.Payout = mongoose.model('Payout', payout); +exports.UserPayout = mongoose.model('UserPayout', user_payout); exports.Pool = mongoose.model('Pool', pool); diff --git a/www/pool-server/utils.js b/www/pool-server/utils.js index 92a9bb13a..292d68383 100644 --- a/www/pool-server/utils.js +++ b/www/pool-server/utils.js @@ -8,4 +8,10 @@ async function get_synced_height() return res.data; } +function calc_eff_space(points_rate) +{ + // points_rate = points per block (height) + return points_rate * config.space_diff_constant * 2.467; +} + exports.get_synced_height = get_synced_height;