add bip39 primitives to the GAM, including an interactive entry dialog

This commit is contained in:
bunnie 2022-07-06 04:52:14 +08:00
parent 5cc51ebdaf
commit 4087312a2c
9 changed files with 2900 additions and 0 deletions

View file

@ -34,6 +34,11 @@ tts-frontend = {path="../tts"}
qrcode = { version = "0.12", default-features = false }
# used in bip39 dialogue
hex = { version = "0.4.3", default-features = false, features = ["alloc"] }
sha2 = {path="../engine-sha512"}
digest = "0.9.0"
[target.'cfg(not(any(windows,unix)))'.dependencies]
utralib = {path = "../../utralib"}

View file

@ -41,6 +41,48 @@
"zh": "",
"en-tts": "Checked "
},
"bip39.invalid_word": {
"en": "Invalid word!",
"ja": "無効な単語",
"zh": "无效词",
"en-tts": "Invalid word!"
},
"bip39.waiting": {
"en": "Waiting for input...",
"ja": "入力を待っています...",
"zh": "等待输入...",
"en-tts": "Waiting for input..."
},
"bip39.enter_to_complete": {
"en": "Press enter to accept current phrase, or keep typing...",
"ja": "Enterキーを押して現在のフレーズを受け入れるか、入力を続けます...",
"zh": "按 Enter 接受当前短语,或继续输入...",
"en-tts": "Press enter to accept current phrase, or keep typing..."
},
"bip39.start_typing": {
"en": "Start typing...",
"ja": "入力を開始します...",
"zh": "开始打字……",
"en-tts": "Start typing..."
},
"bip39.invalid_phrase": {
"en": "Phrase is not valid",
"ja": "フレーズが無効です",
"zh": "短语无效",
"en-tts": "Phrase is not valid"
},
"bip39.valid_phrase": {
"en": "Valid input:",
"ja": "有効な入力:",
"zh": "有效输入:",
"en-tts": "Valid input:"
},
"bip39.invalid_bytes": {
"en": "Invalid BIP39 data length",
"ja": "無効なBIP39データ長",
"zh": "BIP39 数据长度无效",
"en-tts": "Invalid BIP39 data length"
},
"progress.increment": {
"en": "",
"ja": "",

View file

@ -200,6 +200,11 @@ pub(crate) enum Opcode {
SetDebugLevel = 28,
Quit = 29,
/// Bip39 operations -- the GAM has the word list, so to avoid duplicating code it offers a conversion service.
Bip39toBytes = 30,
BytestoBip39 = 31,
Bip39Suggestions = 32,
}
#[derive(Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
@ -292,3 +297,10 @@ pub struct GidRecord {
pub gid: Gid,
pub canvas_type: CanvasType,
}
#[derive(Debug, Copy, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize, Default)]
pub struct Bip39Ipc {
pub data: [u8; 32],
pub data_len: u32,
pub words: [Option<xous_ipc::String::<8>>; 24],
}

276
services/gam/src/bip39.rs Normal file
View file

@ -0,0 +1,276 @@
/// Eventually more language support can be added here:
///
/// In order to integrate this well, we need to re-do the language build
/// system to be based off of af #cfg feature, so that we can pick up
/// the feature in this crate and select the right word list.
///
/// We don't compile all the word lists in because code size is precious.
///
/// Each language should simply create its table assigning to be symbol
/// `const BIP39_TABLE: [&'static str; 2048]`. This allows the rest of
/// the code to refer to the table without change, all we do is swap out
/// which language module is included in the two lines below.
pub mod en;
pub use en::*;
use sha2::*;
use digest::Digest;
#[derive(Debug, Eq, PartialEq)]
pub enum Bip39Error {
InvalidLength,
InvalidChecksum,
InvalidWordAt(usize),
}
/// This routine takes an array of bytes and attempts to return an array of Bip39
/// words. If the bytes do not conform to a valid length, we return `Bip39Error::InvalidLength`.
/// A `Vec::<String>` is returned in case the caller wants to do stupid formatting tricks
/// on the words (saving them effort of parsing a single concatenated String).
pub(crate) fn bytes_to_bip39(bytes: &Vec::<u8>) -> Result<Vec::<String>, Bip39Error> {
let mut result = Vec::<String>::new();
match bytes.len() {
16 | 20 | 24 | 28 | 32 => (),
_ => return Err(Bip39Error::InvalidLength)
}
let mut hasher = sha2::Sha256::new();
hasher.update(bytes);
let digest = hasher.finalize();
let checksum_bits = bytes.len() / 4;
let checksum = digest.as_slice()[0] >> (8 - checksum_bits);
let mut bits_in_bucket = 0;
let mut bucket = 0u32;
for &b in bytes {
bucket <<= 8;
bucket |= b as u32;
bits_in_bucket += 8;
if bits_in_bucket >= 11 {
let codeword = bucket >> (bits_in_bucket - 11);
bucket &= !((0b111_1111_1111u32) << (bits_in_bucket - 11));
bits_in_bucket -= 11;
result.push(BIP39_TABLE[codeword as usize].to_string());
}
}
assert!(bits_in_bucket + checksum_bits == 11);
bucket <<= checksum_bits;
bucket |= checksum as u32;
assert!(bucket < 2048);
result.push(BIP39_TABLE[bucket as usize].to_string());
Ok(result)
}
/// The caller must provide a list of words parsed into individual Bip39 words.
/// The words are case-insensitive. However, if any word is invalid, the routine
/// will return `InvalidWordAt(index of invalid word)` at the first invalid word
/// detected (it may not be the only invalid word).
pub(crate) fn bip39_to_bytes(bip39: &Vec::<String>) -> Result<Vec::<u8>, Bip39Error> {
// this implementation favors small runtime memory allocation over performance
// sifting through a list of 2048 words is reasonably fast, even if doing up to 24 times;
// this is especially in comparison to the screen redraw times. We could also create
// a HashSet or something or a tree to do the search faster but in this system, we fight
// for even 4kiB of RAM savings at times.
// The inefficiency is especially small in comparison to the ridiculous SHA256 computation
// that has to happen to checksum the result.
match bip39.len() {
12 | 15 | 18 | 21 | 24 => (),
_ => return Err(Bip39Error::InvalidLength)
}
let mut indices = Vec::<u32>::new();
for (index, bip) in bip39.iter().enumerate() {
if let Some(i) = BIP39_TABLE.iter().position(|&x| x == bip) {
indices.push(i as u32);
} else {
return Err(Bip39Error::InvalidWordAt(index));
}
}
// collate into u8 vec
let mut data = Vec::<u8>::new();
let mut bucket = 0u32;
let mut bits_in_bucket = 0;
for index in indices {
// add bits to bucket
bucket = (bucket << 11) | index;
bits_in_bucket += 11;
while bits_in_bucket >= 8 {
// extract the top 8 bits from the bucket, put it into the result vector
data.push(
(bucket >> (bits_in_bucket - 8)) as u8
);
// mask off the "used up" bits
bucket &= !(0b1111_1111 << bits_in_bucket - 8);
// subtract the used bits out of the bucket
bits_in_bucket -= 8;
}
}
// the bucket should now just contain the checksum
let entered_checksum = if bits_in_bucket == 0 {
// edge case of exactly enough checksum bits to fill a byte (happens in 256-bit case)
data.pop().unwrap()
} else {
bucket as u8
};
let mut hasher = sha2::Sha256::new();
hasher.update(&data);
let digest = hasher.finalize();
let checksum_bits = data.len() / 4;
let checksum = digest.as_slice()[0] >> (8 - checksum_bits);
if checksum == entered_checksum {
Ok(data)
} else {
log::warn!("checksum didn't match: {:x} vs {:x}", checksum, entered_checksum);
Err(Bip39Error::InvalidChecksum)
}
}
pub(crate) const BIP39_SUGGEST_LIMIT: usize = 5;
/// This turns a string into a list of suggestions. If the String is empty, the
/// suggestion list is empty. The suggestion list is limited to BIP39_SUGGEST_LIMIT hints.
pub(crate) fn suggest_bip39(start: &str) -> Vec::<String> {
let mut ret = Vec::<String>::new();
// first see if any prefixes match; stop when we find enough
for bip in BIP39_TABLE {
if bip.starts_with(start) {
ret.push(bip.to_string());
if ret.len() >= BIP39_SUGGEST_LIMIT {
break;
}
}
}
if ret.len() > 0 {
return ret;
}
// no prefixes match, suggest substrings
for bip in BIP39_TABLE {
if bip.contains(start) {
ret.push(bip.to_string());
if ret.len() >= BIP39_SUGGEST_LIMIT {
break;
}
}
}
ret
}
/// This routine returns `true` if the given word is a valid BIP39 word.
#[allow(dead_code)]
pub(crate) fn is_valid_bip39(word: &str) -> bool {
let lword = word.to_ascii_lowercase();
for w in BIP39_TABLE {
if lword == w {
return true
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
/// PAGE_SIZE is required to be a power of two. 0x1000 -> 0x1000 - 1 = 0xFFF, which forms the bitmasks.
fn test_11_to_8() {
let indices = [
0b00000110001,
0b10110011110,
0b01110010100,
0b00110110010,
0b10001011010,
0b11100111111,
0b01101010011,
0b10000011000,
0b01101011001,
0b10110011111,
0b10001001110,
0b00111100110
];
let refnum = 0b00000110001101100111100111001010000110110010100010110101110011111101101010011100000110000110101100110110011111100010011100011110u128;
// 00000110001 10110011110 01110010100 00110110010 10001011010 11100111111 01101010011 10000011000 01101011001 10110011111 10001001110 00111100110
// 0000_0110|001/1_0110|0111_10/01|1100_1010_0/001_10110010 10001011010 11100111111 01101010011 10000011000 01101011001 10110011111 10001001110 00111100110
// 0000_0110 0011_0110 0111_1001 1100_1010 ... 0001101100101000101101011100111111011010100111000001100001101011001101100111111000100111000111100110
let mut refvec = refnum.to_be_bytes().to_vec();
refvec.push(6); // checksum
let mut data = Vec::<u8>::new();
let mut bucket = 0u32;
let mut bits_in_bucket = 0;
for index in indices {
// add bits to bucket
bucket = (bucket << 11) | index;
bits_in_bucket += 11;
while bits_in_bucket >= 8 {
// extract the top 8 bits from the bucket, put it into the result vector
data.push(
(bucket >> (bits_in_bucket - 8)) as u8
);
// mask off the "used up" bits
bucket &= !(0b1111_1111 << bits_in_bucket - 8);
// subtract the used bits out of the bucket
bits_in_bucket -= 8;
}
}
if bits_in_bucket != 0 {
data.push(bucket as u8);
}
assert!(data.len() == refvec.len());
for (index, (&a, &b)) in refvec.iter().zip(data.iter()).enumerate() {
if a != b {
println!("index {} error: a[{}{:x})] != b[{}({:x})]", index, a, a, b, b);
} else {
println!("index {} match: a[{}({:x})] == b[{}({:x})]", index, a, a, b, b);
}
assert!(a==b);
}
}
#[test]
fn test_bip39_to_bytes() {
let phrase = vec![
"alert".to_string(), "record".to_string(), "income".to_string(), "curve".to_string(),
"mercy".to_string(), "tree".to_string(), "heavy".to_string(), "loan".to_string(),
"hen".to_string(), "recycle".to_string(), "mean".to_string(), "devote".to_string()
];
let refnum = 0b00000110001101100111100111001010000110110010100010110101110011111101101010011100000110000110101100110110011111100010011100011110u128;
let mut refvec = refnum.to_be_bytes().to_vec();
// refvec.push(6); // checksum
assert_eq!(Ok(refvec), bip39_to_bytes(&phrase));
}
#[test]
fn test_is_valid_bip39() {
assert_eq!(is_valid_bip39("alert"), true);
assert_eq!(is_valid_bip39("rEcOrD"), true);
assert_eq!(is_valid_bip39("foobar"), false);
assert_eq!(is_valid_bip39(""), false);
}
#[test]
fn test_suggest_prefix() {
let suggestions = suggest_bip39("ag");
let reference = vec![
"again".to_string(),
"age".to_string(),
"agent".to_string(),
"agree".to_string(),
];
assert_eq!(suggestions, reference);
}
#[test]
fn test_bytes_to_bip39() {
let refnum = 0b00000110001101100111100111001010000110110010100010110101110011111101101010011100000110000110101100110110011111100010011100011110u128;
let refvec = refnum.to_be_bytes().to_vec();
let phrase = vec![
"alert".to_string(), "record".to_string(), "income".to_string(), "curve".to_string(),
"mercy".to_string(), "tree".to_string(), "heavy".to_string(), "loan".to_string(),
"hen".to_string(), "recycle".to_string(), "mean".to_string(), "devote".to_string()
];
assert_eq!(bytes_to_bip39(&refvec), Ok(phrase));
}
}

2050
services/gam/src/bip39/en.rs Normal file

File diff suppressed because it is too large Load diff

View file

@ -444,6 +444,66 @@ impl Gam {
)
.expect("couldn't set debug level");
}
pub fn bytes_to_bip39(&self, bytes: &Vec::<u8>) -> Result<Vec::<std::string::String>, xous::Error> {
match bytes.len() {
16 | 20 | 24 | 28 | 32 => (),
_ => return Err(xous::Error::InvalidString)
}
let mut ipc = Bip39Ipc::default();
ipc.data[..bytes.len()].copy_from_slice(&bytes);
ipc.data_len = bytes.len() as u32;
let mut buf = Buffer::into_buf(ipc).or(Err(xous::Error::InternalError))?;
buf.lend_mut(self.conn, Opcode::BytestoBip39.to_u32().unwrap()).or(Err(xous::Error::InternalError)).expect("couldn't send RaiseMenu opcode");
let result = buf.to_original::<Bip39Ipc, _>().unwrap();
let mut ret = Vec::<std::string::String>::new();
for word in result.words {
if let Some(w) = word {
ret.push(w.as_str().unwrap().to_string());
}
}
if ret.len() == 0 {
Err(xous::Error::InvalidString)
} else {
Ok(ret)
}
}
pub fn bip39_to_bytes(&self, bip39: &Vec::<std::string::String>) -> Result<Vec::<u8>, xous::Error> {
match bip39.len() {
12 | 15 | 18 | 21 | 24 => (),
_ => return Err(xous::Error::InvalidString)
}
let mut ipc = Bip39Ipc::default();
for (word, slot) in bip39.iter().zip(ipc.words.iter_mut()) {
*slot = Some(xous_ipc::String::from_str(word))
}
let mut buf = Buffer::into_buf(ipc).or(Err(xous::Error::InternalError))?;
buf.lend_mut(self.conn, Opcode::Bip39toBytes.to_u32().unwrap()).or(Err(xous::Error::InternalError)).expect("couldn't send RaiseMenu opcode");
let result = buf.to_original::<Bip39Ipc, _>().unwrap();
if result.data_len == 0 {
Err(xous::Error::InvalidString)
} else {
Ok(
result.data[..result.data_len as usize].to_vec()
)
}
}
pub fn bip39_suggestions(&self, start: &str) -> Result<Vec::<std::string::String>, xous::Error> {
let mut ipc = Bip39Ipc::default();
// we abuse this struct a bit by shoving the lookup phrase into a u8-array...
let checked_start = start.as_bytes();
ipc.data[..checked_start.len().min(8)].copy_from_slice(&checked_start[..checked_start.len().min(8)]);
ipc.data_len = checked_start.len().min(8) as u32;
let mut buf = Buffer::into_buf(ipc).or(Err(xous::Error::InternalError))?;
buf.lend_mut(self.conn, Opcode::Bip39Suggestions.to_u32().unwrap()).or(Err(xous::Error::InternalError)).expect("couldn't send RaiseMenu opcode");
let result = buf.to_original::<Bip39Ipc, _>().unwrap();
let mut suggestions = Vec::<std::string::String>::new();
for word in result.words {
if let Some(w) = word {
suggestions.push(w.as_str().unwrap().to_string())
}
}
Ok(suggestions)
}
}
use core::sync::atomic::{AtomicU32, Ordering};

View file

@ -12,6 +12,7 @@ mod layouts;
use layouts::*;
mod contexts;
use contexts::*;
mod bip39;
use graphics_server::*;
use xous_ipc::{Buffer, String};
@ -614,6 +615,55 @@ fn wrapped_main() -> ! {
}
xous::return_scalar(msg.sender, 1).expect("couldn't ack self test");
}),
Some(Opcode::Bip39toBytes) => {
let mut buffer = unsafe { Buffer::from_memory_message_mut(msg.body.memory_message_mut().unwrap()) };
let mut spec = buffer.to_original::<Bip39Ipc, _>().unwrap();
let mut phrase = Vec::<std::string::String>::new();
for maybe_word in spec.words {
if let Some(word) = maybe_word {
phrase.push(word.as_str().unwrap().to_string());
}
}
match bip39::bip39_to_bytes(&phrase) {
Ok(data) => {
spec.data_len = data.len() as u32;
spec.data[..data.len()].copy_from_slice(&data);
}
Err(_) => {
// zero-length data indicates an error
spec.data_len = 0;
}
}
buffer.replace(spec).unwrap();
}
Some(Opcode::BytestoBip39) => {
let mut buffer = unsafe { Buffer::from_memory_message_mut(msg.body.memory_message_mut().unwrap()) };
let mut spec = buffer.to_original::<Bip39Ipc, _>().unwrap();
let data = spec.data[..spec.data_len as usize].to_vec();
match bip39::bytes_to_bip39(&data) {
Ok(phrase) => {
for (word, returned) in phrase.iter().zip(spec.words.iter_mut()) {
*returned = Some(xous_ipc::String::from_str(word));
}
}
Err(_) => {
for returned in spec.words.iter_mut() {
*returned = None;
}
}
}
buffer.replace(spec).unwrap();
}
Some(Opcode::Bip39Suggestions) => {
let mut buffer = unsafe { Buffer::from_memory_message_mut(msg.body.memory_message_mut().unwrap()) };
let mut spec = buffer.to_original::<Bip39Ipc, _>().unwrap();
let start = std::str::from_utf8(&spec.data[..spec.data_len as usize]).unwrap_or("");
let suggestions = bip39::suggest_bip39(start);
for (word, returned) in suggestions.iter().zip(spec.words.iter_mut()) {
*returned = Some(xous_ipc::String::from_str(word));
}
buffer.replace(spec).unwrap();
}
Some(Opcode::Quit) => break,
None => {log::error!("unhandled message {:?}", msg);}
}

