灵符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/onboard.ling
# onboard.ling — first run (create / restore), phrase backup and check,
# and the unlock screen.

fn ob_step() { return st_get("ob_step", "choose") }
# The wallet exists but the phrase backup is not confirmed yet.
fn onboard_in_progress() { return ob_step() == "words" || ob_step() == "verify" }

fn ob_go(s) {
    st_set("ob_step", s)
    st_set("ob_err", "")
    focus_next_frame()
}

# A toy walk so the brand panel has the same living torus as the miner.
fn ob_feed_deco(t) {
    bind w = st_get("ob_walk", [19088743, 2309737967, 324508639, 610839776])
    bind x = list_get(w, 0)
    bind y = list_get(w, 1)
    bind q = list_get(w, 3)
    bind trace = []
    bind i = 0
    while i < 24 {
        bind x = (x + 101924959 + i * 2654435761) % 4294967296
        bind y = (y * 1103515 + x) % 4294967296
        bind q = (q * 69069 + y) % 4294967296
        bind trace = list_push(trace, [x, y, 0, q, floor(x / 1073741824)])
        bind i = i + 1
    }
    st_set("ob_walk", [x, y, 0, q])
    tz_feed("deco", trace, t)
}

# Left brand panel; returns the right column as [x, w].
fn ob_layout() {
    bind w = get_width()
    bind h = get_height()
    bind lw = max(420, floor(w * 42 / 100))
    fill(0, 0, lw, h, C_SIDE)
    vline(lw, 0, h, C_LINE_SOFT, 1)
    ob_feed_deco(u_now())
    tz_draw("deco", 24, 150, lw - 48, h - 260, u_now(), true, false)
    bind gw = logo(48, 44, 64, C_TEXT_STRONG)
    t_text_sp(F_HEAD, 20, 48 + gw + 16, 74, "WALLET", C_TEXT_STRONG, 3)
    label(48 + gw + 16, 96, NET_NAME, C_TEXT_DIM)
    label(48, h - 48, "LW-1 v0.1", C_TEXT_FAINT)
    bind rx = lw + 64
    return [rx, min(600, w - rx - 64)]
}

fn onboard_draw() {
    if st_get("d_state", "") == "unlocked" && !onboard_in_progress() && ob_step() != "choose" { st_set("ob_step", "choose") }
    bind col = ob_layout()
    bind x = list_get(col, 0)
    bind w = list_get(col, 1)
    bind s = ob_step()
    if s == "choose" { ob_choose(x, w) }
    if s == "password" { ob_password(x, w) }
    if s == "words" { ob_words(x, w) }
    if s == "verify" { ob_verify(x, w) }
    if s == "restore" { ob_restore(x, w) }
}

fn ob_choose(x, w) {
    bind y = heading(x, floor(get_height() / 2) - 200, "Welcome", "Set up this computer's wallet.") + 24
    bind k = 0
    while k < 2 {
        bind accent = sel(k == 0, C_GREEN, C_BLUE)
        bind word = sel(k == 0, "CREATE", "RESTORE")
        bind title = sel(k == 0, "New wallet", "From a recovery phrase")
        bind body = sel(k == 0, "Generate fresh keys and a 24-word recovery phrase.", "Rebuild an existing wallet from its 24 words.")
        bind hot = u_hover(x, y, w, 116)
        if hot {
            fill(x, y, w, 116, C_ROW_HOVER)
            hatch(x + w - 120, y + 1, 119, 114, accent, true, 3, 0.18)
        } else {
            fill(x, y, w, 116, C_PANEL)
        }
        outline(x, y, w, 116, sel(hot, accent, C_LINE_SOFT), 1)
        brackets(x - 1, y - 1, w + 2, 118, 10, accent, 1)
        tile_word(x + 24, y + 24, 20, word, accent, hot)
        t_text(F_UI, 20, x + 24, y + 74, title, C_TEXT_STRONG)
        t_text(F_UI, 16, x + 24, y + 96, body, C_TEXT_DIM)
        bind ax = x + w - 40
        bind ay = y + 58
        bind cc = sel(hot, accent, C_TEXT_DIM)
        line(ax, ay - 10, ax + 10, ay, cc, 1)
        line(ax + 10, ay, ax, ay + 10, cc, 1)
        if hot && u_pressed() { ob_go(sel(k == 0, "password", "restore")) }
        bind y = y + 140
        bind k = k + 1
    }
    t_text(F_UI, 15, x, y + 8, "Keys never leave this computer.", C_TEXT_FAINT)
}

