[add] Python client packaging first draft

This commit is contained in:
Lucas PASCAL
2023-07-28 11:00:23 +02:00
parent 6bb2d8ab97
commit e5c82d910e
52 changed files with 179 additions and 45 deletions

43
.github/workflows/python-client.yml vendored Normal file
View File

@@ -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
client/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
*egg-info
dist
*wheel
*~

1
client/MANIFEST.in Normal file
View File

@@ -0,0 +1 @@
include src/ledger_app_clients/ethereum/keychain/*

28
client/README.md Normal file
View File

@@ -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
client/pyproject.toml Normal file
View File

@@ -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 = "hello@ledger.fr" }
]
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"

View File

@@ -0,0 +1 @@
__version__ = "0.0.1"

View File

@@ -1,16 +1,14 @@
from enum import IntEnum, auto import rlp
from typing import Optional from enum import IntEnum
from ragger.backend import BackendInterface from ragger.backend import BackendInterface
from ragger.utils import RAPDU from ragger.utils import RAPDU
from .command_builder import CommandBuilder from .command_builder import CommandBuilder
from .eip712 import EIP712FieldType from .eip712 import EIP712FieldType
from .keychain import sign_data, Key
from .tlv import format_tlv 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 WEI_IN_ETH = 1e+18
@@ -134,7 +132,7 @@ class EthAppClient:
payload += format_tlv(DOMAIN_NAME_TAG.DOMAIN_NAME, name) payload += format_tlv(DOMAIN_NAME_TAG.DOMAIN_NAME, name)
payload += format_tlv(DOMAIN_NAME_TAG.ADDRESS, addr) payload += format_tlv(DOMAIN_NAME_TAG.ADDRESS, addr)
payload += format_tlv(DOMAIN_NAME_TAG.SIGNATURE, 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) chunks = self._cmd_builder.provide_domain_name(payload)
for chunk in chunks[:-1]: for chunk in chunks[:-1]:

View File

@@ -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 import struct
from enum import IntEnum
from ragger.bip import pack_derivation_path
from typing import Iterator
from .eip712 import EIP712FieldType
class InsType(IntEnum): class InsType(IntEnum):
SIGN = 0x04 SIGN = 0x04
@@ -13,12 +15,14 @@ class InsType(IntEnum):
GET_CHALLENGE = 0x20 GET_CHALLENGE = 0x20
PROVIDE_DOMAIN_NAME = 0x22 PROVIDE_DOMAIN_NAME = 0x22
class P1Type(IntEnum): class P1Type(IntEnum):
COMPLETE_SEND = 0x00 COMPLETE_SEND = 0x00
PARTIAL_SEND = 0x01 PARTIAL_SEND = 0x01
SIGN_FIRST_CHUNK = 0x00 SIGN_FIRST_CHUNK = 0x00
SIGN_SUBSQT_CHUNK = 0x80 SIGN_SUBSQT_CHUNK = 0x80
class P2Type(IntEnum): class P2Type(IntEnum):
STRUCT_NAME = 0x00 STRUCT_NAME = 0x00
STRUCT_FIELD = 0xff STRUCT_FIELD = 0xff
@@ -29,6 +33,7 @@ class P2Type(IntEnum):
FILTERING_CONTRACT_NAME = 0x0f FILTERING_CONTRACT_NAME = 0x0f
FILTERING_FIELD_NAME = 0xff FILTERING_FIELD_NAME = 0xff
class CommandBuilder: class CommandBuilder:
_CLA: int = 0xE0 _CLA: int = 0xE0

View File

@@ -1,13 +1,13 @@
#!/usr/bin/env python3
import json
import sys
import re
import hashlib import hashlib
from app.client import EthAppClient, EIP712FieldType import json
import keychain import re
from typing import Callable
import signal 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 # global variables
app_client: EthAppClient = None app_client: EthAppClient = None
@@ -18,8 +18,6 @@ sig_ctx = {}
autonext_handler: Callable = None autonext_handler: Callable = None
# From a string typename, extract the type and all the array depth # From a string typename, extract the type and all the array depth
# Input = "uint8[2][][4]" | "bool" # Input = "uint8[2][][4]" | "bool"
# Output = ('uint8', [2, None, 4]) | ('bool', []) # Output = ('uint8', [2, None, 4]) | ('bool', [])

View File

@@ -0,0 +1 @@
from .struct import EIP712FieldType # noqa

View File

@@ -1,5 +1,6 @@
from enum import IntEnum, auto from enum import IntEnum, auto
class EIP712FieldType(IntEnum): class EIP712FieldType(IntEnum):
CUSTOM = 0, CUSTOM = 0,
INT = auto() INT = auto()

View File

@@ -1,9 +1,10 @@
import os import os
import hashlib import hashlib
from ecdsa.util import sigencode_der
from ecdsa import SigningKey from ecdsa import SigningKey
from ecdsa.util import sigencode_der
from enum import Enum, auto from enum import Enum, auto
# Private key PEM files have to be named the same (lowercase) as their corresponding enum entries # 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 # Example: for an entry in the Enum named DEV, its PEM file must be at keychain/dev.pem
class Key(Enum): class Key(Enum):

View File

@@ -1,8 +1,8 @@
from enum import Enum, auto from enum import Enum, auto
from typing import List
from ragger.firmware import Firmware from ragger.firmware import Firmware
from ragger.navigator import Navigator, NavInsID, NavIns from ragger.navigator import Navigator, NavInsID, NavIns
class SettingID(Enum): class SettingID(Enum):
BLIND_SIGNING = auto() BLIND_SIGNING = auto()
DEBUG_DATA = auto() DEBUG_DATA = auto()

View File

@@ -1,4 +1,4 @@
ragger[speculos] ragger[speculos]
pytest pytest
ecdsa ecdsa
simple-rlp ./client/

View File

@@ -1,12 +1,16 @@
import pytest import pytest
from ragger.error import ExceptionRAPDU from pathlib import Path
from ragger.firmware import Firmware
from ragger.backend import BackendInterface from ragger.backend import BackendInterface
from ragger.firmware import Firmware
from ragger.error import ExceptionRAPDU
from ragger.navigator import Navigator, NavInsID from ragger.navigator import Navigator, NavInsID
from app.client import EthAppClient, StatusWord, ROOT_SCREENSHOT_PATH
from app.settings import SettingID, settings_toggle import ledger_app_clients.ethereum.response_parser as ResponseParser
import app.response_parser as ResponseParser from ledger_app_clients.ethereum.client import EthAppClient, StatusWord
import struct from ledger_app_clients.ethereum.settings import SettingID, settings_toggle
ROOT_SCREENSHOT_PATH = Path(__file__).parent
# Values used across all tests # Values used across all tests
CHAIN_ID = 1 CHAIN_ID = 1
@@ -73,7 +77,6 @@ def test_send_fund_wrong_challenge(firmware: Firmware,
backend: BackendInterface, backend: BackendInterface,
navigator: Navigator): navigator: Navigator):
app_client = EthAppClient(backend) app_client = EthAppClient(backend)
caught = False
challenge = common(app_client) challenge = common(app_client)
try: try:

View File

@@ -1,37 +1,42 @@
import pytest
import os
import fnmatch import fnmatch
from typing import List import os
from ragger.firmware import Firmware import pytest
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
from configparser import ConfigParser
import app.response_parser as ResponseParser
from functools import partial
import time import time
from configparser import ConfigParser
from functools import partial
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" BIP32_PATH = "m/44'/60'/0'/0/0"
def input_files() -> List[str]: def input_files() -> List[str]:
files = [] 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"): if fnmatch.fnmatch(file, "*-data.json"):
files.append(file.path) files.append(file.path)
return sorted(files) return sorted(files)
@pytest.fixture(params=input_files()) @pytest.fixture(params=input_files())
def input_file(request) -> str: def input_file(request) -> str:
return Path(request.param) return Path(request.param)
@pytest.fixture(params=[True, False]) @pytest.fixture(params=[True, False])
def verbose(request) -> bool: def verbose(request) -> bool:
return request.param return request.param
@pytest.fixture(params=[False, True]) @pytest.fixture(params=[False, True])
def filtering(request) -> bool: def filtering(request) -> bool:
return request.param return request.param