mirror of
https://github.com/mimblewimble/grin-wallet.git
synced 2025-01-20 19:11:09 +03:00
API Lifecycle Implementation, Part 2 (#217)
* additon of 'get_mnemonic' API function * rustfmt * added 'change_password' api function * rustfmt * add 'delete_wallet' function' * rustfmt
This commit is contained in:
parent
41c0058e84
commit
78e30aa779
10 changed files with 430 additions and 14 deletions
124
api/src/owner.rs
124
api/src/owner.rs
|
@ -1565,6 +1565,130 @@ where
|
|||
let lc = w_lock.lc_provider()?;
|
||||
lc.close_wallet(name)
|
||||
}
|
||||
|
||||
/// Return the BIP39 mnemonic for the given wallet. This function will decrypt
|
||||
/// the wallet's seed file with the given password, and thus does not need the
|
||||
/// wallet to be open.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name`: Reserved for future use, use `None` for the time being.
|
||||
/// * `password`: The password used to encrypt the seed file.
|
||||
///
|
||||
/// # Returns
|
||||
/// * Ok(BIP-39 mneminc) if successful
|
||||
/// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered.
|
||||
///
|
||||
/// # Example
|
||||
/// Set up as in [`new`](struct.Owner.html#method.new) method above.
|
||||
/// ```
|
||||
/// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config);
|
||||
///
|
||||
/// use grin_core::global::ChainTypes;
|
||||
///
|
||||
/// // Set up as above
|
||||
/// # let api_owner = Owner::new(wallet.clone());
|
||||
///
|
||||
/// let pw = ZeroingString::from("my_password");
|
||||
/// let res = api_owner.get_mnemonic(None, pw);
|
||||
///
|
||||
/// if let Ok(mne) = res {
|
||||
/// // ...
|
||||
/// }
|
||||
/// ```
|
||||
pub fn get_mnemonic(
|
||||
&self,
|
||||
name: Option<&str>,
|
||||
password: ZeroingString,
|
||||
) -> Result<ZeroingString, Error> {
|
||||
let mut w_lock = self.wallet_inst.lock();
|
||||
let lc = w_lock.lc_provider()?;
|
||||
lc.get_mnemonic(name, password)
|
||||
}
|
||||
|
||||
/// Changes a wallet's password, meaning the old seed file is decrypted with the old password,
|
||||
/// and a new seed file is created with the same mnemonic and encrypted with the new password.
|
||||
///
|
||||
/// This function temporarily backs up the old seed file until a test-decryption of the new
|
||||
/// file is confirmed to contain the same seed as the original seed file, at which point the
|
||||
/// backup is deleted. If this operation fails for an unknown reason, the backup file will still
|
||||
/// exist in the wallet's data directory encrypted with the old password.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name`: Reserved for future use, use `None` for the time being.
|
||||
/// * `old`: The password used to encrypt the existing seed file (i.e. old password)
|
||||
/// * `new`: The password to be used to encrypt the new seed file
|
||||
///
|
||||
/// # Returns
|
||||
/// * Ok(()) if successful
|
||||
/// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered.
|
||||
///
|
||||
/// # Example
|
||||
/// Set up as in [`new`](struct.Owner.html#method.new) method above.
|
||||
/// ```
|
||||
/// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config);
|
||||
///
|
||||
/// use grin_core::global::ChainTypes;
|
||||
///
|
||||
/// // Set up as above
|
||||
/// # let api_owner = Owner::new(wallet.clone());
|
||||
///
|
||||
/// let old = ZeroingString::from("my_password");
|
||||
/// let new = ZeroingString::from("new_password");
|
||||
/// let res = api_owner.change_password(None, old, new);
|
||||
///
|
||||
/// if let Ok(mne) = res {
|
||||
/// // ...
|
||||
/// }
|
||||
/// ```
|
||||
pub fn change_password(
|
||||
&self,
|
||||
name: Option<&str>,
|
||||
old: ZeroingString,
|
||||
new: ZeroingString,
|
||||
) -> Result<(), Error> {
|
||||
let mut w_lock = self.wallet_inst.lock();
|
||||
let lc = w_lock.lc_provider()?;
|
||||
lc.change_password(name, old, new)
|
||||
}
|
||||
|
||||
/// Deletes a wallet, removing the config file, seed file and all data files.
|
||||
/// Obviously, use with extreme caution and plenty of user warning
|
||||
///
|
||||
/// Highly recommended that the wallet be explicitly closed first via the `close_wallet`
|
||||
/// function.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name`: Reserved for future use, use `None` for the time being.
|
||||
///
|
||||
/// # Returns
|
||||
/// * Ok if successful
|
||||
/// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered.
|
||||
///
|
||||
/// # Example
|
||||
/// Set up as in [`new`](struct.Owner.html#method.new) method above.
|
||||
/// ```
|
||||
/// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config);
|
||||
///
|
||||
/// use grin_core::global::ChainTypes;
|
||||
///
|
||||
/// // Set up as above
|
||||
/// # let api_owner = Owner::new(wallet.clone());
|
||||
///
|
||||
/// let res = api_owner.delete_wallet(None);
|
||||
///
|
||||
/// if let Ok(_) = res {
|
||||
/// // ...
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
pub fn delete_wallet(&self, name: Option<&str>) -> Result<(), Error> {
|
||||
let mut w_lock = self.wallet_inst.lock();
|
||||
let lc = w_lock.lc_provider()?;
|
||||
lc.delete_wallet(name)
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
|
|
|
@ -1587,6 +1587,102 @@ pub trait OwnerRpcS {
|
|||
*/
|
||||
|
||||
fn close_wallet(&self, name: Option<String>) -> Result<(), ErrorKind>;
|
||||
|
||||
/**
|
||||
Networked version of [Owner::get_mnemonic](struct.Owner.html#method.get_mnemonic).
|
||||
```
|
||||
# grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!(
|
||||
# r#"
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "get_mnemonic",
|
||||
"params": {
|
||||
"name": null,
|
||||
"password": ""
|
||||
},
|
||||
"id": 1
|
||||
}
|
||||
# "#
|
||||
# ,
|
||||
# r#"
|
||||
{
|
||||
"id": 1,
|
||||
"jsonrpc": "2.0",
|
||||
"result": {
|
||||
"Ok": "fat twenty mean degree forget shell check candy immense awful flame next during february bulb bike sun wink theory day kiwi embrace peace lunch"
|
||||
}
|
||||
}
|
||||
# "#
|
||||
# , true, 0, false, false, false);
|
||||
```
|
||||
*/
|
||||
|
||||
fn get_mnemonic(&self, name: Option<String>, password: String) -> Result<String, ErrorKind>;
|
||||
|
||||
/**
|
||||
Networked version of [Owner::change_password](struct.Owner.html#method.change_password).
|
||||
```
|
||||
# grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!(
|
||||
# r#"
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "change_password",
|
||||
"params": {
|
||||
"name": null,
|
||||
"old": "",
|
||||
"new": "new_password"
|
||||
},
|
||||
"id": 1
|
||||
}
|
||||
# "#
|
||||
# ,
|
||||
# r#"
|
||||
{
|
||||
"id": 1,
|
||||
"jsonrpc": "2.0",
|
||||
"result": {
|
||||
"Ok": null
|
||||
}
|
||||
}
|
||||
# "#
|
||||
# , true, 0, false, false, false);
|
||||
```
|
||||
*/
|
||||
fn change_password(
|
||||
&self,
|
||||
name: Option<String>,
|
||||
old: String,
|
||||
new: String,
|
||||
) -> Result<(), ErrorKind>;
|
||||
|
||||
/**
|
||||
Networked version of [Owner::delete_wallet](struct.Owner.html#method.delete_wallet).
|
||||
```
|
||||
# grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!(
|
||||
# r#"
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "delete_wallet",
|
||||
"params": {
|
||||
"name": null
|
||||
},
|
||||
"id": 1
|
||||
}
|
||||
# "#
|
||||
# ,
|
||||
# r#"
|
||||
{
|
||||
"id": 1,
|
||||
"jsonrpc": "2.0",
|
||||
"result": {
|
||||
"Ok": null
|
||||
}
|
||||
}
|
||||
# "#
|
||||
# , true, 0, false, false, false);
|
||||
```
|
||||
*/
|
||||
fn delete_wallet(&self, name: Option<String>) -> Result<(), ErrorKind>;
|
||||
}
|
||||
|
||||
impl<'a, L, C, K> OwnerRpcS for Owner<'a, L, C, K>
|
||||
|
@ -1843,4 +1939,27 @@ where
|
|||
let n = name.as_ref().map(|s| s.as_str());
|
||||
Owner::close_wallet(self, n).map_err(|e| e.kind())
|
||||
}
|
||||
|
||||
fn get_mnemonic(&self, name: Option<String>, password: String) -> Result<String, ErrorKind> {
|
||||
let n = name.as_ref().map(|s| s.as_str());
|
||||
let res =
|
||||
Owner::get_mnemonic(self, n, ZeroingString::from(password)).map_err(|e| e.kind())?;
|
||||
Ok(format!("{}", &*res))
|
||||
}
|
||||
|
||||
fn change_password(
|
||||
&self,
|
||||
name: Option<String>,
|
||||
old: String,
|
||||
new: String,
|
||||
) -> Result<(), ErrorKind> {
|
||||
let n = name.as_ref().map(|s| s.as_str());
|
||||
Owner::change_password(self, n, ZeroingString::from(old), ZeroingString::from(new))
|
||||
.map_err(|e| e.kind())
|
||||
}
|
||||
|
||||
fn delete_wallet(&self, name: Option<String>) -> Result<(), ErrorKind> {
|
||||
let n = name.as_ref().map(|s| s.as_str());
|
||||
Owner::delete_wallet(self, n).map_err(|e| e.kind())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -202,8 +202,9 @@ where
|
|||
}
|
||||
Ok(d) => d,
|
||||
};
|
||||
let wallet_seed = WalletSeed::from_file(&data_dir_name, password)
|
||||
.context(ErrorKind::Lifecycle("Error opening wallet".into()))?;
|
||||
let wallet_seed = WalletSeed::from_file(&data_dir_name, password).context(
|
||||
ErrorKind::Lifecycle("Error opening wallet (is password correct?)".into()),
|
||||
)?;
|
||||
let keychain = wallet_seed
|
||||
.derive_keychain(global::is_floonet())
|
||||
.context(ErrorKind::Lifecycle("Error deriving keychain".into()))?;
|
||||
|
@ -270,12 +271,69 @@ where
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn change_password(&self, _old: String, _new: String) -> Result<(), Error> {
|
||||
unimplemented!()
|
||||
fn change_password(
|
||||
&self,
|
||||
_name: Option<&str>,
|
||||
old: ZeroingString,
|
||||
new: ZeroingString,
|
||||
) -> Result<(), Error> {
|
||||
let mut data_dir_name = PathBuf::from(self.data_dir.clone());
|
||||
data_dir_name.push(GRIN_WALLET_DIR);
|
||||
let data_dir_name = data_dir_name.to_str().unwrap();
|
||||
// get seed for later check
|
||||
|
||||
let orig_wallet_seed = WalletSeed::from_file(&data_dir_name, old).context(
|
||||
ErrorKind::Lifecycle("Error opening wallet seed file".into()),
|
||||
)?;
|
||||
let orig_mnemonic = orig_wallet_seed
|
||||
.to_mnemonic()
|
||||
.context(ErrorKind::Lifecycle("Error recovering mnemonic".into()))?;
|
||||
|
||||
// Back up existing seed, and keep track of filename as we're deleting it
|
||||
// once the password change is confirmed
|
||||
let backup_name = WalletSeed::backup_seed(data_dir_name).context(ErrorKind::Lifecycle(
|
||||
"Error temporarily backing up existing seed".into(),
|
||||
))?;
|
||||
|
||||
// Delete seed file
|
||||
WalletSeed::delete_seed_file(data_dir_name).context(ErrorKind::Lifecycle(
|
||||
"Unable to delete seed file for password change".into(),
|
||||
))?;
|
||||
|
||||
// Init a new file
|
||||
let _ = WalletSeed::init_file(
|
||||
data_dir_name,
|
||||
0,
|
||||
Some(ZeroingString::from(orig_mnemonic)),
|
||||
new.clone(),
|
||||
);
|
||||
info!("Wallet seed file created");
|
||||
|
||||
let new_wallet_seed = WalletSeed::from_file(&data_dir_name, new).context(
|
||||
ErrorKind::Lifecycle("Error opening wallet seed file".into()),
|
||||
)?;
|
||||
|
||||
if orig_wallet_seed != new_wallet_seed {
|
||||
let msg = format!(
|
||||
"New and Old wallet seeds are not equal on password change, not removing backups."
|
||||
);
|
||||
return Err(ErrorKind::Lifecycle(msg).into());
|
||||
}
|
||||
// Removin
|
||||
info!("Password change confirmed, removing old seed file.");
|
||||
fs::remove_file(backup_name).context(ErrorKind::IO)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn delete_wallet(&self, _name: Option<String>, _password: String) -> Result<(), Error> {
|
||||
unimplemented!()
|
||||
fn delete_wallet(&self, _name: Option<&str>) -> Result<(), Error> {
|
||||
let data_dir_name = PathBuf::from(self.data_dir.clone());
|
||||
warn!(
|
||||
"Removing all wallet data from: {}",
|
||||
data_dir_name.to_str().unwrap()
|
||||
);
|
||||
fs::remove_dir_all(data_dir_name).context(ErrorKind::IO)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn wallet_inst(&mut self) -> Result<&mut Box<dyn WalletBackend<'a, C, K> + 'a>, Error> {
|
||||
|
|
|
@ -94,7 +94,7 @@ impl WalletSeed {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn backup_seed(data_file_dir: &str) -> Result<(), Error> {
|
||||
pub fn backup_seed(data_file_dir: &str) -> Result<String, Error> {
|
||||
let seed_file_name = &format!("{}{}{}", data_file_dir, MAIN_SEPARATOR, SEED_FILE,);
|
||||
|
||||
let mut path = Path::new(seed_file_name).to_path_buf();
|
||||
|
@ -114,7 +114,7 @@ impl WalletSeed {
|
|||
))?;
|
||||
}
|
||||
warn!("{} backed up as {}", seed_file_name, backup_seed_file_name);
|
||||
Ok(())
|
||||
Ok(backup_seed_file_name)
|
||||
}
|
||||
|
||||
pub fn recover_from_phrase(
|
||||
|
@ -180,7 +180,6 @@ impl WalletSeed {
|
|||
data_file_dir: &str,
|
||||
password: util::ZeroingString,
|
||||
) -> Result<WalletSeed, Error> {
|
||||
// TODO: Is this desirable any more?
|
||||
// create directory if it doesn't exist
|
||||
fs::create_dir_all(data_file_dir).context(ErrorKind::IO)?;
|
||||
|
||||
|
@ -205,6 +204,15 @@ impl WalletSeed {
|
|||
Err(ErrorKind::WalletSeedDoesntExist)?
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete_seed_file(data_file_dir: &str) -> Result<(), Error> {
|
||||
let seed_file_path = &format!("{}{}{}", data_file_dir, MAIN_SEPARATOR, SEED_FILE,);
|
||||
if Path::new(seed_file_path).exists() {
|
||||
debug!("Deleting wallet seed file at: {}", seed_file_path);
|
||||
fs::remove_file(seed_file_path).context(ErrorKind::IO)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Encrypted wallet seed, for storing on disk and decrypting
|
||||
|
|
|
@ -112,10 +112,15 @@ where
|
|||
) -> Result<(), Error>;
|
||||
|
||||
/// changes password
|
||||
fn change_password(&self, old: String, new: String) -> Result<(), Error>;
|
||||
fn change_password(
|
||||
&self,
|
||||
name: Option<&str>,
|
||||
old: ZeroingString,
|
||||
new: ZeroingString,
|
||||
) -> Result<(), Error>;
|
||||
|
||||
/// deletes wallet
|
||||
fn delete_wallet(&self, name: Option<String>, password: String) -> Result<(), Error>;
|
||||
fn delete_wallet(&self, name: Option<&str>) -> Result<(), Error>;
|
||||
|
||||
/// return wallet instance
|
||||
fn wallet_inst(&mut self) -> Result<&mut Box<dyn WalletBackend<'a, C, K> + 'a>, Error>;
|
||||
|
|
|
@ -405,8 +405,8 @@ where
|
|||
let value: OUT = v;
|
||||
Ok(Ok(value))
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Error deserializing: {:?}", e);
|
||||
Err(_) => {
|
||||
//println!("Error deserializing: {:?}", e);
|
||||
let value: OUT = serde_json::from_value(json!("Null")).unwrap();
|
||||
Ok(Ok(value))
|
||||
}
|
||||
|
|
10
tests/data/v3_reqs/change_password.req.json
Normal file
10
tests/data/v3_reqs/change_password.req.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "change_password",
|
||||
"params": {
|
||||
"name": null,
|
||||
"old": "passwoid",
|
||||
"new": "password"
|
||||
},
|
||||
"id": 1
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
"params": {
|
||||
"name": null,
|
||||
"mnemonic": null,
|
||||
"mnemonic_length": 0,
|
||||
"mnemonic_length": 32,
|
||||
"password": "passwoid"
|
||||
},
|
||||
"id": 1
|
||||
|
|
8
tests/data/v3_reqs/delete_wallet.req.json
Normal file
8
tests/data/v3_reqs/delete_wallet.req.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "delete_wallet",
|
||||
"params": {
|
||||
"name": null
|
||||
},
|
||||
"id": 1
|
||||
}
|
|
@ -367,7 +367,91 @@ fn owner_v3_lifecycle() -> Result<(), grin_wallet_controller::Error> {
|
|||
println!("RES 16: {:?}", res);
|
||||
assert!(res.is_ok());
|
||||
|
||||
//17) Change the password
|
||||
let req = include_str!("data/v3_reqs/close_wallet.req.json");
|
||||
let res =
|
||||
send_request_enc::<String>(1, 1, "http://127.0.0.1:43420/v3/owner", &req, &shared_key)?;
|
||||
println!("RES 17: {:?}", res);
|
||||
assert!(res.is_ok());
|
||||
|
||||
let req = include_str!("data/v3_reqs/change_password.req.json");
|
||||
let res =
|
||||
send_request_enc::<String>(1, 1, "http://127.0.0.1:43420/v3/owner", &req, &shared_key)?;
|
||||
println!("RES 17a: {:?}", res);
|
||||
assert!(res.is_ok());
|
||||
|
||||
// 18) trying to open with old password should fail
|
||||
let req = include_str!("data/v3_reqs/open_wallet.req.json");
|
||||
let res =
|
||||
send_request_enc::<String>(1, 1, "http://127.0.0.1:43420/v3/owner", &req, &shared_key)?;
|
||||
println!("RES 18: {:?}", res);
|
||||
assert!(res.is_err());
|
||||
|
||||
// 19) Open with new password
|
||||
let req = serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "open_wallet",
|
||||
"params": {
|
||||
"name": null,
|
||||
"password": "password"
|
||||
}
|
||||
});
|
||||
let res = send_request_enc::<String>(
|
||||
1,
|
||||
1,
|
||||
"http://127.0.0.1:43420/v3/owner",
|
||||
&req.to_string(),
|
||||
&shared_key,
|
||||
)?;
|
||||
println!("RES 19: {:?}", res);
|
||||
assert!(res.is_ok());
|
||||
let token = res.unwrap();
|
||||
|
||||
// 20) Send a request with new token with changed password, ensure balances are still there and
|
||||
// therefore seed is the same
|
||||
let req = serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "retrieve_summary_info",
|
||||
"params": {
|
||||
"token": token,
|
||||
"refresh_from_node": true,
|
||||
"minimum_confirmations": 1
|
||||
}
|
||||
});
|
||||
|
||||
let res = send_request_enc::<RetrieveSummaryInfoResp>(
|
||||
1,
|
||||
1,
|
||||
"http://127.0.0.1:43420/v3/owner",
|
||||
&req.to_string(),
|
||||
&shared_key,
|
||||
)?;
|
||||
println!("RES 20: {:?}", res);
|
||||
|
||||
thread::sleep(Duration::from_millis(200));
|
||||
assert_eq!(res.unwrap().1.amount_awaiting_finalization, 6000000000);
|
||||
|
||||
// 21) Delete the wallet (close first)
|
||||
let req = include_str!("data/v3_reqs/close_wallet.req.json");
|
||||
let res =
|
||||
send_request_enc::<String>(1, 1, "http://127.0.0.1:43420/v3/owner", &req, &shared_key)?;
|
||||
assert!(res.is_ok());
|
||||
|
||||
let req = include_str!("data/v3_reqs/delete_wallet.req.json");
|
||||
let res =
|
||||
send_request_enc::<String>(1, 1, "http://127.0.0.1:43420/v3/owner", &req, &shared_key)?;
|
||||
println!("RES 21: {:?}", res);
|
||||
assert!(res.is_ok());
|
||||
|
||||
// 22) Wallet should be gone
|
||||
let req = include_str!("data/v3_reqs/open_wallet.req.json");
|
||||
let res =
|
||||
send_request_enc::<String>(1, 1, "http://127.0.0.1:43420/v3/owner", &req, &shared_key)?;
|
||||
println!("RES 22: {:?}", res);
|
||||
assert!(res.is_err());
|
||||
|
||||
clean_output_dir(test_dir);
|
||||
|
||||
Ok(())
|
||||
|
|
Loading…
Reference in a new issue