Skip to content

Commit cbd47d2

Browse files
migrate erdpy ledger support from old repo
1 parent c5f798a commit cbd47d2

13 files changed

Lines changed: 420 additions & 33 deletions

README.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ Elrond Python Command Line Tools and SDK for interacting with the Elrond Network
44
## Documentation
55
[docs.elrond.com](https://docs.elrond.com/sdk-and-tools/erdpy/erdpy/)
66

7-
[pkg.go.dev](https://pkg.go.dev/github.com/ElrondNetwork/elrond-sdk/erdgo)
8-
97
## CLI
108
[CLI](CLI.md)
119

erdpy/CLI.md

Lines changed: 104 additions & 24 deletions
Large diffs are not rendered by default.

erdpy/accounts.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from typing import Any, Union
66

77
from erdpy import constants, errors, utils
8+
from erdpy.errors import LedgerError
89
from erdpy.interfaces import IAccount, IAddress
910
from erdpy.wallet import bech32, generate_pair, pem
1011
from erdpy.wallet.keyfile import get_password, load_from_key_file
@@ -44,11 +45,13 @@ def get_all(self):
4445

4546

4647
class Account(IAccount):
47-
def __init__(self, address: Any = None, pem_file: Union[str, None] = None, pem_index: int = 0, key_file: str = "", pass_file: str = ""):
48+
def __init__(self, address: Any = None, pem_file: Union[str, None] = None, pem_index: int = 0, key_file: str = "", pass_file: str = "",
49+
ledger: bool = False):
4850
self.address = Address(address)
4951
self.pem_file = pem_file
5052
self.pem_index = int(pem_index)
5153
self.nonce: int = 0
54+
self.ledger = ledger
5255

5356
if pem_file:
5457
seed, pubkey = pem.parse(self.pem_file, self.pem_index)
@@ -66,6 +69,8 @@ def sync_nonce(self, proxy: Any):
6669
logger.info(f"Account.sync_nonce() done: {self.nonce}")
6770

6871
def get_seed(self) -> bytes:
72+
if self.ledger:
73+
raise LedgerError("cannot get seed from a Ledger account")
6974
return unhexlify(self.private_key_seed)
7075

7176

erdpy/cli.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import erdpy.cli_data
1414
import erdpy.cli_deps
1515
import erdpy.cli_dispatcher
16+
import erdpy.cli_ledger
1617
import erdpy.cli_network
1718
import erdpy.cli_testnet
1819
import erdpy.cli_transactions
@@ -85,6 +86,7 @@ def setup_parser():
8586
commands.append(erdpy.cli_transactions.setup_parser(subparsers))
8687
commands.append(erdpy.cli_validators.setup_parser(subparsers))
8788
commands.append(erdpy.cli_accounts.setup_parser(subparsers))
89+
commands.append(erdpy.cli_ledger.setup_parser(subparsers))
8890
commands.append(erdpy.cli_wallet.setup_parser(subparsers))
8991
commands.append(erdpy.cli_network.setup_parser(subparsers))
9092
commands.append(erdpy.cli_cost.setup_parser(subparsers))

erdpy/cli_shared.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import argparse
22
import ast
3+
import string
34
import sys
45
from argparse import FileType
56
from typing import Any, List, Text
67

78
from erdpy import config, errors, scope, utils
89
from erdpy.accounts import Account
10+
from erdpy.ledger.ledger_functions import do_get_ledger_address
911
from erdpy.proxy.core import ElrondProxy
1012
from erdpy.transactions import Transaction
1113

@@ -69,18 +71,22 @@ def add_tx_args(sub: Any, with_nonce: bool = True, with_receiver: bool = True, w
6971

7072
sub.add_argument("--chain", default=scope.get_chain_id(), help="the chain identifier (default: %(default)s)")
7173
sub.add_argument("--version", type=int, default=scope.get_tx_version(), help="the transaction version (default: %(default)s)")
74+
sub.add_argument("--options", type=int, default=0, help="the transaction options (default: 0)")
7275

7376

7477
def add_wallet_args(sub: Any):
75-
sub.add_argument("--pem", required=not (utils.is_arg_present("--keyfile", sys.argv)), help="🔑 the PEM file, if keyfile not provided")
78+
sub.add_argument("--pem", required=check_if_sign_method_required("--pem"), help="🔑 the PEM file, if keyfile not provided")
7679
sub.add_argument("--pem-index", default=0, help="🔑 the index in the PEM file (default: %(default)s)")
77-
sub.add_argument("--keyfile", required=not (utils.is_arg_present("--pem", sys.argv)), help="🔑 a JSON keyfile, if PEM not provided")
78-
sub.add_argument("--passfile", required=not (utils.is_arg_present("--pem", sys.argv)), help="🔑 a file containing keyfile's password, if keyfile provided")
80+
sub.add_argument("--keyfile", required=check_if_sign_method_required("--keyfile"), help="🔑 a JSON keyfile, if PEM not provided")
81+
sub.add_argument("--passfile", required=(utils.is_arg_present("--keyfile", sys.argv)), help="🔑 a file containing keyfile's password, if keyfile provided")
82+
sub.add_argument("--ledger", action="store_true", required=check_if_sign_method_required("--ledger"), default=False, help="🔐 bool flag for signing transaction using ledger")
83+
sub.add_argument("--ledger-account-index", type=int, default=0, help="🔐 the index of the account when using Ledger")
84+
sub.add_argument("--ledger-address-index", type=int, default=0, help="🔐 the index of the address when using Ledger")
7985
sub.add_argument("--sender-username", required=False, help="🖄 the username of the sender")
8086

8187

8288
def add_proxy_arg(sub: Any):
83-
sub.add_argument("--proxy", default=scope.get_proxy(), help="🖧 the URL of the proxy (default: %(default)s)")
89+
sub.add_argument("--proxy", default=scope.get_proxy(), help="🔗 the URL of the proxy (default: %(default)s)")
8490

8591

8692
def add_outfile_arg(sub: Any, what: str = ""):
@@ -109,6 +115,9 @@ def prepare_nonce_in_args(args: Any):
109115
account = Account(pem_file=args.pem, pem_index=args.pem_index)
110116
elif args.keyfile and args.passfile:
111117
account = Account(key_file=args.keyfile, pass_file=args.passfile)
118+
elif args.ledger:
119+
address = do_get_ledger_address(account_index=args.ledger_account_index, address_index=args.ledger_address_index)
120+
account = Account(address=address)
112121
else:
113122
raise errors.NoWalletProvided()
114123

@@ -138,3 +147,17 @@ def send_or_simulate(tx: Transaction, args: Any):
138147
elif args.simulate:
139148
response = tx.simulate(ElrondProxy(args.proxy))
140149
utils.dump_out_json(response)
150+
151+
152+
def check_if_sign_method_required(checked_method: string) -> bool:
153+
methods = ["--pem", "--keyfile", "--ledger"]
154+
rest_of_methods = []
155+
for method in methods:
156+
if method != checked_method:
157+
rest_of_methods.append(method)
158+
159+
for method in rest_of_methods:
160+
if utils.is_arg_present(method, sys.argv):
161+
return False
162+
163+
return True

erdpy/cli_transactions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from typing import Any
33

44
from erdpy import cli_shared, utils
5+
from erdpy.ledger.ledger_functions import do_get_ledger_address
56
from erdpy.proxy.core import ElrondProxy
67
from erdpy.transactions import Transaction, do_prepare_transaction
78

@@ -51,6 +52,9 @@ def create_transaction(args: Any):
5152
cli_shared.check_broadcast_args(args)
5253
cli_shared.prepare_nonce_in_args(args)
5354

55+
if args.ledger:
56+
args.ledger_address = do_get_ledger_address(account_index=args.ledger_account_index, address_index=args.ledger_address_index)
57+
5458
if args.data_file:
5559
args.data = utils.read_file(args.data_file)
5660

erdpy/errors.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,3 +178,8 @@ def __init__(self):
178178
class TestnetError(KnownError):
179179
def __init__(self, message: str):
180180
super().__init__(message)
181+
182+
183+
class LedgerError(KnownError):
184+
def __init__(self, message: str):
185+
super().__init__("Ledger error: " + message)

erdpy/ledger/__init__.py

Whitespace-only changes.

erdpy/ledger/config.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import string
2+
3+
4+
def load_ledger_config_from_response(response: bytes):
5+
config = ElrondLedgerAppConfiguration()
6+
7+
config.data_activated = False
8+
if response[0] == 0x01:
9+
config.data_activated = True
10+
11+
config.account_index = response[1]
12+
config.address_index = response[2]
13+
14+
version = str(response[3]) + "." + str(response[4]) + "." + str(response[5])
15+
config.version = version
16+
17+
return config
18+
19+
20+
def compare_versions(version1: string, version2: string) -> int:
21+
version1_tuple = version_tuple(version1)
22+
version2_tuple = version_tuple(version2)
23+
if version1_tuple == version2_tuple:
24+
return 0
25+
if version1_tuple < version2_tuple:
26+
return -1
27+
return 1
28+
29+
30+
def version_tuple(v):
31+
filled = []
32+
for point in v.split("."):
33+
filled.append(point.zfill(8))
34+
return tuple(filled)
35+
36+
37+
class ElrondLedgerAppConfiguration:
38+
data_activated: bool
39+
account_index: int
40+
address_index: int
41+
version: string

erdpy/ledger/ledger_app_handler.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
from ledgercomm import Transport
2+
3+
from erdpy.errors import LedgerError
4+
from erdpy.ledger.config import load_ledger_config_from_response, ElrondLedgerAppConfiguration
5+
6+
SIGN_USING_HASH_VERSION = "1.0.11"
7+
8+
9+
class Apdu:
10+
cla: int
11+
ins: int
12+
p1: int
13+
p2: int
14+
data: bytes
15+
16+
17+
class ElrondLedgerApp:
18+
def __init__(self):
19+
self.transport = Transport(interface="hid", debug=False) # Nano S/X using HID interface
20+
21+
def close(self):
22+
self.transport.close()
23+
24+
def set_address(self, account_index=0, address_index=0):
25+
data = account_index.to_bytes(4, byteorder='big') + address_index.to_bytes(4, byteorder='big')
26+
self.transport.send(cla=0xed, ins=0x05, p1=0x00, p2=0x00, cdata=data)
27+
sw, response = self.transport.recv()
28+
err = get_error(sw)
29+
if err != '':
30+
raise LedgerError(err)
31+
32+
def get_address(self, account_index=0, address_index=0):
33+
data = account_index.to_bytes(4, byteorder='big') + address_index.to_bytes(4, byteorder='big')
34+
self.transport.send(cla=0xed, ins=0x03, p1=0x00, p2=0x00, cdata=data)
35+
sw, response = self.transport.recv()
36+
err = get_error(sw)
37+
if err != '':
38+
raise LedgerError(err)
39+
40+
address = response[1:].decode("utf-8")
41+
return address
42+
43+
def get_app_configuration(self) -> ElrondLedgerAppConfiguration:
44+
self.transport.send(cla=0xed, ins=0x02, p1=0x00, p2=0x00, cdata=b"")
45+
sw, response = self.transport.recv()
46+
err = get_error(sw)
47+
if err != '':
48+
raise LedgerError(err)
49+
return load_ledger_config_from_response(response)
50+
51+
def get_version(self):
52+
config = self.get_app_configuration()
53+
return config.version
54+
55+
def sign_transaction(self, marshaled_tx: bytes, should_use_hash_signing: bool):
56+
total_size = len(marshaled_tx)
57+
max_chunk_size = 150
58+
59+
apdus = list()
60+
61+
offset = 0
62+
while offset != total_size:
63+
is_first = offset == 0
64+
65+
apdu = Apdu()
66+
67+
if is_first:
68+
apdu.p1 = 0x00
69+
else:
70+
apdu.p1 = 0x80
71+
72+
has_more = offset + max_chunk_size < total_size
73+
chunk_size = total_size - offset
74+
if has_more:
75+
chunk_size = max_chunk_size
76+
77+
ins_signing_method = 0x04
78+
if should_use_hash_signing:
79+
ins_signing_method = 0x07
80+
81+
apdu.ins = ins_signing_method
82+
apdu.p2 = 0x00
83+
apdu.cla = 0xed
84+
apdu.data = marshaled_tx[offset:offset + chunk_size]
85+
86+
apdus.append(apdu)
87+
88+
offset += chunk_size
89+
90+
return self.get_signature_from_apdus(apdus)
91+
92+
def get_signature_from_apdus(self, apdus):
93+
sw = []
94+
response = []
95+
for apdu in apdus:
96+
self.transport.send(
97+
cla=apdu.cla,
98+
ins=apdu.ins,
99+
p1=apdu.p1,
100+
p2=apdu.p2,
101+
cdata=apdu.data)
102+
sw, response = self.transport.recv()
103+
104+
if len(response) != 65 or response[0] != 64 or get_error(sw) != '':
105+
err_message = "signature failed"
106+
err = get_error(sw)
107+
if err != '':
108+
err_message += ': ' + err
109+
raise LedgerError(err_message)
110+
111+
signature = response[1:len(response)].hex()
112+
return signature
113+
114+
115+
def get_error(code):
116+
switcher = {
117+
0x9000: '',
118+
0x6985: 'user denied',
119+
0x6D00: 'unknown instruction',
120+
0x6E00: 'wrong cla',
121+
0x6E10: 'signature failed',
122+
0x6E01: 'invalid arguments',
123+
0x6E02: 'invalid message',
124+
0x6E03: 'invalid p1',
125+
0x6E04: 'message too long',
126+
0x6E05: 'receiver too long',
127+
0x6E06: 'amount too long',
128+
0x6E07: 'contract data disabled',
129+
0x6E08: 'message incomplete',
130+
0x6E09: 'wrong tx version',
131+
0x6E0A: 'nonce too long',
132+
0x6E0B: 'invalid amount',
133+
0x6E0C: 'invalid fee',
134+
0x6E0D: 'pretty failed',
135+
0x6E0E: 'data too long',
136+
0x6E0F: 'wrong tx options',
137+
0x6E11: 'regular signing is deprecated',
138+
}
139+
140+
return switcher.get(code, "unknown error code: " + hex(code))

0 commit comments

Comments
 (0)