use crate::phrase::{Language, MnemonicType};
use crate::{to_hex, Address, Phrase, Private, Public, Seed};
use anyhow::{anyhow, Context};
use rand::RngCore;
use serde::de::Error;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::collections::HashMap;
use std::convert::TryFrom;
use std::fmt::{Debug, Formatter};
use std::path::PathBuf;
use std::str::FromStr;
use tokio::fs::File;
use crate::FeelessError;
pub struct WalletManager {
path: PathBuf,
}
impl WalletManager {
pub fn new<P: Into<PathBuf>>(path: P) -> Self {
Self { path: path.into() }
}
pub async fn ensure(&self) -> anyhow::Result<()> {
if self.path.exists() {
return Ok(());
}
let store = WalletStorage::new();
let file = File::create(&self.path).await?;
serde_json::to_writer_pretty(file.into_std().await, &store)?;
Ok(())
}
async fn load_unlocked(&self) -> anyhow::Result<WalletStorage> {
let file = File::open(&self.path)
.await
.with_context(|| format!("Opening {:?}", &self.path))?;
let store: WalletStorage = serde_json::from_reader(&file.into_std().await)?;
Ok(store)
}
async fn save_unlocked(&self, file: File, store: WalletStorage) -> anyhow::Result<()> {
Ok(serde_json::to_writer_pretty(file.into_std().await, &store)?)
}
pub async fn wallet(&self, reference: &WalletId) -> anyhow::Result<Wallet> {
let store = self.load_unlocked().await?;
Ok(store
.wallets
.get(&reference)
.ok_or_else(|| anyhow!("Wallet reference not found: {:?}", &reference))?
.to_owned())
}
pub async fn add_random_phrase(
&self,
id: WalletId,
mnemonic_type: MnemonicType,
lang: Language,
) -> anyhow::Result<Wallet> {
let wallet = Wallet::Phrase(Phrase::random(mnemonic_type, lang));
self.add(id, wallet.clone()).await?;
Ok(wallet)
}
pub async fn add_random_seed(&self, id: WalletId) -> anyhow::Result<Wallet> {
let wallet = Wallet::Seed(Seed::random());
self.add(id, wallet.clone()).await?;
Ok(wallet)
}
pub async fn add_random_private(&self, reference: WalletId) -> anyhow::Result<Wallet> {
let wallet = Wallet::Private(Private::random());
self.add(reference, wallet.clone()).await?;
Ok(wallet)
}
pub async fn add(&self, reference: WalletId, wallet: Wallet) -> anyhow::Result<()> {
let mut storage = self.load_unlocked().await?;
if storage.wallets.contains_key(&reference) {
return Err(anyhow!("Wallet reference already exists: {:?}", &reference));
}
storage.wallets.insert(reference.clone(), wallet);
let file = File::create(&self.path)
.await
.with_context(|| format!("Creating file {:?}", &self.path))?;
self.save_unlocked(file, storage).await?;
Ok(())
}
pub async fn delete(&self, reference: &WalletId) -> anyhow::Result<()> {
let mut storage = self.load_unlocked().await?;
if !storage.wallets.contains_key(reference) {
return Err(anyhow!("Wallet reference doesn't exist: {:?}", &reference));
}
storage.wallets.remove(reference);
let file = File::create(&self.path)
.await
.with_context(|| format!("Creating file {:?}", &self.path))?;
self.save_unlocked(file, storage).await?;
Ok(())
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum Wallet {
Phrase(Phrase),
Seed(Seed),
Private(Private),
}
impl Wallet {
pub fn private(&self, index: u32) -> Result<Private, FeelessError> {
match &self {
Wallet::Seed(seed) => Ok(seed.derive(index)),
Wallet::Private(private) => {
if index != 0 {
return Err(FeelessError::WalletError);
}
Ok(private.to_owned())
}
Wallet::Phrase(phrase) => Ok(phrase.to_private(index, "")?),
}
}
pub fn public(&self, index: u32) -> Result<Public, FeelessError> {
self.private(index)?.to_public()
}
pub fn address(&self, index: u32) -> anyhow::Result<Address> {
Ok(self.public(index)?.to_address())
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct WalletStorage {
wallets: HashMap<WalletId, Wallet>,
}
impl WalletStorage {
pub fn new() -> Self {
Self {
wallets: Default::default(),
}
}
}
#[derive(Hash, Eq, PartialEq, Clone)]
pub struct WalletId([u8; WalletId::LEN]);
impl WalletId {
pub(crate) const LEN: usize = 32;
pub(crate) fn zero() -> Self {
Self([0u8; 32])
}
pub fn random() -> Self {
let mut id = Self::zero();
rand::thread_rng().fill_bytes(&mut id.0);
id
}
}
impl FromStr for WalletId {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let vec = hex::decode(s.as_bytes())?;
let decoded = vec.as_slice();
let d = <[u8; WalletId::LEN]>::try_from(decoded)?;
Ok(Self(d))
}
}
impl Serialize for WalletId {
fn serialize<S>(&self, serializer: S) -> Result<<S as Serializer>::Ok, <S as Serializer>::Error>
where
S: Serializer,
{
serializer.serialize_str(to_hex(&self.0).as_str())
}
}
impl<'de> Deserialize<'de> for WalletId {
fn deserialize<D>(deserializer: D) -> Result<Self, <D as Deserializer<'de>>::Error>
where
D: Deserializer<'de>,
{
let s: String = Deserialize::deserialize(deserializer)?;
Self::from_str(s.as_str()).map_err(D::Error::custom)
}
}
impl Debug for WalletId {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
crate::encoding::hex_formatter(f, &self.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::remove_file;
use std::str::FromStr;
struct Clean(PathBuf);
impl Drop for Clean {
fn drop(&mut self) {
remove_file(&self.0).unwrap()
}
}
async fn prepare(p: &str) -> (Clean, WalletManager) {
let p = PathBuf::from_str(p).unwrap();
if p.exists() {
remove_file(p.clone()).unwrap();
}
let manager = WalletManager::new(p.clone());
manager.ensure().await.unwrap();
(Clean(p), manager)
}
#[tokio::test]
async fn sanity_check() {
let (_clean, manager) = prepare("test.wallet").await;
let w1 = manager.add_random_seed(WalletId::zero()).await.unwrap();
let w2 = manager.wallet(&WalletId::zero()).await.unwrap();
assert_eq!(w1.address(0).unwrap(), w2.address(0).unwrap())
}
#[tokio::test]
async fn import_seed() {
let (_clean, manager) = prepare("import_seed.wallet").await;
let seed =
Seed::from_str("0000000000000000000000000000000000000000000000000000000000000000")
.unwrap();
let wallet = Wallet::Seed(seed);
let reference = WalletId::zero();
manager.add(reference, wallet.to_owned()).await.unwrap();
assert_eq!(
wallet.address(0).unwrap(),
Address::from_str("nano_3i1aq1cchnmbn9x5rsbap8b15akfh7wj7pwskuzi7ahz8oq6cobd99d4r3b7")
.unwrap()
);
}
}