fn ob_password(x, w) {
    bind y = floor(get_height() / 2) - 250
    steps(x, y, ["Password", "Phrase", "Confirm"], 0, C_GREEN)
    bind y = heading(x, y + 56, "Create a wallet", "Choose a password to encrypt the wallet file.") + 40
    bind r1 = labeled("ob_pw", x, y, w, 44, "Password", "at least 10 characters", C_GREEN, "password")
    bind y = y + 56
    strength_meter(x, y, w, 8, tf_get("ob_pw"))
    bind y = y + 48
    bind r2 = labeled("ob_pw2", x, y, w, 44, "Confirm password", "type it again", C_GREEN, "password")
    bind y = y + 68
    bind pw = tf_get("ob_pw")
    bind mismatch = tf_get("ob_pw2") != "" && tf_get("ob_pw2") != pw
    if mismatch {
        t_text(F_UI, 16, x, y + 4, "Passwords don't match.", C_RED)
    } else if st_get("ob_err", "") != "" {
        t_text(F_UI, 16, x, y + 4, st_get("ob_err", ""), C_RED)
    }
    bind y = y + 20
    bind y = y + callout(x, y, w, C_BLUE, "info", "The password protects this computer's copy only", "It can't recover funds. The 24-word phrase on the next step can, so keep that safe instead.") + 24
    bind bw = floor((w - 16) / 2)
    if button(x, y, bw, 44, "Back", C_TEXT_DIM, "plain", true, false, "back") || u_key("escape") {
        ob_go("choose")
        return ()
    }
    bind good = pw_strength(pw) >= 1 && pw == tf_get("ob_pw2")
    bind submit = list_get(r1, 1) || list_get(r2, 1)
    if button(x + bw + 16, y, bw, 44, "Create wallet", C_GREEN, "primary", good, true, "") || (submit && good) {
        bind r = be_create(pw)
        match r {
            ok(words) => ob_created(words),
            bad(e) => st_set("ob_err", e),
        }
    }
}

fn ob_created(words) {
    st_set("ob_words", words)
    tf_clear("ob_pw")
    tf_clear("ob_pw2")
    bind picks = []
    bind seed = floor(time_now() * 1000003) % 2147483647
    while len(picks) < 3 {
        bind seed = (seed * 48271 + 11) % 2147483647
        bind k = seed % 24
        bind dup = false
        bind i = 0
        while i < len(picks) {
            if list_get(picks, i) == k { bind dup = true }
            bind i = i + 1
        }
        if !dup { bind picks = list_push(picks, k) }
    }
    st_set("ob_ask", list_sort3(picks))
    ob_go("words")
}
fn list_sort3(l) {
    bind a = list_get(l, 0)
    bind b = list_get(l, 1)
    bind c = list_get(l, 2)
    bind lo = min(a, min(b, c))
    bind hi = max(a, max(b, c))
    return [lo, a + b + c - lo - hi, hi]
}

# Numbered 4x6 grid of words.
fn word_grid(x, y, w, words, accent) {
    bind gap = 8
    bind cw = floor((w - 3 * gap) / 4)
    bind i = 0
    while i < min(24, len(words)) {
        bind cx = x + (i % 4) * (cw + gap)
        bind cy = y + floor(i / 4) * 48
        fill(cx, cy, cw, 40, C_PANEL)
        outline(cx, cy, cw, 40, C_LINE_SOFT, 1)
        fill(cx, cy, 30, 40, C_BG_DEEP)
        vline(cx + 30, cy, 40, C_LINE_SOFT, 1)
        t_center(F_MONO, 18, cx, cy, 30, 40, pad2(i + 1), shade(accent, 0.2))
        t_mid(F_UI, sel(cw < 150, 17, 19), cx + 40, cy + 20, list_get(words, i), C_TEXT_STRONG)
        bind i = i + 1
    }
}

fn ob_words(x, w) {
    bind y = max(32, floor(get_height() / 2) - 320)
    steps(x, y, ["Password", "Phrase", "Confirm"], 1, C_GREEN)
    bind y = heading(x, y + 56, "Your recovery phrase", "Write these 24 words on paper, in this order.") + 16
    word_grid(x, y, w, st_get("ob_words", []), C_GREEN)
    bind y = y + 6 * 48 + 16
    bind y = y + callout(x, y, w, C_ORANGE, "warn", "Anyone with these words can spend your LNG", "Never type them into a website or share a photo of them. Ling Wallet will never ask for them online.") + 24
    if button(x, y, w, 44, "I've written this down", C_GREEN, "primary", true, true, "") {
        tf_clear("ob_a0")
        tf_clear("ob_a1")
        tf_clear("ob_a2")
        ob_go("verify")
    }
}

