Skip to content

Instantly share code, notes, and snippets.

@bargst
Created June 3, 2018 21:08
Show Gist options
  • Save bargst/5f896e4a5984593d43f1b4eb66f79d68 to your computer and use it in GitHub Desktop.
Save bargst/5f896e4a5984593d43f1b4eb66f79d68 to your computer and use it in GitHub Desktop.
Python Ledger Account list and sign
# Requirement: pip install ledgerblue
from ledgerblue.comm import getDongle
import struct
ETH_DERIVATION_PATH_PREFIX = "44'/60'/0'/"
class LedgerAccount:
"""
Ledger Ethereum App Protocol Spec is located at:
<https://github.com/LedgerHQ/blue-app-eth/blob/master/doc/ethapp.asc>
References:
- https://github.com/LedgerHQ/blue-app-eth/blob/master/getPublicKey.py
- https://github.com/bargst/pyethoff/blob/master/tx_sign.py
"""
def __init__(self):
self.dongle = getDongle(debug=False)
def _parse_bip32_path(self, offset):
"""
Convert an offset to a bytes payload to be sent to the ledger
representing bip32 derivation path.
"""
path = ETH_DERIVATION_PATH_PREFIX + str(offset)
result = bytes()
elements = path.split('/')
for pathElement in elements:
element = pathElement.split("'")
if len(element) == 1:
result = result + struct.pack(">I", int(element[0]))
else:
result = result + struct.pack(">I", 0x80000000 | int(element[0]))
return result
def get_address(self, offset):
"""
Query the ledger device for a public ethereum address.
Offset is the number in the HD wallet tree
"""
donglePath = self._parse_bip32_path(offset)
apdu = bytes.fromhex('e0020000') # https://github.com/LedgerHQ/blue-app-eth/blob/master/doc/ethapp.asc#get-eth-public-address
apdu += bytes([len(donglePath) + 1])
apdu += bytes([len(donglePath) // 4])
apdu += donglePath
result = self.dongle.exchange(apdu, timeout=60)
# Parse result
offset = 1 + result[0]
address = result[offset + 1 : offset + 1 + result[offset]]
return f'0x{address.decode()}'
def get_offset(self, address):
"""
Convert an address to the HD wallet tree offset
"""
offset = 0
while address != self.get_address(offset):
offset += 1
return offset
def list(self, limit=5, page=0):
"""
List Ethereum HD wallet adrress of the ledger device
"""
return list(map(lambda offset: self.get_address(offset), range(page*limit, (page+1)*limit)))
def sign(self, rlp_encoded_tx, offset=None, address=''):
"""
Sign an RLP encoded transaction
"""
if offset is None:
# Convert an address to an offset
if address == '':
raise Exception('Invalid offset and address provided')
else:
#TODO check address validity
offset = self.get_offset(address)
donglePath = self._parse_bip32_path(offset)
apdu = bytes.fromhex('e0040000') # https://github.com/LedgerHQ/blue-app-eth/blob/master/doc/ethapp.asc#sign-eth-transaction
apdu += bytes([len(donglePath) + 1 + len(rlp_encoded_tx)])
apdu += bytes([len(donglePath) // 4])
apdu += donglePath
apdu += rlp_encoded_tx
# Sign with dongle
result = self.dongle.exchange(apdu, timeout=60)
# Retrieve VRS from sig
v = result[0]
r = int.from_bytes(result[1:1 + 32], 'big')
s = int.from_bytes(result[1 + 32: 1 + 32 + 32], 'big')
return (v, r, s)
# Requirements: pip install rlp eth-account
from ledger import LedgerAccount
import rlp
from eth_account.internal.transactions import serializable_unsigned_transaction_from_dict, encode_transaction
ledger = LedgerAccount()
print(ledger.list())
transaction = {
# Note that the address must be in checksum format:
'to': '0xF0109fC8DF283027b6285cc889F5aA624EaC1F55',
'value': 0,
'gas': 2000000,
'gasPrice': 65536,
'nonce': 0,
'chainId': 1
}
unsigned_transaction = serializable_unsigned_transaction_from_dict(transaction)
(v, r, s) = ledger.sign(rlp.encode(unsigned_transaction), offset=0)
encoded_transaction = encode_transaction(unsigned_transaction, vrs=(v, r, s))
print('0x' + encoded_transaction.hex())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment