灵符lingfu
L

ling-wallet

↓ 0 downloads

Ling Wallet: a post-quantum privacy-coin wallet, full node and HyperTorus miner for LNG

Windows · x86-64 ✕Linux · x86-64 ✕LingOS · x86-64LingOS · Pi 5 ARM64 ✕
Keywords wallet, cryptocurrency, lng, node, miner, ling
lingfu install ling-wallet
LicenseMIT
Authorsling-lin
READMECode / FilesVersionsDependency GraphDependentsSecurityBuildsSettings

Code / Files (116)

ling/ui/shell.ling
# shell.ling — cached backend reads, sidebar, status strip, section routing,
# keyboard shortcuts and auto-lock.

bind SECTIONS = ["WALLET", "SEND", "RECEIVE", "MINING", "NETWORK", "SECURITY", "ABOUT"]
bind SECTION_CAPS = ["Balance & activity", "Pay a verified address", "Your address & key bundle", "Background miner", "Peers, chain & supply audit", "Phrase, password, auto-lock", "Standards & limitations"]

fn section_color(i) {
    return list_get([C_GREEN, C_ORANGE, C_BLUE, C_YELLOW, C_INDIGO, C_VIOLET, C_TEXT_DIM], i)
}

fn section() { return st_get("section", 0) }
fn set_section(i) {
    if i != section() {
        st_set("section", i)
        focus_next_frame()
    }
}

# Backend reads, refreshed four times a second (or when forced).
fn refresh(force) {
    bind t = u_now()
    bind state = be_state()
    st_set("d_state", state)
    if !force && t - st_get("d_last", 0 - 10) < 0.25 { return () }
    st_set("d_last", t)
    bind sm = match be_summary() {
        ok(v) => v,
        bad(e) => (),
    }
    st_set("d_summary", sm)
    if state == "unlocked" {
        st_set("d_history", be_history(50))
        st_set("d_net", be_network())
        st_set("d_audit", be_audit())
        if st_get("d_bundle", "") == "" { st_set("d_bundle", be_export_bundle()) }
    }
    if section() != 3 || st_has("d_mining") < 1 { st_set("d_mining", be_mining()) }
}
fn d_summary() { return st_get("d_summary", ()) }
fn d_net() { return st_get("d_net", ()) }

# Lock now and drop anything sensitive the UI holds.
fn ui_lock() {
    be_lock()
    send_reset()
    sec_wipe()
    st_set("d_bundle", "")
    tf_clear("unlock_pw")
    st_set("d_state", be_state())
    focus_next_frame()
}

fn autolock_secs() { return list_get([60, 300, 600, 1800, 0 - 1], st_get("autolock", 2)) }

# One frame of the whole app.
fn app_frame() {
    bind t = u_now()
    refresh(st_get("shot", 0) > 0)
    bind state = st_get("d_state", "nowallet")
    if state == "unlocked" && section() == 3 {
        bind m = be_mining()
        st_set("d_mining", m)
        tz_feed("tz", m.trace, t)
    }
    if state != st_get("last_state", "") {
        focus_next_frame()
        st_set("last_state", state)
    }
    bind lim = autolock_secs()
    if state == "unlocked" && !onboard_in_progress() && lim > 0 && st_get("shot", 0) < 1 {
        if t - st_get("activity_t", t) > lim {
            ui_lock()
            toast("Locked after inactivity")
            bind state = st_get("d_state", "locked")
        }
    }
    bind w = get_width()
    bind h = get_height()
    fill(0, 0, w, h, C_BG)
    if onboard_in_progress() || state == "nowallet" {
        onboard_draw()
    } else if state == "locked" {
        unlock_draw()
    } else {
        shell_draw()
    }
}

fn shell_draw() {
    if u_ctrl() {
        bind i = 0
        while i < 7 {
            if u_key(num_str(i + 1)) { set_section(i) }
            bind i = i + 1
        }
        if u_key("l") {
            ui_lock()
            return ()
        }
    }
    sidebar()
    if status_strip() {
        ui_lock()
        return ()
    }
    bind w = get_width()
    bind h = get_height()
    bind ax = SIDE_W + PAD
    bind ay = TOP_H + PAD + 8
    bind aw = w - SIDE_W - 2 * PAD
    bind ah = h - TOP_H - 2 * PAD - 8
    clip_push(SIDE_W + 1, TOP_H + 1, w - SIDE_W - 1, h - TOP_H - 1)
    bind s = section()
    if s == 0 { wallet_draw(ax, ay, aw, ah) }
    if s == 1 { send_draw(ax, ay, aw, ah) }
    if s == 2 { receive_draw(ax, ay, aw, ah) }
    if s == 3 { mining_draw(ax, ay, aw, ah) }
    if s == 4 { network_draw(ax, ay, aw, ah) }
    if s == 5 { security_draw(ax, ay, aw, ah) }
    if s == 6 { about_draw(ax, ay, aw, ah) }
    clip_pop()
}