View file

@ -18,6 +18,8 @@ pub use consoleinput::*;
mod image;
#[cfg(feature="ditherpunk")]
pub use image::*;
mod bip39entry;
pub use bip39entry::*;
use enum_dispatch::enum_dispatch;
@ -37,6 +39,7 @@ pub const MAX_ITEMS: usize = 8;
#[enum_dispatch(ActionApi)]
pub enum ActionType {
TextEntry,
Bip39Entry,
RadioButtons,
CheckBoxes,
Slider,
@ -76,6 +79,14 @@ impl ItemName {
}
}
#[derive(Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize, Copy, Clone, Eq, PartialEq, Default)]
pub struct Bip39EntryPayload {
// up to 32 bytes (256 bits) could be entered
pub data: [u8; 32],
// the actual length entered is reported here
pub len: u32,
}
#[derive(Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize, Copy, Clone, Eq, PartialEq, Default)]
pub struct TextEntryPayload {
dirty: bool,

View file

@ -0,0 +1,394 @@
use crate::*;
use graphics_server::api::*;
use locales::t;
use glyphstyle::GlyphStyle;
use xous_ipc::Buffer;
use std::string::String;
use core::fmt::Write;
use core::cell::Cell;
pub struct Bip39Entry {
pub is_password: bool,
pub action_conn: xous::CID,
pub action_opcode: u32,
pub accepted_words: Vec::<String>,
pub user_input: String,
pub payload: Option<Vec::<u8>>,
pub suggested_words: Vec::<String>,
suggestion_index: Cell::<i16>,
line_height: Cell::<i16>,
margin: Cell::<i16>,
gam: crate::Gam,
}
impl Default for Bip39Entry {
fn default() -> Self {
// this is slightly horriffic but we keep the bip39 phrase book inside the GAM and not
// in this client-side library to avoid having 24kiB of phrases being compiled into every
// process that potentially handles graphics. So, this modal needs a dedicated GAM connection
// to handle bip39 processing on key entry. :-/
let xns = xous_names::XousNames::new().unwrap();
let gam = crate::Gam::new(&xns).unwrap();
Self {
is_password: Default::default(),
action_conn: Default::default(),
action_opcode: Default::default(),
accepted_words: Vec::new(),
user_input: String::new(),
payload: None,
suggested_words: Vec::new(),
suggestion_index: Cell::new(0),
line_height: Cell::new(0),
margin: Cell::new(0),
gam,
}
}
}
impl Bip39Entry {
pub fn new(
is_password: bool,
action_conn: xous::CID,
action_opcode: u32,
) -> Self {
Self {
is_password,
action_conn,
action_opcode,
..Default::default()
}
}
}
const NUM_RECCOS: i16 = 5;
const AESTHETIC_GAP: i16 = 5;
const STYLE_OVERRIDE: GlyphStyle = GlyphStyle::Bold;
const ACCEPTED_WORD_LINES: i16 = 4;
const STATUS_LINES: i16 = 3;
impl ActionApi for Bip39Entry {
fn set_action_opcode(&mut self, op: u32) {self.action_opcode = op}
fn is_password(&self) -> bool {
self.is_password
}
/// The total canvas height is computed with this API call
/// The canvas height is not dynamically adjustable for modals.
fn height(&self, glyph_height: i16, margin: i16) -> i16 {
/*
cat <-- active entry 1 line + 2x margin
-------------------
* cat <-- suggestion list (up to 5 lines, blank space if no suggestions)
catalog "INVALID WORD" if active entry line has no matches
catch "Start typing..." if line is blank and BIP39 invalid
category "Presse enter to accept or keep typing..." if line is blank and BIP39 is valid
-------------------
Invalid phrase <-- whether or not the words are valid (1 line)
0x1234 <-- binary data preview (if word is valid, blank if not valid) (1 line)
-------------------
eager dish museum <-- words accepted so far (6 lines reserved)
pole aisle ...
up/down arrow: selects which suggestion. auto-inserts suggested text into active entry line
enter/center button: selects the suggestion. can only select valid suggestions
backspace while text is present: remove one letter
backspace while no text is present: remove latest word in bip39 list
24 word example:
oven color flag rich custom
crawl century oak decad
dilemma evolve company
original arctic cat clever
truth air chuckle radar
polar silly soda idle
*/
// the glyph_height is an opaque value because the final value depends upon a lookup table
// stored in the graphics_server crate. To obtain this would require a handle to that server
// which is private to the GAM. Thus we receive a copy of this from our caller and stash it
// here for future reference. `glyph_height` can change depending upon the locale; in particular,
// CJK languages have taller glyphs.
self.line_height.set(glyph_to_height_hint(STYLE_OVERRIDE) as i16 + 2);
self.margin.set(6);
// compute the overall_height of the entry fields
let overall_height =
glyph_height // active entry
+ 2*margin // divider line
+ glyph_height * NUM_RECCOS // recco list
+ 2*margin // divider line
+ glyph_height * STATUS_LINES // status line
+ 2*margin // divider line
+ glyph_height * ACCEPTED_WORD_LINES // words accepted so far
+ margin * 2 // top/bottom spacing
;
overall_height
}
fn redraw(&self, at_height: i16, modal: &Modal) {
let color = if self.is_password {
PixelColor::Light
} else {
PixelColor::Dark
};
let mut current_height = at_height;
let bullet_margin = 17;
// ------ draw the current text entry -------
let mut tv = TextView::new(
modal.canvas,
TextBounds::BoundingBox(Rectangle::new(
Point::new(self.margin.get(), current_height),
Point::new(modal.canvas_width - self.margin.get(), current_height + self.line_height.get() + self.margin.get()))
));
tv.text.clear();
tv.bounds_computed = None;
tv.draw_border = false;
tv.invert = self.is_password;
tv.style = STYLE_OVERRIDE;
write!(tv, "{}", self.user_input).ok();
modal.gam.post_textview(&mut tv).expect("couldn't post tv");
current_height += self.line_height.get();
// ------- draw the line under the text entry -----
modal.gam.draw_line(modal.canvas, Line::new_with_style(
Point::new(self.margin.get(), current_height + AESTHETIC_GAP), // we manually place the line to an aesthetic position
Point::new(modal.canvas_width - self.margin.get(), current_height + AESTHETIC_GAP),
DrawStyle::new(color, color, 1))
).expect("couldn't draw entry line");
current_height += self.margin.get() * 2;
// ------- draw the suggestion list --------
let mut draw_at = current_height; // split current_height into a separate variable for this op, because it's a variable length list
if self.suggested_words.len() > 0 {
for (index, recco) in self.suggested_words.iter().enumerate() {
if index >= NUM_RECCOS as usize {
break;
}
if index as i16 == self.suggestion_index.get() {
// draw the dot
let mut tv = TextView::new(
modal.canvas,
TextBounds::BoundingBox(Rectangle::new(
Point::new(self.margin.get() + AESTHETIC_GAP, draw_at - 4),
Point::new(modal.canvas_width - self.margin.get(), draw_at + self.line_height.get() - 4))
));
tv.text.clear();
tv.bounds_computed = None;
tv.draw_border = false;
tv.invert = self.is_password;
write!(tv, "•").unwrap(); // emoji glyph will be summoned in this case
modal.gam.post_textview(&mut tv).expect("couldn't post tv");
}
let left_text_margin = self.margin.get() + AESTHETIC_GAP + bullet_margin; // space for the bullet point on the left
// draw the reccommendation text
let mut tv = TextView::new(
modal.canvas,
TextBounds::BoundingBox(Rectangle::new(
Point::new(left_text_margin, draw_at),
Point::new(modal.canvas_width - (self.margin.get() + bullet_margin), draw_at + self.line_height.get()))
));
tv.ellipsis = true;
tv.invert = self.is_password;
tv.text.clear();
tv.draw_border =false;
tv.margin = Point::new(0, 0);
write!(tv, "{}", recco).ok();
modal.gam.post_textview(&mut tv).expect("couldn't post tv");
draw_at += self.line_height.get();
}
} else {
// no suggested words:
// put in some error/guidance text instead of a recco list
let mut guidance = String::new();
if self.user_input.len() > 0 {
guidance.push_str(t!("bip39.invalid_word", xous::LANG));
} else {
// no input yet, give some options
if self.payload.is_some() {
guidance.push_str(t!("bip39.enter_to_complete", xous::LANG));
} else {
guidance.push_str(t!("bip39.start_typing", xous::LANG));
}
}
let mut tv = TextView::new(
modal.canvas,
TextBounds::CenteredTop(Rectangle::new(
Point::new(self.margin.get(), draw_at),
Point::new(modal.canvas_width - self.margin.get(), draw_at + self.line_height.get() * NUM_RECCOS))
));
tv.invert = self.is_password;
tv.text.clear();
tv.draw_border =false;
write!(tv, "{}", guidance).ok();
modal.gam.post_textview(&mut tv).expect("couldn't post tv");
}
current_height += self.line_height.get() * NUM_RECCOS;
// -------- draw a divider line ----------
modal.gam.draw_line(modal.canvas, Line::new_with_style(
Point::new(self.margin.get(), current_height + self.margin.get()),
Point::new(modal.canvas_width - self.margin.get(), current_height + self.margin.get()),
DrawStyle::new(color, color, 1))
).expect("couldn't draw entry line");
current_height += self.margin.get() * 2;
// ------- draw word list -------
let mut words = String::new();
for word in self.accepted_words.iter() {
words.push_str(word);
words.push_str(" ");
}
let mut tv = TextView::new(
modal.canvas,
TextBounds::CenteredTop(Rectangle::new(
Point::new(self.margin.get(), current_height),
Point::new(modal.canvas_width - self.margin.get(), current_height + self.line_height.get() * ACCEPTED_WORD_LINES + self.margin.get()))
));
tv.invert = self.is_password;
tv.ellipsis = true;
tv.text.clear();
tv.draw_border =false;
tv.style = STYLE_OVERRIDE;
write!(tv, "{}", words).ok();
modal.gam.post_textview(&mut tv).expect("couldn't post tv");
current_height += self.line_height.get() * ACCEPTED_WORD_LINES;
// -------- draw a divider line ----------
modal.gam.draw_line(modal.canvas, Line::new_with_style(
Point::new(self.margin.get(), current_height + self.margin.get()),
Point::new(modal.canvas_width - self.margin.get(), current_height + self.margin.get()),
DrawStyle::new(color, color, 1))
).expect("couldn't draw entry line");
current_height += self.margin.get() * 2;
// ------- status --------
let mut status = String::new();
if let Some(p) = &self.payload {
status.push_str(t!("bip39.valid_phrase", xous::LANG));
status.push_str( " ");
status.push_str(&hex::encode(&p));
} else {
if self.user_input.len() == 0 && self.accepted_words.len() == 0 {
status.push_str(t!("bip39.waiting", xous::LANG));
} else {
status.push_str(t!("bip39.invalid_phrase", xous::LANG));
}
}
let mut tv = TextView::new(
modal.canvas,
TextBounds::CenteredTop(Rectangle::new(
Point::new(self.margin.get(), current_height),
Point::new(modal.canvas_width - self.margin.get(), current_height + self.line_height.get() * STATUS_LINES + self.margin.get()))
));
tv.invert = self.is_password;
tv.text.clear();
tv.draw_border = false;
tv.ellipsis = true;
write!(tv, "{}", status).ok();
modal.gam.post_textview(&mut tv).expect("couldn't post tv");
}
fn key_action(&mut self, k: char) -> (Option<ValidatorErr>, bool) {
let can_move_downwards = !(self.suggestion_index.get() + 1 == NUM_RECCOS);
let can_move_upwards = !(self.suggestion_index.get() - 1 < 0);
log::info!("key_action: {}", k);
match k {
'∴' | '\u{d}' => {
if self.user_input.len() == 0 {
if let Some(data) = &self.payload {
let mut ret = Bip39EntryPayload::default();
ret.data[..data.len()].copy_from_slice(&data);
ret.len = data.len() as u32;
let buf = Buffer::into_buf(ret).expect("couldn't convert message to payload");
buf.send(self.action_conn, self.action_opcode).map(|_| ()).expect("couldn't send action message");
return (None, true)
}
} else {
if self.suggested_words.len() > 0 {
if (self.suggestion_index.get() as usize) < self.suggested_words.len() { // should "always" be true, but good to check
self.accepted_words.push(self.suggested_words[self.suggestion_index.get() as usize].to_string());
match self.gam.bip39_to_bytes(&self.accepted_words) {
Ok(data) => {
self.payload = Some(data)
}
_ => self.payload = None,
}
self.user_input.clear();
self.suggested_words.clear();
self.suggestion_index.set(0);
}
}
}
}
'↑' => {
if can_move_upwards {
self.suggestion_index.set(self.suggestion_index.get() - 1);
}
}
'↓' => {
if can_move_downwards {
self.suggestion_index.set(self.suggestion_index.get() + 1);
}
}
'\u{0}' => {
// ignore null messages
}
'\u{8}' => { // backspace
#[cfg(feature="tts")]
{
let xns = xous_names::XousNames::new().unwrap();
let tts = tts_frontend::TtsFrontend::new(&xns).unwrap();
tts.tts_blocking(locales::t!("input.delete-tts", xous::LANG)).unwrap();
}
if self.user_input.len() > 0 { // don't backspace if we have no string.
self.user_input.pop();
if self.user_input.len() > 0 {
self.suggested_words = self.gam.bip39_suggestions(&self.user_input)
.unwrap_or(Vec::<String>::new());
} else {
self.suggested_words.clear();
}
self.suggestion_index.set(0);
} else {
self.accepted_words.pop();
self.suggested_words.clear();
self.suggestion_index.set(0);
match self.gam.bip39_to_bytes(&self.accepted_words) {
Ok(data) => {
self.payload = Some(data)
}
_ => self.payload = None,
}
}
}
_ => { // text entry
#[cfg(feature="tts")]
{
let xns = xous_names::XousNames::new().unwrap();
let tts = tts_frontend::TtsFrontend::new(&xns).unwrap();
tts.tts_blocking(&k.to_string()).unwrap();
}
if k.is_ascii_alphabetic() { // ignore any other input, since it's invalid.
let lk = k.to_lowercase();
for c in lk {
self.user_input.push(c);
}
// now regenerate all the hints
self.suggested_words = self.gam.bip39_suggestions(&self.user_input)
.unwrap_or(Vec::<String>::new());
self.suggestion_index.set(0);
}
}
}
(None, false)
}
}