Repository: opentensor/btwallet (language: python)

Issue:
Add base and mro updating logic before add Wallet Keypair class to the main_module
- added python test
- fix for https://github.com/opentensor/bittensor/issues/2434
set_coldkeypub arguments passed in wrong order
Method called with `overwrite=overwrite, encrypt=false`, but the method accepts these two arguments in the other order - `encrypt` first, then `overwrite`:

https://github.com/opentensor/btwallet/blob/main/src/wallet.rs#L831
```rust
self.set_coldkeypub(keypair, overwrite, false, py)?;
```

https://github.com/opentensor/btwallet/blob/main/src/wallet.rs#L515-L521
```rust
pub fn set_coldkeypub(
    &mut self,
    keypair: Keypair,
    encrypt: bool,
    overwrite: bool,
    py: Python,
) -> PyResult<()> {
```



Discussion / hints:

Fix patch:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 295aebd..20b48f5 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -91,7 +91,7 @@ jobs:
     strategy:
       matrix:
         platform:
-          - runner: macos-12
+          - runner: macos-13
             target: x86_64
           - runner: macos-14
             target: aarch64
diff --git a/Cargo.toml b/Cargo.toml
index 3ee74de..8708aa2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -9,7 +9,7 @@ crate-type = ["cdylib"]
 
 [dependencies]
 sp-core = "34.0.0"
-pyo3 = { version = "0.22.3", features = ["gil-refs"] }
+pyo3 = { version = "0.22.6", features = ["gil-refs"] }
 bip39 = { version = "2.0.0", features = ["rand"] }
 rand = "0.8.5"
 hex = "0.4.3"
diff --git a/pyproject.toml b/pyproject.toml
index 2ba4f6d..7152b6f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [project]
 name = "bittensor-wallet"
-version = "2.1.0"
+version = "2.1.1"
 description = ""
 readme = "README.md"
 license = {file = "LICENSE"}
@@ -15,7 +15,8 @@ dependencies = [
     "ansible_vault~=2.1",
     "munch~=2.5.0",
     "rich",
-    "py-bip39-bindings==0.1.11"
+    "py-bip39-bindings==0.1.11",
+    "substrate-interface~=1.7.9"
 ]
 requires-python = ">= 3.9"
 
diff --git a/requirements/prod.txt b/requirements/prod.txt
index 71a642f..95ddf2b 100644
--- a/requirements/prod.txt
+++ b/requirements/prod.txt
@@ -4,7 +4,6 @@ eth-utils<2.3.0
 substrate-interface~=1.7.9
 password_strength
 cryptography~=43.0.1
-ansible~=6.7
 ansible_vault~=2.1
 munch~=2.5.0
 rich
\ No newline at end of file
diff --git a/src/config.rs b/src/config.rs
index 891dad4..f07fd65 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -39,7 +39,7 @@ impl Config {
     }
     fn __str__(&self) -> PyResult<String> {
         Ok(format!(
-            "Config(name: '{}', path: '~/{}', hotkey: '{}')",
+            "Config(name: '{}', path: '{}', hotkey: '{}')",
             self.wallet.name, self.wallet.path, self.wallet.hotkey
         ))
     }