# The brand mark: one solid "$" in the Daemon face.
fn logo(x, y, size, col) {
    t_text(F_LOGO, size, x, y + round(size * list_get(FONT_CAP, F_LOGO)), "$", col)
    return round(t_w(F_LOGO, size, "$"))
}

fn sidebar() {
    bind h = get_height()
    fill(0, 0, SIDE_W, h, C_SIDE)
    vline(SIDE_W, 0, h, C_LINE_SOFT, 1)
    cross(SIDE_W - 6, 0, 5, h, C_LINE_SOFT, 3, 0.5)

    bind lw = logo(PAD, 20, 40, C_TEXT_STRONG)
    bind bx = PAD + lw + 12
    t_text_sp(F_HEAD, 13, bx, 35, "WALLET", C_TEXT_STRONG, 2)
    label(bx, 52, "LNG", C_TEXT_DIM)
    hline(PAD, 72, SIDE_W - 2 * PAD - 6, C_LINE_SOFT, 1)
    hatch(PAD, 75, 40, 4, C_BLUE, true, 3, 0.6)

    bind y = 92
    bind i = 0
    while i < len(SECTIONS) {
        bind on = section() == i
        bind hot = u_hover(0, y, SIDE_W - 6, 60)
        bind sc = section_color(i)
        if on {
            fill(0, y, SIDE_W - 6, 60, C_ROW_ACTIVE)
            fill(0, y, 3, 60, sc)
            hatch(3, y, 10, 60, sc, true, 3, 0.35)
        } else if hot {
            fill(0, y, SIDE_W - 6, 60, C_ROW_HOVER)
        }
        bind col = sel(on || hot, sc, shade(sc, 0.35))
        tile_word(PAD, y + 11, 17, list_get(SECTIONS, i), col, on)
        t_text(F_UI, 15, PAD, y + 48, list_get(SECTION_CAPS, i), sel(on, C_TEXT, C_TEXT_FAINT))
        if hot && u_pressed() { set_section(i) }
        bind y = y + 60
        bind i = i + 1
    }

    bind fy = h - 72
    hline(PAD, fy, SIDE_W - 2 * PAD - 6, C_LINE_SOFT, 1)
    bind s = d_summary()
    bind height = "-"
    if s != () {
        if s.sync_kind != "connecting" { bind height = num_str(s.sync_height) }
    }
    label(PAD, fy + 24, "Block", C_TEXT_FAINT)
    t_right(F_MONO, 20, SIDE_W - PAD - 6, fy + 25, height, C_TEXT)
    label(PAD, fy + 48, "Protocol", C_TEXT_FAINT)
    t_right(F_MONO, 20, SIDE_W - PAD - 6, fy + 49, "LW-1 v0.1", C_TEXT_DIM)
}

# Returns true when Lock was pressed.
fn status_strip() {
    bind w = get_width()
    fill(SIDE_W + 1, 0, w - SIDE_W - 1, TOP_H, C_BG)
    hline(SIDE_W + 1, TOP_H, w - SIDE_W - 1, C_LINE_SOFT, 1)
    bind s = d_summary()
    bind n = d_net()
    bind x = SIDE_W + 1 + PAD
    bind cap_y = 22
    bind val_y = 43

    bind name = ""
    if s != () { bind name = s.network_name }
    label(x, cap_y, "Network", C_TEXT_FAINT)
    bind tw = t_text(F_UI, 18, x, val_y, name, C_TEXT_STRONG)
    bind x = x + round(max(tw, label_w("Network"))) + 20
    vline(x, 12, 32, C_LINE_SOFT, 1)
    bind x = x + 20

    label(x, cap_y, "Sync", C_TEXT_FAINT)
    bind kind = "connecting"
    if s != () { bind kind = s.sync_kind }
    if kind == "synced" {
        disc(x + 4, val_y - 5, 3.5, C_GREEN, 1)
        glow(x + 4, val_y - 5, 7, C_GREEN, 0.3)
        bind a = round(t_text(F_UI, 18, x + 14, val_y, "Synced", C_TEXT_STRONG))
        bind b = round(t_text(F_MONO, 20, x + 22 + a, val_y, "#" + num_str(s.sync_height), C_TEXT_DIM))
        bind x = x + 22 + a + b
    } else if kind == "syncing" {
        bind p = 0
        if s.sync_target > 0 { bind p = min(1, s.sync_height / s.sync_target) }
        disc(x + 4, val_y - 5, 3.5, C_YELLOW, 1)
        bind a = round(t_text(F_UI, 18, x + 14, val_y, "Syncing", C_TEXT_STRONG))
        bind b = round(t_text(F_MONO, 20, x + 22 + a, val_y, num_str(s.sync_height) + "/" + num_str(s.sync_target), C_TEXT_DIM))
        bind bw = 14 + a + 8 + b
        fill(x, val_y + 6, bw, 2, C_LINE_SOFT)
        fill(x, val_y + 6, floor(bw * p), 2, C_YELLOW)
        bind x = x + bw
    } else {
        bind pulse = 0.4 + 0.6 * abs(sin(u_now() * 3))
        disc(x + 4, val_y - 5, 3.5, C_ORANGE, pulse)
        bind x = x + 14 + round(t_text(F_UI, 18, x + 14, val_y, "Connecting…", C_TEXT_STRONG))
    }
    bind x = x + 20
    vline(x, 12, 32, C_LINE_SOFT, 1)
    bind x = x + 20

    bind peers = 0
    if n != () { bind peers = len(n.peers) }
    label(x, cap_y, "Peers", C_TEXT_FAINT)
    bind b2 = 0
    while b2 < 4 {
        bind bh = 4 + b2 * 3
        fill(x + b2 * 5, val_y - bh, 3, bh, sel(b2 < min(peers, 4), C_BLUE, C_LINE_SOFT))
        bind b2 = b2 + 1
    }
    bind pw = round(t_text(F_MONO, 22, x + 26, val_y, num_str(peers), C_TEXT_STRONG))
    bind x = x + 26 + max(pw, 10) + 20
    vline(x, 12, 32, C_LINE_SOFT, 1)
    bind x = x + 20

    bind route = "-"
    if n != () { bind route = n.route }
    bind tor = str_starts_with(str_lower(route), "tor")
    label(x, cap_y, "Route", C_TEXT_FAINT)
    icon_draw("lock", x, val_y - 6, sel(tor, C_VIOLET, C_GREEN))
    t_text(F_UI, 18, x + 24, val_y, list_get(str_split(route, " "), 0), C_TEXT_STRONG)

    bind lx = w - PAD - 104
    u_tip(lx, 10, 104, 36, "Lock the wallet now (Ctrl+L)")
    return button(lx, 10, 104, 36, "Lock", C_VIOLET, "plain", true, false, "lock")
}

