L
ling-wallet
↓ 0 downloadsLing Wallet: a post-quantum privacy-coin wallet, full node and HyperTorus miner for LNG
Keywords wallet, cryptocurrency, lng, node, miner, ling
lingfu install ling-walletCode / 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")
}