Skip to content

Commit

Permalink
[add] Python client packaging first draft
Browse files Browse the repository at this point in the history
  • Loading branch information
lpascal-ledger committed Aug 7, 2023
1 parent 6bb2d8a commit e5c82d9
Show file tree
Hide file tree
Showing 52 changed files with 177 additions and 43 deletions.
43 changes: 43 additions & 0 deletions .github/workflows/python-client.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Python client checks, package build and deployment

on:
workflow_dispatch:
push:
branches:
- develop
- master
pull_request:

jobs:
lint:
name: Linting
runs-on: ubuntu-latest
steps:
- name: Clone
uses: actions/checkout@v3
- run: pip install flake8
- name: Flake8 lint Python code
run: find client/src/ -type f -name '*.py' -exec flake8 --max-line-length=120 '{}' '+'

mypy:
name: Type checking
runs-on: ubuntu-latest
steps:
- name: Clone
uses: actions/checkout@v3
- run: pip install mypy
- name: Mypy type checking
run: mypy client/src

build:
name: Building the package
runs-on: ubuntu-latest
steps:
- name: Clone
uses: actions/checkout@v3
- run: pip install --upgrade pip build twine
- name: Build and test the package
run: |
cd client/
python -m build .
python -m twine check dist/*
4 changes: 4 additions & 0 deletions client/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
*egg-info
dist
*wheel
*~
1 change: 1 addition & 0 deletions client/MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include src/ledger_app_clients/ethereum/keychain/*
28 changes: 28 additions & 0 deletions client/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Ethereum app Python client

This package allows to communicate with the Ethereum application, either on a
real device, or emulated on Speculos.

## Installation

This package is deployed:

- on `pypi.org` for the stable version. This version will work with the
application available on the `master` branch.
```bash
pip install ledger_app_clients.ethereum`
```
- on `test.pypi.org` for the rolling release. This verison will work with the
application code on the `develop` branch.
```bash
pip install --extra-index-url https://test.pypi.org/simple/ ledger_app_clients.ethereum`
```

### Installation from sources

You can install the client from this repo:

```bash
cd client/
pip install .
```
45 changes: 45 additions & 0 deletions client/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
[build-system]
requires = [
"setuptools>=45",
"setuptools_scm[toml]>=6.2",
"wheel"
]
build-backend = "setuptools.build_meta"

[project]
name = "ledger_app_clients.ethereum"
authors = [
{ name = "Ledger", email = "[email protected]" }
]
description = "Ledger Ethereum Python client"
readme = { file = "README.md", content-type = "text/markdown" }
# license = { file = "LICENSE" }
classifiers = [
"License :: OSI Approved :: Apache License 2.0",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Operating System :: POSIX :: Linux",
"Operating System :: Microsoft :: Windows",
"Operating System :: MacOS :: MacOS X",
]
dynamic = [ "version" ]
requires-python = ">=3.7"
dependencies = [
"ragger[speculos]",
"simple-rlp",
]

[tools.setuptools]
include-package-data = true

[tool.setuptools.dynamic]
version = {attr = "ledger_app_clients.ethereum.__version__"}

[project.urls]
Home = "https://github.com/LedgerHQ/app-ethereum"

# [tool.setuptools_scm]
# write_to = "ledgerwallet/__version__.py"
# local_scheme = "no-local-version"
1 change: 1 addition & 0 deletions client/src/ledger_app_clients/ethereum/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ = "0.0.1"
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
from enum import IntEnum, auto
from typing import Optional
import rlp
from enum import IntEnum
from ragger.backend import BackendInterface
from ragger.utils import RAPDU

from .command_builder import CommandBuilder
from .eip712 import EIP712FieldType
from .keychain import sign_data, Key
from .tlv import format_tlv
from pathlib import Path
import keychain
import rlp


ROOT_SCREENSHOT_PATH = Path(__file__).parent.parent
WEI_IN_ETH = 1e+18


Expand Down Expand Up @@ -134,7 +132,7 @@ def provide_domain_name(self, challenge: int, name: str, addr: bytes):
payload += format_tlv(DOMAIN_NAME_TAG.DOMAIN_NAME, name)
payload += format_tlv(DOMAIN_NAME_TAG.ADDRESS, addr)
payload += format_tlv(DOMAIN_NAME_TAG.SIGNATURE,
keychain.sign_data(keychain.Key.DOMAIN_NAME, payload))
sign_data(Key.DOMAIN_NAME, payload))

chunks = self._cmd_builder.provide_domain_name(payload)
for chunk in chunks[:-1]:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from enum import IntEnum, auto
from typing import Iterator, Optional
from .eip712 import EIP712FieldType
from ragger.bip import pack_derivation_path
import struct
from enum import IntEnum
from ragger.bip import pack_derivation_path
from typing import Iterator

from .eip712 import EIP712FieldType


class InsType(IntEnum):
SIGN = 0x04
Expand All @@ -13,12 +15,14 @@ class InsType(IntEnum):
GET_CHALLENGE = 0x20
PROVIDE_DOMAIN_NAME = 0x22


class P1Type(IntEnum):
COMPLETE_SEND = 0x00
PARTIAL_SEND = 0x01
SIGN_FIRST_CHUNK = 0x00
SIGN_SUBSQT_CHUNK = 0x80


class P2Type(IntEnum):
STRUCT_NAME = 0x00
STRUCT_FIELD = 0xff
Expand All @@ -29,6 +33,7 @@ class P2Type(IntEnum):
FILTERING_CONTRACT_NAME = 0x0f
FILTERING_FIELD_NAME = 0xff


class CommandBuilder:
_CLA: int = 0xE0

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
#!/usr/bin/env python3

import hashlib
import json
import sys
import re
import hashlib
from app.client import EthAppClient, EIP712FieldType
import keychain
from typing import Callable
import signal
import sys
from typing import Callable

from ledger_app_clients.ethereum import keychain
from ledger_app_clients.ethereum.client import EthAppClient, EIP712FieldType


# global variables
app_client: EthAppClient = None
Expand All @@ -18,8 +18,6 @@
autonext_handler: Callable = None




# From a string typename, extract the type and all the array depth
# Input = "uint8[2][][4]" | "bool"
# Output = ('uint8', [2, None, 4]) | ('bool', [])
Expand Down
1 change: 1 addition & 0 deletions client/src/ledger_app_clients/ethereum/eip712/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .struct import EIP712FieldType # noqa
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from enum import IntEnum, auto


class EIP712FieldType(IntEnum):
CUSTOM = 0,
INT = auto()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import os
import hashlib
from ecdsa.util import sigencode_der
from ecdsa import SigningKey
from ecdsa.util import sigencode_der
from enum import Enum, auto


# Private key PEM files have to be named the same (lowercase) as their corresponding enum entries
# Example: for an entry in the Enum named DEV, its PEM file must be at keychain/dev.pem
class Key(Enum):
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from enum import Enum, auto
from typing import List
from ragger.firmware import Firmware
from ragger.navigator import Navigator, NavInsID, NavIns


class SettingID(Enum):
BLIND_SIGNING = auto()
DEBUG_DATA = auto()
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion tests/ragger/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
ragger[speculos]
pytest
ecdsa
simple-rlp
./client/
17 changes: 10 additions & 7 deletions tests/ragger/test_domain_name.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import pytest
from ragger.error import ExceptionRAPDU
from ragger.firmware import Firmware
from pathlib import Path
from ragger.backend import BackendInterface
from ragger.firmware import Firmware
from ragger.error import ExceptionRAPDU
from ragger.navigator import Navigator, NavInsID
from app.client import EthAppClient, StatusWord, ROOT_SCREENSHOT_PATH
from app.settings import SettingID, settings_toggle
import app.response_parser as ResponseParser
import struct

import ledger_app_clients.ethereum.response_parser as ResponseParser
from ledger_app_clients.ethereum.client import EthAppClient, StatusWord
from ledger_app_clients.ethereum.settings import SettingID, settings_toggle


ROOT_SCREENSHOT_PATH = Path(__file__).parent

# Values used across all tests
CHAIN_ID = 1
Expand Down Expand Up @@ -73,7 +77,6 @@ def test_send_fund_wrong_challenge(firmware: Firmware,
backend: BackendInterface,
navigator: Navigator):
app_client = EthAppClient(backend)
caught = False
challenge = common(app_client)

try:
Expand Down
31 changes: 18 additions & 13 deletions tests/ragger/test_eip712.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,42 @@
import pytest
import os
import fnmatch
from typing import List
from ragger.firmware import Firmware
from ragger.backend import BackendInterface
from ragger.navigator import Navigator, NavInsID
from app.client import EthAppClient
from app.settings import SettingID, settings_toggle
from eip712 import InputData
from pathlib import Path
import os
import pytest
import time
from configparser import ConfigParser
import app.response_parser as ResponseParser
from functools import partial
import time
from pathlib import Path
from ragger.backend import BackendInterface
from ragger.firmware import Firmware
from ragger.navigator import Navigator, NavInsID
from typing import List

import ledger_app_clients.ethereum.response_parser as ResponseParser
from ledger_app_clients.ethereum.client import EthAppClient
from ledger_app_clients.ethereum.eip712 import InputData
from ledger_app_clients.ethereum.settings import SettingID, settings_toggle


BIP32_PATH = "m/44'/60'/0'/0/0"


def input_files() -> List[str]:
files = []
for file in os.scandir("%s/eip712/input_files" % (os.path.dirname(__file__))):
for file in os.scandir("%s/eip712_input_files" % (os.path.dirname(__file__))):
if fnmatch.fnmatch(file, "*-data.json"):
files.append(file.path)
return sorted(files)


@pytest.fixture(params=input_files())
def input_file(request) -> str:
return Path(request.param)


@pytest.fixture(params=[True, False])
def verbose(request) -> bool:
return request.param


@pytest.fixture(params=[False, True])
def filtering(request) -> bool:
return request.param
Expand Down

0 comments on commit e5c82d9

Please sign in to comment.