diff --git a/src/keyfile.rs b/src/keyfile.rs
index c6a7bbe..3466f94 100644
--- a/src/keyfile.rs
+++ b/src/keyfile.rs
@@ -186,12 +186,15 @@ pub fn validate_password(_py: Python, password: &str) -> PyResult<bool> {
 #[pyfunction]
 pub fn ask_password(py: Python, validation_required: bool) -> PyResult<String> {
     let mut valid = false;
-    let password = utils::prompt_password("Enter your password: ".to_string());
-
+    let mut password = utils::prompt_password("Enter your password: ".to_string());
+    
     if validation_required {
         while !valid {
-            if let Some(ref password) = password {
-                valid = validate_password(py, &password)?;
+            if let Some(ref pwd) = password {
+                valid = validate_password(py, &pwd)?;
+                if !valid {
+                    password = utils::prompt_password("Enter your password again: ".to_string());
+                }
             } else {
                 valid = true
             }
diff --git a/src/lib.rs b/src/lib.rs
index c54af28..cad13af 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,5 +1,7 @@
 /// Main module for declaration of python package structure
 use pyo3::prelude::*;
+use pyo3::types::{PyTuple, PyType};
+use pyo3::ffi;
 
 mod config;
 mod constants;
@@ -10,7 +12,7 @@ mod utils;
 mod wallet;
 
 #[pymodule]
-fn bittensor_wallet(module: &Bound<'_, PyModule>) -> PyResult<()> {
+fn bittensor_wallet(py: Python<'_>,module: &Bound<'_, PyModule>) -> PyResult<()> {
     // classes to main module
     module.add_class::<config::Config>()?;
     module.add_class::<keyfile::Keyfile>()?;
@@ -20,7 +22,7 @@ fn bittensor_wallet(module: &Bound<'_, PyModule>) -> PyResult<()> {
     register_config_module(module)?;
     register_errors_module(module)?;
     register_keyfile_module(module)?;
-    register_keypair_module(module)?;
+    register_keypair_module(py, module)?;
     register_utils_module(module)?;
     register_wallet_module(module)?;
     Ok(())
@@ -97,12 +99,34 @@ fn register_keyfile_module(main_module: &Bound<'_, PyModule>) -> PyResult<()> {
 }
 
 // keypair module with functions
-fn register_keypair_module(main_module: &Bound<'_, PyModule>) -> PyResult<()> {
-    let keypair_module = PyModule::new_bound(main_module.py(), "keypair")?;
-    keypair_module.add_class::<keypair::Keypair>()?;
+fn register_keypair_module(py: Python, main_module: &Bound<'_, PyModule>) -> PyResult<()> {
+    let keypair_module = PyModule::new_bound(py, "keypair")?;
+
+    // import python substrateinterface into the rust
+    let substrate_module = py.import_bound("substrateinterface")?;
+    let origin_keypair_class = substrate_module.getattr("Keypair")?;
+
+    // defining Wallet Keypair class
+    let keypair_type = PyType::new_bound::<keypair::Keypair>(py);
+
+    // update base and mro in Wallet Keypair type
+    unsafe {
+        (*keypair_type.as_type_ptr()).tp_base = origin_keypair_class.as_ptr() as *mut _;
+
+        let mro_tuple = PyTuple::new_bound(py, &[keypair_type.as_ref(), &origin_keypair_class]);
+        ffi::Py_INCREF(mro_tuple.as_ptr());
+        (*keypair_type.as_type_ptr()).tp_mro = mro_tuple.as_ptr() as *mut _;
+
+        if ffi::PyType_Ready(keypair_type.as_type_ptr()) != 0 {
+            return Err(PyErr::fetch(py));
+        }
+    }
+
+    keypair_module.add("Keypair", keypair_type)?;
     main_module.add_submodule(&keypair_module)
 }
 
+
 // utils module with functions
 fn register_utils_module(main_module: &Bound<'_, PyModule>) -> PyResult<()> {
     let utils_module = PyModule::new_bound(main_module.py(), "utils")?;
diff --git a/src/wallet.rs b/src/wallet.rs
index bd1f65d..9f792ca 100644
--- a/src/wallet.rs
+++ b/src/wallet.rs
@@ -190,7 +190,7 @@ impl Wallet {
         let default_hotkey =
             env::var("BT_WALLET_HOTKEY").unwrap_or_else(|_| BT_WALLET_HOTKEY.to_string());
         let default_path = env::var("BT_WALLET_PATH")
-            .unwrap_or_else(|_| format!("~/{}", BT_WALLET_PATH.to_string()));
+            .unwrap_or_else(|_| BT_WALLET_PATH.to_string());
 
         let prefix_str = if let Some(value) = prefix {
             format!("\"{}\"", value)


Test patch:
diff --git a/tests/test_keypair.py b/tests/test_keypair.py
new file mode 100644
index 0000000..33ed8ad
--- /dev/null
+++ b/tests/test_keypair.py
@@ -0,0 +1,29 @@
+from substrateinterface import Keypair
+import pytest
+from bittensor_wallet import Wallet
+import time
+
+
+@pytest.fixture
+def mock_wallet():
+    wallet = Wallet(
+        name=f"mock-{str(time.time())}",
+        hotkey=f"mock-{str(time.time())}",
+        path="/tmp/tests_wallets/do_not_use",
+    )
+    wallet.create_new_coldkey(use_password=False, overwrite=True, suppress=True)
+    wallet.create_new_hotkey(use_password=False, overwrite=True, suppress=True)
+
+    return wallet
+
+
+def test_keypair_type(mock_wallet):
+    """Makes sure that the wallet fields coldkey, hotkey, coldkeypub are compatible with substrateinterface.Keypair."""
+    # Preps
+    wallet = mock_wallet
+
+    # Assertions
+
+    assert isinstance(wallet.coldkey, Keypair)
+    assert isinstance(wallet.hotkey, Keypair)
+    assert isinstance(wallet.coldkeypub, Keypair)
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 9731f06..8435c2c 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -19,7 +19,7 @@
 from bittensor_wallet import utils
 
 
-def test_get_ss58_format(mocker):
+def test_get_ss58_format():
     """Checks that `get_ss58_format` function uses `ss58.get_ss58_format` call."""
     # Preps
     fake_ss58_addressss58_address = "5FvUhL6sQ5egLAvnyiY1n7gTuhmYg9sD3oJbvASWkpNULt3n"
diff --git a/tests/test_wallet.py b/tests/test_wallet.py
index b371683..8d282f2 100644
--- a/tests/test_wallet.py
+++ b/tests/test_wallet.py
@@ -544,3 +544,38 @@ def test_unlock_coldkeypub(mock_wallet):
     assert result.seed_hex == mock_wallet.get_coldkeypub().seed_hex
     assert result.crypto_type == mock_wallet.get_coldkeypub().crypto_type
     assert result.mnemonic == mock_wallet.get_coldkeypub().mnemonic
+
+
+def test_wallet_string_representation_with_default_arguments():
+    """Tests wallet string representation with default arguments."""
+    # Call
+    w = Wallet()
+
+    # Asserts
+    assert (
+        str(w)
+        == "Wallet (Name: 'default', Hotkey: 'default', Path: '~/.bittensor/wallets/')"
+    )
+    assert w.name == "default"
+    assert w.hotkey_str == "default"
+    assert w.path == "~/.bittensor/wallets/"
+
+
+def test_wallet_string_representation_with_custom_arguments():
+    """Tests wallet string representation with custom arguments."""
+    # Preps
+    wallet_name = "test_wallet"
+    wallet_hotkey = "test_hotkey"
+    wallet_path = "/tmp/tests_wallets/"
+
+    # Call
+    w = Wallet(name="test_wallet", hotkey="test_hotkey", path="/tmp/tests_wallets/")
+
+    # Asserts
+    assert (
+        str(w)
+        == f"Wallet (Name: '{wallet_name}', Hotkey: '{wallet_hotkey}', Path: '{wallet_path}')"
+    )
+    assert w.name == wallet_name
+    assert w.hotkey_str == wallet_hotkey
+    assert w.path == wallet_path