-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #106 from KeystoneHQ/feat/avax
merge Feat/avax
- Loading branch information
Showing
9 changed files
with
669 additions
and
420 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
use crate::cbor::{cbor_array, cbor_map}; | ||
use crate::error::{URError, URResult}; | ||
use crate::registry_types::{RegistryType, AVAX_SIGN_REQUEST, UUID}; | ||
use crate::traits::{From as FromCbor, RegistryItem, To}; | ||
use crate::types::{Bytes, Fingerprint}; | ||
use alloc::string::{String, ToString}; | ||
use alloc::vec::Vec; | ||
use minicbor::data::{Int, Tag}; | ||
use minicbor::encode::Write; | ||
use minicbor::{Decoder, Encoder}; | ||
|
||
const REQUEST_ID: u8 = 1; | ||
const SIGN_DATA: u8 = 2; | ||
const MASTER_FINGERPRINT: u8 = 3; | ||
const XPUB: u8 = 6; | ||
const WALLET_INDEX: u8 = 7; | ||
|
||
#[derive(Debug, Clone, Default)] | ||
pub struct AvaxSignRequest { | ||
request_id: Bytes, | ||
sign_data: Bytes, | ||
master_fingerprint: Fingerprint, | ||
xpub: String, | ||
wallet_index: u64, | ||
} | ||
|
||
impl AvaxSignRequest { | ||
pub fn new( | ||
request_id: Bytes, | ||
sign_data: Bytes, | ||
master_fingerprint: Fingerprint, | ||
xpub: String, | ||
wallet_index: u64, | ||
) -> Self { | ||
AvaxSignRequest { | ||
request_id, | ||
sign_data, | ||
master_fingerprint, | ||
xpub, | ||
wallet_index, | ||
} | ||
} | ||
|
||
pub fn get_request_id(&self) -> Bytes { | ||
self.request_id.clone() | ||
} | ||
|
||
pub fn set_request_id(&mut self, id: Bytes) { | ||
self.request_id = id; | ||
} | ||
|
||
pub fn get_tx_data(&self) -> Bytes { | ||
self.sign_data.clone() | ||
} | ||
|
||
pub fn set_tx_data(&mut self, data: Bytes) { | ||
self.sign_data = data; | ||
} | ||
|
||
pub fn get_master_fingerprint(&self) -> Fingerprint { | ||
self.master_fingerprint | ||
} | ||
|
||
pub fn get_xpub(&self) -> String { | ||
self.xpub.clone() | ||
} | ||
|
||
pub fn set_xpub(&mut self, xpub: String) { | ||
self.xpub = xpub; | ||
} | ||
|
||
pub fn get_wallet_index(&self) -> u64 { | ||
self.wallet_index | ||
} | ||
|
||
pub fn set_wallet_index(&mut self, index: u64) { | ||
self.wallet_index = index; | ||
} | ||
} | ||
|
||
impl RegistryItem for AvaxSignRequest { | ||
fn get_registry_type() -> RegistryType<'static> { | ||
AVAX_SIGN_REQUEST | ||
} | ||
} | ||
|
||
impl<C> minicbor::Encode<C> for AvaxSignRequest { | ||
fn encode<W: Write>( | ||
&self, | ||
e: &mut Encoder<W>, | ||
_ctx: &mut C, | ||
) -> Result<(), minicbor::encode::Error<W::Error>> { | ||
e.map(5)?; | ||
e.int(Int::from(REQUEST_ID))? | ||
.tag(Tag::Unassigned(UUID.get_tag()))? | ||
.bytes(&self.request_id)?; | ||
e.int(Int::from(SIGN_DATA))?.bytes(&self.sign_data)?; | ||
e.int(Int::from(MASTER_FINGERPRINT))?.int( | ||
Int::try_from(u32::from_be_bytes(self.master_fingerprint)) | ||
.map_err(|e| minicbor::encode::Error::message(e.to_string()))?, | ||
)?; | ||
e.int(Int::from(XPUB))?.str(&self.xpub)?; | ||
e.int(Int::from(WALLET_INDEX))?.u64(self.wallet_index)?; | ||
Ok(()) | ||
} | ||
} | ||
|
||
impl<'b, C> minicbor::Decode<'b, C> for AvaxSignRequest { | ||
fn decode(d: &mut Decoder<'b>, _ctx: &mut C) -> Result<Self, minicbor::decode::Error> { | ||
let mut result = AvaxSignRequest::default(); | ||
|
||
cbor_map(d, &mut result, |key, obj, d| { | ||
let key = | ||
u8::try_from(key).map_err(|e| minicbor::decode::Error::message(e.to_string()))?; | ||
match key { | ||
REQUEST_ID => { | ||
d.tag()?; | ||
obj.request_id = d.bytes()?.to_vec(); | ||
} | ||
SIGN_DATA => { | ||
obj.sign_data = d.bytes()?.to_vec(); | ||
} | ||
MASTER_FINGERPRINT => { | ||
let mfp = u32::try_from(d.int()?) | ||
.map_err(|e| minicbor::decode::Error::message(e.to_string())); | ||
obj.master_fingerprint = u32::to_be_bytes(mfp?); | ||
} | ||
XPUB => { | ||
obj.xpub = d.str()?.to_string(); | ||
} | ||
WALLET_INDEX => { | ||
obj.wallet_index = d.u64()?; | ||
} | ||
_ => {} | ||
} | ||
Ok(()) | ||
})?; | ||
Ok(result) | ||
} | ||
} | ||
|
||
impl To for AvaxSignRequest { | ||
fn to_bytes(&self) -> URResult<Vec<u8>> { | ||
minicbor::to_vec(self.clone()).map_err(|e| URError::CborEncodeError(e.to_string())) | ||
} | ||
} | ||
|
||
impl FromCbor<AvaxSignRequest> for AvaxSignRequest { | ||
fn from_cbor(bytes: Vec<u8>) -> URResult<AvaxSignRequest> { | ||
minicbor::decode(&bytes).map_err(|e| URError::CborDecodeError(e.to_string())) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
use crate::traits::RegistryItem; | ||
use alloc::vec::Vec; | ||
use hex::FromHex; | ||
extern crate std; | ||
use std::println; | ||
|
||
#[test] | ||
fn test_avax_encode() { | ||
let unsigned_data = AvaxSignRequest { | ||
request_id: [12, 34, 56, 78].to_vec(), | ||
sign_data: Vec::from_hex("000000000022000000050000000000000000000000000000000000000000000000000000000000000000000000023d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa0000000700000000000f42400000000000000000000000010000000132336f8715dd313a426155cccc15ba27c3033dae3d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa00000007000000004d58ade90000000000000000000000010000000132336f8715dd313a426155cccc15ba27c3033dae00000001410b47f7c7aa13f88122be58735c5e985edc65d86fb0baf0b016359c22253d75000000013d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa00000005000000004d680464000000010000000000000000") | ||
.unwrap(), | ||
master_fingerprint: [0, 0, 0, 0], | ||
xpub: "xpub6DXryz8Kd7XchtXvDnkjara83shGJH8ubu7KZhHhPfp4L1shvDEYiFZm32EKHnyo4bva4gxXjabFGqY7fNs8Ggd4khYz2oNs2KYLf56a9GX".to_string(), | ||
wallet_index: 0, | ||
}; | ||
let result: Vec<u8> = unsigned_data.try_into().unwrap(); | ||
println!("result = {:?}", hex::encode(&result)); | ||
let ur = ur::encode(&result, AvaxSignRequest::get_registry_type().get_type()); | ||
assert_eq!(ur, "ur:avax-sign-request/onadtpdafybncpetglaohkaddmaeaeaeaeaecpaeaeaeahaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaofsndtnrtwecakobwdytkisbazcwmcyfwbznnqdlttbtdmdbnmtyltdmyhsrkvopkaeaeaeataeaeaeaeaebsfwfzaeaeaeaeaeaeaeaeaeaeaeadaeaeaeadeyeojlltbzutehftfwhsgosfsfbzrddisraxfsplfsndtnrtwecakobwdytkisbazcwmcyfwbznnqdlttbtdmdbnmtyltdmyhsrkvopkaeaeaeataeaeaeaegthdpmwlaeaeaeaeaeaeaeaeaeaeaeadaeaeaeadeyeojlltbzutehftfwhsgosfsfbzrddisraxfsplaeaeaeadfpbdflylstpkbwyalycprnhdjkhhhymkhyuoihtpjlpfrdwtpfcmecnscpdafskpaeaeaeadfsndtnrtwecakobwdytkisbazcwmcyfwbznnqdlttbtdmdbnmtyltdmyhsrkvopkaeaeaeahaeaeaeaegtisaaieaeaeaeadaeaeaeaeaeaeaeaeaxaeamksjlksjokpidenfyhdjpkkknetgrieemhdiaisjyhdkofyjtjeimhsjphseteojkisflgefdetkpidkpemgrhtisfdisgdiyjoeegsehjkiskofyfehkinfghtjneoeyfegrfdjtkkjleeidkohseeiokshdimhsidfgfljshkemiygljketflioieeejeishkkneyjlgljkeygrhkgsiyecenhsesflhdataedapawecf"); | ||
} | ||
|
||
#[test] | ||
fn test_avax_decode() { | ||
let bytes = | ||
Vec::from_hex("a501d825440c22384e0258de00000000000000000001ed5f38341e436e5d46e2bb00b45d62ae97d1b050c64bc634ae10626739e35c4b0000000121e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff00000007000000000089544000000000000000000000000100000001512e7191685398f00663e12197a3d8f6012d9ea300000001db720ad6707915cc4751fb7e5491a3af74e127a1d81817abe9438590c0833fe10000000021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff000000050000000000989680000000010000000000000000031a0102030406786f7870756236445872797a384b6437586368745876446e6b6a61726138337368474a4838756275374b5a684868506670344c3173687644455969465a6d3332454b486e796f34627661346778586a61624647715937664e7338476764346b68597a326f4e73324b594c663536613947580706") | ||
.unwrap(); | ||
let data = AvaxSignRequest::try_from(bytes).unwrap(); | ||
assert_eq!( | ||
data.get_tx_data(), | ||
Vec::from_hex("00000000000000000001ed5f38341e436e5d46e2bb00b45d62ae97d1b050c64bc634ae10626739e35c4b0000000121e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff00000007000000000089544000000000000000000000000100000001512e7191685398f00663e12197a3d8f6012d9ea300000001db720ad6707915cc4751fb7e5491a3af74e127a1d81817abe9438590c0833fe10000000021e67317cbc4be2aeb00677ad6462778a8f52274b9d605df2591b23027a87dff000000050000000000989680000000010000000000000000") | ||
.unwrap() | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
use crate::cbor::cbor_map; | ||
use crate::error::{URError, URResult}; | ||
use crate::registry_types::{RegistryType, AVAX_SIGNATURE, UUID}; | ||
use crate::traits::{From as FromCbor, RegistryItem, To}; | ||
use crate::types::Bytes; | ||
use alloc::string::ToString; | ||
use alloc::vec::Vec; | ||
use minicbor::data::{Int, Tag}; | ||
use minicbor::encode::Write; | ||
use minicbor::{Decoder, Encoder}; | ||
|
||
const REQUEST_ID: u8 = 1; | ||
const SIGNATURE: u8 = 2; | ||
|
||
#[derive(Clone, Debug, Default)] | ||
pub struct AvaxSignature { | ||
request_id: Bytes, | ||
signature: Bytes, | ||
} | ||
|
||
impl AvaxSignature { | ||
pub fn default() -> Self { | ||
Default::default() | ||
} | ||
|
||
pub fn set_request_id(&mut self, id: Bytes) { | ||
self.request_id = id; | ||
} | ||
|
||
pub fn set_signature(&mut self, signature: Bytes) { | ||
self.signature = signature; | ||
} | ||
|
||
pub fn new(request_id: Bytes, signature: Bytes) -> Self { | ||
AvaxSignature { | ||
request_id, | ||
signature, | ||
} | ||
} | ||
|
||
pub fn get_request_id(&self) -> Bytes { | ||
self.request_id.clone() | ||
} | ||
pub fn get_signature(&self) -> Bytes { | ||
self.signature.clone() | ||
} | ||
} | ||
|
||
impl RegistryItem for AvaxSignature { | ||
fn get_registry_type() -> RegistryType<'static> { | ||
AVAX_SIGNATURE | ||
} | ||
} | ||
|
||
impl<C> minicbor::Encode<C> for AvaxSignature { | ||
fn encode<W: Write>( | ||
&self, | ||
e: &mut Encoder<W>, | ||
_ctx: &mut C, | ||
) -> Result<(), minicbor::encode::Error<W::Error>> { | ||
let mut size = 2; | ||
e.map(size)?; | ||
e.int(Int::from(REQUEST_ID))?.bytes(&self.request_id)?; | ||
e.int(Int::from(SIGNATURE))?.bytes(&self.signature)?; | ||
Ok(()) | ||
} | ||
} | ||
|
||
impl<'b, C> minicbor::Decode<'b, C> for AvaxSignature { | ||
fn decode(d: &mut Decoder<'b>, _ctx: &mut C) -> Result<Self, minicbor::decode::Error> { | ||
let mut result = AvaxSignature::default(); | ||
cbor_map(d, &mut result, |key, obj, d| { | ||
let key = | ||
u8::try_from(key).map_err(|e| minicbor::decode::Error::message(e.to_string()))?; | ||
match key { | ||
REQUEST_ID => { | ||
obj.request_id = d.bytes()?.to_vec(); | ||
} | ||
SIGNATURE => { | ||
obj.signature = d.bytes()?.to_vec(); | ||
} | ||
_ => {} | ||
} | ||
Ok(()) | ||
})?; | ||
Ok(result) | ||
} | ||
} | ||
|
||
impl To for AvaxSignature { | ||
fn to_bytes(&self) -> URResult<Vec<u8>> { | ||
minicbor::to_vec(self.clone()).map_err(|e| URError::CborEncodeError(e.to_string())) | ||
} | ||
} | ||
|
||
impl FromCbor<AvaxSignature> for AvaxSignature { | ||
fn from_cbor(bytes: Vec<u8>) -> URResult<AvaxSignature> { | ||
minicbor::decode(&bytes).map_err(|e| URError::CborDecodeError(e.to_string())) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use crate::{avalanche::avax_signature::AvaxSignature, traits::{From as FromCbor, To}}; | ||
use alloc::vec::Vec; | ||
use hex::FromHex; | ||
|
||
#[test] | ||
fn test_avax_signature_encode() { | ||
let request_id = | ||
[01, 02, 03, 04, 05, 06, 07, 08, 09, 10, 11, 12, 13, 14, 15].to_vec(); | ||
let signature = hex::decode("80337c3a47f1b69a38544c69f379a4aa0ea8ef1f853b718d992c6a73c643e63ca6dff9186cd2f41a45c6405ef6b71353c3b6864c799699964e559afa7aa7f7c345c1966c998193539985e2724831025beadb0a1a269f54ec4a95c69a3bc4295a5c6c5f926dcc84fbf2251b56c841f764b162e062c8db5302090aa1d528d83cf48b53aa0709009f3975d63ea8ff26e80b4f2f01380e100860b304fccbbc0877278efbf72fb045331f76df132a5119bd51590f0502350d3cb31f14daba731893c5834e2e8bfa5bf517ac63693b81041cf7f8ed7293d034b3e54c4d02c66542d3b9648e9ecf912101a20b87f39d75d4f1a02c816f424c8a1fda05a9e7e8ccf064d31c0bf10c661872a7f40c0b1d75dbfae6a95ddcc81eead3f49cfa3803517cf9d79f2541041416c3e8ecfc0292d864f34fe613866e86b7b0bc7abc5b3f84e6ee3b06933c4f82552bb985f6b7fac0a580e94d7a0e8e295dd2e49ece66ead0ee6a46b84553302b94701a9d24b91c085154b7e67a7ac59e3a41ae96c8e1afd1aa778633457005555cff4198820c2aa8ea1ff0f86a9f4ae03d96b215449c63bff7cae9a114c9db05cc4e4d9993a13149393b6a6992b6042bb82d34ffdc7f1aeaf17fa5240ca6ebd9e62fd6c90bce91747af37bf8fc3c72859a1dfec2cf2c49295e1ccdc09b91d9074d204dea74a70002baa05fc86acfcff45fe7f0dd7e5e24c8f69575").unwrap(); | ||
let avax_signature = AvaxSignature::new(request_id, signature); | ||
assert_eq!( | ||
"a2014f0102030405060708090a0b0c0d0e0f0259020080337c3a47f1b69a38544c69f379a4aa0ea8ef1f853b718d992c6a73c643e63ca6dff9186cd2f41a45c6405ef6b71353c3b6864c799699964e559afa7aa7f7c345c1966c998193539985e2724831025beadb0a1a269f54ec4a95c69a3bc4295a5c6c5f926dcc84fbf2251b56c841f764b162e062c8db5302090aa1d528d83cf48b53aa0709009f3975d63ea8ff26e80b4f2f01380e100860b304fccbbc0877278efbf72fb045331f76df132a5119bd51590f0502350d3cb31f14daba731893c5834e2e8bfa5bf517ac63693b81041cf7f8ed7293d034b3e54c4d02c66542d3b9648e9ecf912101a20b87f39d75d4f1a02c816f424c8a1fda05a9e7e8ccf064d31c0bf10c661872a7f40c0b1d75dbfae6a95ddcc81eead3f49cfa3803517cf9d79f2541041416c3e8ecfc0292d864f34fe613866e86b7b0bc7abc5b3f84e6ee3b06933c4f82552bb985f6b7fac0a580e94d7a0e8e295dd2e49ece66ead0ee6a46b84553302b94701a9d24b91c085154b7e67a7ac59e3a41ae96c8e1afd1aa778633457005555cff4198820c2aa8ea1ff0f86a9f4ae03d96b215449c63bff7cae9a114c9db05cc4e4d9993a13149393b6a6992b6042bb82d34ffdc7f1aeaf17fa5240ca6ebd9e62fd6c90bce91747af37bf8fc3c72859a1dfec2cf2c49295e1ccdc09b91d9074d204dea74a70002baa05fc86acfcff45fe7f0dd7e5e24c8f69575", | ||
hex::encode(avax_signature.to_bytes().unwrap()).to_lowercase() | ||
); | ||
} | ||
|
||
#[test] | ||
fn test_avax_signature_decode() { | ||
let bytes = Vec::from_hex( | ||
"a2014f0102030405060708090a0b0c0d0e0f0259020080337c3a47f1b69a38544c69f379a4aa0ea8ef1f853b718d992c6a73c643e63ca6dff9186cd2f41a45c6405ef6b71353c3b6864c799699964e559afa7aa7f7c345c1966c998193539985e2724831025beadb0a1a269f54ec4a95c69a3bc4295a5c6c5f926dcc84fbf2251b56c841f764b162e062c8db5302090aa1d528d83cf48b53aa0709009f3975d63ea8ff26e80b4f2f01380e100860b304fccbbc0877278efbf72fb045331f76df132a5119bd51590f0502350d3cb31f14daba731893c5834e2e8bfa5bf517ac63693b81041cf7f8ed7293d034b3e54c4d02c66542d3b9648e9ecf912101a20b87f39d75d4f1a02c816f424c8a1fda05a9e7e8ccf064d31c0bf10c661872a7f40c0b1d75dbfae6a95ddcc81eead3f49cfa3803517cf9d79f2541041416c3e8ecfc0292d864f34fe613866e86b7b0bc7abc5b3f84e6ee3b06933c4f82552bb985f6b7fac0a580e94d7a0e8e295dd2e49ece66ead0ee6a46b84553302b94701a9d24b91c085154b7e67a7ac59e3a41ae96c8e1afd1aa778633457005555cff4198820c2aa8ea1ff0f86a9f4ae03d96b215449c63bff7cae9a114c9db05cc4e4d9993a13149393b6a6992b6042bb82d34ffdc7f1aeaf17fa5240ca6ebd9e62fd6c90bce91747af37bf8fc3c72859a1dfec2cf2c49295e1ccdc09b91d9074d204dea74a70002baa05fc86acfcff45fe7f0dd7e5e24c8f69575", | ||
) | ||
.unwrap(); | ||
let avax_signature = AvaxSignature::from_cbor(bytes).unwrap(); | ||
assert_eq!( | ||
[01, 02, 03, 04, 05, 06, 07, 08, 09, 10, 11, 12, 13, 14, 15].to_vec(), | ||
avax_signature.get_request_id() | ||
); | ||
assert_eq!(hex::decode("80337c3a47f1b69a38544c69f379a4aa0ea8ef1f853b718d992c6a73c643e63ca6dff9186cd2f41a45c6405ef6b71353c3b6864c799699964e559afa7aa7f7c345c1966c998193539985e2724831025beadb0a1a269f54ec4a95c69a3bc4295a5c6c5f926dcc84fbf2251b56c841f764b162e062c8db5302090aa1d528d83cf48b53aa0709009f3975d63ea8ff26e80b4f2f01380e100860b304fccbbc0877278efbf72fb045331f76df132a5119bd51590f0502350d3cb31f14daba731893c5834e2e8bfa5bf517ac63693b81041cf7f8ed7293d034b3e54c4d02c66542d3b9648e9ecf912101a20b87f39d75d4f1a02c816f424c8a1fda05a9e7e8ccf064d31c0bf10c661872a7f40c0b1d75dbfae6a95ddcc81eead3f49cfa3803517cf9d79f2541041416c3e8ecfc0292d864f34fe613866e86b7b0bc7abc5b3f84e6ee3b06933c4f82552bb985f6b7fac0a580e94d7a0e8e295dd2e49ece66ead0ee6a46b84553302b94701a9d24b91c085154b7e67a7ac59e3a41ae96c8e1afd1aa778633457005555cff4198820c2aa8ea1ff0f86a9f4ae03d96b215449c63bff7cae9a114c9db05cc4e4d9993a13149393b6a6992b6042bb82d34ffdc7f1aeaf17fa5240ca6ebd9e62fd6c90bce91747af37bf8fc3c72859a1dfec2cf2c49295e1ccdc09b91d9074d204dea74a70002baa05fc86acfcff45fe7f0dd7e5e24c8f69575").unwrap(), avax_signature.get_signature()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
pub mod avax_sign_request; | ||
pub mod avax_signature; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.