-- generated by cesdk 0.1.0, do not edit -- project: peerfunder 0.1 local __mods, __cache = {}, {} local function require(name) local c = __cache[name] if c ~= nil then return c end local f = __mods[name] if not f then error("cesdk bundle: module '" .. name .. "' not found", 2) end local r = f() if r == nil then r = true end __cache[name] = r return r end __mods["cesdk.manifest"] = function() return { name = "peerfunder", version = "0.1", protocols = { "/peerfunder/1" } } end __mods["main"] = function() -- peerfunder entry point. Identity (CES_MANIFEST) is injected by the bundler from -- project.lua. local Peerfunder = require("peerfunder") local peerfunder = Peerfunder.new("/s/peerfunder.conf") -- Relay command channel (cesh dial): line-buffered text commands. local lbuf = {} ces.conn.set_listener({ on_data = function(conn, data) local buf = (lbuf[conn.id] or "") .. data while true do local line, rest = buf:match("^(.-)\n(.*)$") if not line then break end buf = rest peerfunder:admin(conn, line) end lbuf[conn.id] = buf end, on_close = function(conn) lbuf[conn.id] = nil end, }) -- The emission tick. Re-armable so emit_ms can be retuned live; wrapped so a fault -- never kills the timer. local tick local function arm() if tick then ces.cancel(tick) end tick = ces.every(peerfunder.emit_ms, function() local ok, err = pcall(function() peerfunder:emit() end) if not ok then ces.log("error", "peerfunder: emit error: " .. tostring(err)) end end) end arm() -- One apply path for both the host's on_config push and the panel's config -- form: retune the knobs and re-arm the tick when the interval changed. local function apply_cfg(c) local prev = peerfunder.emit_ms peerfunder:reconfigure(c) if peerfunder.emit_ms ~= prev then arm() end end -- Dashboard contract: live stats, a force-emit command, the config the editor -- seeds, and live retune of the numeric knobs. On hosts with the mene library -- installed, a declarative panel (stats, candidate table, blacklist editor, -- config form) supersedes the flat status lane; the status map stays for API -- consumers. if ces.extension_admin then local spec = { status = function() return { budget = tostring(peerfunder:budget()), peers = tostring(#peerfunder:candidates()), granted = tostring(peerfunder.granted_total), ticks = tostring(peerfunder.ticks), } end, commands = { { id = "emit", label = "Emit now" } }, on_command = function(id) if id == "emit" then local count, paid = peerfunder:emit() return string.format("emitted to %d peers, %d paid", count, paid) end end, config_defaults = "emit_ms = 600000\n" .. "emit_per_peer = 10000000\n" .. "per_peer_cap = 0\n" .. "min_reserve = 0\n" .. "require_inbound = 1\n", on_config = apply_cfg, } if mene then spec.panel = peerfunder:panel_app(apply_cfg) end ces.extension_admin(spec) end ces.log("peerfunder: up, emit_per_peer=" .. peerfunder.emit_per_peer .. " every " .. peerfunder.emit_ms .. "ms") ces.run() end __mods["peerfunder"] = function() -- peerfunder: a /s/ extension. Each tick it transfers a bounded amount of credit -- from its program account to each peer (reachable, inbound, not blacklisted, not -- self), crediting the peer's balance with us. Reads only the local ledger. local conf = require("conf") local M = {} local Peerfunder = {} Peerfunder.__index = Peerfunder local function hex(s) return (s:gsub(".", function(c) return string.format("%02x", c:byte()) end)) end local function unhex(h) if type(h) ~= "string" or #h % 2 ~= 0 or h == "" or h:match("[^%x]") then return nil end return (h:gsub("..", function(cc) return string.char(tonumber(cc, 16)) end)) end -- Defaults; an operator overrides via /s/peerfunder.conf. Amounts are raw credit units -- (PRICE_UNIT = 1e8 per whole credit), so 10000000 = 0.1 credit per peer per tick. local DEFAULTS = { emit_ms = 600000, -- tick interval (10 min) emit_per_peer = 10000000, -- paid to each peer per tick (0.1 credit) per_peer_cap = 0, -- skip a peer already holding >= this with us (0 = no cap) min_reserve = 0, -- keep at least this in the program account require_inbound = 1, -- only fund peers that are IN (have sent us PoW); 0 = any reachable } function M.new(cfgpath) local c = conf.load(cfgpath or "/s/peerfunder.conf") local self = setmetatable({ emit_ms = conf.num(c, "emit_ms", DEFAULTS.emit_ms), emit_per_peer = conf.num(c, "emit_per_peer", DEFAULTS.emit_per_peer), per_peer_cap = conf.num(c, "per_peer_cap", DEFAULTS.per_peer_cap), min_reserve = conf.num(c, "min_reserve", DEFAULTS.min_reserve), require_inbound = conf.num(c, "require_inbound", DEFAULTS.require_inbound) ~= 0, blacklist = {}, granted_total = 0, ticks = 0, last_emitted = 0, }, Peerfunder) for _, h in ipairs(conf.list(c, "blacklist")) do local k = unhex(h) if k and #k == 32 then self.blacklist[k] = true end end return self end -- Live retune of all knobs (dashboard on_config); main.lua re-arms the tick when -- emit_ms changes. function Peerfunder:reconfigure(c) self.emit_ms = conf.num(c, "emit_ms", self.emit_ms) self.emit_per_peer = conf.num(c, "emit_per_peer", self.emit_per_peer) self.per_peer_cap = conf.num(c, "per_peer_cap", self.per_peer_cap) self.min_reserve = conf.num(c, "min_reserve", self.min_reserve) self.require_inbound = conf.num(c, "require_inbound", self.require_inbound and 1 or 0) ~= 0 end -- Remaining emission budget = our program account balance (our own ledger). function Peerfunder:budget() local acc = ces.account_read(ces.program_pubkey()) return (acc and acc.balance) or 0 end -- Candidate peers: reachable, identified, not blacklisted, not self, and (when -- require_inbound is set) inbound -- have sent us PoW. function Peerfunder:candidates() local me = ces.owner_pubkey() local out = {} for _, p in ipairs(ces.peers() or {}) do if p.reachable and p.pubkey and #p.pubkey == 32 and p.pubkey ~= me and not self.blacklist[p.pubkey] and (not self.require_inbound or p.inbound) then out[#out + 1] = p end end return out end -- One emission tick: spend up to the budget across candidates, bounded per peer by -- the cap (read from our own ledger). Returns count, total paid, budget left. function Peerfunder:emit() local cands = self:candidates() local left = self:budget() - self.min_reserve local count, paid = 0, 0 for _, p in ipairs(cands) do if left < self.emit_per_peer then break end local skip = false if self.per_peer_cap > 0 then local acc = ces.account_read(p.pubkey) -- our ledger, truthful if acc and acc.balance and acc.balance >= self.per_peer_cap then skip = true end end if not skip then local ok = ces.transfer(p.pubkey, self.emit_per_peer) if ok then count = count + 1 paid = paid + self.emit_per_peer left = left - self.emit_per_peer ces.log("trace", string.format("peerfunder: paid %d to %s", self.emit_per_peer, hex(p.pubkey):sub(1, 16))) end end end self.ticks = self.ticks + 1 self.last_emitted = count self.granted_total = self.granted_total + paid local budget = self:budget() ces.log("debug", string.format( "peerfunder: tick funded %d/%d peers, paid %d, budget %d", count, #cands, paid, budget)) return count, paid, budget end -- Serialize the LIVE state (knobs + blacklist) as conf-file text, so every -- panel-side change persists exactly what is running. The blacklist csv makes -- panel blacklist edits durable across restarts. local function self_conf_text(pf) local bl = {} for k in pairs(pf.blacklist) do bl[#bl + 1] = hex(k) end table.sort(bl) return "emit_ms = " .. pf.emit_ms .. "\n" .. "emit_per_peer = " .. pf.emit_per_peer .. "\n" .. "per_peer_cap = " .. pf.per_peer_cap .. "\n" .. "min_reserve = " .. pf.min_reserve .. "\n" .. "require_inbound = " .. (pf.require_inbound and 1 or 0) .. "\n" .. "blacklist = " .. table.concat(bl, ",") .. "\n" end -- Persist the live state if the host supports it; returns a status suffix. local function persist(pf) if ces.extension_admin.save_config then ces.extension_admin.save_config(self_conf_text(pf)) return " + saved" end return " (host cannot persist)" end -- The mene admin panel (webadmin Extensions tab): budget/emission stats, the -- candidate table with live balances, force-emit, blacklist management, and a -- typed config form that applies live and persists /s/peerfunder.conf. -- `apply_cfg` is main's on_config path (reconfigure + tick re-arm). Uses the -- host-installed global `mene`; only called when it exists. -- Deterministic blacklist ordering. view and update both derive it from the -- live set, so a row-click index resolves without the view having to stash a -- snapshot in the model (views stay pure: they only read). local function blacklist_keys(pf) local bl = {} for k in pairs(pf.blacklist) do bl[#bl + 1] = k end table.sort(bl) return bl end function Peerfunder:panel_app(apply_cfg) local pf = self -- Bound the per-render work: each candidate row costs a ledger read, and -- the view runs on every host render tick while the panel is watched. local MAX_ROWS = 50 return mene.app{ model = { last_msg = "" }, view = function(m) local cands = pf:candidates() local budget = pf:budget() local rows = {} for i, p in ipairs(cands) do if i > MAX_ROWS then break end local acc = ces.account_read(p.pubkey) rows[#rows + 1] = { hex(p.pubkey):sub(1, 16), (acc and acc.balance) or 0, p.inbound and "in" or "out" } end local bl_rows = {} for _, k in ipairs(blacklist_keys(pf)) do bl_rows[#bl_rows + 1] = { hex(k) } end return mene.card({ title = "Peer Funder", subtitle = "seeds channel liquidity at peers" }, mene.grid({ cols = 4, gap = 12 }, mene.stat({ label = "budget", value = budget, tone = budget > pf.min_reserve and "ok" or "warn" }), mene.stat({ label = "candidates", value = #cands }), mene.stat({ label = "granted total", value = pf.granted_total }), mene.stat({ label = "ticks / last emitted", value = pf.ticks .. " / " .. pf.last_emitted })), mene.row({ align = "between" }, mene.text({ tone = "muted" }, m.last_msg), mene.button({ on = "emit", kind = "primary" }, "Emit now")), mene.section({ title = "candidate peers" }, #rows > 0 and mene.table({ sortable = true, page_size = 10, cols = { "peer", "balance with us", "dir" }, rows = rows }) or mene.text({ tone = "muted" }, "no fundable peers right now"), #cands > MAX_ROWS and mene.text({ tone = "muted" }, "showing " .. MAX_ROWS .. " of " .. #cands) or nil), mene.section({ title = "blacklist" }, #bl_rows > 0 and mene.table({ on = "unbl", cols = { "pubkey (click to remove)" }, rows = bl_rows }) or mene.text({ tone = "muted" }, "empty"), mene.form({ on = "bl" }, mene.field({ name = "key", label = "pubkey (64 hex)", placeholder = "aabb..." }), mene.submit({ kind = "danger" }, "Blacklist"))), mene.section({ title = "config (applies live + persists)" }, mene.form({ on = "cfg" }, mene.grid({ cols = 4, gap = 10 }, mene.field({ name = "emit_ms", kind = "number", label = "emit interval ms", value = pf.emit_ms }), mene.field({ name = "emit_per_peer", kind = "number", label = "per peer per tick (raw)", value = pf.emit_per_peer }), mene.field({ name = "per_peer_cap", kind = "number", label = "per-peer cap (0 = none)", value = pf.per_peer_cap }), mene.field({ name = "min_reserve", kind = "number", label = "min reserve", value = pf.min_reserve })), mene.checkbox({ name = "require_inbound", label = "only fund inbound peers (sent us PoW)", checked = pf.require_inbound }), mene.submit({ kind = "primary" }, "Apply + save")))) end, update = function(ev, m) if ev.on == "emit" then local count, paid = pf:emit() m.last_msg = string.format("emitted to %d peers, %d paid", count, paid) elseif ev.on == "cfg" and type(ev.value) == "table" then local c = {} for k, x in pairs(ev.value) do if type(x) == "boolean" then x = x and "1" or "0" end c[k] = tostring(x) end apply_cfg(c) m.last_msg = "config applied" .. persist(pf) elseif ev.on == "bl" then local k = unhex((ev.value and ev.value.key or ""):gsub("%s", "")) if k and #k == 32 then pf.blacklist[k] = true m.last_msg = "blacklisted" .. persist(pf) else m.last_msg = "bad key: need 64 hex chars" end elseif ev.on == "unbl" and ev.value and ev.value.row ~= nil then -- Same deterministic ordering the view rendered from. local k = blacklist_keys(pf)[ev.value.row + 1] -- row index is 0-based if k then pf.blacklist[k] = nil m.last_msg = "unblacklisted " .. hex(k):sub(1, 16) .. persist(pf) end end return m end, } end -- Relay command channel (cesh dial): one-line text commands for ops and tests. function Peerfunder:admin(conn, line) local cmd, rest = line:match("^(%S+)%s*(.*)$") if cmd == "status" then conn:write(string.format( "budget=%d peers=%d granted_total=%d ticks=%d last_emitted=%d program=%s\n", self:budget(), #self:candidates(), self.granted_total, self.ticks, self.last_emitted, hex(ces.program_pubkey()))) elseif cmd == "emit" then local count, paid, budget = self:emit() conn:write(string.format("emitted=%d paid=%d budget=%d\n", count, paid, budget)) elseif cmd == "peers" then local parts = {} for _, p in ipairs(self:candidates()) do local acc = ces.account_read(p.pubkey) parts[#parts + 1] = string.format("%s=%d", hex(p.pubkey):sub(1, 16), (acc and acc.balance) or 0) end conn:write("peers=" .. #parts .. (#parts > 0 and (" " .. table.concat(parts, " ")) or "") .. "\n") elseif cmd == "blacklist" then local k = unhex(rest) if not k or #k ~= 32 then conn:write("err bad-key\n"); return end self.blacklist[k] = true conn:write("ok blacklisted\n") elseif cmd == "unblacklist" then local k = unhex(rest) if not k or #k ~= 32 then conn:write("err bad-key\n"); return end self.blacklist[k] = nil conn:write("ok\n") elseif cmd == "selftest" then local ok = ces.program_pubkey and #ces.program_pubkey() == 32 and self.emit_per_peer > 0 conn:write((ok and "ok" or "bad") .. "\n") else conn:write("err unknown\n") end end return M end __mods["conf"] = function() -- conf - load an operator-deployed config file for an L2 extension from the file -- store. Format: "key = value" lines; "#" starts a comment; blank and malformed -- lines are ignored. load() returns a string->string table ({} if the file is -- absent or file ops are unavailable, so a program always has working defaults); -- num() and list() coerce a value. The same extension reads the same path in -- production and under test; only the deployed file differs. local M = {} function M.load(path) local out = {} if not ces.file_read or not ces.file_stat then return out end -- READ rejects a range past EOF, so size the read to the file (stat first). local st = ces.file_stat(path) if not st or not st.size or st.size == 0 then return out end local n = st.size < 65536 and st.size or 65536 local data = ces.file_read(path, 0, n) if not data or data == "" then return out end for line in (data .. "\n"):gmatch("(.-)\n") do local s = line:gsub("#.*$", "") local k, v = s:match("^%s*([%w_]+)%s*=%s*(.-)%s*$") if k then out[k] = v end end return out end -- Numeric value of key k, or default. function M.num(t, k, default) local n = t[k] and tonumber(t[k]) return n or default end -- Comma-separated value of key k as a list of trimmed, non-empty strings ({}). function M.list(t, k) local out = {} if t[k] then for item in (t[k] .. ","):gmatch("%s*([^,]-)%s*,") do if item ~= "" then out[#out + 1] = item end end end return out end return M end CES_MANIFEST = { name = "Peer Funder", version = "0.1", description = "Unilateral peer funder: gives bounded credit to peers to seed channel liquidity." } return require("main")