banking_model.js 43 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138
  1. const crypto = require("crypto");
  2. const fs = require("fs");
  3. const path = require("path");
  4. const pull = require("../server/node_modules/pull-stream");
  5. const { getConfig } = require("../configs/config-manager.js");
  6. const { config } = require("../server/SSB_server.js");
  7. const clamp = (x, lo, hi) => Math.max(lo, Math.min(hi, x));
  8. const MAX_PENDING_EPOCHS = 12;
  9. const DEFAULT_RULES = {
  10. epochKind: "MONTHLY",
  11. alpha: 0.2,
  12. reserveMin: 500,
  13. capPerEpoch: 2000,
  14. caps: { M_max: 3, T_max: 1.5, P_max: 2, cap_user_epoch: 50, w_min: 0.2, w_max: 6 },
  15. coeffs: { a1: 0.6, a2: 0.4, a3: 0.3, a4: 0.5, b1: 0.5, b2: 1.0 },
  16. graceDays: 30
  17. };
  18. const STORAGE_DIR = path.join(__dirname, "..", "configs");
  19. const EPOCHS_PATH = path.join(STORAGE_DIR, "banking-epochs.json");
  20. const TRANSFERS_PATH = path.join(STORAGE_DIR, "banking-allocations.json");
  21. const ADDR_PATH = path.join(STORAGE_DIR, "wallet-addresses.json");
  22. function ensureStoreFiles() {
  23. if (!fs.existsSync(STORAGE_DIR)) fs.mkdirSync(STORAGE_DIR, { recursive: true });
  24. if (!fs.existsSync(EPOCHS_PATH)) fs.writeFileSync(EPOCHS_PATH, "[]");
  25. if (!fs.existsSync(TRANSFERS_PATH)) fs.writeFileSync(TRANSFERS_PATH, "[]");
  26. if (!fs.existsSync(ADDR_PATH)) fs.writeFileSync(ADDR_PATH, "{}");
  27. }
  28. function epochIdNow() {
  29. const d = new Date();
  30. const yyyy = d.getUTCFullYear();
  31. const mm = String(d.getUTCMonth() + 1).padStart(2, "0");
  32. return `${yyyy}-${mm}`;
  33. }
  34. async function getAnyWalletAddress() {
  35. const tryOne = async (method, params = []) => {
  36. const r = await rpcCall(method, params, "user");
  37. if (!r) return null;
  38. if (typeof r === "string" && isValidEcoinAddress(r)) return r;
  39. if (Array.isArray(r) && r.length && isValidEcoinAddress(r[0])) return r[0];
  40. if (r && typeof r === "object") {
  41. const keys = Object.keys(r);
  42. if (keys.length && isValidEcoinAddress(keys[0])) return keys[0];
  43. if (r.address && isValidEcoinAddress(r.address)) return r.address;
  44. }
  45. return null;
  46. };
  47. return await tryOne("getnewaddress")
  48. || await tryOne("getaddress")
  49. || await tryOne("getaccountaddress", [""])
  50. || await tryOne("getaddressesbyaccount", [""])
  51. || await tryOne("getaddressesbylabel", [""])
  52. || await tryOne("getaddressesbylabel", ["default"]);
  53. }
  54. async function ensureSelfAddressPublished() {
  55. const me = config.keys.id;
  56. const local = readAddrMap();
  57. const current = typeof local[me] === "string" ? local[me] : (local[me] && local[me].address) || null;
  58. if (current && isValidEcoinAddress(current)) return { status: "present", address: current };
  59. const cfg = getWalletCfg("user") || {};
  60. if (!cfg.url) return { status: "skipped" };
  61. const addr = await getAnyWalletAddress();
  62. if (addr && isValidEcoinAddress(addr)) {
  63. const m = readAddrMap();
  64. m[me] = addr;
  65. writeAddrMap(m);
  66. let ssb = null;
  67. try {
  68. if (services?.cooler?.open) ssb = await services.cooler.open();
  69. else if (global.ssb) ssb = global.ssb;
  70. else {
  71. try {
  72. const srv = require("../server/SSB_server.js");
  73. ssb = srv?.ssb || srv?.server || srv?.default || null;
  74. } catch (_) {}
  75. }
  76. } catch (_) {}
  77. if (ssb && ssb.publish) {
  78. await new Promise((resolve, reject) =>
  79. ssb.publish(
  80. { type: "wallet", coin: "ECO", address: addr, timestamp: Date.now(), updatedAt: new Date().toISOString() },
  81. (err) => err ? reject(err) : resolve()
  82. )
  83. );
  84. }
  85. return { status: "published", address: addr };
  86. }
  87. return { status: "error" };
  88. }
  89. function readJson(p, d) {
  90. try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch { return d; }
  91. }
  92. function writeJson(p, v) {
  93. fs.writeFileSync(p, JSON.stringify(v, null, 2));
  94. }
  95. async function rpcCall(method, params, kind = "user") {
  96. const cfg = getWalletCfg(kind);
  97. if (!cfg?.url) {
  98. return null;
  99. }
  100. const headers = {
  101. "Content-Type": "application/json",
  102. };
  103. if (cfg.user || cfg.pass) {
  104. headers.authorization = "Basic " + Buffer.from(`${cfg.user}:${cfg.pass}`).toString("base64");
  105. }
  106. try {
  107. const res = await fetch(cfg.url, {
  108. method: "POST",
  109. headers: headers,
  110. body: JSON.stringify({
  111. jsonrpc: "1.0",
  112. id: "oasis",
  113. method: method,
  114. params: params,
  115. }),
  116. });
  117. if (!res.ok) {
  118. return null;
  119. }
  120. const data = await res.json();
  121. if (data.error) {
  122. return null;
  123. }
  124. return data.result;
  125. } catch (err) {
  126. return null;
  127. }
  128. }
  129. async function safeGetBalance(kind = "user") {
  130. try {
  131. const r = await rpcCall("getbalance", [], kind);
  132. return Number(r) || 0;
  133. } catch {
  134. return 0;
  135. }
  136. }
  137. function readAddrMap() {
  138. ensureStoreFiles();
  139. const raw = readJson(ADDR_PATH, {});
  140. return raw && typeof raw === "object" ? raw : {};
  141. }
  142. function writeAddrMap(m) {
  143. ensureStoreFiles();
  144. writeJson(ADDR_PATH, m || {});
  145. }
  146. function getLogLimit() {
  147. return getConfig().ssbLogStream?.limit || 1000;
  148. }
  149. function isValidEcoinAddress(addr) {
  150. return typeof addr === "string" && /^[A-Za-z0-9]{20,64}$/.test(addr);
  151. }
  152. function getWalletCfg(kind) {
  153. const cfg = getConfig() || {};
  154. if (kind === "pub") {
  155. if (!isPubNode()) return null;
  156. return cfg.wallet || null;
  157. }
  158. return cfg.wallet || null;
  159. }
  160. function isPubNode() {
  161. const pubId = (getConfig() || {}).walletPub?.pubId || "";
  162. const myId = config?.keys?.id || "";
  163. return !!pubId && !!myId && pubId === myId;
  164. }
  165. function getConfiguredPubId() {
  166. return (getConfig() || {}).walletPub?.pubId || "";
  167. }
  168. function resolveUserId(maybeId) {
  169. const s = String(maybeId || "").trim();
  170. if (s) return s;
  171. return config?.keys?.id || "";
  172. }
  173. let FEED_SRC = "none";
  174. module.exports = ({ services } = {}) => {
  175. const transfersRepo = {
  176. listAll: async () => { ensureStoreFiles(); return readJson(TRANSFERS_PATH, []); },
  177. listByTag: async (tag) => { ensureStoreFiles(); return readJson(TRANSFERS_PATH, []).filter(t => (t.tags || []).includes(tag)); },
  178. findById: async (id) => { ensureStoreFiles(); return readJson(TRANSFERS_PATH, []).find(t => t.id === id) || null; },
  179. create: async (t) => { ensureStoreFiles(); const all = readJson(TRANSFERS_PATH, []); all.push(t); writeJson(TRANSFERS_PATH, all); },
  180. 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); } }
  181. };
  182. const epochsRepo = {
  183. list: async () => { ensureStoreFiles(); return readJson(EPOCHS_PATH, []); },
  184. 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); },
  185. get: async (id) => { ensureStoreFiles(); return readJson(EPOCHS_PATH, []).find(e => e.id === id) || null; }
  186. };
  187. let ssbInstance;
  188. async function openSsb() {
  189. if (ssbInstance) return ssbInstance;
  190. if (services?.cooler?.open) ssbInstance = await services.cooler.open();
  191. else if (cooler?.open) ssbInstance = await cooler.open();
  192. else if (global.ssb) ssbInstance = global.ssb;
  193. else {
  194. try {
  195. const srv = require("../server/SSB_server.js");
  196. ssbInstance = srv?.ssb || srv?.server || srv?.default || null;
  197. } catch (_) {
  198. ssbInstance = null;
  199. }
  200. }
  201. return ssbInstance;
  202. }
  203. async function scanLogStream() {
  204. const ssb = await openSsb();
  205. if (!ssb) return [];
  206. return new Promise((resolve, reject) =>
  207. pull(
  208. ssb.createLogStream({ limit: getLogLimit(), reverse: true }),
  209. pull.collect((err, arr) => err ? reject(err) : resolve(arr))
  210. )
  211. );
  212. }
  213. async function getWalletFromSSB(userId) {
  214. const msgs = await scanLogStream();
  215. for (const m of msgs) {
  216. const v = m.value || {};
  217. const c = v.content || {};
  218. if (v.author === userId && c && c.type === "wallet" && c.coin === "ECO" && typeof c.address === "string") {
  219. return c.address;
  220. }
  221. }
  222. return null;
  223. }
  224. async function scanAllWalletsSSB() {
  225. const latest = {};
  226. const msgs = await scanLogStream();
  227. for (const m of msgs) {
  228. const v = m.value || {};
  229. const c = v.content || {};
  230. if (c && c.type === "wallet" && c.coin === "ECO" && typeof c.address === "string") {
  231. if (!latest[v.author]) latest[v.author] = c.address;
  232. }
  233. }
  234. return latest;
  235. }
  236. async function publishSelfAddress(address) {
  237. const ssb = await openSsb();
  238. if (!ssb) return false;
  239. const msg = { type: "wallet", coin: "ECO", address, updatedAt: new Date().toISOString() };
  240. await new Promise((resolve, reject) => ssb.publish(msg, (err, val) => err ? reject(err) : resolve(val)));
  241. return true;
  242. }
  243. async function listUsers() {
  244. const addrLocal = readAddrMap();
  245. const ids = Object.keys(addrLocal);
  246. if (ids.length > 0) return ids.map(id => ({ id }));
  247. return [{ id: config.keys.id }];
  248. }
  249. async function getUserAddress(userId) {
  250. const v = readAddrMap()[userId];
  251. if (v === "__removed__") return null;
  252. const local = typeof v === "string" ? v : (v && v.address) || null;
  253. if (local) return local;
  254. const ssbAddr = await getWalletFromSSB(userId);
  255. return ssbAddr;
  256. }
  257. async function setUserAddress(userId, address, publishIfSelf) {
  258. const m = readAddrMap();
  259. m[userId] = address;
  260. writeAddrMap(m);
  261. if (publishIfSelf && idsEqual(userId, config.keys.id)) await publishSelfAddress(address);
  262. return true;
  263. }
  264. async function addAddress({ userId, address }) {
  265. if (!userId || !address || !isValidEcoinAddress(address)) return { status: "invalid" };
  266. const m = readAddrMap();
  267. const prev = m[userId];
  268. m[userId] = address;
  269. writeAddrMap(m);
  270. if (idsEqual(userId, config.keys.id)) await publishSelfAddress(address);
  271. return { status: prev ? (prev === address || (prev && prev.address === address) ? "exists" : "updated") : "added" };
  272. }
  273. async function removeAddress({ userId }) {
  274. if (!userId) return { status: "invalid" };
  275. const m = readAddrMap();
  276. if (m[userId]) {
  277. delete m[userId];
  278. writeAddrMap(m);
  279. return { status: "deleted" };
  280. }
  281. const ssbAll = await scanAllWalletsSSB();
  282. if (!ssbAll[userId]) return { status: "not_found" };
  283. m[userId] = "__removed__";
  284. writeAddrMap(m);
  285. return { status: "deleted" };
  286. }
  287. async function listAddressesMerged() {
  288. const local = readAddrMap();
  289. const ssbAll = await scanAllWalletsSSB();
  290. const keys = new Set([...Object.keys(local), ...Object.keys(ssbAll)]);
  291. const out = [];
  292. for (const id of keys) {
  293. if (local[id] === "__removed__") continue;
  294. if (local[id]) out.push({ id, address: typeof local[id] === "string" ? local[id] : local[id].address, source: "local" });
  295. else if (ssbAll[id]) out.push({ id, address: ssbAll[id], source: "ssb" });
  296. }
  297. return out;
  298. }
  299. function idsEqual(a, b) {
  300. if (!a || !b) return false;
  301. const A = String(a).trim();
  302. const B = String(b).trim();
  303. if (A === B) return true;
  304. const strip = s => s.replace(/^@/, "").replace(/\.ed25519$/, "");
  305. return strip(A) === strip(B);
  306. }
  307. function inferType(c = {}) {
  308. if (c.vote) return "vote";
  309. if (c.votes) return "votes";
  310. if (c.address && c.coin === "ECO" && c.type === "wallet") return "bankWallet";
  311. if (c.type === "ubiClaimResult" && c.txid && c.epochId) return "ubiClaimResult";
  312. if (typeof c.amount !== "undefined" && c.epochId && c.allocationId) return "bankClaim";
  313. if (typeof c.item_type !== "undefined" && typeof c.status !== "undefined") return "market";
  314. if (typeof c.goal !== "undefined" && typeof c.progress !== "undefined") return "project";
  315. if (typeof c.members !== "undefined" && typeof c.isAnonymous !== "undefined") return "tribe";
  316. if (typeof c.date !== "undefined" && typeof c.location !== "undefined") return "event";
  317. if (typeof c.priority !== "undefined" && typeof c.status !== "undefined" && c.title) return "task";
  318. if (typeof c.confirmations !== "undefined" && typeof c.severity !== "undefined") return "report";
  319. if (typeof c.job_type !== "undefined" && typeof c.status !== "undefined") return "job";
  320. if (typeof c.url !== "undefined" && typeof c.mimeType !== "undefined" && c.type === "audio") return "audio";
  321. if (typeof c.url !== "undefined" && typeof c.mimeType !== "undefined" && c.type === "video") return "video";
  322. if (typeof c.url !== "undefined" && c.title && c.key) return "document";
  323. if (typeof c.text !== "undefined" && typeof c.refeeds !== "undefined") return "feed";
  324. if (typeof c.text !== "undefined" && typeof c.contentWarning !== "undefined") return "post";
  325. if (typeof c.contact !== "undefined") return "contact";
  326. if (typeof c.about !== "undefined") return "about";
  327. if (typeof c.concept !== "undefined" && typeof c.amount !== "undefined" && c.status) return "transfer";
  328. return "";
  329. }
  330. function normalizeType(a) {
  331. const t = a.type || a.content?.type || inferType(a.content) || "";
  332. return String(t).toLowerCase();
  333. }
  334. function priorityBump(p) {
  335. const s = String(p || "").toUpperCase();
  336. if (s === "HIGH") return 3;
  337. if (s === "MEDIUM") return 1;
  338. return 0;
  339. }
  340. function severityBump(s) {
  341. const x = String(s || "").toUpperCase();
  342. if (x === "CRITICAL") return 6;
  343. if (x === "HIGH") return 4;
  344. if (x === "MEDIUM") return 2;
  345. return 0;
  346. }
  347. function scoreMarket(c) {
  348. const st = String(c.status || "").toUpperCase();
  349. let s = 5;
  350. if (st === "SOLD") s += 8;
  351. else if (st === "ACTIVE") s += 3;
  352. const bids = Array.isArray(c.auctions_poll) ? c.auctions_poll.length : 0;
  353. s += Math.min(10, bids);
  354. return s;
  355. }
  356. function scoreProject(c) {
  357. const st = String(c.status || "ACTIVE").toUpperCase();
  358. const prog = Number(c.progress || 0);
  359. let s = 8 + Math.min(10, prog / 10);
  360. if (st === "FUNDED") s += 10;
  361. return s;
  362. }
  363. function calculateOpinionScore(content) {
  364. const cats = content?.opinions || {};
  365. let s = 0;
  366. for (const k in cats) {
  367. if (!Object.prototype.hasOwnProperty.call(cats, k)) continue;
  368. if (k === "interesting" || k === "inspiring") s += 5;
  369. else if (k === "boring" || k === "spam" || k === "propaganda") s -= 3;
  370. else s += 1;
  371. }
  372. return s;
  373. }
  374. async function listAllActions() {
  375. if (services?.feed?.listAll) {
  376. const arr = await services.feed.listAll();
  377. FEED_SRC = "services.feed.listAll";
  378. return normalizeFeedArray(arr);
  379. }
  380. if (services?.activity?.list) {
  381. const arr = await services.activity.list();
  382. FEED_SRC = "services.activity.list";
  383. return normalizeFeedArray(arr);
  384. }
  385. if (typeof global.listFeed === "function") {
  386. const arr = await global.listFeed("all");
  387. FEED_SRC = "global.listFeed('all')";
  388. return normalizeFeedArray(arr);
  389. }
  390. const ssb = await openSsb();
  391. if (!ssb || !ssb.createLogStream) {
  392. FEED_SRC = "none";
  393. return [];
  394. }
  395. const msgs = await scanLogStream();
  396. FEED_SRC = "ssb.createLogStream";
  397. return msgs.map(m => {
  398. const v = m.value || {};
  399. const c = v.content || {};
  400. return {
  401. id: v.key || m.key,
  402. author: v.author,
  403. type: (c.type || "").toLowerCase(),
  404. value: v,
  405. content: c
  406. };
  407. });
  408. }
  409. function normalizeFeedArray(arr) {
  410. if (!Array.isArray(arr)) return [];
  411. return arr.map(x => {
  412. const value = x.value || {};
  413. const content = x.content || value.content || {};
  414. const author = x.author || value.author || content.author || null;
  415. const type = (content.type || "").toLowerCase();
  416. return { id: x.id || value.key || x.key, author, type, value, content };
  417. });
  418. }
  419. async function publishKarmaScore(userId, karmaScore) {
  420. const ssb = await openSsb();
  421. if (!ssb || !ssb.publish) return false;
  422. const timestamp = new Date().toISOString();
  423. const content = { type: "karmaScore", karmaScore, userId, timestamp };
  424. return new Promise((resolve, reject) => {
  425. ssb.publish(content, (err, msg) => err ? reject(err) : resolve(msg));
  426. });
  427. }
  428. async function fetchUserActions(userId) {
  429. const me = resolveUserId(userId);
  430. const actions = await listAllActions();
  431. const authored = actions.filter(a =>
  432. (a.author && a.author === me) || (a.value?.author && a.value.author === me)
  433. );
  434. if (authored.length) return authored;
  435. return actions.filter(a => {
  436. const c = a.content || {};
  437. const fields = [c.author, c.organizer, c.seller, c.about, c.contact];
  438. return fields.some(f => f && f === me);
  439. });
  440. }
  441. function scoreFromActions(actions) {
  442. let score = 0;
  443. const nowMs = Date.now();
  444. for (const action of actions) {
  445. const t = normalizeType(action);
  446. const c = action.content || {};
  447. const rawType = String(c.type || "").toLowerCase();
  448. const ts = action.value?.timestamp;
  449. const ageDays = ts ? (nowMs - ts) / 86400000 : Infinity;
  450. const decay = ageDays <= 30 ? 1.0 : ageDays <= 90 ? 0.5 : 0.25;
  451. if (t === "post") score += 10 * decay;
  452. else if (t === "comment") score += 5 * decay;
  453. else if (t === "like") score += 2 * decay;
  454. else if (t === "image") score += 8 * decay;
  455. else if (t === "video") score += 12 * decay;
  456. else if (t === "audio") score += 8 * decay;
  457. else if (t === "document") score += 6 * decay;
  458. else if (t === "bookmark") score += 2 * decay;
  459. else if (t === "feed") score += 6 * decay;
  460. else if (t === "forum") score += (c.root ? 5 : 10) * decay;
  461. else if (t === "vote") score += (3 + calculateOpinionScore(c)) * decay;
  462. else if (t === "votes") score += Math.min(10, Number(c.totalVotes || 0)) * decay;
  463. else if (t === "market") score += scoreMarket(c) * decay;
  464. else if (t === "project") score += scoreProject(c) * decay;
  465. else if (t === "tribe") score += (6 + Math.min(10, Array.isArray(c.members) ? c.members.length * 0.5 : 0)) * decay;
  466. else if (t === "event") score += (4 + Math.min(10, Array.isArray(c.attendees) ? c.attendees.length : 0)) * decay;
  467. else if (t === "task") score += (3 + priorityBump(c.priority)) * decay;
  468. else if (t === "report") score += (4 + (Array.isArray(c.confirmations) ? c.confirmations.length : 0) + severityBump(c.severity)) * decay;
  469. else if (t === "curriculum") score += 5 * decay;
  470. else if (t === "aiexchange") score += (Array.isArray(c.ctx) ? Math.min(10, c.ctx.length) : 0) * decay;
  471. else if (t === "job") score += (4 + (Array.isArray(c.subscribers) ? c.subscribers.length : 0)) * decay;
  472. else if (t === "bankclaim") score += Math.min(20, Math.log(1 + Math.max(0, Number(c.amount) || 0)) * 5) * decay;
  473. else if (t === "bankwallet") score += 2 * decay;
  474. else if (t === "transfer") score += 1 * decay;
  475. else if (t === "about") score += 1 * decay;
  476. else if (t === "contact") score += 1 * decay;
  477. else if (t === "pub") score += 1 * decay;
  478. else if (t === "parliamentcandidature" || rawType === "parliamentcandidature") score += 12 * decay;
  479. else if (t === "parliamentterm" || rawType === "parliamentterm") score += 25 * decay;
  480. else if (t === "parliamentproposal" || rawType === "parliamentproposal") score += 8 * decay;
  481. else if (t === "parliamentlaw" || rawType === "parliamentlaw") score += 16 * decay;
  482. else if (t === "parliamentrevocation" || rawType === "parliamentrevocation") score += 10 * decay;
  483. else if (t === "courts_case" || t === "courtscase" || rawType === "courts_case") score += 4 * decay;
  484. else if (t === "courts_evidence" || t === "courtsevidence" || rawType === "courts_evidence") score += 3 * decay;
  485. else if (t === "courts_answer" || t === "courtsanswer" || rawType === "courts_answer") score += 4 * decay;
  486. else if (t === "courts_verdict" || t === "courtsverdict" || rawType === "courts_verdict") score += 10 * decay;
  487. else if (t === "courts_settlement" || t === "courtssettlement" || rawType === "courts_settlement") score += 8 * decay;
  488. else if (t === "courts_nomination" || t === "courtsnomination" || rawType === "courts_nomination") score += 6 * decay;
  489. else if (t === "courts_nom_vote" || t === "courtsnomvote" || rawType === "courts_nom_vote") score += 3 * decay;
  490. else if (t === "courts_public_pref" || t === "courtspublicpref" || rawType === "courts_public_pref") score += 1 * decay;
  491. else if (t === "courts_mediators" || t === "courtsmediators" || rawType === "courts_mediators") score += 6 * decay;
  492. else if (t === "courts_open_support" || t === "courtsopensupport" || rawType === "courts_open_support") score += 2 * decay;
  493. else if (t === "courts_verdict_vote" || t === "courtsverdictvote" || rawType === "courts_verdict_vote") score += 3 * decay;
  494. else if (t === "courts_judge_assign" || t === "courtsjudgeassign" || rawType === "courts_judge_assign") score += 5 * decay;
  495. }
  496. return Math.max(0, Math.round(score));
  497. }
  498. async function getUserEngagementScore(userId) {
  499. const ssb = await openSsb();
  500. const uid = resolveUserId(userId);
  501. const actions = await fetchUserActions(uid);
  502. const karmaScore = scoreFromActions(actions);
  503. const prev = await getLastKarmaScore(uid);
  504. const lastPublishedTimestamp = await getLastPublishedTimestamp(uid);
  505. const isSelf = idsEqual(uid, ssb.id);
  506. const hasSSB = !!(ssb && ssb.publish);
  507. const changed = (prev === null) || (karmaScore !== prev);
  508. const nowMs = Date.now();
  509. const lastMs = lastPublishedTimestamp ? new Date(lastPublishedTimestamp).getTime() : 0;
  510. const cooldownOk = (nowMs - lastMs) >= 24 * 60 * 60 * 1000;
  511. if (isSelf && hasSSB && changed && cooldownOk) {
  512. await publishKarmaScore(uid, karmaScore);
  513. }
  514. return karmaScore;
  515. }
  516. async function getLastKarmaScore(userId) {
  517. const ssb = await openSsb();
  518. if (!ssb) return null;
  519. return new Promise((resolve) => {
  520. const source = ssb.messagesByType
  521. ? ssb.messagesByType({ type: "karmaScore", reverse: true })
  522. : ssb.createLogStream && ssb.createLogStream({ reverse: true });
  523. if (!source) return resolve(null);
  524. pull(
  525. source,
  526. pull.filter(msg => {
  527. const v = msg.value || msg;
  528. const c = v.content || {};
  529. return c && c.type === "karmaScore" && c.userId === userId;
  530. }),
  531. pull.take(1),
  532. pull.collect((err, arr) => {
  533. if (err || !arr || !arr.length) return resolve(null);
  534. const v = arr[0].value || arr[0];
  535. const c = v.content || {};
  536. resolve(Number(c.karmaScore) || 0);
  537. })
  538. );
  539. });
  540. }
  541. async function getLastPublishedTimestamp(userId) {
  542. const ssb = await openSsb();
  543. if (!ssb) return new Date(0).toISOString();
  544. const fallback = new Date(0).toISOString();
  545. return new Promise((resolve) => {
  546. const source = ssb.messagesByType
  547. ? ssb.messagesByType({ type: "karmaScore", reverse: true })
  548. : ssb.createLogStream && ssb.createLogStream({ reverse: true });
  549. if (!source) return resolve(fallback);
  550. pull(
  551. source,
  552. pull.filter(msg => {
  553. const v = msg.value || msg;
  554. const c = v.content || {};
  555. return c && c.type === "karmaScore" && c.userId === userId;
  556. }),
  557. pull.take(1),
  558. pull.collect((err, arr) => {
  559. if (err || !arr || !arr.length) return resolve(fallback);
  560. const v = arr[0].value || arr[0];
  561. const c = v.content || {};
  562. resolve(c.timestamp || fallback);
  563. })
  564. );
  565. });
  566. }
  567. function computePoolVars(pubBal, rules) {
  568. const alphaCap = (rules.alpha || DEFAULT_RULES.alpha) * pubBal;
  569. const available = Math.max(0, pubBal - (rules.reserveMin || DEFAULT_RULES.reserveMin));
  570. const rawMin = Math.min(available, (rules.capPerEpoch || DEFAULT_RULES.capPerEpoch), alphaCap);
  571. const pool = clamp(rawMin, 0, Number.MAX_SAFE_INTEGER);
  572. return { pubBal, alphaCap, available, rawMin, pool };
  573. }
  574. async function computeEpoch({ epochId, userId, rules = DEFAULT_RULES }) {
  575. const pubBal = await safeGetBalance("pub");
  576. const pv = computePoolVars(pubBal, rules);
  577. const addresses = await listAddressesMerged();
  578. const eligible = addresses.filter(a => a.address && isValidEcoinAddress(a.address));
  579. const capUser = (rules.caps && rules.caps.cap_user_epoch) || DEFAULT_RULES.caps.cap_user_epoch;
  580. const wMin = (rules.caps && rules.caps.w_min) || DEFAULT_RULES.caps.w_min;
  581. const wMax = (rules.caps && rules.caps.w_max) || DEFAULT_RULES.caps.w_max;
  582. const weights = [];
  583. for (const entry of eligible) {
  584. const score = await getUserEngagementScore(entry.id);
  585. weights.push({ user: entry.id, w: clamp(1 + score / 100, wMin, wMax) });
  586. }
  587. if (!weights.length && userId) {
  588. const score = await getUserEngagementScore(userId);
  589. weights.push({ user: userId, w: clamp(1 + score / 100, wMin, wMax) });
  590. }
  591. const W = weights.reduce((acc, x) => acc + x.w, 0) || 1;
  592. const floorUbi = 1;
  593. const allocations = weights.map(({ user, w }) => {
  594. const amount = Math.max(floorUbi, Math.min(pv.pool * w / W, capUser));
  595. return {
  596. id: `alloc:${epochId}:${user}`,
  597. epoch: epochId,
  598. user,
  599. weight: Number(w.toFixed(6)),
  600. amount: Number(amount.toFixed(6))
  601. };
  602. });
  603. const snapshot = JSON.stringify({ epochId, pool: pv.pool, weights, allocations, rules }, null, 2);
  604. const hash = crypto.createHash("sha256").update(snapshot).digest("hex");
  605. return { epoch: { id: epochId, pool: Number(pv.pool.toFixed(6)), weightsSum: Number(W.toFixed(6)), rules, hash }, allocations };
  606. }
  607. async function executeEpoch({ epochId, rules = DEFAULT_RULES } = {}) {
  608. const eid = epochId || epochIdNow();
  609. await expireOldAllocations();
  610. const existing = await epochsRepo.get(eid);
  611. if (existing) return { epoch: existing, allocations: await transfersRepo.listByTag(`epoch:${eid}`) };
  612. const { epoch, allocations } = await computeEpoch({ epochId: eid, userId: config.keys.id, rules });
  613. await epochsRepo.save(epoch);
  614. for (const a of allocations) {
  615. if (a.amount <= 0) continue;
  616. const record = {
  617. id: a.id,
  618. from: config.keys.id,
  619. to: a.user,
  620. amount: a.amount,
  621. concept: `UBI ${eid}`,
  622. status: "UNCLAIMED",
  623. createdAt: new Date().toISOString(),
  624. deadline: new Date(Date.now() + (rules.graceDays || DEFAULT_RULES.graceDays) * 86400000).toISOString(),
  625. tags: ["UBI", `epoch:${eid}`],
  626. opinions: {}
  627. };
  628. await transfersRepo.create(record);
  629. try { await publishUbiAllocation(record); } catch (_) {}
  630. }
  631. return { epoch, allocations };
  632. }
  633. async function publishBankClaim({ amount, epochId, allocationId, txid }) {
  634. const ssbClient = await openSsb();
  635. const content = { type: "bankClaim", amount, epochId, allocationId, txid, timestamp: Date.now() };
  636. return new Promise((resolve, reject) => ssbClient.publish(content, (err, res) => err ? reject(err) : resolve(res)));
  637. }
  638. async function claimAllocation({ transferId, claimerId, forcePub = false }) {
  639. const allocation = await transfersRepo.findById(transferId);
  640. if (!allocation || (allocation.status !== "UNCLAIMED" && allocation.status !== "UNCONFIRMED")) throw new Error("Invalid allocation or already claimed.");
  641. if (claimerId && allocation.to !== claimerId) throw new Error("This allocation is not for you.");
  642. const addr = await getUserAddress(allocation.to);
  643. if (!addr || !isValidEcoinAddress(addr)) throw new Error("No valid ECOin address registered.");
  644. const txid = await rpcCall("sendtoaddress", [addr, allocation.amount, `UBI ${allocation.concept || "claim"}`], "pub");
  645. if (!txid) throw new Error("RPC sendtoaddress failed. Check PUB wallet configuration.");
  646. await transfersRepo.markClosed(transferId, txid);
  647. return { txid };
  648. }
  649. async function claimUBI(userId) {
  650. const uid = resolveUserId(userId);
  651. const epochId = epochIdNow();
  652. const pubId = getConfiguredPubId();
  653. if (!pubId) throw new Error("no_pub_configured");
  654. const alreadyClaimed = await hasClaimedThisMonth(uid);
  655. if (alreadyClaimed) throw new Error("already_claimed");
  656. const karmaScore = await getUserEngagementScore(uid);
  657. const wMin = DEFAULT_RULES.caps.w_min;
  658. const wMax = DEFAULT_RULES.caps.w_max;
  659. const capUser = DEFAULT_RULES.caps.cap_user_epoch;
  660. const userW = clamp(1 + karmaScore / 100, wMin, wMax);
  661. const amount = Number(Math.max(1, Math.min(capUser * (userW / wMax), capUser)).toFixed(6));
  662. const ssb = await openSsb();
  663. if (!ssb || !ssb.publish) throw new Error("ssb_unavailable");
  664. const now = new Date().toISOString();
  665. const transferContent = {
  666. type: "transfer",
  667. from: pubId,
  668. to: uid,
  669. concept: `UBI ${epochId} ${uid}`,
  670. amount: String(amount),
  671. createdAt: now,
  672. updatedAt: now,
  673. deadline: null,
  674. confirmedBy: [pubId],
  675. status: "UNCONFIRMED",
  676. tags: ["UBI", "PENDING"],
  677. opinions: {},
  678. opinions_inhabitants: []
  679. };
  680. const transferRes = await new Promise((resolve, reject) => ssb.publish(transferContent, (err, res) => err ? reject(err) : resolve(res)));
  681. const transferId = transferRes?.key || "";
  682. const claimContent = { type: "ubiClaim", pubId, amount, epochId, claimedAt: now, transferId };
  683. await new Promise((resolve, reject) => ssb.publish(claimContent, (err, res) => err ? reject(err) : resolve(res)));
  684. return { status: "claimed_pending", amount, epochId };
  685. }
  686. async function updateAllocationStatus(allocationId, status, txid) {
  687. if (status === "CLOSED") {
  688. await transfersRepo.markClosed(allocationId, txid);
  689. return;
  690. }
  691. ensureStoreFiles();
  692. const all = readJson(TRANSFERS_PATH, []);
  693. const idx = all.findIndex(t => t.id === allocationId);
  694. if (idx >= 0) {
  695. all[idx].status = status;
  696. if (txid) all[idx].txid = txid;
  697. writeJson(TRANSFERS_PATH, all);
  698. }
  699. }
  700. async function hasClaimedThisMonth(userId) {
  701. const epochId = epochIdNow();
  702. const msgs = await scanLogStream();
  703. for (const m of msgs) {
  704. const v = m.value || {};
  705. const c = v.content || {};
  706. if (c.type === "ubiClaimResult" && c.userId === userId && c.epochId === epochId) return true;
  707. if (c.type === "ubiClaim" && v.author === userId && c.epochId === epochId) return true;
  708. }
  709. return false;
  710. }
  711. async function getUbiClaimHistory(userId) {
  712. const msgs = await scanLogStream();
  713. let lastClaimedDate = null;
  714. let totalClaimed = 0;
  715. let claimCount = 0;
  716. for (const m of msgs) {
  717. const v = m.value || {};
  718. const c = v.content || {};
  719. if (c.type === "ubiClaimResult" && c.userId === userId) {
  720. totalClaimed += Number(c.amount) || 0;
  721. claimCount += 1;
  722. const d = c.processedAt || null;
  723. if (d && (!lastClaimedDate || d > lastClaimedDate)) lastClaimedDate = d;
  724. }
  725. }
  726. return { lastClaimedDate, totalClaimed: Number(totalClaimed.toFixed(6)), claimCount };
  727. }
  728. async function getUbiAllocationsFromSSB() {
  729. const pubId = getConfiguredPubId();
  730. if (!pubId) return [];
  731. const msgs = await scanLogStream();
  732. const out = [];
  733. for (const m of msgs) {
  734. const v = m.value || {};
  735. const c = v.content || {};
  736. if (v.author === pubId && c && c.type === "ubiAllocation") {
  737. out.push({
  738. id: c.allocationId,
  739. from: pubId,
  740. to: c.to,
  741. amount: c.amount,
  742. concept: c.concept,
  743. epochId: c.epochId,
  744. status: c.status || "UNCLAIMED",
  745. createdAt: c.createdAt || new Date().toISOString()
  746. });
  747. }
  748. }
  749. return out;
  750. }
  751. async function publishPubAvailability() {
  752. if (!isPubNode()) return;
  753. const balance = await safeGetBalance("pub");
  754. const floor = Math.max(1, DEFAULT_RULES?.caps?.floor_user || 1);
  755. const available = Number(balance) >= floor;
  756. const ssb = await openSsb();
  757. if (!ssb || !ssb.publish) return;
  758. const content = { type: "pubAvailability", available, coin: "ECO", timestamp: Date.now() };
  759. await new Promise((resolve, reject) => ssb.publish(content, (err, res) => err ? reject(err) : resolve(res)));
  760. return available;
  761. }
  762. async function getPubAvailabilityFromSSB() {
  763. const pubId = getConfiguredPubId();
  764. if (!pubId) return false;
  765. const msgs = await scanLogStream();
  766. let latest = null;
  767. for (const m of msgs) {
  768. const v = m.value || {};
  769. const c = v.content || {};
  770. if (v.author === pubId && c && c.type === "pubAvailability" && c.coin === "ECO") {
  771. if (!latest || (Number(c.timestamp) || 0) > (Number(latest.timestamp) || 0)) latest = c;
  772. }
  773. }
  774. return !!(latest && latest.available);
  775. }
  776. async function listBanking(filter = "overview", userId) {
  777. const uid = resolveUserId(userId);
  778. const epochId = epochIdNow();
  779. let pubBalance = 0;
  780. let ubiAvailable = false;
  781. let allocations;
  782. if (isPubNode()) {
  783. pubBalance = await safeGetBalance("pub");
  784. const floor = Math.max(1, DEFAULT_RULES?.caps?.floor_user || 1);
  785. ubiAvailable = Number(pubBalance) >= floor;
  786. try { await publishPubAvailability(); } catch (_) {}
  787. const all = await transfersRepo.listByTag("UBI");
  788. allocations = all.map(t => ({
  789. id: t.id, concept: t.concept, from: t.from, to: t.to, amount: t.amount, status: t.status,
  790. createdAt: t.createdAt || t.deadline || new Date().toISOString(), txid: t.txid
  791. }));
  792. } else {
  793. ubiAvailable = await getPubAvailabilityFromSSB();
  794. allocations = await getUbiAllocationsFromSSB();
  795. }
  796. const userBalance = await safeGetBalance("user");
  797. const epochs = await epochsRepo.list();
  798. let computed = null;
  799. try { computed = await computeEpoch({ epochId, userId: uid, rules: DEFAULT_RULES }); } catch {}
  800. const pv = computePoolVars(pubBalance, DEFAULT_RULES);
  801. const actions = await fetchUserActions(uid);
  802. const engagementScore = scoreFromActions(actions);
  803. const poolForEpoch = computed?.epoch?.pool || pv.pool || 0;
  804. const futureUBI = Number(((engagementScore / 100) * poolForEpoch).toFixed(6));
  805. const addresses = await listAddressesMerged();
  806. const alreadyClaimed = await hasClaimedThisMonth(uid);
  807. const pubId = getConfiguredPubId();
  808. const userAddress = await getUserAddress(uid);
  809. const userWalletCfg = getWalletCfg("user") || {};
  810. const hasValidWallet = !!(userAddress && isValidEcoinAddress(userAddress) && userWalletCfg.url);
  811. const summary = {
  812. userBalance,
  813. epochId,
  814. pool: poolForEpoch,
  815. weightsSum: computed?.epoch?.weightsSum || 0,
  816. userEngagementScore: engagementScore,
  817. futureUBI,
  818. alreadyClaimed,
  819. pubId,
  820. hasValidWallet,
  821. ubiAvailability: ubiAvailable ? "OK" : "NO_FUNDS"
  822. };
  823. const exchange = await calculateEcoinValue();
  824. return { summary, allocations, epochs, rules: DEFAULT_RULES, addresses, exchange };
  825. }
  826. async function getAllocationById(id) {
  827. const t = await transfersRepo.findById(id);
  828. if (!t) return null;
  829. 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 };
  830. }
  831. async function getEpochById(id) {
  832. const existing = await epochsRepo.get(id);
  833. if (existing) return existing;
  834. const all = await transfersRepo.listAll();
  835. const filtered = all.filter(t => (t.tags || []).includes(`epoch:${id}`));
  836. const pool = filtered.reduce((s, t) => s + Number(t.amount || 0), 0);
  837. return { id, pool, weightsSum: 0, rules: DEFAULT_RULES, hash: "-" };
  838. }
  839. async function listEpochAllocations(id) {
  840. const all = await transfersRepo.listAll();
  841. return all.filter(t => (t.tags || []).includes(`epoch:${id}`)).map(t => ({
  842. 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
  843. }));
  844. }
  845. let genesisTimeCache = null;
  846. async function getAvgBlockSeconds(blocks) {
  847. if (!blocks || blocks < 2) return 0;
  848. try {
  849. if (!genesisTimeCache) {
  850. const h1 = await rpcCall("getblockhash", [1]);
  851. if (!h1) return 0;
  852. const b1 = await rpcCall("getblock", [h1]);
  853. genesisTimeCache = b1?.time || null;
  854. if (!genesisTimeCache) return 0;
  855. }
  856. const hCur = await rpcCall("getblockhash", [blocks]);
  857. if (!hCur) return 0;
  858. const bCur = await rpcCall("getblock", [hCur]);
  859. const curTime = bCur?.time || 0;
  860. if (!curTime) return 0;
  861. const elapsed = curTime - genesisTimeCache;
  862. return elapsed > 0 ? elapsed / (blocks - 1) : 0;
  863. } catch (_) { return 0; }
  864. }
  865. async function calculateEcoinValue() {
  866. const totalSupply = 25500000;
  867. let circulatingSupply = 0;
  868. let blocks = 0;
  869. let blockValueEco = 0;
  870. let isSynced = false;
  871. try {
  872. const info = await rpcCall("getinfo", []);
  873. circulatingSupply = info?.moneysupply || 0;
  874. blocks = info?.blocks || 0;
  875. isSynced = circulatingSupply > 0;
  876. const mining = await rpcCall("getmininginfo", []);
  877. blockValueEco = (mining?.blockvalue || 0) / 1e8;
  878. } catch (_) {}
  879. const avgSec = await getAvgBlockSeconds(blocks);
  880. const ecoValuePerHour = avgSec > 0 ? (3600 / avgSec) * blockValueEco : 0;
  881. const maturity = totalSupply > 0 ? circulatingSupply / totalSupply : 0;
  882. const ecoTimeMs = maturity * 3600 * 1000;
  883. const annualIssuance = ecoValuePerHour * 24 * 365;
  884. const inflationFactor = circulatingSupply > 0 ? (annualIssuance / circulatingSupply) * 100 : 0;
  885. const inflationMonthly = inflationFactor / 12;
  886. return {
  887. ecoValue: Number(ecoValuePerHour.toFixed(6)),
  888. ecoTimeMs: Number(ecoTimeMs.toFixed(3)),
  889. totalSupply,
  890. inflationFactor: Number(inflationFactor.toFixed(2)),
  891. inflationMonthly: Number(inflationMonthly.toFixed(2)),
  892. currentSupply: circulatingSupply,
  893. isSynced
  894. };
  895. }
  896. async function getBankingData(userId) {
  897. const ecoValue = await calculateEcoinValue();
  898. const karmaScore = await getUserEngagementScore(userId);
  899. let estimatedUBI = 0;
  900. try {
  901. const pubBal = isPubNode() ? await safeGetBalance("pub") : 0;
  902. const pv = computePoolVars(pubBal, DEFAULT_RULES);
  903. const pool = pv.pool || 0;
  904. const addresses = await listAddressesMerged();
  905. const eligible = addresses.filter(a => a.address && isValidEcoinAddress(a.address));
  906. const totalW = eligible.length > 0 ? eligible.length + eligible.length * (karmaScore / 100) : 1;
  907. const userW = 1 + karmaScore / 100;
  908. const cap = DEFAULT_RULES.caps?.cap_user_epoch ?? 50;
  909. estimatedUBI = Math.min(pool * (userW / Math.max(1, totalW)), cap);
  910. } catch (_) {}
  911. const claimHistory = await getUbiClaimHistory(userId).catch(() => ({ lastClaimedDate: null, totalClaimed: 0 }));
  912. return {
  913. ecoValue,
  914. karmaScore,
  915. estimatedUBI,
  916. lastClaimedDate: claimHistory.lastClaimedDate,
  917. totalClaimed: claimHistory.totalClaimed
  918. };
  919. }
  920. async function expireOldAllocations() {
  921. const cutoffMs = MAX_PENDING_EPOCHS * 30 * 86400000;
  922. const now = Date.now();
  923. const allocs = await transfersRepo.listAll();
  924. for (const a of allocs) {
  925. if ((a.status === "UNCLAIMED" || a.status === "UNCONFIRMED") &&
  926. (now - new Date(a.createdAt).getTime()) > cutoffMs) {
  927. await updateAllocationStatus(a.id, "EXPIRED");
  928. }
  929. }
  930. }
  931. async function publishUbiAllocation(allocation) {
  932. const ssb = await openSsb();
  933. if (!ssb) return;
  934. const epochTag = (allocation.tags || []).find(t => t.startsWith("epoch:"));
  935. const content = {
  936. type: "ubiAllocation",
  937. allocationId: allocation.id,
  938. to: allocation.to,
  939. amount: allocation.amount,
  940. concept: allocation.concept,
  941. epochId: epochTag ? epochTag.slice(6) : "",
  942. status: "UNCLAIMED",
  943. createdAt: allocation.createdAt
  944. };
  945. return new Promise((resolve, reject) => ssb.publish(content, (err, res) => err ? reject(err) : resolve(res)));
  946. }
  947. async function publishUbiClaim(allocationId, epochId) {
  948. const ssb = await openSsb();
  949. if (!ssb) return;
  950. const content = { type: "ubiClaim", allocationId, epochId, claimedAt: new Date().toISOString() };
  951. return new Promise((resolve, reject) => ssb.publish(content, (err, res) => err ? reject(err) : resolve(res)));
  952. }
  953. async function publishUbiClaimResult(allocationId, epochId, txid, userId, amount) {
  954. const ssb = await openSsb();
  955. if (!ssb) return;
  956. const content = { type: "ubiClaimResult", allocationId, epochId, txid, userId, amount, processedAt: new Date().toISOString() };
  957. return new Promise((resolve, reject) => ssb.publish(content, (err, res) => err ? reject(err) : resolve(res)));
  958. }
  959. async function processPendingClaims() {
  960. if (!isPubNode()) return;
  961. const ssb = await openSsb();
  962. if (!ssb) return;
  963. const claims = [];
  964. const results = [];
  965. await new Promise((resolve, reject) => {
  966. pull(ssb.messagesByType({ type: "ubiClaim", reverse: false }),
  967. pull.drain(msg => {
  968. if (msg.value?.content?.type === "ubiClaim") {
  969. claims.push({ ...msg.value.content, _author: msg.value.author });
  970. }
  971. },
  972. err => err ? reject(err) : resolve()));
  973. });
  974. await new Promise((resolve, reject) => {
  975. pull(ssb.messagesByType({ type: "ubiClaimResult", reverse: false }),
  976. pull.drain(msg => { if (msg.value?.content?.type === "ubiClaimResult") results.push(msg.value.content); },
  977. err => err ? reject(err) : resolve()));
  978. });
  979. const processedEpochUser = new Set(results.map(r => `${r.epochId}:${r.userId}`));
  980. const epochId = epochIdNow();
  981. for (const claim of claims) {
  982. const claimantId = claim._author;
  983. if (!claimantId) continue;
  984. const claimEpoch = claim.epochId || epochId;
  985. if (processedEpochUser.has(`${claimEpoch}:${claimantId}`)) continue;
  986. try {
  987. const addr = await getUserAddress(claimantId);
  988. if (!addr || !isValidEcoinAddress(addr)) continue;
  989. const pubBal = await safeGetBalance("pub");
  990. if (pubBal <= 0) continue;
  991. const pv = computePoolVars(pubBal, DEFAULT_RULES);
  992. const addresses = await listAddressesMerged();
  993. const eligible = addresses.filter(a => a.address && isValidEcoinAddress(a.address));
  994. const karmaScore = await getUserEngagementScore(claimantId);
  995. const wMin = DEFAULT_RULES.caps.w_min;
  996. const wMax = DEFAULT_RULES.caps.w_max;
  997. const capUser = DEFAULT_RULES.caps.cap_user_epoch;
  998. const userW = clamp(1 + karmaScore / 100, wMin, wMax);
  999. const totalW = eligible.reduce((acc) => acc + clamp(1, wMin, wMax), 0) || 1;
  1000. const amount = Number(Math.max(1, Math.min(pv.pool * userW / totalW, capUser)).toFixed(6));
  1001. const txid = await rpcCall("sendtoaddress", [addr, amount, `UBI ${claimEpoch}`], "pub");
  1002. if (!txid) continue;
  1003. await publishUbiClaimResult(claim.allocationId || `claim:${claimEpoch}:${claimantId}`, claimEpoch, txid, claimantId, amount);
  1004. await publishBankClaim({ amount, epochId: claimEpoch, allocationId: claim.allocationId || `claim:${claimEpoch}:${claimantId}`, txid });
  1005. const now = new Date().toISOString();
  1006. await new Promise((resolve, reject) => ssb.publish({
  1007. type: "transfer",
  1008. from: config.keys.id,
  1009. to: claimantId,
  1010. concept: `UBI ${claimEpoch} ${claimantId}`,
  1011. amount: String(amount),
  1012. createdAt: now,
  1013. updatedAt: now,
  1014. deadline: null,
  1015. confirmedBy: [config.keys.id],
  1016. status: "UNCONFIRMED",
  1017. tags: ["UBI"],
  1018. opinions: {},
  1019. opinions_inhabitants: [],
  1020. txid
  1021. }, (err, msg) => err ? reject(err) : resolve(msg)));
  1022. } catch (_) {}
  1023. }
  1024. }
  1025. return {
  1026. DEFAULT_RULES,
  1027. isPubNode,
  1028. getConfiguredPubId,
  1029. computeEpoch,
  1030. executeEpoch,
  1031. getUserEngagementScore,
  1032. publishBankClaim,
  1033. publishUbiAllocation,
  1034. publishUbiClaim,
  1035. publishUbiClaimResult,
  1036. publishPubAvailability,
  1037. getPubAvailabilityFromSSB,
  1038. hasClaimedThisMonth,
  1039. getUbiClaimHistory,
  1040. claimUBI,
  1041. processPendingClaims,
  1042. expireOldAllocations,
  1043. claimAllocation,
  1044. listBanking,
  1045. getAllocationById,
  1046. getEpochById,
  1047. listEpochAllocations,
  1048. addAddress,
  1049. removeAddress,
  1050. ensureSelfAddressPublished,
  1051. getUserAddress,
  1052. setUserAddress,
  1053. listAddressesMerged,
  1054. calculateEcoinValue,
  1055. getBankingData
  1056. };
  1057. };