remote-wallet: Add helpers for locating remote wallets

This commit is contained in:
Trent Nelson 2021-04-20 16:35:28 -06:00 committed by Trent Nelson
parent 722de942ca
commit 64fcb792c2
4 changed files with 638 additions and 5 deletions

12
Cargo.lock generated
View File

@ -3002,6 +3002,15 @@ dependencies = [
"prost",
]
[[package]]
name = "qstring"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d464fae65fff2680baf48019211ce37aaec0c78e9264c84a3e484717f965104e"
dependencies = [
"percent-encoding 2.1.0",
]
[[package]]
name = "quick-error"
version = "1.2.3"
@ -5002,10 +5011,11 @@ dependencies = [
"num-derive",
"num-traits",
"parking_lot 0.10.2",
"qstring",
"semver 0.9.0",
"solana-sdk",
"thiserror",
"url 2.2.0",
"uriparse",
]
[[package]]

View File

@ -2076,6 +2076,15 @@ dependencies = [
"unicode-xid 0.2.0",
]
[[package]]
name = "qstring"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d464fae65fff2680baf48019211ce37aaec0c78e9264c84a3e484717f965104e"
dependencies = [
"percent-encoding",
]
[[package]]
name = "quote"
version = "0.6.13"
@ -3372,10 +3381,11 @@ dependencies = [
"num-derive 0.3.0",
"num-traits",
"parking_lot 0.10.2",
"qstring",
"semver 0.9.0",
"solana-sdk",
"thiserror",
"url",
"uriparse",
]
[[package]]

View File

@ -18,10 +18,11 @@ log = "0.4.11"
num-derive = { version = "0.3" }
num-traits = { version = "0.2" }
parking_lot = "0.10"
qstring = "0.7.2"
semver = "0.9"
solana-sdk = { path = "../sdk", version = "=1.7.0" }
thiserror = "1.0"
url = "2.1.1"
uriparse = "0.6.3"
[features]
default = ["linux-static-hidraw"]

View File

