from enum import Enum
from multiversx_sdk.ledger.config import LedgerAppConfiguration
from multiversx_sdk.ledger.errors import LedgerError
CLA = 0xED
CONNECTION_ERROR_MSG = "check if device is plugged in, unlocked and on MultiversX app"
# account index is always 0 for MultiversX
DEFAULT_ACCOUNT_INDEX = 0
[docs]
class Instructions(Enum):
SIGN_HASH_TX_INS = 0x07
SIGN_MESSAGE_INS = 0x06
PROVIDE_ESDT_INFO_INS = 0x08
GET_ADDRESS_AUTH_TOKEN_INS = 0x09
[docs]
class Apdu:
cla: int
ins: int
p1: int
p2: int
data: bytes
[docs]
class LedgerApp:
def __init__(self) -> None:
try:
from ledgercomm import Transport
except ImportError as e:
raise ImportError(
"The ledgercomm package is not installed. Please install it using "
"pip install multiversx_sdk[ledger]."
) from e
try:
self.transport = Transport(interface="hid", debug=False) # Nano S/X using HID interface
except Exception:
raise LedgerError(CONNECTION_ERROR_MSG)
[docs]
def set_address(self, address_index: int = 0):
data = DEFAULT_ACCOUNT_INDEX.to_bytes(4, byteorder="big") + address_index.to_bytes(4, byteorder="big")
self.transport.send(cla=0xED, ins=0x05, p1=0x00, p2=0x00, cdata=data)
sw, _ = self.transport.recv()
err = get_error(sw)
if err != "":
raise LedgerError(err)
[docs]
def get_address(self, address_index: int = 0) -> str:
data = DEFAULT_ACCOUNT_INDEX.to_bytes(4, byteorder="big") + address_index.to_bytes(4, byteorder="big")
self.transport.send(cla=0xED, ins=0x03, p1=0x00, p2=0x00, cdata=data)
sw, response = self.transport.recv()
assert isinstance(response, bytes)
err = get_error(sw)
if err != "":
raise LedgerError(CONNECTION_ERROR_MSG + " (" + err + ")")
response_body = response[1:]
address = response_body.decode("utf-8")
return address
[docs]
def get_app_configuration(self) -> LedgerAppConfiguration:
self.transport.send(cla=0xED, ins=0x02, p1=0x00, p2=0x00, cdata=b"")
sw, response = self.transport.recv()
err = get_error(sw)
if err != "":
raise LedgerError(CONNECTION_ERROR_MSG + " (" + err + ")")
return self._load_ledger_config_from_response(response)
[docs]
def get_version(self) -> str:
config = self.get_app_configuration()
return config.version
[docs]
def sign_transaction(self, tx_bytes: bytes) -> str:
return self._do_sign(tx_bytes, Instructions.SIGN_HASH_TX_INS.value)
[docs]
def sign_message(self, message_bytes: bytes) -> str:
return self._do_sign(message_bytes, Instructions.SIGN_MESSAGE_INS.value)
def _do_sign(self, data: bytes, ins_signing_method: int) -> str:
total_size = len(data)
max_chunk_size = 150
apdus: list[Apdu] = []
offset = 0
while offset != total_size:
is_first = offset == 0
apdu = Apdu()
if is_first:
apdu.p1 = 0x00
else:
apdu.p1 = 0x80
has_more = offset + max_chunk_size < total_size
chunk_size = total_size - offset
if has_more:
chunk_size = max_chunk_size
apdu.ins = ins_signing_method
apdu.p2 = 0x00
apdu.cla = CLA
apdu.data = data[offset : offset + chunk_size]
apdus.append(apdu)
offset += chunk_size
return self.get_signature_from_apdus(apdus)
[docs]
def get_signature_from_apdus(self, apdus: list[Apdu]) -> str:
sw = 0
response = b""
for apdu in apdus:
self.transport.send(cla=apdu.cla, ins=apdu.ins, p1=apdu.p1, p2=apdu.p2, cdata=apdu.data)
sw, response = self.transport.recv()
assert len(response)
if len(response) != 65 or response[0] != 64 or get_error(sw) != "":
err_message = "signature failed"
err = get_error(sw)
if err != "":
err_message += ": " + err
raise LedgerError(err_message)
response_body = response[1:]
signature = response_body.hex()
return signature
[docs]
def close(self):
self.transport.close()
def _load_ledger_config_from_response(self, response: bytes) -> LedgerAppConfiguration:
config = LedgerAppConfiguration()
config.data_activated = False
if response[0] == 0x01:
config.data_activated = True
config.account_index = response[1]
config.address_index = response[2]
version = str(response[3]) + "." + str(response[4]) + "." + str(response[5])
config.version = version
return config
[docs]
def get_error(code: int):
errors = {
0x9000: "",
0x6985: "user denied",
0x6D00: "unknown instruction",
0x6E00: "wrong cla",
0x6E10: "signature failed",
0x6E01: "invalid arguments",
0x6E02: "invalid message",
0x6E03: "invalid p1",
0x6E04: "message too long",
0x6E05: "receiver too long",
0x6E06: "amount too long",
0x6E07: "contract data disabled",
0x6E08: "message incomplete",
0x6E09: "wrong tx version",
0x6E0A: "nonce too long",
0x6E0B: "invalid amount",
0x6E0C: "invalid fee",
0x6E0D: "pretty failed",
0x6E0E: "data too long",
0x6E0F: "wrong tx options",
0x6E11: "regular signing is deprecated",
}
return errors.get(code, "unknown error code: " + hex(code))