[add] Python client packaging first draft
This commit is contained in:
43
.github/workflows/python-client.yml
vendored
Normal file
43
.github/workflows/python-client.yml
vendored
Normal 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
4
client/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
*egg-info
|
||||||
|
dist
|
||||||
|
*wheel
|
||||||
|
*~
|
||||||
1
client/MANIFEST.in
Normal file
1
client/MANIFEST.in
Normal file
@@ -0,0 +1 @@
|
|||||||
|
include src/ledger_app_clients/ethereum/keychain/*
|
||||||
28
client/README.md
Normal file
28
client/README.md
Normal 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
45
client/pyproject.toml
Normal 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"
|
||||||
1
client/src/ledger_app_clients/ethereum/__init__.py
Normal file
1
client/src/ledger_app_clients/ethereum/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
__version__ = "0.0.1"
|
||||||
@@ -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]:
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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', [])
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
from .struct import EIP712FieldType # noqa
|
||||||
@@ -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()
|
||||||
@@ -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):
|
||||||
@@ -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()
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
ragger[speculos]
|
ragger[speculos]
|
||||||
pytest
|
pytest
|
||||||
ecdsa
|
ecdsa
|
||||||
simple-rlp
|
./client/
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user