banking_model.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596
  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 DEFAULT_RULES = {
  9. epochKind: "WEEKLY",
  10. alpha: 0.2,
  11. reserveMin: 500,
  12. capPerEpoch: 2000,
  13. caps: { M_max: 3, T_max: 1.5, P_max: 2, cap_user_epoch: 50, w_min: 0.2, w_max: 6 },
  14. coeffs: { a1: 0.6, a2: 0.4, a3: 0.3, a4: 0.5, b1: 0.5, b2: 1.0 },
  15. graceDays: 14
  16. };
  17. const STORAGE_DIR = path.join(__dirname, "..", "configs");
  18. const EPOCHS_PATH = path.join(STORAGE_DIR, "banking-epochs.json");
  19. const TRANSFERS_PATH = path.join(STORAGE_DIR, "banking-allocations.json");
  20. const ADDR_PATH = path.join(STORAGE_DIR, "wallet-addresses.json");
  21. function ensureStoreFiles() {
  22. if (!fs.existsSync(STORAGE_DIR)) fs.mkdirSync(STORAGE_DIR, { recursive: true });
  23. if (!fs.existsSync(EPOCHS_PATH)) fs.writeFileSync(EPOCHS_PATH, "[]");
  24. if (!fs.existsSync(TRANSFERS_PATH)) fs.writeFileSync(TRANSFERS_PATH, "[]");
  25. if (!fs.existsSync(ADDR_PATH)) fs.writeFileSync(ADDR_PATH, "{}");
  26. }
  27. function epochIdNow() {
  28. const d = new Date();
  29. const tmp = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
  30. const dayNum = tmp.getUTCDay() || 7;
  31. tmp.setUTCDate(tmp.getUTCDate() + 4 - dayNum);
  32. const yearStart = new Date(Date.UTC(tmp.getUTCFullYear(), 0, 1));
  33. const weekNo = Math.ceil((((tmp - yearStart) / 86400000) + 1) / 7);
  34. const yyyy = tmp.getUTCFullYear();
  35. return `${yyyy}-${String(weekNo).padStart(2, "0")}`;
  36. }
  37. async function ensureSelfAddressPublished() {
  38. const me = config.keys.id;
  39. const local = readAddrMap();
  40. const current = typeof local[me] === "string" ? local[me] : (local[me] && local[me].address) || null;
  41. if (current && isValidEcoinAddress(current)) return { status: "present", address: current };
  42. const cfg = getWalletCfg("user") || {};
  43. if (!cfg.url) return { status: "skipped" };
  44. try {
  45. const addr = await rpcCall("getaddress", []);
  46. if (addr && isValidEcoinAddress(addr)) {
  47. await setUserAddress(me, addr, true);
  48. return { status: "published", address: addr };
  49. }
  50. } catch (_) {
  51. return { status: "error" };
  52. }
  53. return { status: "noop" };
  54. }
  55. function readJson(p, d) {
  56. try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch { return d; }
  57. }
  58. function writeJson(p, v) {
  59. fs.writeFileSync(p, JSON.stringify(v, null, 2));
  60. }
  61. async function rpcCall(method, params, kind = "user") {
  62. const cfg = getWalletCfg(kind);
  63. if (!cfg?.url) throw new Error(`${kind.toUpperCase()} RPC not configured`);
  64. const headers = { "content-type": "application/json" };
  65. if (cfg.user || cfg.pass) {
  66. headers.authorization = "Basic " + Buffer.from(`${cfg.user}:${cfg.pass}`).toString("base64");
  67. }
  68. const res = await fetch(cfg.url, { method: "POST", headers, body: JSON.stringify({ jsonrpc: "1.0", id: "oasis", method, params }) });
  69. if (!res.ok) throw new Error(`RPC ${method} failed`);
  70. const data = await res.json();
  71. if (data.error) throw new Error(data.error.message);
  72. return data.result;
  73. }
  74. async function safeGetBalance(kind = "user") {
  75. try {
  76. const r = await rpcCall("getbalance", [], kind);
  77. return Number(r) || 0;
  78. } catch {
  79. return 0;
  80. }
  81. }
  82. function readAddrMap() {
  83. ensureStoreFiles();
  84. const raw = readJson(ADDR_PATH, {});
  85. return raw && typeof raw === "object" ? raw : {};
  86. }
  87. function writeAddrMap(m) {
  88. ensureStoreFiles();
  89. writeJson(ADDR_PATH, m || {});
  90. }
  91. function getLogLimit() {
  92. return getConfig().ssbLogStream?.limit || 1000;
  93. }
  94. function isValidEcoinAddress(addr) {
  95. return typeof addr === "string" && /^[A-Za-z0-9]{20,64}$/.test(addr);
  96. }
  97. function getWalletCfg(kind) {
  98. const cfg = getConfig() || {};
  99. if (kind === "pub") {
  100. return cfg.walletPub || cfg.pubWallet || (cfg.pub && cfg.pub.wallet) || null;
  101. }
  102. return cfg.wallet || null;
  103. }
  104. function resolveUserId(maybeId) {
  105. const s = String(maybeId || "").trim();
  106. if (s) return s;
  107. return config?.keys?.id || "";
  108. }
  109. let FEED_SRC = "none";
  110. module.exports = ({ services } = {}) => {
  111. const transfersRepo = {
  112. listAll: async () => { ensureStoreFiles(); return readJson(TRANSFERS_PATH, []); },
  113. listByTag: async (tag) => { ensureStoreFiles(); return readJson(TRANSFERS_PATH, []).filter(t => (t.tags || []).includes(tag)); },
  114. findById: async (id) => { ensureStoreFiles(); return readJson(TRANSFERS_PATH, []).find(t => t.id === id) || null; },
  115. create: async (t) => { ensureStoreFiles(); const all = readJson(TRANSFERS_PATH, []); all.push(t); writeJson(TRANSFERS_PATH, all); },
  116. 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); } }
  117. };
  118. const epochsRepo = {
  119. list: async () => { ensureStoreFiles(); return readJson(EPOCHS_PATH, []); },
  120. 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); },
  121. get: async (id) => { ensureStoreFiles(); return readJson(EPOCHS_PATH, []).find(e => e.id === id) || null; }
  122. };
  123. async function openSsb() {
  124. if (services?.ssb) return services.ssb;
  125. if (services?.cooler?.open) return services.cooler.open();
  126. if (global.ssb) return global.ssb;
  127. try {
  128. const srv = require("../server/SSB_server.js");
  129. if (srv?.ssb) return srv.ssb;
  130. if (srv?.server) return srv.server;
  131. if (srv?.default) return srv.default;
  132. } catch (_) {}
  133. return null;
  134. }
  135. async function getWalletFromSSB(userId) {
  136. const ssb = await openSsb();
  137. if (!ssb) return null;
  138. const msgs = await new Promise((resolve, reject) =>
  139. pull(
  140. ssb.createLogStream({ limit: getLogLimit() }),
  141. pull.collect((err, arr) => err ? reject(err) : resolve(arr))
  142. )
  143. );
  144. for (let i = msgs.length - 1; i >= 0; i--) {
  145. const v = msgs[i].value || {};
  146. const c = v.content || {};
  147. if (v.author === userId && c && c.type === "wallet" && c.coin === "ECO" && typeof c.address === "string") {
  148. return c.address;
  149. }
  150. }
  151. return null;
  152. }
  153. async function scanAllWalletsSSB() {
  154. const ssb = await openSsb();
  155. if (!ssb) return {};
  156. const latest = {};
  157. const msgs = await new Promise((resolve, reject) =>
  158. pull(
  159. ssb.createLogStream({ limit: getLogLimit() }),
  160. pull.collect((err, arr) => err ? reject(err) : resolve(arr))
  161. )
  162. );
  163. for (let i = msgs.length - 1; i >= 0; i--) {
  164. const v = msgs[i].value || {};
  165. const c = v.content || {};
  166. if (c && c.type === "wallet" && c.coin === "ECO" && typeof c.address === "string") {
  167. if (!latest[v.author]) latest[v.author] = c.address;
  168. }
  169. }
  170. return latest;
  171. }
  172. async function publishSelfAddress(address) {
  173. const ssb = await openSsb();
  174. if (!ssb) return false;
  175. const msg = { type: "wallet", coin: "ECO", address, updatedAt: new Date().toISOString() };
  176. await new Promise((resolve, reject) => ssb.publish(msg, (err, val) => err ? reject(err) : resolve(val)));
  177. return true;
  178. }
  179. async function listUsers() {
  180. const addrLocal = readAddrMap();
  181. const ids = Object.keys(addrLocal);
  182. if (ids.length > 0) return ids.map(id => ({ id }));
  183. return [{ id: config.keys.id }];
  184. }
  185. async function getUserAddress(userId) {
  186. const v = readAddrMap()[userId];
  187. const local = typeof v === "string" ? v : (v && v.address) || null;
  188. if (local) return local;
  189. const ssbAddr = await getWalletFromSSB(userId);
  190. return ssbAddr;
  191. }
  192. async function setUserAddress(userId, address, publishIfSelf) {
  193. const m = readAddrMap();
  194. m[userId] = address;
  195. writeAddrMap(m);
  196. if (publishIfSelf && userId === config.keys.id) await publishSelfAddress(address);
  197. return true;
  198. }
  199. async function addAddress({ userId, address }) {
  200. if (!userId || !address || !isValidEcoinAddress(address)) return { status: "invalid" };
  201. const m = readAddrMap();
  202. const prev = m[userId];
  203. if (prev && (prev === address || (prev.address && prev.address === address))) return { status: "exists" };
  204. m[userId] = address;
  205. writeAddrMap(m);
  206. if (userId === config.keys.id) await publishSelfAddress(address);
  207. return { status: prev ? "updated" : "added" };
  208. }
  209. async function removeAddress({ userId }) {
  210. if (!userId) return { status: "invalid" };
  211. const m = readAddrMap();
  212. if (!m[userId]) return { status: "not_found" };
  213. delete m[userId];
  214. writeAddrMap(m);
  215. return { status: "deleted" };
  216. }
  217. async function listAddressesMerged() {
  218. const local = readAddrMap();
  219. const ssbAll = await scanAllWalletsSSB();
  220. const keys = new Set([...Object.keys(local), ...Object.keys(ssbAll)]);
  221. const out = [];
  222. for (const id of keys) {
  223. if (local[id]) out.push({ id, address: typeof local[id] === "string" ? local[id] : local[id].address, source: "local" });
  224. else if (ssbAll[id]) out.push({ id, address: ssbAll[id], source: "ssb" });
  225. }
  226. return out;
  227. }
  228. function idsEqual(a, b) {
  229. if (!a || !b) return false;
  230. const A = String(a).trim();
  231. const B = String(b).trim();
  232. if (A === B) return true;
  233. const strip = s => s.replace(/^@/, "").replace(/\.ed25519$/, "");
  234. return strip(A) === strip(B);
  235. }
  236. function inferType(c = {}) {
  237. if (c.vote) return "vote";
  238. if (c.votes) return "votes";
  239. if (c.address && c.coin === "ECO" && c.type === "wallet") return "bankWallet";
  240. if (typeof c.amount !== "undefined" && c.epochId && c.allocationId) return "bankClaim";
  241. if (typeof c.item_type !== "undefined" && typeof c.status !== "undefined") return "market";
  242. if (typeof c.goal !== "undefined" && typeof c.progress !== "undefined") return "project";
  243. if (typeof c.members !== "undefined" && typeof c.isAnonymous !== "undefined") return "tribe";
  244. if (typeof c.date !== "undefined" && typeof c.location !== "undefined") return "event";
  245. if (typeof c.priority !== "undefined" && typeof c.status !== "undefined" && c.title) return "task";
  246. if (typeof c.confirmations !== "undefined" && typeof c.severity !== "undefined") return "report";
  247. if (typeof c.job_type !== "undefined" && typeof c.status !== "undefined") return "job";
  248. if (typeof c.url !== "undefined" && typeof c.mimeType !== "undefined" && c.type === "audio") return "audio";
  249. if (typeof c.url !== "undefined" && typeof c.mimeType !== "undefined" && c.type === "video") return "video";
  250. if (typeof c.url !== "undefined" && c.title && c.key) return "document";
  251. if (typeof c.text !== "undefined" && typeof c.refeeds !== "undefined") return "feed";
  252. if (typeof c.text !== "undefined" && typeof c.contentWarning !== "undefined") return "post";
  253. if (typeof c.contact !== "undefined") return "contact";
  254. if (typeof c.about !== "undefined") return "about";
  255. if (typeof c.concept !== "undefined" && typeof c.amount !== "undefined" && c.status) return "transfer";
  256. return "";
  257. }
  258. function normalizeType(a) {
  259. const t = a.type || a.content?.type || inferType(a.content) || "";
  260. return String(t).toLowerCase();
  261. }
  262. function priorityBump(p) {
  263. const s = String(p || "").toUpperCase();
  264. if (s === "HIGH") return 3;
  265. if (s === "MEDIUM") return 1;
  266. return 0;
  267. }
  268. function severityBump(s) {
  269. const x = String(s || "").toUpperCase();
  270. if (x === "CRITICAL") return 6;
  271. if (x === "HIGH") return 4;
  272. if (x === "MEDIUM") return 2;
  273. return 0;
  274. }
  275. function scoreMarket(c) {
  276. const st = String(c.status || "").toUpperCase();
  277. let s = 5;
  278. if (st === "SOLD") s += 8;
  279. else if (st === "ACTIVE") s += 3;
  280. const bids = Array.isArray(c.auctions_poll) ? c.auctions_poll.length : 0;
  281. s += Math.min(10, bids);
  282. return s;
  283. }
  284. function scoreProject(c) {
  285. const st = String(c.status || "ACTIVE").toUpperCase();
  286. const prog = Number(c.progress || 0);
  287. let s = 8 + Math.min(10, prog / 10);
  288. if (st === "FUNDED") s += 10;
  289. return s;
  290. }
  291. function calculateOpinionScore(content) {
  292. const cats = content?.opinions || {};
  293. let s = 0;
  294. for (const k in cats) {
  295. if (!Object.prototype.hasOwnProperty.call(cats, k)) continue;
  296. if (k === "interesting" || k === "inspiring") s += 5;
  297. else if (k === "boring" || k === "spam" || k === "propaganda") s -= 3;
  298. else s += 1;
  299. }
  300. return s;
  301. }
  302. async function listAllActions() {
  303. if (services?.feed?.listAll) {
  304. const arr = await services.feed.listAll();
  305. FEED_SRC = "services.feed.listAll";
  306. return normalizeFeedArray(arr);
  307. }
  308. if (services?.activity?.list) {
  309. const arr = await services.activity.list();
  310. FEED_SRC = "services.activity.list";
  311. return normalizeFeedArray(arr);
  312. }
  313. if (typeof global.listFeed === "function") {
  314. const arr = await global.listFeed("all");
  315. FEED_SRC = "global.listFeed('all')";
  316. return normalizeFeedArray(arr);
  317. }
  318. const ssb = await openSsb();
  319. if (!ssb || !ssb.createLogStream) {
  320. FEED_SRC = "none";
  321. return [];
  322. }
  323. const msgs = await new Promise((resolve, reject) =>
  324. pull(
  325. ssb.createLogStream({ limit: getLogLimit() }),
  326. pull.collect((err, arr) => err ? reject(err) : resolve(arr))
  327. )
  328. );
  329. FEED_SRC = "ssb.createLogStream";
  330. return msgs.map(m => {
  331. const v = m.value || {};
  332. const c = v.content || {};
  333. return {
  334. id: v.key || m.key,
  335. author: v.author,
  336. type: (c.type || "").toLowerCase(),
  337. value: v,
  338. content: c
  339. };
  340. });
  341. }
  342. function normalizeFeedArray(arr) {
  343. if (!Array.isArray(arr)) return [];
  344. return arr.map(x => {
  345. const value = x.value || {};
  346. const content = x.content || value.content || {};
  347. const author = x.author || value.author || content.author || null;
  348. const type = (content.type || "").toLowerCase();
  349. return { id: x.id || value.key || x.key, author, type, value, content };
  350. });
  351. }
  352. async function fetchUserActions(userId) {
  353. const me = resolveUserId(userId);
  354. const actions = await listAllActions();
  355. const authored = actions.filter(a =>
  356. (a.author && a.author === me) || (a.value?.author && a.value.author === me)
  357. );
  358. if (authored.length) return authored;
  359. return actions.filter(a => {
  360. const c = a.content || {};
  361. const fields = [c.author, c.organizer, c.seller, c.about, c.contact];
  362. return fields.some(f => f && f === me);
  363. });
  364. }
  365. function scoreFromActions(actions) {
  366. let score = 0;
  367. for (const action of actions) {
  368. const t = normalizeType(action);
  369. const c = action.content || {};
  370. if (t === "post") score += 10;
  371. else if (t === "comment") score += 5;
  372. else if (t === "like") score += 2;
  373. else if (t === "image") score += 8;
  374. else if (t === "video") score += 12;
  375. else if (t === "audio") score += 8;
  376. else if (t === "document") score += 6;
  377. else if (t === "bookmark") score += 2;
  378. else if (t === "feed") score += 6;
  379. else if (t === "forum") score += c.root ? 5 : 10;
  380. else if (t === "vote") score += 3 + calculateOpinionScore(c);
  381. else if (t === "votes") score += Math.min(10, Number(c.totalVotes || 0));
  382. else if (t === "market") score += scoreMarket(c);
  383. else if (t === "project") score += scoreProject(c);
  384. else if (t === "tribe") score += 6 + Math.min(10, Array.isArray(c.members) ? c.members.length * 0.5 : 0);
  385. else if (t === "event") score += 4 + Math.min(10, Array.isArray(c.attendees) ? c.attendees.length : 0);
  386. else if (t === "task") score += 3 + priorityBump(c.priority);
  387. else if (t === "report") score += 4 + (Array.isArray(c.confirmations) ? c.confirmations.length : 0) + severityBump(c.severity);
  388. else if (t === "curriculum") score += 5;
  389. else if (t === "aiexchange") score += Array.isArray(c.ctx) ? Math.min(10, c.ctx.length) : 0;
  390. else if (t === "job") score += 4 + (Array.isArray(c.subscribers) ? c.subscribers.length : 0);
  391. else if (t === "bankclaim") score += Math.min(20, Math.log(1 + Math.max(0, Number(c.amount) || 0)) * 5);
  392. else if (t === "bankwallet") score += 2;
  393. else if (t === "transfer") score += 1;
  394. else if (t === "about") score += 1;
  395. else if (t === "contact") score += 1;
  396. else if (t === "pub") score += 1;
  397. }
  398. return Math.max(0, Math.round(score));
  399. }
  400. async function getUserEngagementScore(userId) {
  401. const actions = await fetchUserActions(userId);
  402. return scoreFromActions(actions);
  403. }
  404. function computePoolVars(pubBal, rules) {
  405. const alphaCap = (rules.alpha || DEFAULT_RULES.alpha) * pubBal;
  406. const available = Math.max(0, pubBal - (rules.reserveMin || DEFAULT_RULES.reserveMin));
  407. const rawMin = Math.min(available, (rules.capPerEpoch || DEFAULT_RULES.capPerEpoch), alphaCap);
  408. const pool = clamp(rawMin, 0, Number.MAX_SAFE_INTEGER);
  409. return { pubBal, alphaCap, available, rawMin, pool };
  410. }
  411. async function computeEpoch({ epochId, userId, rules = DEFAULT_RULES }) {
  412. const pubBal = await safeGetBalance("pub");
  413. const pv = computePoolVars(pubBal, rules);
  414. const engagementScore = await getUserEngagementScore(userId);
  415. const userWeight = 1 + engagementScore / 100;
  416. const weights = [{ user: userId, w: userWeight }];
  417. const W = weights.reduce((acc, x) => acc + x.w, 0) || 1;
  418. const capUser = (rules.caps && rules.caps.cap_user_epoch) || DEFAULT_RULES.caps.cap_user_epoch;
  419. const allocations = weights.map(({ user, w }) => {
  420. const amount = Math.min(pv.pool * w / W, capUser);
  421. return {
  422. id: `alloc:${epochId}:${user}`,
  423. epoch: epochId,
  424. user,
  425. weight: Number(w.toFixed(6)),
  426. amount: Number(amount.toFixed(6))
  427. };
  428. });
  429. const snapshot = JSON.stringify({ epochId, pool: pv.pool, weights, allocations, rules }, null, 2);
  430. const hash = crypto.createHash("sha256").update(snapshot).digest("hex");
  431. return { epoch: { id: epochId, pool: Number(pv.pool.toFixed(6)), weightsSum: Number(W.toFixed(6)), rules, hash }, allocations };
  432. }
  433. async function executeEpoch({ epochId, rules = DEFAULT_RULES }) {
  434. const { epoch, allocations } = await computeEpoch({ epochId, userId: config.keys.id, rules });
  435. await epochsRepo.save(epoch);
  436. for (const a of allocations) {
  437. if (a.amount <= 0) continue;
  438. await transfersRepo.create({
  439. id: a.id,
  440. from: "PUB",
  441. to: a.user,
  442. amount: a.amount,
  443. concept: `UBI ${epochId}`,
  444. status: "UNCONFIRMED",
  445. createdAt: new Date().toISOString(),
  446. deadline: new Date(Date.now() + DEFAULT_RULES.graceDays * 86400000).toISOString(),
  447. tags: ["UBI", `epoch:${epochId}`],
  448. opinions: {}
  449. });
  450. }
  451. return { epoch, allocations };
  452. }
  453. async function publishBankClaim({ amount, epochId, allocationId, txid }) {
  454. const ssbClient = await openSsb();
  455. const content = { type: "bankClaim", amount, epochId, allocationId, txid, timestamp: Date.now() };
  456. return new Promise((resolve, reject) => ssbClient.publish(content, (err, res) => err ? reject(err) : resolve(res)));
  457. }
  458. async function claimAllocation({ transferId, claimerId, pubWalletUrl, pubWalletUser, pubWalletPass }) {
  459. const allocation = await transfersRepo.findById(transferId);
  460. if (!allocation || allocation.status !== "UNCONFIRMED") throw new Error("Invalid allocation or already confirmed.");
  461. if (allocation.to !== claimerId) throw new Error("This allocation is not for you.");
  462. const txid = await rpcCall("sendtoaddress", [pubWalletUrl, allocation.amount, "UBI claim", pubWalletUser, pubWalletPass]);
  463. return { txid };
  464. }
  465. async function updateAllocationStatus(allocationId, status, txid) {
  466. const all = await transfersRepo.listAll();
  467. const idx = all.findIndex(t => t.id === allocationId);
  468. if (idx >= 0) {
  469. all[idx].status = status;
  470. all[idx].txid = txid;
  471. await transfersRepo.create(all[idx]);
  472. }
  473. }
  474. async function listBanking(filter = "overview", userId) {
  475. const uid = resolveUserId(userId);
  476. const epochId = epochIdNow();
  477. const pubBalance = await safeGetBalance("pub");
  478. const userBalance = await safeGetBalance("user");
  479. const epochs = await epochsRepo.list();
  480. const all = await transfersRepo.listByTag("UBI");
  481. const allocations = all.map(t => ({
  482. id: t.id, concept: t.concept, from: t.from, to: t.to, amount: t.amount, status: t.status,
  483. createdAt: t.createdAt || t.deadline || new Date().toISOString(), txid: t.txid
  484. }));
  485. let computed = null;
  486. try { computed = await computeEpoch({ epochId, userId: uid, rules: DEFAULT_RULES }); } catch {}
  487. const pv = computePoolVars(pubBalance, DEFAULT_RULES);
  488. const actions = await fetchUserActions(uid);
  489. const engagementScore = scoreFromActions(actions);
  490. const poolForEpoch = computed?.epoch?.pool || pv.pool || 0;
  491. const futureUBI = Number(((engagementScore / 100) * poolForEpoch).toFixed(6));
  492. const addresses = await listAddressesMerged();
  493. const summary = {
  494. userBalance,
  495. pubBalance,
  496. epochId,
  497. pool: poolForEpoch,
  498. weightsSum: computed?.epoch?.weightsSum || 0,
  499. userEngagementScore: engagementScore,
  500. futureUBI
  501. };
  502. return { summary, allocations, epochs, rules: DEFAULT_RULES, addresses };
  503. }
  504. async function getAllocationById(id) {
  505. const t = await transfersRepo.findById(id);
  506. if (!t) return null;
  507. 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 };
  508. }
  509. async function getEpochById(id) {
  510. const existing = await epochsRepo.get(id);
  511. if (existing) return existing;
  512. const all = await transfersRepo.listAll();
  513. const filtered = all.filter(t => (t.tags || []).includes(`epoch:${id}`));
  514. const pool = filtered.reduce((s, t) => s + Number(t.amount || 0), 0);
  515. return { id, pool, weightsSum: 0, rules: DEFAULT_RULES, hash: "-" };
  516. }
  517. async function listEpochAllocations(id) {
  518. const all = await transfersRepo.listAll();
  519. return all.filter(t => (t.tags || []).includes(`epoch:${id}`)).map(t => ({
  520. 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
  521. }));
  522. }
  523. return {
  524. DEFAULT_RULES,
  525. computeEpoch,
  526. executeEpoch,
  527. getUserEngagementScore,
  528. publishBankClaim,
  529. claimAllocation,
  530. listBanking,
  531. getAllocationById,
  532. getEpochById,
  533. listEpochAllocations,
  534. addAddress,
  535. removeAddress,
  536. ensureSelfAddressPublished,
  537. getUserAddress,
  538. setUserAddress,
  539. listAddressesMerged
  540. };
  541. };