@ -7,16 +7,17 @@ use {
parking_lot::{Mutex, RwLock},
solana_sdk::{
derivation_path::{DerivationPath, DerivationPathError},
pubkey::Pubkey,
pubkey::{ParsePubkeyError, Pubkey},
signature::{Signature, SignerError},
},
std::{
convert::{Infallible, TryFrom, TryInto},
str::FromStr,
sync::Arc,
time::{Duration, Instant},
},
thiserror::Error,
url::Url,
uriparse::{URIReference, URIReferenceBuilder, URIReferenceError},
};
const HID_GLOBAL_USAGE_PAGE: u16 = 0xFF00;
@ -250,6 +251,197 @@ pub struct RemoteWalletInfo {
pub error: Option<RemoteWalletError>,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Manufacturer {
Unknown,
Ledger,
}
impl Default for Manufacturer {
fn default() -> Self {
Self::Unknown
}
}
const MFR_UNKNOWN: &str = "unknown";
const MFR_LEDGER: &str = "ledger";
#[derive(Clone, Debug, Error, PartialEq)]
#[error("not a manufacturer")]
pub struct ManufacturerError;
impl From<Infallible> for ManufacturerError {
fn from(_: Infallible) -> Self {
ManufacturerError
}
}
impl FromStr for Manufacturer {
type Err = ManufacturerError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.to_ascii_lowercase();
match s.as_str() {
MFR_LEDGER => Ok(Self::Ledger),
_ => Err(ManufacturerError),
}
}
}
impl TryFrom<&str> for Manufacturer {
type Error = ManufacturerError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
Manufacturer::from_str(s)
}
}
impl AsRef<str> for Manufacturer {
fn as_ref(&self) -> &str {
match self {
Self::Unknown => MFR_UNKNOWN,
Self::Ledger => MFR_LEDGER,
}
}
}
impl std::fmt::Display for Manufacturer {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let s: &str = self.as_ref();
write!(f, "{}", s)
}
}
#[derive(Clone, Debug, Error, PartialEq)]
pub enum LocatorError {
#[error(transparent)]
ManufacturerError(#[from] ManufacturerError),
#[error(transparent)]
PubkeyError(#[from] ParsePubkeyError),
#[error(transparent)]
DerivationPathError(#[from] DerivationPathError),
#[error(transparent)]
UriReferenceError(#[from] URIReferenceError),
#[error("unimplemented scheme")]
UnimplementedScheme,
#[error("infallible")]
Infallible,
}
impl From<Infallible> for LocatorError {
fn from(_: Infallible) -> Self {
Self::Infallible
}
}
#[derive(Debug, PartialEq)]
pub struct Locator {
manufacturer: Manufacturer,
pubkey: Option<Pubkey>,
derivation_path: Option<DerivationPath>,
}
impl std::fmt::Display for Locator {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let maybe_path = self.pubkey.map(|p| p.to_string());
let path = maybe_path.as_deref().unwrap_or("/");
let maybe_query = self.derivation_path.as_ref().map(|d| d.get_query());
let maybe_query2 = maybe_query.as_ref().map(|q| &q[1..]);
let mut builder = URIReferenceBuilder::new();
builder
.try_scheme(Some("usb"))
.unwrap()
.try_authority(Some(self.manufacturer.as_ref()))
.unwrap()
.try_path(path)
.unwrap()
.try_query(maybe_query2)
.unwrap();
let uri = builder.build().unwrap();
write!(f, "{}", uri)
}
}
impl Locator {
pub fn new_from_path<P: AsRef<str>>(path: P) -> Result<Self, LocatorError> {
let path = path.as_ref();
let uri = URIReference::try_from(path)?;
Self::new_from_uri(&uri)
}
pub fn new_from_uri(uri: &URIReference<'_>) -> Result<Self, LocatorError> {
let scheme = uri.scheme().map(|s| s.as_str().to_ascii_lowercase());
let host = uri.host().map(|h| h.to_string());
match (scheme, host) {
(Some(scheme), Some(host)) if scheme == "usb" => {
let path = uri.path().segments().get(0).and_then(|s| {
if !s.is_empty() {
Some(s.as_str())
} else {
None
}
});
let key = if let Some(query) = uri.query() {
let query_str = query.as_str();
let query = qstring::QString::from(query_str);
if query.len() > 1 {
return Err(DerivationPathError::InvalidDerivationPath(
"invalid query string, extra fields not supported".to_string(),
)
.into());
}
let key = query.get("key");
if key.is_none() {
return Err(DerivationPathError::InvalidDerivationPath(format!(
"invalid query string `{}`, only `key` supported",
query_str,
))
.into());
}
key.map(|v| v.to_string())
} else {
None
};
Self::new_from_parts(host.as_str(), path, key.as_deref())
}
(Some(_scheme), Some(_host)) => Err(LocatorError::UnimplementedScheme),
(None, Some(_host)) => Err(LocatorError::UnimplementedScheme),
(_, None) => Err(LocatorError::ManufacturerError(ManufacturerError)),
}
}
pub fn new_from_parts<V, VE, P, PE, D, DE>(
manufacturer: V,
pubkey: Option<P>,
derivation_path: Option<D>,
) -> Result<Self, LocatorError>
where
VE: Into<LocatorError>,
V: TryInto<Manufacturer, Error = VE>,
PE: Into<LocatorError>,
P: TryInto<Pubkey, Error = PE>,
DE: Into<LocatorError>,
D: TryInto<DerivationPath, Error = DE>,
{
let manufacturer = manufacturer.try_into().map_err(|e| e.into())?;
let pubkey = if let Some(pubkeyable) = pubkey {
Some(pubkeyable.try_into().map_err(|e| e.into())?)
} else {
None
};
let derivation_path = if let Some(derivation_path) = derivation_path {
Some(derivation_path.try_into().map_err(|e| e.into())?)
} else {
None
};
Ok(Self {
manufacturer,
pubkey,
derivation_path,
})
}
}
impl RemoteWalletInfo {
pub fn parse_path(path: String) -> Result<(Self, DerivationPath), RemoteWalletError> {
let wallet_path = Url::parse(&path).map_err(|e| {
@ -496,4 +688,424 @@ mod tests {
format!("usb://ledger/{}", pubkey_str)
);
}
#[test]
fn test_manufacturer() {
assert_eq!(MFR_LEDGER.try_into(), Ok(Manufacturer::Ledger));
assert!(matches!(Manufacturer::from_str(MFR_LEDGER), Ok(v) if v == Manufacturer::Ledger));
assert_eq!(Manufacturer::Ledger.as_ref(), MFR_LEDGER);
assert!(
matches!(Manufacturer::from_str("bad-manufacturer"), Err(e) if e == ManufacturerError)
);
}
#[test]
fn test_locator_new_from_parts() {
let manufacturer = Manufacturer::Ledger;
let manufacturer_str = "ledger";
let pubkey = Pubkey::new_unique();
let pubkey_str = pubkey.to_string();
let derivation_path = DerivationPath::new_bip44(Some(0), Some(0));
let derivation_path_str = "0/0";
let expect = Locator {
manufacturer,
pubkey: None,
derivation_path: None,
};
assert!(matches!(
Locator::new_from_parts(manufacturer, None::<Pubkey>, None::<DerivationPath>),
Ok(e) if e == expect,
));
assert!(matches!(
Locator::new_from_parts(manufacturer_str, None::<Pubkey>, None::<DerivationPath>),
Ok(e) if e == expect,
));
let expect = Locator {
manufacturer,
pubkey: Some(pubkey),
derivation_path: None,
};
assert!(matches!(
Locator::new_from_parts(manufacturer, Some(pubkey), None::<DerivationPath>),
Ok(e) if e == expect,
));
assert!(matches!(
Locator::new_from_parts(manufacturer_str, Some(pubkey_str.as_str()), None::<DerivationPath>),
Ok(e) if e == expect,
));
let expect = Locator {
manufacturer,
pubkey: None,
derivation_path: Some(derivation_path.clone()),
};
assert!(matches!(
Locator::new_from_parts(manufacturer, None::<Pubkey>, Some(derivation_path)),
Ok(e) if e == expect,
));
assert!(matches!(
Locator::new_from_parts(manufacturer, None::<Pubkey>, Some(derivation_path_str)),
Ok(e) if e == expect,
));
assert!(matches!(
Locator::new_from_parts("bad-manufacturer", None::<Pubkey>, None::<DerivationPath>),
Err(LocatorError::ManufacturerError(e)) if e == ManufacturerError,
));
assert!(matches!(
Locator::new_from_parts(manufacturer, Some("bad-pubkey"), None::<DerivationPath>),
Err(LocatorError::PubkeyError(e)) if e == ParsePubkeyError::Invalid,
));
let bad_path = "bad-derivation-path".to_string();
assert!(matches!(
Locator::new_from_parts(manufacturer, None::<Pubkey>, Some(bad_path.as_str())),
Err(LocatorError::DerivationPathError(
DerivationPathError::InvalidDerivationPath(_)
)),
));
}
#[test]
fn test_locator_new_from_uri() {
let derivation_path = DerivationPath::new_bip44(Some(0), Some(0));
let manufacturer = Manufacturer::Ledger;
let pubkey = Pubkey::new_unique();
let pubkey_str = pubkey.to_string();
// usb://ledger/{PUBKEY}?key=0'/0'
let mut builder = URIReferenceBuilder::new();
builder
.try_scheme(Some("usb"))
.unwrap()
.try_authority(Some(Manufacturer::Ledger.as_ref()))
.unwrap()
.try_path(pubkey_str.as_str())
.unwrap()
.try_query(Some("key=0/0"))
.unwrap();
let uri = builder.build().unwrap();
let expect = Locator {
manufacturer,
pubkey: Some(pubkey),
derivation_path: Some(derivation_path.clone()),
};
assert_eq!(Locator::new_from_uri(&uri), Ok(expect));
// usb://ledger/{PUBKEY}
let mut builder = URIReferenceBuilder::new();
builder
.try_scheme(Some("usb"))
.unwrap()
.try_authority(Some(Manufacturer::Ledger.as_ref()))
.unwrap()
.try_path(pubkey_str.as_str())
.unwrap();
let uri = builder.build().unwrap();
let expect = Locator {
manufacturer,
pubkey: Some(pubkey),
derivation_path: None,
};
assert_eq!(Locator::new_from_uri(&uri), Ok(expect));
// usb://ledger
let mut builder = URIReferenceBuilder::new();
builder
.try_scheme(Some("usb"))
.unwrap()
.try_authority(Some(Manufacturer::Ledger.as_ref()))
.unwrap()
.try_path("")
.unwrap();
let uri = builder.build().unwrap();
let expect = Locator {
manufacturer,
pubkey: None,
derivation_path: None,
};
assert_eq!(Locator::new_from_uri(&uri), Ok(expect));
// usb://ledger/
let mut builder = URIReferenceBuilder::new();
builder
.try_scheme(Some("usb"))
.unwrap()
.try_authority(Some(Manufacturer::Ledger.as_ref()))
.unwrap()
.try_path("/")
.unwrap();
let uri = builder.build().unwrap();
let expect = Locator {
manufacturer,
pubkey: None,
derivation_path: None,
};
assert_eq!(Locator::new_from_uri(&uri), Ok(expect));
// usb://ledger?key=0'/0'
let mut builder = URIReferenceBuilder::new();
builder
.try_scheme(Some("usb"))
.unwrap()
.try_authority(Some(Manufacturer::Ledger.as_ref()))
.unwrap()
.try_path("")
.unwrap()
.try_query(Some("key=0/0"))
.unwrap();
let uri = builder.build().unwrap();
let expect = Locator {
manufacturer,
pubkey: None,
derivation_path: Some(derivation_path.clone()),
};
assert_eq!(Locator::new_from_uri(&uri), Ok(expect));
// usb://ledger/?key=0'/0'
let mut builder = URIReferenceBuilder::new();
builder
.try_scheme(Some("usb"))
.unwrap()
.try_authority(Some(Manufacturer::Ledger.as_ref()))
.unwrap()
.try_path("/")
.unwrap()
.try_query(Some("key=0/0"))
.unwrap();
let uri = builder.build().unwrap();
let expect = Locator {
manufacturer,
pubkey: None,
derivation_path: Some(derivation_path),
};
assert_eq!(Locator::new_from_uri(&uri), Ok(expect));
// bad-scheme://ledger
let mut builder = URIReferenceBuilder::new();
builder
.try_scheme(Some("bad-scheme"))
.unwrap()
.try_authority(Some(Manufacturer::Ledger.as_ref()))
.unwrap()
.try_path("")
.unwrap();
let uri = builder.build().unwrap();
assert_eq!(
Locator::new_from_uri(&uri),
Err(LocatorError::UnimplementedScheme)
);
// usb://bad-manufacturer
let mut builder = URIReferenceBuilder::new();
builder
.try_scheme(Some("usb"))
.unwrap()
.try_authority(Some("bad-manufacturer"))
.unwrap()
.try_path("")
.unwrap();
let uri = builder.build().unwrap();
assert_eq!(
Locator::new_from_uri(&uri),
Err(LocatorError::ManufacturerError(ManufacturerError))
);
// usb://ledger/bad-pubkey
let mut builder = URIReferenceBuilder::new();
builder
.try_scheme(Some("usb"))
.unwrap()
.try_authority(Some(Manufacturer::Ledger.as_ref()))
.unwrap()
.try_path("bad-pubkey")
.unwrap();
let uri = builder.build().unwrap();
assert_eq!(
Locator::new_from_uri(&uri),
Err(LocatorError::PubkeyError(ParsePubkeyError::Invalid))
);
// usb://ledger?bad-key=0/0
let mut builder = URIReferenceBuilder::new();
builder
.try_scheme(Some("usb"))
.unwrap()
.try_authority(Some(Manufacturer::Ledger.as_ref()))
.unwrap()
.try_path("")
.unwrap()
.try_query(Some("bad-key=0/0"))
.unwrap();
let uri = builder.build().unwrap();
assert!(matches!(
Locator::new_from_uri(&uri),
Err(LocatorError::DerivationPathError(_))
));
// usb://ledger?key=bad-value
let mut builder = URIReferenceBuilder::new();
builder
.try_scheme(Some("usb"))
.unwrap()
.try_authority(Some(Manufacturer::Ledger.as_ref()))
.unwrap()
.try_path("")
.unwrap()
.try_query(Some("key=bad-value"))
.unwrap();
let uri = builder.build().unwrap();
assert!(matches!(
Locator::new_from_uri(&uri),
Err(LocatorError::DerivationPathError(_))
));
// usb://ledger?key=
let mut builder = URIReferenceBuilder::new();
builder
.try_scheme(Some("usb"))
.unwrap()
.try_authority(Some(Manufacturer::Ledger.as_ref()))
.unwrap()
.try_path("")
.unwrap()
.try_query(Some("key="))
.unwrap();
let uri = builder.build().unwrap();
assert!(matches!(
Locator::new_from_uri(&uri),
Err(LocatorError::DerivationPathError(_))
));
// usb://ledger?key
let mut builder = URIReferenceBuilder::new();
builder
.try_scheme(Some("usb"))
.unwrap()
.try_authority(Some(Manufacturer::Ledger.as_ref()))
.unwrap()
.try_path("")
.unwrap()
.try_query(Some("key"))
.unwrap();
let uri = builder.build().unwrap();
assert!(matches!(
Locator::new_from_uri(&uri),
Err(LocatorError::DerivationPathError(_))
));
}
#[test]
fn test_locator_new_from_path() {
let derivation_path = DerivationPath::new_bip44(Some(0), Some(0));
let manufacturer = Manufacturer::Ledger;
let pubkey = Pubkey::new_unique();
let path = format!("usb://ledger/{}?key=0/0", pubkey);
Locator::new_from_path(path).unwrap();
// usb://ledger/{PUBKEY}?key=0'/0'
let path = format!("usb://ledger/{}?key=0'/0'", pubkey);
let expect = Locator {
manufacturer,
pubkey: Some(pubkey),
derivation_path: Some(derivation_path.clone()),
};
assert_eq!(Locator::new_from_path(path), Ok(expect));
// usb://ledger/{PUBKEY}
let path = format!("usb://ledger/{}", pubkey);
let expect = Locator {
manufacturer,
pubkey: Some(pubkey),
derivation_path: None,
};
assert_eq!(Locator::new_from_path(path), Ok(expect));
// usb://ledger
let path = "usb://ledger";
let expect = Locator {
manufacturer,
pubkey: None,
derivation_path: None,
};
assert_eq!(Locator::new_from_path(path), Ok(expect));
// usb://ledger/
let path = "usb://ledger/";
let expect = Locator {
manufacturer,
pubkey: None,
derivation_path: None,
};
assert_eq!(Locator::new_from_path(path), Ok(expect));
// usb://ledger?key=0'/0'
let path = "usb://ledger?key=0'/0'";
let expect = Locator {
manufacturer,
pubkey: None,
derivation_path: Some(derivation_path.clone()),
};
assert_eq!(Locator::new_from_path(path), Ok(expect));
// usb://ledger/?key=0'/0'
let path = "usb://ledger?key=0'/0'";
let expect = Locator {
manufacturer,
pubkey: None,
derivation_path: Some(derivation_path),
};
assert_eq!(Locator::new_from_path(path), Ok(expect));
// bad-scheme://ledger
let path = "bad-scheme://ledger";
assert_eq!(
Locator::new_from_path(path),
Err(LocatorError::UnimplementedScheme)
);
// usb://bad-manufacturer
let path = "usb://bad-manufacturer";
assert_eq!(
Locator::new_from_path(path),
Err(LocatorError::ManufacturerError(ManufacturerError))
);
// usb://ledger/bad-pubkey
let path = "usb://ledger/bad-pubkey";
assert_eq!(
Locator::new_from_path(path),
Err(LocatorError::PubkeyError(ParsePubkeyError::Invalid))
);
// usb://ledger?bad-key=0/0
let path = "usb://ledger?bad-key=0'/0'";
assert!(matches!(
Locator::new_from_path(path),
Err(LocatorError::DerivationPathError(_))
));
// usb://ledger?key=bad-value
let path = format!("usb://ledger/{}?key=bad-value", pubkey);
assert!(matches!(
Locator::new_from_path(path),
Err(LocatorError::DerivationPathError(_))
));
// usb://ledger?key=
let path = format!("usb://ledger/{}?key=", pubkey);
assert!(matches!(
Locator::new_from_path(path),
Err(LocatorError::DerivationPathError(_))
));
// usb://ledger?key
let path = format!("usb://ledger/{}?key", pubkey);
assert!(matches!(
Locator::new_from_path(path),
Err(LocatorError::DerivationPathError(_))
));
}
}