Skip to content

feat: Add unified cross-platform Ledger installer (Fixes #7795)#7803

Open
taiwofelix35 wants to merge 1 commit into
tari-project:developmentfrom
taiwofelix35:development
Open

feat: Add unified cross-platform Ledger installer (Fixes #7795)#7803
taiwofelix35 wants to merge 1 commit into
tari-project:developmentfrom
taiwofelix35:development

Conversation

@taiwofelix35
Copy link
Copy Markdown

Description

This PR adds a unified cross-platform installer for the Minotari Ledger Wallet that auto-detects the connected Ledger model and installs the correct app in one step.

Closes

Fixes #7795

What’s Included

  • Single Python installer that works on Windows, macOS, and Linux
  • Auto-detects Ledger model (Flex, Nano S, Nano S Plus, Nano X, Stax)
  • Downloads the correct release asset via GitHub API
  • Clear success/failure status and error handling
  • PowerShell + Bash wrappers for easy usage
  • Test suite covering detection, selection, and error handling

Testing

  • python applications/minotari_ledger_wallet/wallet/test_installer.py

Copilot AI review requested due to automatic review settings May 4, 2026 19:21
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 4, 2026

⚠️ This PR contains unsigned commits. To get your PR merged, please sign those commits (git rebase --exec 'git commit -S --amend --no-edit -n' @{upstream}) and force push them to this branch (git push --force-with-lease).

If you're new to commit signing, there are different ways to set it up:

Sign commits with gpg

Follow the steps below to set up commit signing with gpg:

  1. Generate a GPG key
  2. Add the GPG key to your GitHub account
  3. Configure git to use your GPG key for commit signing
Sign commits with ssh-agent

Follow the steps below to set up commit signing with ssh-agent:

  1. Generate an SSH key and add it to ssh-agent
  2. Add the SSH key to your GitHub account
  3. Configure git to use your SSH key for commit signing
Sign commits with 1Password

You can also sign commits using 1Password, which lets you sign commits with biometrics without the signing key leaving the local 1Password process.

Learn how to use 1Password to sign your commits.

Watch the demo

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a unified, cross-platform installer for the Minotari Ledger Wallet, featuring a core Python script with automated device detection and release management, supported by platform-specific wrappers and documentation. Feedback focuses on improving the robustness of command execution by using sys.executable -m for ledgerctl, addressing a potential path traversal vulnerability during ZIP extraction, and correcting test cases that currently fail to exercise the actual implementation logic.


try:
result = subprocess.run(
["ledgerctl", "list"],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Calling ledgerctl directly assumes it is already in the system's PATH. Since this script installs ledgerctl via pip in the same execution, the PATH might not be updated yet, or the user's environment might not include the Python scripts directory (common on Windows). It is more robust to invoke it using sys.executable -m ledgerctl.

Suggested change
["ledgerctl", "list"],
[sys.executable, "-m", "ledgerctl", "list"],

print_info(f"Installing app from {os.path.basename(app_json_path)}...")

try:
result = subprocess.run(["ledgerctl", "install", app_json_path], timeout=120)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Similar to the detection step, it is safer to invoke ledgerctl via sys.executable -m to ensure the command is found regardless of whether the scripts directory is in the user's PATH.

Suggested change
result = subprocess.run(["ledgerctl", "install", app_json_path], timeout=120)
result = subprocess.run([sys.executable, "-m", "ledgerctl", "install", app_json_path], timeout=120)


try:
with zipfile.ZipFile(zip_path, "r") as zip_ref:
zip_ref.extractall(extract_dir)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

Using zip_ref.extractall() without validating the paths of the files inside the archive can lead to a "Zip Slip" vulnerability (path traversal). While the source is a trusted repository, it is a security best practice to verify that all extracted files are within the target directory before extraction.

Comment on lines +100 to +131
def test_parse_release_json(self, mock_urlopen):
mock_response = MagicMock()
mock_response.read.return_value = json.dumps(
{
"assets": [
{
"name": "minotari_ledger_wallet-flex.zip",
"browser_download_url": "https://example.com/flex.zip",
},
{
"name": "minotari_ledger_wallet-nanox.zip",
"browser_download_url": "https://example.com/nanox.zip",
},
]
}
).encode()

mock_urlopen.return_value.__enter__.return_value = mock_response

with patch("urllib.request.urlretrieve"):
with patch("os.path.join", return_value="/tmp/test.zip"):
try:
data = json.loads(mock_response.read().decode())
assets = [
asset
for asset in data["assets"]
if "minotari_ledger_wallet-flex" in asset["name"]
]
self.assertEqual(len(assets), 1)
self.assertEqual(assets[0]["name"], "minotari_ledger_wallet-flex.zip")
except json.JSONDecodeError:
self.fail("Failed to parse JSON response")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This test case does not actually call installer.download_release(). Instead, it duplicates some of the logic from that function and asserts on local variables. This means the actual implementation in install_minotari_ledger.py remains untested. The test should be refactored to call the target function and verify its behavior.

Comment on lines +136 to +145
def test_extract_zip(self, mock_zipfile):
mock_zip_instance = MagicMock()
mock_zipfile.return_value.__enter__.return_value = mock_zip_instance

import tempfile

with tempfile.TemporaryDirectory() as tmpdir:
test_zip = os.path.join(tmpdir, "test.zip")
Path(test_zip).touch()
self.assertTrue(os.path.exists(test_zip))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This test case is a "no-op" with respect to the installer logic. It mocks zipfile.ZipFile but never calls installer.extract_release(). It simply creates a file in a temporary directory and checks if it exists, which does not verify any of the extraction or manifest-finding logic.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a unified, cross-platform (Windows/macOS/Linux) installer workflow for the Minotari Ledger Wallet app, including a Python-based installer plus thin Bash/PowerShell wrappers and a lightweight unit test suite.

Changes:

  • Added install_minotari_ledger.py to detect Ledger model, download the matching GitHub release asset, extract the manifest, and install via ledgerctl.
  • Added platform wrappers (install_minotari_ledger.sh, install_minotari_ledger.ps1) and a dedicated usage guide (README_UNIFIED_INSTALLER.md).
  • Added a new unittest-based test suite (test_installer.py) intended to cover detection/selection/error handling.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
applications/minotari_ledger_wallet/wallet/install_minotari_ledger.py Core unified installer logic: dependency setup, model detection, release download/extract, and install via ledgerctl.
applications/minotari_ledger_wallet/wallet/install_minotari_ledger.sh Bash wrapper to run the Python installer on macOS/Linux.
applications/minotari_ledger_wallet/wallet/install_minotari_ledger.ps1 PowerShell wrapper to run the Python installer on Windows.
applications/minotari_ledger_wallet/wallet/README_UNIFIED_INSTALLER.md Documentation for running the unified installer and troubleshooting.
applications/minotari_ledger_wallet/wallet/test_installer.py Unit tests for key installer functions (currently with gaps and a few reliability issues).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +65 to +73
try:
__import__(package)
print_success(f"{package} already installed")
except ImportError:
print_info(f"Installing {package}...")
subprocess.check_call([sys.executable, "-m", "pip", "install", package])
print_success(f"{package} installed")


Comment on lines +241 to +242
if not check_command("pip") and not check_command("pip3"):
print_error("pip not found. Install Python with pip enabled.")
Comment on lines +81 to +87
try:
result = subprocess.run(
["ledgerctl", "list"],
capture_output=True,
text=True,
timeout=10,
)
Comment on lines +192 to +193
zip_ref.extractall(extract_dir)

Comment on lines +31 to +36
mock_version = MagicMock()
mock_version.major = 3
mock_version.minor = 6
mock_version.__lt__ = lambda self, other: (3, 6) < other

with patch("sys.version_info", mock_version):
Comment on lines +98 to +132
class TestGitHubReleaseDownload(unittest.TestCase):
@patch("urllib.request.urlopen")
def test_parse_release_json(self, mock_urlopen):
mock_response = MagicMock()
mock_response.read.return_value = json.dumps(
{
"assets": [
{
"name": "minotari_ledger_wallet-flex.zip",
"browser_download_url": "https://example.com/flex.zip",
},
{
"name": "minotari_ledger_wallet-nanox.zip",
"browser_download_url": "https://example.com/nanox.zip",
},
]
}
).encode()

mock_urlopen.return_value.__enter__.return_value = mock_response

with patch("urllib.request.urlretrieve"):
with patch("os.path.join", return_value="/tmp/test.zip"):
try:
data = json.loads(mock_response.read().decode())
assets = [
asset
for asset in data["assets"]
if "minotari_ledger_wallet-flex" in asset["name"]
]
self.assertEqual(len(assets), 1)
self.assertEqual(assets[0]["name"], "minotari_ledger_wallet-flex.zip")
except json.JSONDecodeError:
self.fail("Failed to parse JSON response")

Comment on lines +135 to +147
@patch("zipfile.ZipFile")
def test_extract_zip(self, mock_zipfile):
mock_zip_instance = MagicMock()
mock_zipfile.return_value.__enter__.return_value = mock_zip_instance

import tempfile

with tempfile.TemporaryDirectory() as tmpdir:
test_zip = os.path.join(tmpdir, "test.zip")
Path(test_zip).touch()
self.assertTrue(os.path.exists(test_zip))


Comment on lines +111 to +122
The old per-model installation scripts are available in separate directories:
- flex/ - Ledger Flex
- nanosplus/ - Ledger Nano S Plus
- nanox/ - Ledger Nano X
- stax/ - Ledger Stax

Each has:
- install_minotari_ledger_[model].sh (macOS/Linux)
- install_ledger_win.ps1 (Windows)

These are maintained for compatibility but the unified installer is recommended.


### ledgerctl not found after installation
- The installer should auto-install ledgerctl via pip
- If issues persist, try: pip install ledgerctl
Comment on lines +60 to +71
def install_dependencies() -> None:
required_packages = ["protobuf", "setuptools", "ecdsa", "ledgerwallet", "ledgerctl"]

print_info("Installing Python dependencies...")
for package in required_packages:
try:
__import__(package)
print_success(f"{package} already installed")
except ImportError:
print_info(f"Installing {package}...")
subprocess.check_call([sys.executable, "-m", "pip", "install", package])
print_success(f"{package} installed")
Copy link
Copy Markdown
Collaborator

@SWvheerden SWvheerden left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the co-pilot and Gemini stuff is relevant
Take a look at the current install script to see how its done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Create better ledger installer

3 participants