banking_model.js 43 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115
  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 ssb = await openSsb();
  657. if (!ssb || !ssb.publish) throw new Error("ssb_unavailable");
  658. const now = new Date().toISOString();
  659. const claimContent = { type: "ubiClaim", pubId, epochId, claimedAt: now };
  660. await new Promise((resolve, reject) => ssb.publish(claimContent, (err, res) => err ? reject(err) : resolve(res)));
  661. return { status: "claimed_pending", epochId };
  662. }
  663. async function updateAllocationStatus(allocationId, status, txid) {
  664. if (status === "CLOSED") {
  665. await transfersRepo.markClosed(allocationId, txid);
  666. return;
  667. }
  668. ensureStoreFiles();
  669. const all = readJson(TRANSFERS_PATH, []);
  670. const idx = all.findIndex(t => t.id === allocationId);
  671. if (idx >= 0) {
  672. all[idx].status = status;
  673. if (txid) all[idx].txid = txid;
  674. writeJson(TRANSFERS_PATH, all);
  675. }
  676. }
  677. async function hasClaimedThisMonth(userId) {
  678. const epochId = epochIdNow();
  679. const msgs = await scanLogStream();
  680. for (const m of msgs) {
  681. const v = m.value || {};
  682. const c = v.content || {};
  683. if (c.type === "ubiClaimResult" && c.userId === userId && c.epochId === epochId) return true;
  684. if (c.type === "ubiClaim" && v.author === userId && c.epochId === epochId) return true;
  685. }
  686. return false;
  687. }
  688. async function getUbiClaimHistory(userId) {
  689. const msgs = await scanLogStream();
  690. let lastClaimedDate = null;
  691. let totalClaimed = 0;
  692. let claimCount = 0;
  693. for (const m of msgs) {
  694. const v = m.value || {};
  695. const c = v.content || {};
  696. if (c.type === "ubiClaimResult" && c.userId === userId) {
  697. totalClaimed += Number(c.amount) || 0;
  698. claimCount += 1;
  699. const d = c.processedAt || null;
  700. if (d && (!lastClaimedDate || d > lastClaimedDate)) lastClaimedDate = d;
  701. }
  702. }
  703. return { lastClaimedDate, totalClaimed: Number(totalClaimed.toFixed(6)), claimCount };
  704. }
  705. async function getUbiAllocationsFromSSB() {
  706. const pubId = getConfiguredPubId();
  707. if (!pubId) return [];
  708. const msgs = await scanLogStream();
  709. const out = [];
  710. for (const m of msgs) {
  711. const v = m.value || {};
  712. const c = v.content || {};
  713. if (v.author === pubId && c && c.type === "ubiAllocation") {
  714. out.push({
  715. id: c.allocationId,
  716. from: pubId,
  717. to: c.to,
  718. amount: c.amount,
  719. concept: c.concept,
  720. epochId: c.epochId,
  721. status: c.status || "UNCLAIMED",
  722. createdAt: c.createdAt || new Date().toISOString()
  723. });
  724. }
  725. }
  726. return out;
  727. }
  728. async function publishPubAvailability() {
  729. if (!isPubNode()) return;
  730. const balance = await safeGetBalance("pub");
  731. const floor = Math.max(1, DEFAULT_RULES?.caps?.floor_user || 1);
  732. const available = Number(balance) >= floor;
  733. const ssb = await openSsb();
  734. if (!ssb || !ssb.publish) return;
  735. const content = { type: "pubAvailability", available, coin: "ECO", timestamp: Date.now() };
  736. await new Promise((resolve, reject) => ssb.publish(content, (err, res) => err ? reject(err) : resolve(res)));
  737. return available;
  738. }
  739. async function getPubAvailabilityFromSSB() {
  740. const pubId = getConfiguredPubId();
  741. if (!pubId) return false;
  742. const msgs = await scanLogStream();
  743. let latest = null;
  744. for (const m of msgs) {
  745. const v = m.value || {};
  746. const c = v.content || {};
  747. if (v.author === pubId && c && c.type === "pubAvailability" && c.coin === "ECO") {
  748. if (!latest || (Number(c.timestamp) || 0) > (Number(latest.timestamp) || 0)) latest = c;
  749. }
  750. }
  751. return !!(latest && latest.available);
  752. }
  753. async function listBanking(filter = "overview", userId) {
  754. const uid = resolveUserId(userId);
  755. const epochId = epochIdNow();
  756. let pubBalance = 0;
  757. let ubiAvailable = false;
  758. let allocations;
  759. if (isPubNode()) {
  760. pubBalance = await safeGetBalance("pub");
  761. const floor = Math.max(1, DEFAULT_RULES?.caps?.floor_user || 1);
  762. ubiAvailable = Number(pubBalance) >= floor;
  763. try { await publishPubAvailability(); } catch (_) {}
  764. const all = await transfersRepo.listByTag("UBI");
  765. allocations = all.map(t => ({
  766. id: t.id, concept: t.concept, from: t.from, to: t.to, amount: t.amount, status: t.status,
  767. createdAt: t.createdAt || t.deadline || new Date().toISOString(), txid: t.txid
  768. }));
  769. } else {
  770. ubiAvailable = await getPubAvailabilityFromSSB();
  771. allocations = await getUbiAllocationsFromSSB();
  772. }
  773. const userBalance = await safeGetBalance("user");
  774. const epochs = await epochsRepo.list();
  775. let computed = null;
  776. try { computed = await computeEpoch({ epochId, userId: uid, rules: DEFAULT_RULES }); } catch {}
  777. const pv = computePoolVars(pubBalance, DEFAULT_RULES);
  778. const actions = await fetchUserActions(uid);
  779. const engagementScore = scoreFromActions(actions);
  780. const poolForEpoch = computed?.epoch?.pool || pv.pool || 0;
  781. const futureUBI = Number(((engagementScore / 100) * poolForEpoch).toFixed(6));
  782. const addresses = await listAddressesMerged();
  783. const alreadyClaimed = await hasClaimedThisMonth(uid);
  784. const pubId = getConfiguredPubId();
  785. const userAddress = await getUserAddress(uid);
  786. const userWalletCfg = getWalletCfg("user") || {};
  787. const hasValidWallet = !!(userAddress && isValidEcoinAddress(userAddress) && userWalletCfg.url);
  788. const summary = {
  789. userBalance,
  790. epochId,
  791. pool: poolForEpoch,
  792. weightsSum: computed?.epoch?.weightsSum || 0,
  793. userEngagementScore: engagementScore,
  794. futureUBI,
  795. alreadyClaimed,
  796. pubId,
  797. hasValidWallet,
  798. ubiAvailability: ubiAvailable ? "OK" : "NO_FUNDS"
  799. };
  800. const exchange = await calculateEcoinValue();
  801. return { summary, allocations, epochs, rules: DEFAULT_RULES, addresses, exchange };
  802. }
  803. async function getAllocationById(id) {
  804. const t = await transfersRepo.findById(id);
  805. if (!t) return null;
  806. 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 };
  807. }
  808. async function getEpochById(id) {
  809. const existing = await epochsRepo.get(id);
  810. if (existing) return existing;
  811. const all = await transfersRepo.listAll();
  812. const filtered = all.filter(t => (t.tags || []).includes(`epoch:${id}`));
  813. const pool = filtered.reduce((s, t) => s + Number(t.amount || 0), 0);
  814. return { id, pool, weightsSum: 0, rules: DEFAULT_RULES, hash: "-" };
  815. }
  816. async function listEpochAllocations(id) {
  817. const all = await transfersRepo.listAll();
  818. return all.filter(t => (t.tags || []).includes(`epoch:${id}`)).map(t => ({
  819. 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
  820. }));
  821. }
  822. let genesisTimeCache = null;
  823. async function getAvgBlockSeconds(blocks) {
  824. if (!blocks || blocks < 2) return 0;
  825. try {
  826. if (!genesisTimeCache) {
  827. const h1 = await rpcCall("getblockhash", [1]);
  828. if (!h1) return 0;
  829. const b1 = await rpcCall("getblock", [h1]);
  830. genesisTimeCache = b1?.time || null;
  831. if (!genesisTimeCache) return 0;
  832. }
  833. const hCur = await rpcCall("getblockhash", [blocks]);
  834. if (!hCur) return 0;
  835. const bCur = await rpcCall("getblock", [hCur]);
  836. const curTime = bCur?.time || 0;
  837. if (!curTime) return 0;
  838. const elapsed = curTime - genesisTimeCache;
  839. return elapsed > 0 ? elapsed / (blocks - 1) : 0;
  840. } catch (_) { return 0; }
  841. }
  842. async function calculateEcoinValue() {
  843. const totalSupply = 25500000;
  844. let circulatingSupply = 0;
  845. let blocks = 0;
  846. let blockValueEco = 0;
  847. let isSynced = false;
  848. try {
  849. const info = await rpcCall("getinfo", []);
  850. circulatingSupply = info?.moneysupply || 0;
  851. blocks = info?.blocks || 0;
  852. isSynced = circulatingSupply > 0;
  853. const mining = await rpcCall("getmininginfo", []);
  854. blockValueEco = (mining?.blockvalue || 0) / 1e8;
  855. } catch (_) {}
  856. const avgSec = await getAvgBlockSeconds(blocks);
  857. const ecoValuePerHour = avgSec > 0 ? (3600 / avgSec) * blockValueEco : 0;
  858. const maturity = totalSupply > 0 ? circulatingSupply / totalSupply : 0;
  859. const ecoTimeMs = maturity * 3600 * 1000;
  860. const annualIssuance = ecoValuePerHour * 24 * 365;
  861. const inflationFactor = circulatingSupply > 0 ? (annualIssuance / circulatingSupply) * 100 : 0;
  862. const inflationMonthly = inflationFactor / 12;
  863. return {
  864. ecoValue: Number(ecoValuePerHour.toFixed(6)),
  865. ecoTimeMs: Number(ecoTimeMs.toFixed(3)),
  866. totalSupply,
  867. inflationFactor: Number(inflationFactor.toFixed(2)),
  868. inflationMonthly: Number(inflationMonthly.toFixed(2)),
  869. currentSupply: circulatingSupply,
  870. isSynced
  871. };
  872. }
  873. async function getBankingData(userId) {
  874. const ecoValue = await calculateEcoinValue();
  875. const karmaScore = await getUserEngagementScore(userId);
  876. let estimatedUBI = 0;
  877. try {
  878. const pubBal = isPubNode() ? await safeGetBalance("pub") : 0;
  879. const pv = computePoolVars(pubBal, DEFAULT_RULES);
  880. const pool = pv.pool || 0;
  881. const addresses = await listAddressesMerged();
  882. const eligible = addresses.filter(a => a.address && isValidEcoinAddress(a.address));
  883. const totalW = eligible.length > 0 ? eligible.length + eligible.length * (karmaScore / 100) : 1;
  884. const userW = 1 + karmaScore / 100;
  885. const cap = DEFAULT_RULES.caps?.cap_user_epoch ?? 50;
  886. estimatedUBI = Math.min(pool * (userW / Math.max(1, totalW)), cap);
  887. } catch (_) {}
  888. const claimHistory = await getUbiClaimHistory(userId).catch(() => ({ lastClaimedDate: null, totalClaimed: 0 }));
  889. return {
  890. ecoValue,
  891. karmaScore,
  892. estimatedUBI,
  893. lastClaimedDate: claimHistory.lastClaimedDate,
  894. totalClaimed: claimHistory.totalClaimed
  895. };
  896. }
  897. async function expireOldAllocations() {
  898. const cutoffMs = MAX_PENDING_EPOCHS * 30 * 86400000;
  899. const now = Date.now();
  900. const allocs = await transfersRepo.listAll();
  901. for (const a of allocs) {
  902. if ((a.status === "UNCLAIMED" || a.status === "UNCONFIRMED") &&
  903. (now - new Date(a.createdAt).getTime()) > cutoffMs) {
  904. await updateAllocationStatus(a.id, "EXPIRED");
  905. }
  906. }
  907. }
  908. async function publishUbiAllocation(allocation) {
  909. const ssb = await openSsb();
  910. if (!ssb) return;
  911. const epochTag = (allocation.tags || []).find(t => t.startsWith("epoch:"));
  912. const content = {
  913. type: "ubiAllocation",
  914. allocationId: allocation.id,
  915. to: allocation.to,
  916. amount: allocation.amount,
  917. concept: allocation.concept,
  918. epochId: epochTag ? epochTag.slice(6) : "",
  919. status: "UNCLAIMED",
  920. createdAt: allocation.createdAt
  921. };
  922. return new Promise((resolve, reject) => ssb.publish(content, (err, res) => err ? reject(err) : resolve(res)));
  923. }
  924. async function publishUbiClaim(allocationId, epochId) {
  925. const ssb = await openSsb();
  926. if (!ssb) return;
  927. const content = { type: "ubiClaim", allocationId, epochId, claimedAt: new Date().toISOString() };
  928. return new Promise((resolve, reject) => ssb.publish(content, (err, res) => err ? reject(err) : resolve(res)));
  929. }
  930. async function publishUbiClaimResult(allocationId, epochId, txid, userId, amount) {
  931. const ssb = await openSsb();
  932. if (!ssb) return;
  933. const content = { type: "ubiClaimResult", allocationId, epochId, txid, userId, amount, processedAt: new Date().toISOString() };
  934. return new Promise((resolve, reject) => ssb.publish(content, (err, res) => err ? reject(err) : resolve(res)));
  935. }
  936. async function processPendingClaims() {
  937. if (!isPubNode()) return;
  938. const ssb = await openSsb();
  939. if (!ssb) return;
  940. const claims = [];
  941. const results = [];
  942. await new Promise((resolve, reject) => {
  943. pull(ssb.messagesByType({ type: "ubiClaim", reverse: false }),
  944. pull.drain(msg => {
  945. if (msg.value?.content?.type === "ubiClaim") {
  946. claims.push({ ...msg.value.content, _author: msg.value.author });
  947. }
  948. },
  949. err => err ? reject(err) : resolve()));
  950. });
  951. await new Promise((resolve, reject) => {
  952. pull(ssb.messagesByType({ type: "ubiClaimResult", reverse: false }),
  953. pull.drain(msg => { if (msg.value?.content?.type === "ubiClaimResult") results.push(msg.value.content); },
  954. err => err ? reject(err) : resolve()));
  955. });
  956. const processedEpochUser = new Set(results.map(r => `${r.epochId}:${r.userId}`));
  957. const epochId = epochIdNow();
  958. for (const claim of claims) {
  959. const claimantId = claim._author;
  960. if (!claimantId) continue;
  961. const claimEpoch = claim.epochId || epochId;
  962. if (processedEpochUser.has(`${claimEpoch}:${claimantId}`)) continue;
  963. try {
  964. const addr = await getUserAddress(claimantId);
  965. if (!addr || !isValidEcoinAddress(addr)) continue;
  966. const pubBal = await safeGetBalance("pub");
  967. if (pubBal <= 0) continue;
  968. const pv = computePoolVars(pubBal, DEFAULT_RULES);
  969. const addresses = await listAddressesMerged();
  970. const eligible = addresses.filter(a => a.address && isValidEcoinAddress(a.address));
  971. const karmaScore = await getUserEngagementScore(claimantId);
  972. const wMin = DEFAULT_RULES.caps.w_min;
  973. const wMax = DEFAULT_RULES.caps.w_max;
  974. const capUser = DEFAULT_RULES.caps.cap_user_epoch;
  975. const userW = clamp(1 + karmaScore / 100, wMin, wMax);
  976. const totalW = eligible.reduce((acc) => acc + clamp(1, wMin, wMax), 0) || 1;
  977. const amount = Number(Math.max(1, Math.min(pv.pool * userW / totalW, capUser)).toFixed(6));
  978. const txid = await rpcCall("sendtoaddress", [addr, amount, `UBI ${claimEpoch}`], "pub");
  979. if (!txid) continue;
  980. await publishUbiClaimResult(claim.allocationId || `claim:${claimEpoch}:${claimantId}`, claimEpoch, txid, claimantId, amount);
  981. await publishBankClaim({ amount, epochId: claimEpoch, allocationId: claim.allocationId || `claim:${claimEpoch}:${claimantId}`, txid });
  982. const now = new Date().toISOString();
  983. await new Promise((resolve, reject) => ssb.publish({
  984. type: "transfer",
  985. from: config.keys.id,
  986. to: claimantId,
  987. concept: `UBI ${claimEpoch} ${claimantId}`,
  988. amount: String(amount),
  989. createdAt: now,
  990. updatedAt: now,
  991. deadline: null,
  992. confirmedBy: [config.keys.id],
  993. status: "UNCONFIRMED",
  994. tags: ["UBI"],
  995. opinions: {},
  996. opinions_inhabitants: [],
  997. txid
  998. }, (err, msg) => err ? reject(err) : resolve(msg)));
  999. } catch (_) {}
  1000. }
  1001. }
  1002. return {
  1003. DEFAULT_RULES,
  1004. isPubNode,
  1005. getConfiguredPubId,
  1006. computeEpoch,
  1007. executeEpoch,
  1008. getUserEngagementScore,
  1009. publishBankClaim,
  1010. publishUbiAllocation,
  1011. publishUbiClaim,
  1012. publishUbiClaimResult,
  1013. publishPubAvailability,
  1014. getPubAvailabilityFromSSB,
  1015. hasClaimedThisMonth,
  1016. getUbiClaimHistory,
  1017. claimUBI,
  1018. processPendingClaims,
  1019. expireOldAllocations,
  1020. claimAllocation,
  1021. listBanking,
  1022. getAllocationById,
  1023. getEpochById,
  1024. listEpochAllocations,
  1025. addAddress,
  1026. removeAddress,
  1027. ensureSelfAddressPublished,
  1028. getUserAddress,
  1029. setUserAddress,
  1030. listAddressesMerged,
  1031. calculateEcoinValue,
  1032. getBankingData
  1033. };
  1034. };