# Format atomic units as LNG with up to 8 decimals, trailing zeros dropped.
fn format_lng(atomic) {
    bind a = round(atomic)
    bind whole = floor(a / 100000000)
    bind frac = a - whole * 100000000
    if frac == 0 { return thousands_plain(whole) }
    bind fs = num_str(frac + 100000000)
    bind fs = substr(fs, 1, 8)
    while str_ends_with(fs, "0") { bind fs = substr(fs, 0, str_len(fs) - 1) }
    return thousands_plain(whole) + "." + fs
}
fn thousands_plain(v) { return num_str(v) }

# Parse "12.5" into atomic units; -1 when invalid.
fn parse_lng(s) {
    if s == "" || s == "." { return 0 - 1 }
    bind parts = str_split(s, ".")
    if len(parts) > 2 { return 0 - 1 }
    bind whole = list_get(parts, 0)
    bind frac = ""
    if len(parts) == 2 { bind frac = list_get(parts, 1) }
    if str_len(frac) > 8 { return 0 - 1 }
    bind w = 0
    if whole != "" { bind w = to_number(whole) }
    bind f = 0
    if frac != "" { bind f = to_number(frac + str_repeat("0", 8 - str_len(frac))) }
    return w * 100000000 + f
}

# Thousands separators for a decimal string or number.
fn thousands(v) {
    bind s = to_str(v)
    bind out = ""
    bind n = str_len(s)
    bind i = 0
    while i < n {
        if i > 0 && (n - i) % 3 == 0 { bind out = out + "," }
        bind out = out + substr(s, i, 1)
        bind i = i + 1
    }
    return out
}

fn ago(ts) {
    bind d = max(0, floor(now_unix() - ts))
    if d < 60 { return "just now" }
    if d < 3600 { return num_str(floor(d / 60)) + "m ago" }
    if d < 86400 { return num_str(floor(d / 3600)) + "h " + num_str(floor(d % 3600 / 60)) + "m ago" }
    return num_str(floor(d / 86400)) + "d ago"
}
fn pad2(n) { return sel(n < 10, "0" + num_str(n), num_str(n)) }
fn duration(secs0) {
    bind secs = floor(secs0)
    if secs < 60 { return num_str(secs) + "s" }
    if secs < 3600 { return num_str(floor(secs / 60)) + "m " + pad2(secs % 60) + "s" }
    if secs < 86400 { return num_str(floor(secs / 3600)) + "h " + pad2(floor(secs % 3600 / 60)) + "m" }
    return num_str(floor(secs / 86400)) + "d " + pad2(floor(secs % 86400 / 3600)) + "h"
}

# True while something on screen moves (keeps the frame rate up).
fn animating() {
    bind st0 = st_get("d_state", "nowallet")
    if st0 != "unlocked" || onboard_in_progress() { return true }
    bind s = section()
    if s == 3 { return true }
    if s == 1 && st_get("send_phase", "compose") != "compose" { return true }
    if s == 1 && st_get("send_lookup_kind", "") == "pending" { return true }
    return false
}

← Back to index