fn ob_verify(x, w) {
    bind y = floor(get_height() / 2) - 230
    steps(x, y, ["Password", "Phrase", "Confirm"], 2, C_GREEN)
    bind y = heading(x, y + 56, "Check your backup", "Type these words from your paper copy.") + 40
    bind ask = st_get("ob_ask", [2, 11, 19])
    bind words = st_get("ob_words", [])
    bind submit = false
    bind i = 0
    while i < 3 {
        bind k = list_get(ask, i)
        bind id = "ob_a" + num_str(i)
        bind good = tf_get(id) != "" && str_lower(tf_get(id)) == list_get(words, k)
        bind r = labeled(id, x, y, w, 44, "Word #" + num_str(k + 1), "", C_GREEN, "word")
        if good { icon_draw("check", x + w - 32, y + 22, C_GREEN) }
        if list_get(r, 1) { bind submit = true }
        if list_get(r, 2) && i < 2 { focus_set("ob_a" + num_str(i + 1)) }
        bind y = y + 84
        bind i = i + 1
    }
    if st_get("ob_err", "") != "" { t_text(F_UI, 16, x, y - 16, st_get("ob_err", ""), C_RED) }
    bind y = y + 8
    bind bw = floor((w - 16) / 2)
    if button(x, y, bw, 44, "Show words again", C_TEXT_DIM, "plain", true, false, "back") {
        ob_go("words")
        return ()
    }
    if button(x + bw + 16, y, bw, 44, "Confirm", C_GREEN, "primary", true, true, "") || submit {
        bind bad_i = 0 - 1
        bind j = 2
        while j >= 0 {
            if str_lower(tf_get("ob_a" + num_str(j))) != list_get(words, list_get(ask, j)) { bind bad_i = j }
            bind j = j - 1
        }
        if bad_i >= 0 {
            st_set("ob_err", "Word #" + num_str(list_get(ask, bad_i) + 1) + " doesn't match. Check your paper copy.")
        } else {
            st_set("ob_words", [])
            tf_clear("ob_a0")
            tf_clear("ob_a1")
            tf_clear("ob_a2")
            ob_go("choose")
            toast("Backup confirmed; wallet ready")
        }
    }
}

fn ob_restore(x, w) {
    bind y = heading(x, max(32, floor(get_height() / 2) - 330), "Restore a wallet", "Enter your 24 words. Paste the whole phrase into any box.") + 16
    bind gap = 8
    bind cw = floor((w - 3 * gap) / 4)
    bind paste_at = 0 - 1
    bind paste = ""
    bind submit = false
    bind filled = 0
    bind i = 0
    while i < 24 {
        bind cx = x + (i % 4) * (cw + gap)
        bind cy = y + floor(i / 4) * 48
        fill(cx, cy, 30, 40, C_PANEL)
        outline(cx, cy, 30, 40, C_LINE_FAINT, 1)
        t_center(F_MONO, 18, cx, cy, 30, 40, pad2(i + 1), C_TEXT_FAINT)
        bind r = field("rs" + num_str(i), cx + 30, cy, cw - 30, 40, "", C_BLUE, "word", 0)
        if list_get(r, 3) != "" && len(str_split(list_get(r, 3), " ")) > 1 {
            bind paste_at = i
            bind paste = list_get(r, 3)
        }
        if list_get(r, 2) && i < 23 { focus_set("rs" + num_str(i + 1)) }
        if list_get(r, 1) { bind submit = true }
        if tf_get("rs" + num_str(i)) != "" { bind filled = filled + 1 }
        bind i = i + 1
    }
    if paste_at >= 0 {
        bind ws = str_split(paste, " ")
        bind k = 0
        bind slot = paste_at
        while k < len(ws) && slot < 24 {
            bind wd = str_lower(list_get(ws, k))
            if wd != "" {
                tf_set("rs" + num_str(slot), tf_filter("word", wd))
                bind slot = slot + 1
            }
            bind k = k + 1
        }
    }
    bind y = y + 6 * 48 + 32
    bind half = floor((w - 16) / 2)
    bind a = labeled("rs_pw", x, y, half, 44, "New password", "10+ characters", C_BLUE, "password")
    bind b = labeled("rs_pw2", x + half + 16, y, half, 44, "Confirm", "", C_BLUE, "password")
    if list_get(a, 1) || list_get(b, 1) { bind submit = true }
    bind y = y + 56
    strength_meter(x, y, half, 8, tf_get("rs_pw"))
    bind y = y + 24
    bind mismatch = tf_get("rs_pw2") != "" && tf_get("rs_pw2") != tf_get("rs_pw")
    if st_get("ob_err", "") != "" {
        t_text(F_UI, 16, x, y + 12, st_get("ob_err", ""), C_RED)
    } else if mismatch {
        t_text(F_UI, 16, x, y + 12, "Passwords don't match.", C_RED)
    } else {
        t_text(F_UI, 16, x, y + 12, num_str(filled) + " / 24 words entered", sel(filled == 24, C_GREEN, C_TEXT_DIM))
    }
    bind y = y + 32
    bind bw = floor((w - 16) / 2)
    if button(x, y, bw, 44, "Back", C_TEXT_DIM, "plain", true, false, "back") || u_key("escape") {
        ob_go("choose")
        return ()
    }
    bind good = filled == 24 && tf_get("rs_pw") != "" && tf_get("rs_pw") == tf_get("rs_pw2")
    if button(x + bw + 16, y, bw, 44, "Restore wallet", C_BLUE, "primary", good, true, "") || (submit && good) {
        bind phrase = ""
        bind j = 0
        while j < 24 {
            bind phrase = phrase + sel(j == 0, "", " ") + tf_get("rs" + num_str(j))
            bind j = j + 1
        }
        bind r = be_restore(phrase, tf_get("rs_pw"))
        match r {
            ok(v) => ob_restored(),
            bad(e) => st_set("ob_err", e),
        }
    }
}

