[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
|
||||
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
|
||||
|
||||
|
||||
@@ -134,7 +132,7 @@ class EthAppClient:
|
||||
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]:
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -29,6 +33,7 @@ class P2Type(IntEnum):
|
||||
FILTERING_CONTRACT_NAME = 0x0f
|
||||
FILTERING_FIELD_NAME = 0xff
|
||||
|
||||
|
||||
class CommandBuilder:
|
||||
_CLA: int = 0xE0
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import json
|
||||
import sys
|
||||
import re
|
||||
import hashlib
|
||||
from app.client import EthAppClient, EIP712FieldType
|
||||
import keychain
|
||||
from typing import Callable
|
||||
import json
|
||||
import re
|
||||
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
|
||||
@@ -18,8 +18,6 @@ sig_ctx = {}
|
||||
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', [])
|
||||
@@ -0,0 +1 @@
|
||||
from .struct import EIP712FieldType # noqa
|
||||
@@ -1,5 +1,6 @@
|
||||
from enum import IntEnum, auto
|
||||
|
||||
|
||||
class EIP712FieldType(IntEnum):
|
||||
CUSTOM = 0,
|
||||
INT = auto()
|
||||
@@ -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):
|
||||
@@ -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()
|
||||
@@ -1,4 +1,4 @@
|
||||
ragger[speculos]
|
||||
pytest
|
||||
ecdsa
|
||||
simple-rlp
|
||||
./client/
|
||||
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
from configparser import ConfigParser
|
||||
import app.response_parser as ResponseParser
|
||||
from functools import partial
|
||||
import os
|
||||
import pytest
|
||||
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"
|
||||
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user