Skip to content

Commit

Permalink
Merge pull request #466 from LedgerHQ/feat/apa/nft_on_other_chains
Browse files Browse the repository at this point in the history
NFT on other chains
  • Loading branch information
apaillier-ledger authored Sep 7, 2023
2 parents 11974b4 + 52c0270 commit 994b1ce
Show file tree
Hide file tree
Showing 588 changed files with 703 additions and 96 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/ci-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,9 @@ jobs:
- name: Build testing binaries
run: |
mkdir tests/speculos/elfs
make clean && make -j DEBUG=1 NFT_TESTING_KEY=1 BOLOS_SDK=$NANOS_SDK && mv bin/app.elf tests/speculos/elfs/nanos.elf
make clean && make -j DEBUG=1 NFT_TESTING_KEY=1 BOLOS_SDK=$NANOX_SDK && mv bin/app.elf tests/speculos/elfs/nanox.elf
make clean && make -j DEBUG=1 NFT_TESTING_KEY=1 BOLOS_SDK=$NANOSP_SDK && mv bin/app.elf tests/speculos/elfs/nanosp.elf
make clean && make -j DEBUG=1 NFT_STAGING_KEY=1 BOLOS_SDK=$NANOS_SDK && mv bin/app.elf tests/speculos/elfs/nanos.elf
make clean && make -j DEBUG=1 NFT_STAGING_KEY=1 BOLOS_SDK=$NANOX_SDK && mv bin/app.elf tests/speculos/elfs/nanox.elf
make clean && make -j DEBUG=1 NFT_STAGING_KEY=1 BOLOS_SDK=$NANOSP_SDK && mv bin/app.elf tests/speculos/elfs/nanosp.elf
- name: Upload app binaries
uses: actions/upload-artifact@v3
Expand Down Expand Up @@ -171,7 +171,7 @@ jobs:
uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_build.yml@v1
with:
upload_app_binaries_artifact: "ragger_elfs"
flags: "DEBUG=1 CAL_CI_KEY=1 DOMAIN_NAME_TEST_KEY=1"
flags: "DEBUG=1 CAL_TEST_KEY=1 DOMAIN_NAME_TEST_KEY=1 SET_PLUGIN_TEST_KEY=1 NFT_TEST_KEY=1"

jobs-ragger-tests:
name: Run Ragger tests
Expand Down
47 changes: 34 additions & 13 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -136,25 +136,40 @@ endif
endif

# Enables direct data signing without having to specify it in the settings. Useful when testing with speculos.
ALLOW_DATA:=0
ALLOW_DATA?=0
ifneq ($(ALLOW_DATA),0)
DEFINES += HAVE_ALLOW_DATA
endif

# Bypass the signature verification for setExternalPlugin, setPlugin, provideERC20TokenInfo and provideNFTInfo calls
BYPASS_SIGNATURES:=0
BYPASS_SIGNATURES?=0
ifneq ($(BYPASS_SIGNATURES),0)
DEFINES += HAVE_BYPASS_SIGNATURES
endif

# Enable the SET_PLUGIN test key
SET_PLUGIN_TEST_KEY?=0
ifneq ($(SET_PLUGIN_TEST_KEY),0)
DEFINES += HAVE_SET_PLUGIN_TEST_KEY
endif

# NFTs
ifneq ($(TARGET_NAME),TARGET_NANOS)
DEFINES += HAVE_NFT_SUPPORT
# Enable the NFT testing key
NFT_TESTING_KEY:=0
ifneq ($(NFT_TESTING_KEY),0)
DEFINES += HAVE_NFT_TESTING_KEY
DEFINES += HAVE_NFT_SUPPORT
NFT_TEST_KEY?=0
ifneq ($(NFT_TEST_KEY),0)
DEFINES += HAVE_NFT_TEST_KEY
endif
NFT_STAGING_KEY?=0
ifneq ($(NFT_STAGING_KEY),0)
# Key used by the staging backend
DEFINES += HAVE_NFT_STAGING_KEY
endif
endif
ifneq (,$(filter $(DEFINES),HAVE_NFT_TEST_KEY))
ifneq (, $(filter $(DEFINES),HAVE_NFT_STAGING_KEY))
$(error Multiple alternative NFT keys set at once)
endif
endif

# Dynamic memory allocator
Expand All @@ -168,19 +183,25 @@ DEFINES += HAVE_EIP712_FULL_SUPPORT
endif

# CryptoAssetsList key
CAL_TEST_KEY:=0
CAL_CI_KEY:=0
CAL_TEST_KEY?=0
ifneq ($(CAL_TEST_KEY),0)
DEFINES += HAVE_CAL_TEST_KEY
DEFINES += HAVE_CAL_TEST_KEY
endif
CAL_STAGING_KEY?=0
ifneq ($(CAL_STAGING_KEY),0)
# Key used by the staging CAL
DEFINES += HAVE_CAL_STAGING_KEY
endif
ifneq ($(CAL_CI_KEY),0)
DEFINES += HAVE_CAL_CI_KEY
ifneq (,$(filter $(DEFINES),HAVE_CAL_TEST_KEY))
ifneq (, $(filter $(DEFINES),HAVE_CAL_STAGING_KEY))
$(error Multiple alternative CAL keys set at once)
endif
endif