fn ob_restored() {
    bind j = 0
    while j < 24 {
        tf_clear("rs" + num_str(j))
        bind j = j + 1
    }
    tf_clear("rs_pw")
    tf_clear("rs_pw2")
    ob_go("choose")
    toast("Wallet restored; rescanning the chain")
}

fn draw_big_lock(x, y, s, col) {
    bind cx = x + s / 2
    bind cy = y + s / 2
    fill(cx - 14, cy - 4, 28, 20, C_BG)
    outline(cx - 14, cy - 4, 28, 20, col, 1)
    outline(cx - 12, cy - 2, 24, 16, shade(col, 0.4), 1)
    fill(cx - 1, cy + 2, 3, 7, lift(col, 0.3))
    vline(cx - 9, cy - 14, 10, col, 1)
    vline(cx + 9, cy - 14, 10, col, 1)
    hline(cx - 6, cy - 18, 13, col, 1)
    line(cx - 9, cy - 14, cx - 6, cy - 18, col, 1)
    line(cx + 9, cy - 14, cx + 6, cy - 18, col, 1)
}

fn unlock_draw() {
    bind col = ob_layout()
    bind x = list_get(col, 0)
    bind w = list_get(col, 1)
    bind y = floor(get_height() / 2) - 170
    fill(x, y, 64, 64, C_PANEL)
    outline(x, y, 64, 64, C_LINE_SOFT, 1)
    brackets(x, y, 64, 64, 14, C_VIOLET, 1)
    cross(x + 4, y + 4, 56, 56, C_VIOLET, 3, 0.25)
    draw_big_lock(x, y, 64, C_VIOLET)
    bind y = heading(x, y + 88, "Wallet locked", "Enter your password to unlock.") + 40
    bind r = labeled("unlock_pw", x, y, w, 44, "Password", "", C_VIOLET, "password")
    if list_get(r, 0) { st_set("unlock_err", "") }
    bind y = y + 60
    if st_get("unlock_err", "") != "" {
        icon_draw("warn", x, y + 10, C_RED)
        t_text(F_UI, 16, x + 26, y + 16, st_get("unlock_err", ""), C_RED)
    }
    bind y = y + 40
    if button(x, y, w, 44, "Unlock", C_VIOLET, "primary", true, true, "unlock") || list_get(r, 1) {
        bind res = be_unlock(tf_get("unlock_pw"))
        match res {
            ok(v) => unlock_ok(),
            bad(e) => unlock_bad(e),
        }
    }
    t_text(F_UI, 15, x, y + 76, "Keys stay encrypted on disk until you unlock.", C_TEXT_FAINT)
}
fn unlock_ok() {
    tf_clear("unlock_pw")
    st_set("unlock_err", "")
    st_set("d_last", 0 - 10)
    st_set("activity_t", u_now())
}
fn unlock_bad(e) {
    st_set("unlock_err", e)
    st_set("tf_all", "unlock_pw")
}

← Back to index