123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778 |
- const crypto = require("crypto");
- const fs = require("fs");
- const path = require("path");
- const pull = require("../server/node_modules/pull-stream");
- const { getConfig } = require("../configs/config-manager.js");
- const { config } = require("../server/SSB_server.js");
- const clamp = (x, lo, hi) => Math.max(lo, Math.min(hi, x));
- const DEFAULT_RULES = {
- epochKind: "WEEKLY",
- alpha: 0.2,
- reserveMin: 500,
- capPerEpoch: 2000,
- caps: { M_max: 3, T_max: 1.5, P_max: 2, cap_user_epoch: 50, w_min: 0.2, w_max: 6 },
- coeffs: { a1: 0.6, a2: 0.4, a3: 0.3, a4: 0.5, b1: 0.5, b2: 1.0 },
- graceDays: 14
- };
- const STORAGE_DIR = path.join(__dirname, "..", "configs");
- const EPOCHS_PATH = path.join(STORAGE_DIR, "banking-epochs.json");
- const TRANSFERS_PATH = path.join(STORAGE_DIR, "banking-allocations.json");
- const ADDR_PATH = path.join(STORAGE_DIR, "wallet-addresses.json");
- function ensureStoreFiles() {
- if (!fs.existsSync(STORAGE_DIR)) fs.mkdirSync(STORAGE_DIR, { recursive: true });
- if (!fs.existsSync(EPOCHS_PATH)) fs.writeFileSync(EPOCHS_PATH, "[]");
- if (!fs.existsSync(TRANSFERS_PATH)) fs.writeFileSync(TRANSFERS_PATH, "[]");
- if (!fs.existsSync(ADDR_PATH)) fs.writeFileSync(ADDR_PATH, "{}");
- }
- function epochIdNow() {
- const d = new Date();
- const tmp = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
- const dayNum = tmp.getUTCDay() || 7;
- tmp.setUTCDate(tmp.getUTCDate() + 4 - dayNum);
- const yearStart = new Date(Date.UTC(tmp.getUTCFullYear(), 0, 1));
- const weekNo = Math.ceil((((tmp - yearStart) / 86400000) + 1) / 7);
- const yyyy = tmp.getUTCFullYear();
- return `${yyyy}-${String(weekNo).padStart(2, "0")}`;
- }
- async function ensureSelfAddressPublished() {
- const me = config.keys.id;
- const local = readAddrMap();
- const current = typeof local[me] === "string" ? local[me] : (local[me] && local[me].address) || null;
- if (current && isValidEcoinAddress(current)) return { status: "present", address: current };
- const cfg = getWalletCfg("user") || {};
- if (!cfg.url) return { status: "skipped" };
- try {
- const addr = await rpcCall("getaddress", []);
- if (addr && isValidEcoinAddress(addr)) {
- await setUserAddress(me, addr, true);
- return { status: "published", address: addr };
- }
- } catch (_) {
- return { status: "error" };
- }
- return { status: "noop" };
- }
- function readJson(p, d) {
- try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch { return d; }
- }
- function writeJson(p, v) {
- fs.writeFileSync(p, JSON.stringify(v, null, 2));
- }
- async function rpcCall(method, params, kind = "user") {
- const cfg = getWalletCfg(kind);
- if (!cfg?.url) {
- return null;
- }
- const headers = {
- "Content-Type": "application/json",
- };
- if (cfg.user || cfg.pass) {
- headers.authorization = "Basic " + Buffer.from(`${cfg.user}:${cfg.pass}`).toString("base64");
- }
- try {
- const res = await fetch(cfg.url, {
- method: "POST",
- headers: headers,
- body: JSON.stringify({
- jsonrpc: "1.0",
- id: "oasis",
- method: method,
- params: params,
- }),
- });
- if (!res.ok) {
- return null;
- }
- const data = await res.json();
- if (data.error) {
- return null;
- }
- return data.result;
- } catch (err) {
- return null;
- }
- }
- async function safeGetBalance(kind = "user") {
- try {
- const r = await rpcCall("getbalance", [], kind);
- return Number(r) || 0;
- } catch {
- return 0;
- }
- }
- function readAddrMap() {
- ensureStoreFiles();
- const raw = readJson(ADDR_PATH, {});
- return raw && typeof raw === "object" ? raw : {};
- }
- function writeAddrMap(m) {
- ensureStoreFiles();
- writeJson(ADDR_PATH, m || {});
- }
- function getLogLimit() {
- return getConfig().ssbLogStream?.limit || 1000;
- }
- function isValidEcoinAddress(addr) {
- return typeof addr === "string" && /^[A-Za-z0-9]{20,64}$/.test(addr);
- }
- function getWalletCfg(kind) {
- const cfg = getConfig() || {};
- if (kind === "pub") {
- return cfg.walletPub || cfg.pubWallet || (cfg.pub && cfg.pub.wallet) || null;
- }
- return cfg.wallet || null;
- }
- function resolveUserId(maybeId) {
- const s = String(maybeId || "").trim();
- if (s) return s;
- return config?.keys?.id || "";
- }
- let FEED_SRC = "none";
- module.exports = ({ services } = {}) => {
- const transfersRepo = {
- listAll: async () => { ensureStoreFiles(); return readJson(TRANSFERS_PATH, []); },
- listByTag: async (tag) => { ensureStoreFiles(); return readJson(TRANSFERS_PATH, []).filter(t => (t.tags || []).includes(tag)); },
- findById: async (id) => { ensureStoreFiles(); return readJson(TRANSFERS_PATH, []).find(t => t.id === id) || null; },
- create: async (t) => { ensureStoreFiles(); const all = readJson(TRANSFERS_PATH, []); all.push(t); writeJson(TRANSFERS_PATH, all); },
- markClosed: async (id, txid) => { ensureStoreFiles(); const all = readJson(TRANSFERS_PATH, []); const i = all.findIndex(x => x.id === id); if (i >= 0) { all[i].status = "CLOSED"; all[i].txid = txid; writeJson(TRANSFERS_PATH, all); } }
- };
- const epochsRepo = {
- list: async () => { ensureStoreFiles(); return readJson(EPOCHS_PATH, []); },
- save: async (epoch) => { ensureStoreFiles(); const all = readJson(EPOCHS_PATH, []); const i = all.findIndex(e => e.id === epoch.id); if (i >= 0) all[i] = epoch; else all.push(epoch); writeJson(EPOCHS_PATH, all); },
- get: async (id) => { ensureStoreFiles(); return readJson(EPOCHS_PATH, []).find(e => e.id === id) || null; }
- };
- async function openSsb() {
- if (services?.ssb) return services.ssb;
- if (services?.cooler?.open) return services.cooler.open();
- if (global.ssb) return global.ssb;
- try {
- const srv = require("../server/SSB_server.js");
- if (srv?.ssb) return srv.ssb;
- if (srv?.server) return srv.server;
- if (srv?.default) return srv.default;
- } catch (_) {}
- return null;
- }
- async function getWalletFromSSB(userId) {
- const ssb = await openSsb();
- if (!ssb) return null;
- const msgs = await new Promise((resolve, reject) =>
- pull(
- ssb.createLogStream({ limit: getLogLimit() }),
- pull.collect((err, arr) => err ? reject(err) : resolve(arr))
- )
- );
- for (let i = msgs.length - 1; i >= 0; i--) {
- const v = msgs[i].value || {};
- const c = v.content || {};
- if (v.author === userId && c && c.type === "wallet" && c.coin === "ECO" && typeof c.address === "string") {
- return c.address;
- }
- }
- return null;
- }
- async function scanAllWalletsSSB() {
- const ssb = await openSsb();
- if (!ssb) return {};
- const latest = {};
- const msgs = await new Promise((resolve, reject) =>
- pull(
- ssb.createLogStream({ limit: getLogLimit() }),
- pull.collect((err, arr) => err ? reject(err) : resolve(arr))
- )
- );
- for (let i = msgs.length - 1; i >= 0; i--) {
- const v = msgs[i].value || {};
- const c = v.content || {};
- if (c && c.type === "wallet" && c.coin === "ECO" && typeof c.address === "string") {
- if (!latest[v.author]) latest[v.author] = c.address;
- }
- }
- return latest;
- }
- async function publishSelfAddress(address) {
- const ssb = await openSsb();
- if (!ssb) return false;
- const msg = { type: "wallet", coin: "ECO", address, updatedAt: new Date().toISOString() };
- await new Promise((resolve, reject) => ssb.publish(msg, (err, val) => err ? reject(err) : resolve(val)));
- return true;
- }
- async function listUsers() {
- const addrLocal = readAddrMap();
- const ids = Object.keys(addrLocal);
- if (ids.length > 0) return ids.map(id => ({ id }));
- return [{ id: config.keys.id }];
- }
- async function getUserAddress(userId) {
- const v = readAddrMap()[userId];
- const local = typeof v === "string" ? v : (v && v.address) || null;
- if (local) return local;
- const ssbAddr = await getWalletFromSSB(userId);
- return ssbAddr;
- }
- async function setUserAddress(userId, address, publishIfSelf) {
- const m = readAddrMap();
- m[userId] = address;
- writeAddrMap(m);
- if (publishIfSelf && userId === config.keys.id) await publishSelfAddress(address);
- return true;
- }
- async function addAddress({ userId, address }) {
- if (!userId || !address || !isValidEcoinAddress(address)) return { status: "invalid" };
- const m = readAddrMap();
- const prev = m[userId];
- if (prev && (prev === address || (prev.address && prev.address === address))) return { status: "exists" };
- m[userId] = address;
- writeAddrMap(m);
- if (userId === config.keys.id) await publishSelfAddress(address);
- return { status: prev ? "updated" : "added" };
- }
- async function removeAddress({ userId }) {
- if (!userId) return { status: "invalid" };
- const m = readAddrMap();
- if (!m[userId]) return { status: "not_found" };
- delete m[userId];
- writeAddrMap(m);
- return { status: "deleted" };
- }
- async function listAddressesMerged() {
- const local = readAddrMap();
- const ssbAll = await scanAllWalletsSSB();
- const keys = new Set([...Object.keys(local), ...Object.keys(ssbAll)]);
- const out = [];
- for (const id of keys) {
- if (local[id]) out.push({ id, address: typeof local[id] === "string" ? local[id] : local[id].address, source: "local" });
- else if (ssbAll[id]) out.push({ id, address: ssbAll[id], source: "ssb" });
- }
- return out;
- }
- function idsEqual(a, b) {
- if (!a || !b) return false;
- const A = String(a).trim();
- const B = String(b).trim();
- if (A === B) return true;
- const strip = s => s.replace(/^@/, "").replace(/\.ed25519$/, "");
- return strip(A) === strip(B);
- }
- function inferType(c = {}) {
- if (c.vote) return "vote";
- if (c.votes) return "votes";
- if (c.address && c.coin === "ECO" && c.type === "wallet") return "bankWallet";
- if (typeof c.amount !== "undefined" && c.epochId && c.allocationId) return "bankClaim";
- if (typeof c.item_type !== "undefined" && typeof c.status !== "undefined") return "market";
- if (typeof c.goal !== "undefined" && typeof c.progress !== "undefined") return "project";
- if (typeof c.members !== "undefined" && typeof c.isAnonymous !== "undefined") return "tribe";
- if (typeof c.date !== "undefined" && typeof c.location !== "undefined") return "event";
- if (typeof c.priority !== "undefined" && typeof c.status !== "undefined" && c.title) return "task";
- if (typeof c.confirmations !== "undefined" && typeof c.severity !== "undefined") return "report";
- if (typeof c.job_type !== "undefined" && typeof c.status !== "undefined") return "job";
- if (typeof c.url !== "undefined" && typeof c.mimeType !== "undefined" && c.type === "audio") return "audio";
- if (typeof c.url !== "undefined" && typeof c.mimeType !== "undefined" && c.type === "video") return "video";
- if (typeof c.url !== "undefined" && c.title && c.key) return "document";
- if (typeof c.text !== "undefined" && typeof c.refeeds !== "undefined") return "feed";
- if (typeof c.text !== "undefined" && typeof c.contentWarning !== "undefined") return "post";
- if (typeof c.contact !== "undefined") return "contact";
- if (typeof c.about !== "undefined") return "about";
- if (typeof c.concept !== "undefined" && typeof c.amount !== "undefined" && c.status) return "transfer";
- return "";
- }
- function normalizeType(a) {
- const t = a.type || a.content?.type || inferType(a.content) || "";
- return String(t).toLowerCase();
- }
- function priorityBump(p) {
- const s = String(p || "").toUpperCase();
- if (s === "HIGH") return 3;
- if (s === "MEDIUM") return 1;
- return 0;
- }
- function severityBump(s) {
- const x = String(s || "").toUpperCase();
- if (x === "CRITICAL") return 6;
- if (x === "HIGH") return 4;
- if (x === "MEDIUM") return 2;
- return 0;
- }
- function scoreMarket(c) {
- const st = String(c.status || "").toUpperCase();
- let s = 5;
- if (st === "SOLD") s += 8;
- else if (st === "ACTIVE") s += 3;
- const bids = Array.isArray(c.auctions_poll) ? c.auctions_poll.length : 0;
- s += Math.min(10, bids);
- return s;
- }
- function scoreProject(c) {
- const st = String(c.status || "ACTIVE").toUpperCase();
- const prog = Number(c.progress || 0);
- let s = 8 + Math.min(10, prog / 10);
- if (st === "FUNDED") s += 10;
- return s;
- }
- function calculateOpinionScore(content) {
- const cats = content?.opinions || {};
- let s = 0;
- for (const k in cats) {
- if (!Object.prototype.hasOwnProperty.call(cats, k)) continue;
- if (k === "interesting" || k === "inspiring") s += 5;
- else if (k === "boring" || k === "spam" || k === "propaganda") s -= 3;
- else s += 1;
- }
- return s;
- }
- async function listAllActions() {
- if (services?.feed?.listAll) {
- const arr = await services.feed.listAll();
- FEED_SRC = "services.feed.listAll";
- return normalizeFeedArray(arr);
- }
- if (services?.activity?.list) {
- const arr = await services.activity.list();
- FEED_SRC = "services.activity.list";
- return normalizeFeedArray(arr);
- }
- if (typeof global.listFeed === "function") {
- const arr = await global.listFeed("all");
- FEED_SRC = "global.listFeed('all')";
- return normalizeFeedArray(arr);
- }
- const ssb = await openSsb();
- if (!ssb || !ssb.createLogStream) {
- FEED_SRC = "none";
- return [];
- }
- const msgs = await new Promise((resolve, reject) =>
- pull(
- ssb.createLogStream({ limit: getLogLimit() }),
- pull.collect((err, arr) => err ? reject(err) : resolve(arr))
- )
- );
- FEED_SRC = "ssb.createLogStream";
- return msgs.map(m => {
- const v = m.value || {};
- const c = v.content || {};
- return {
- id: v.key || m.key,
- author: v.author,
- type: (c.type || "").toLowerCase(),
- value: v,
- content: c
- };
- });
- }
- function normalizeFeedArray(arr) {
- if (!Array.isArray(arr)) return [];
- return arr.map(x => {
- const value = x.value || {};
- const content = x.content || value.content || {};
- const author = x.author || value.author || content.author || null;
- const type = (content.type || "").toLowerCase();
- return { id: x.id || value.key || x.key, author, type, value, content };
- });
- }
- async function publishKarmaScore(userId, karmaScore) {
- const ssb = await openSsb();
- if (!ssb) return false;
- const timestamp = new Date().toISOString();
- const content = {
- type: "karmaScore",
- karmaScore,
- userId: userId,
- timestamp: timestamp,
- };
- return new Promise((resolve, reject) => {
- ssb.publish(content, (err, msg) => {
- if (err) reject(err);
- else resolve(msg);
- });
- });
- }
- async function fetchUserActions(userId) {
- const me = resolveUserId(userId);
- const actions = await listAllActions();
- const authored = actions.filter(a =>
- (a.author && a.author === me) || (a.value?.author && a.value.author === me)
- );
- if (authored.length) return authored;
- return actions.filter(a => {
- const c = a.content || {};
- const fields = [c.author, c.organizer, c.seller, c.about, c.contact];
- return fields.some(f => f && f === me);
- });
- }
- function scoreFromActions(actions) {
- let score = 0;
- for (const action of actions) {
- const t = normalizeType(action);
- const c = action.content || {};
- if (t === "post") score += 10;
- else if (t === "comment") score += 5;
- else if (t === "like") score += 2;
- else if (t === "image") score += 8;
- else if (t === "video") score += 12;
- else if (t === "audio") score += 8;
- else if (t === "document") score += 6;
- else if (t === "bookmark") score += 2;
- else if (t === "feed") score += 6;
- else if (t === "forum") score += c.root ? 5 : 10;
- else if (t === "vote") score += 3 + calculateOpinionScore(c);
- else if (t === "votes") score += Math.min(10, Number(c.totalVotes || 0));
- else if (t === "market") score += scoreMarket(c);
- else if (t === "project") score += scoreProject(c);
- else if (t === "tribe") score += 6 + Math.min(10, Array.isArray(c.members) ? c.members.length * 0.5 : 0);
- else if (t === "event") score += 4 + Math.min(10, Array.isArray(c.attendees) ? c.attendees.length : 0);
- else if (t === "task") score += 3 + priorityBump(c.priority);
- else if (t === "report") score += 4 + (Array.isArray(c.confirmations) ? c.confirmations.length : 0) + severityBump(c.severity);
- else if (t === "curriculum") score += 5;
- else if (t === "aiexchange") score += Array.isArray(c.ctx) ? Math.min(10, c.ctx.length) : 0;
- else if (t === "job") score += 4 + (Array.isArray(c.subscribers) ? c.subscribers.length : 0);
- else if (t === "bankclaim") score += Math.min(20, Math.log(1 + Math.max(0, Number(c.amount) || 0)) * 5);
- else if (t === "bankwallet") score += 2;
- else if (t === "transfer") score += 1;
- else if (t === "about") score += 1;
- else if (t === "contact") score += 1;
- else if (t === "pub") score += 1;
- }
- return Math.max(0, Math.round(score));
- }
- async function getUserEngagementScore(userId) {
- const actions = await fetchUserActions(userId);
- const karmaScore = scoreFromActions(actions);
- const previousKarmaScore = await getLastKarmaScore(userId);
- const lastPublishedTimestamp = await getLastPublishedTimestamp(userId);
- const currentTimestamp = Date.now();
- const timeDifference = currentTimestamp - new Date(lastPublishedTimestamp).getTime();
- const shouldPublish = karmaScore !== previousKarmaScore && timeDifference >= 24 * 60 * 60 * 1000;
- const canPublish = Boolean(services?.ssb || global.ssb);
- if (shouldPublish && canPublish) {
- await publishKarmaScore(userId, karmaScore);
- }
- return karmaScore;
- }
- async function getLastKarmaScore(userId) {
- const ssb = await openSsb();
- if (!ssb) return 0;
- const matchOne = (arr) => {
- if (!arr || !arr.length) return 0;
- const v = arr[0].value || arr[0];
- const c = v.content || {};
- return Number(c.karmaScore) || 0;
- };
- return new Promise((resolve) => {
- const source = ssb.messagesByType
- ? ssb.messagesByType({ type: "karmaScore", reverse: true })
- : ssb.createLogStream && ssb.createLogStream({ reverse: true });
- if (!source) return resolve(0);
- pull(
- source,
- pull.filter((msg) => {
- const v = msg.value || msg;
- const c = v.content || {};
- return c && c.type === "karmaScore" && c.userId === userId;
- }),
- pull.take(1),
- pull.collect((err, arr) => {
- if (err) return resolve(0);
- resolve(matchOne(arr));
- })
- );
- });
- }
- async function getLastPublishedTimestamp(userId) {
- const ssb = await openSsb();
- if (!ssb) return new Date(0).toISOString();
- const fallback = new Date(0).toISOString();
- return new Promise((resolve) => {
- const source = ssb.messagesByType
- ? ssb.messagesByType({ type: "karmaScore", reverse: true })
- : ssb.createLogStream && ssb.createLogStream({ reverse: true });
- if (!source) return resolve(fallback);
- pull(
- source,
- pull.filter((msg) => {
- const v = msg.value || msg;
- const c = v.content || {};
- return c && c.type === "karmaScore" && c.userId === userId;
- }),
- pull.take(1),
- pull.collect((err, arr) => {
- if (err || !arr || !arr.length) return resolve(fallback);
- const v = arr[0].value || arr[0];
- const c = v.content || {};
- resolve(c.timestamp || fallback);
- })
- );
- });
- }
-
- function computePoolVars(pubBal, rules) {
- const alphaCap = (rules.alpha || DEFAULT_RULES.alpha) * pubBal;
- const available = Math.max(0, pubBal - (rules.reserveMin || DEFAULT_RULES.reserveMin));
- const rawMin = Math.min(available, (rules.capPerEpoch || DEFAULT_RULES.capPerEpoch), alphaCap);
- const pool = clamp(rawMin, 0, Number.MAX_SAFE_INTEGER);
- return { pubBal, alphaCap, available, rawMin, pool };
- }
- async function computeEpoch({ epochId, userId, rules = DEFAULT_RULES }) {
- const pubBal = await safeGetBalance("pub");
- const pv = computePoolVars(pubBal, rules);
- const engagementScore = await getUserEngagementScore(userId);
- const userWeight = 1 + engagementScore / 100;
- const weights = [{ user: userId, w: userWeight }];
- const W = weights.reduce((acc, x) => acc + x.w, 0) || 1;
- const capUser = (rules.caps && rules.caps.cap_user_epoch) || DEFAULT_RULES.caps.cap_user_epoch;
- const allocations = weights.map(({ user, w }) => {
- const amount = Math.min(pv.pool * w / W, capUser);
- return {
- id: `alloc:${epochId}:${user}`,
- epoch: epochId,
- user,
- weight: Number(w.toFixed(6)),
- amount: Number(amount.toFixed(6))
- };
- });
- const snapshot = JSON.stringify({ epochId, pool: pv.pool, weights, allocations, rules }, null, 2);
- const hash = crypto.createHash("sha256").update(snapshot).digest("hex");
- return { epoch: { id: epochId, pool: Number(pv.pool.toFixed(6)), weightsSum: Number(W.toFixed(6)), rules, hash }, allocations };
- }
- async function executeEpoch({ epochId, rules = DEFAULT_RULES }) {
- const { epoch, allocations } = await computeEpoch({ epochId, userId: config.keys.id, rules });
- await epochsRepo.save(epoch);
- for (const a of allocations) {
- if (a.amount <= 0) continue;
- await transfersRepo.create({
- id: a.id,
- from: "PUB",
- to: a.user,
- amount: a.amount,
- concept: `UBI ${epochId}`,
- status: "UNCONFIRMED",
- createdAt: new Date().toISOString(),
- deadline: new Date(Date.now() + DEFAULT_RULES.graceDays * 86400000).toISOString(),
- tags: ["UBI", `epoch:${epochId}`],
- opinions: {}
- });
- }
- return { epoch, allocations };
- }
- async function publishBankClaim({ amount, epochId, allocationId, txid }) {
- const ssbClient = await openSsb();
- const content = { type: "bankClaim", amount, epochId, allocationId, txid, timestamp: Date.now() };
- return new Promise((resolve, reject) => ssbClient.publish(content, (err, res) => err ? reject(err) : resolve(res)));
- }
- async function claimAllocation({ transferId, claimerId, pubWalletUrl, pubWalletUser, pubWalletPass }) {
- const allocation = await transfersRepo.findById(transferId);
- if (!allocation || allocation.status !== "UNCONFIRMED") throw new Error("Invalid allocation or already confirmed.");
- if (allocation.to !== claimerId) throw new Error("This allocation is not for you.");
- const txid = await rpcCall("sendtoaddress", [pubWalletUrl, allocation.amount, "UBI claim", pubWalletUser, pubWalletPass]);
- return { txid };
- }
- async function updateAllocationStatus(allocationId, status, txid) {
- const all = await transfersRepo.listAll();
- const idx = all.findIndex(t => t.id === allocationId);
- if (idx >= 0) {
- all[idx].status = status;
- all[idx].txid = txid;
- await transfersRepo.create(all[idx]);
- }
- }
- async function listBanking(filter = "overview", userId) {
- const uid = resolveUserId(userId);
- const epochId = epochIdNow();
- const pubBalance = await safeGetBalance("pub");
- const userBalance = await safeGetBalance("user");
- const epochs = await epochsRepo.list();
- const all = await transfersRepo.listByTag("UBI");
- const allocations = all.map(t => ({
- id: t.id, concept: t.concept, from: t.from, to: t.to, amount: t.amount, status: t.status,
- createdAt: t.createdAt || t.deadline || new Date().toISOString(), txid: t.txid
- }));
- let computed = null;
- try { computed = await computeEpoch({ epochId, userId: uid, rules: DEFAULT_RULES }); } catch {}
- const pv = computePoolVars(pubBalance, DEFAULT_RULES);
- const actions = await fetchUserActions(uid);
- const engagementScore = scoreFromActions(actions);
- const poolForEpoch = computed?.epoch?.pool || pv.pool || 0;
- const futureUBI = Number(((engagementScore / 100) * poolForEpoch).toFixed(6));
- const addresses = await listAddressesMerged();
- const summary = {
- userBalance,
- pubBalance,
- epochId,
- pool: poolForEpoch,
- weightsSum: computed?.epoch?.weightsSum || 0,
- userEngagementScore: engagementScore,
- futureUBI
- };
- return { summary, allocations, epochs, rules: DEFAULT_RULES, addresses };
- }
- async function getAllocationById(id) {
- const t = await transfersRepo.findById(id);
- if (!t) return null;
- return { id: t.id, concept: t.concept, from: t.from, to: t.to, amount: t.amount, status: t.status, createdAt: t.createdAt || new Date().toISOString(), txid: t.txid };
- }
- async function getEpochById(id) {
- const existing = await epochsRepo.get(id);
- if (existing) return existing;
- const all = await transfersRepo.listAll();
- const filtered = all.filter(t => (t.tags || []).includes(`epoch:${id}`));
- const pool = filtered.reduce((s, t) => s + Number(t.amount || 0), 0);
- return { id, pool, weightsSum: 0, rules: DEFAULT_RULES, hash: "-" };
- }
- async function listEpochAllocations(id) {
- const all = await transfersRepo.listAll();
- return all.filter(t => (t.tags || []).includes(`epoch:${id}`)).map(t => ({
- id: t.id, concept: t.concept, from: t.from, to: t.to, amount: t.amount, status: t.status, createdAt: t.createdAt || new Date().toISOString(), txid: t.txid
- }));
- }
-
- async function calculateEcoinValue() {
- let isSynced = false;
- let circulatingSupply = 0;
- try {
- circulatingSupply = await getCirculatingSupply();
- isSynced = circulatingSupply > 0;
- } catch (error) {
- circulatingSupply = 0;
- isSynced = false;
- }
- const totalSupply = 25500000;
- const ecoValuePerHour = await calculateEcoValuePerHour(circulatingSupply);
- const ecoInHours = calculateEcoinHours(circulatingSupply, ecoValuePerHour);
- const inflationFactor = await calculateInflationFactor(circulatingSupply, totalSupply);
- return {
- ecoValue: ecoValuePerHour,
- ecoInHours: Number(ecoInHours.toFixed(2)),
- totalSupply: totalSupply,
- inflationFactor: inflationFactor ? Number(inflationFactor.toFixed(2)) : 0,
- currentSupply: circulatingSupply,
- isSynced: isSynced
- };
- }
- async function calculateEcoValuePerHour(circulatingSupply) {
- const issuanceRate = await getIssuanceRate();
- const inflation = await calculateInflationFactor(circulatingSupply, 25500000);
- const ecoValuePerHour = (circulatingSupply / 100000) * (1 + inflation / 100);
- return ecoValuePerHour;
- }
- function calculateEcoinHours(circulatingSupply, ecoValuePerHour) {
- const ecoInHours = circulatingSupply / ecoValuePerHour;
- return ecoInHours;
- }
- async function calculateInflationFactor(circulatingSupply, totalSupply) {
- const issuanceRate = await getIssuanceRate();
- if (circulatingSupply > 0) {
- const inflationRate = (issuanceRate / circulatingSupply) * 100;
- return inflationRate;
- }
- return 0;
- }
- async function getIssuanceRate() {
- try {
- const result = await rpcCall("getmininginfo", []);
- const blockValue = result?.blockvalue || 0;
- const blocks = result?.blocks || 0;
- return (blockValue / 1e8) * blocks;
- } catch (error) {
- return 0.02;
- }
- }
- async function getCirculatingSupply() {
- try {
- const result = await rpcCall("getinfo", []);
- return result?.moneysupply || 0;
- } catch (error) {
- return 0;
- }
- }
-
- async function getBankingData(userId) {
- const ecoValue = await calculateEcoinValue();
- const karmaScore = await getUserEngagementScore(userId);
- return {
- ecoValue,
- karmaScore,
- };
- }
- return {
- DEFAULT_RULES,
- computeEpoch,
- executeEpoch,
- getUserEngagementScore,
- publishBankClaim,
- claimAllocation,
- listBanking,
- getAllocationById,
- getEpochById,
- listEpochAllocations,
- addAddress,
- removeAddress,
- ensureSelfAddressPublished,
- getUserAddress,
- setUserAddress,
- listAddressesMerged,
- calculateEcoinValue,
- getBankingData
- };
- };
|