# ENS
ifneq ($(TARGET_NAME),TARGET_NANOS)
DEFINES += HAVE_DOMAIN_NAME
DOMAIN_NAME_TEST_KEY:=0
DOMAIN_NAME_TEST_KEY?=0
ifneq ($(DOMAIN_NAME_TEST_KEY),0)
DEFINES += HAVE_DOMAIN_NAME_TEST_KEY
endif
Expand Down
1 change: 1 addition & 0 deletions client/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ requires-python = ">=3.7"
dependencies = [
"ragger[speculos]",
"simple-rlp",
"pysha3",
]

[tools.setuptools]
Expand Down
160 changes: 140 additions & 20 deletions client/src/ledger_app_clients/ethereum/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,14 @@


WEI_IN_ETH = 1e+18
GWEI_IN_ETH = 1e+9

class TxData:
selector: bytes
parameters: list[bytes]
def __init__(self, selector: bytes, params: list[bytes]):
self.selector = selector
self.parameters = params

class StatusWord(IntEnum):
OK = 0x9000
Expand Down Expand Up @@ -96,31 +103,79 @@ def eip712_filtering_message_info(self, name: str, filters_count: int, sig: byte
def eip712_filtering_show_field(self, name: str, sig: bytes):
return self._send(self._cmd_builder.eip712_filtering_show_field(name, sig))

def send_fund(self,
bip32_path: str,
nonce: int,
gas_price: int,
gas_limit: int,
to: bytes,
amount: float,
chain_id: int):
data: List[Union[int, bytes]] = list()
data.append(nonce)
data.append(gas_price)
data.append(gas_limit)
data.append(to)
data.append(int(amount * WEI_IN_ETH))
data.append(bytes())
data.append(chain_id)
data.append(bytes())
data.append(bytes())

chunks = self._cmd_builder.sign(bip32_path, rlp.encode(data))
def _sign(self, bip32_path: str, raw_tx: bytes):
chunks = self._cmd_builder.sign(bip32_path, raw_tx)
for chunk in chunks[:-1]:
with self._send(chunk):
pass
return self._send(chunks[-1])

def _data_to_payload(self, data: TxData) -> bytes:
payload = bytearray(data.selector)
for param in data.parameters:
payload += param.rjust(32, b'\x00')
return payload

def _sign_common(self,
tx: list,
gas_price: float,
gas_limit: int,
destination: bytes,
amount: float,
data: TxData):
tx.append(int(gas_price * GWEI_IN_ETH))
tx.append(gas_limit)
tx.append(destination)
if amount > 0:
tx.append(int(amount * WEI_IN_ETH))
else:
tx.append(bytes())
if data is not None:
tx.append(self._data_to_payload(data))
else:
tx.append(bytes())
return tx

def sign_legacy(self,
bip32_path: str,
nonce: int,
gas_price: float,
gas_limit: int,
destination: bytes,
amount: float,
chain_id: int,
data: TxData = None):
tx = list()
tx.append(nonce)
tx = self._sign_common(tx, gas_price, gas_limit, destination, amount, data)
tx.append(chain_id)
tx.append(bytes())
tx.append(bytes())
return self._sign(bip32_path, rlp.encode(tx))

def sign_1559(self,
bip32_path: str,
chain_id: int,
nonce: int,
max_prio_gas_price: float,
max_gas_price: float,
gas_limit: int,
destination: bytes,
amount: float,
data: TxData = None,
access_list = list()):
tx = list()
tx.append(chain_id)
tx.append(nonce)
tx.append(int(max_prio_gas_price * GWEI_IN_ETH))
tx = self._sign_common(tx, max_gas_price, gas_limit, destination, amount, data)
tx.append(access_list)
tx.append(False)
tx.append(bytes())
tx.append(bytes())
# prefix with transaction type
return self._sign(bip32_path, b'\x02' + rlp.encode(tx))

def get_challenge(self):
return self._send(self._cmd_builder.get_challenge())

Expand Down Expand Up @@ -151,3 +206,68 @@ def provide_domain_name(self, challenge: int, name: str, addr: bytes):
with self._send(chunk):
pass
return self._send(chunks[-1])

def set_plugin(self,
plugin_name: str,
contract_addr: bytes,
selector: bytes,
chain_id: int,
type_: int = 1,
version: int = 1,
key_id: int = 2,
algo_id: int = 1,
sig: Optional[bytes] = None):
if sig is None:
# Temporarily get a command with an empty signature to extract the payload and
# compute the signature on it
tmp = self._cmd_builder.set_plugin(type_,
version,
plugin_name,
contract_addr,
selector,
chain_id,
key_id,
algo_id,
bytes())
# skip APDU header & empty sig
sig = sign_data(Key.SET_PLUGIN, tmp[5:-1])
return self._send(self._cmd_builder.set_plugin(type_,
version,
plugin_name,
contract_addr,
selector,
chain_id,
key_id,
algo_id,
sig))

def provide_nft_metadata(self,
collection: str,
addr: bytes,
chain_id: int,
type_: int = 1,
version: int = 1,
key_id: int = 1,
algo_id: int = 1,
sig: Optional[bytes] = None):
if sig is None:
# Temporarily get a command with an empty signature to extract the payload and
# compute the signature on it
tmp = self._cmd_builder.provide_nft_information(type_,
version,
collection,
addr,
chain_id,
key_id,
algo_id,
bytes())
# skip APDU header & empty sig
sig = sign_data(Key.NFT, tmp[5:-1])
return self._send(self._cmd_builder.provide_nft_information(type_,
version,
collection,
addr,
chain_id,
key_id,
algo_id,
sig))
48 changes: 48 additions & 0 deletions client/src/ledger_app_clients/ethereum/command_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
class InsType(IntEnum):
GET_PUBLIC_ADDR = 0x02
SIGN = 0x04
PROVIDE_NFT_INFORMATION = 0x14
SET_PLUGIN = 0x16
EIP712_SEND_STRUCT_DEF = 0x1a
EIP712_SEND_STRUCT_IMPL = 0x1c
EIP712_SEND_FILTERING = 0x1e
Expand Down Expand Up @@ -219,3 +221,49 @@ def get_public_addr(self,
int(display),
int(chaincode),
payload)

def set_plugin(self,
type_: int,
version: int,
plugin_name: str,
contract_addr: bytes,
selector: bytes,
chain_id: int,
key_id: int,
algo_id: int,
sig: bytes) -> bytes:
payload = bytearray()
payload.append(type_)
payload.append(version)
payload.append(len(plugin_name))
payload += plugin_name.encode()
payload += contract_addr
payload += selector
payload += struct.pack(">Q", chain_id)
payload.append(key_id)
payload.append(algo_id)
payload.append(len(sig))
payload += sig
return self._serialize(InsType.SET_PLUGIN, 0x00, 0x00, payload)

def provide_nft_information(self,
type_: int,
version: int,
collection_name: str,
addr: bytes,
chain_id: int,
key_id: int,
algo_id: int,
sig: bytes):
payload = bytearray()
payload.append(type_)
payload.append(version)
payload.append(len(collection_name))
payload += collection_name.encode()
payload += addr
payload += struct.pack(">Q", chain_id)
payload.append(key_id)
payload.append(algo_id)
payload.append(len(sig))
payload += sig
return self._serialize(InsType.PROVIDE_NFT_INFORMATION, 0x00, 0x00, payload)
2 changes: 2 additions & 0 deletions client/src/ledger_app_clients/ethereum/keychain.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
class Key(Enum):
CAL = auto()
DOMAIN_NAME = auto()
SET_PLUGIN = auto()
NFT = auto()


_keys: Dict[Key, SigningKey] = dict()
Expand Down
8 changes: 8 additions & 0 deletions client/src/ledger_app_clients/ethereum/keychain/nft.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-----BEGIN EC PARAMETERS-----
BgUrgQQACg==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MHQCAQEEIK69Gt4o0bzkOaEwUE5X2tI+Ks80FQi785Co+6woU9hioAcGBSuBBAAK
oUQDQgAEPPtfsxkF9L052dU1pAwmqrUcXX0yGbKKyUK5gPsgbPswtRzC3iEZrAOO
uw191lQXcCBKPO06eeKLMvu2cmRowA==
-----END EC PRIVATE KEY-----
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-----BEGIN EC PARAMETERS-----
BgUrgQQACg==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MHQCAQEEIBErwcYvqeKSOlmQ/j3xPkVcwFf+j1aiMsA+RabczvN7oAcGBSuBBAAK
oUQDQgAEwFW8Ts8FXi2FCF01Eno95nBcf4hQVc1wceh2cb8ZH+M8yPAavC8ofIGa
FIq+G1gd8bSUCvXU3DpOa2AZF3ErNw==
-----END EC PRIVATE KEY-----
4 changes: 4 additions & 0 deletions client/src/ledger_app_clients/ethereum/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import sha3

def get_selector_from_function(fn: str) -> bytes:
return sha3.keccak_256(fn.encode()).digest()[0:4]
2 changes: 1 addition & 1 deletion doc/ethapp.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -528,7 +528,7 @@ The plugin names `ERC20`, `ERC721` and `ERC1155` are reserved. Additional plugin

The signature is computed on

type || version || len(pluginName) || pluginName || address || selector || chainId || keyId || algorithmId || len(signature) || signature
type || version || len(pluginName) || pluginName || address || selector || chainId || keyId || algorithmId

#### Coding

Expand Down
Loading

0 comments on commit 994b1ce

Please sign in to comment.