diff --git a/tests/ragger/abis/erc1155.json b/tests/ragger/abis/erc1155.json new file mode 100644 index 0000000..3c53ad8 --- /dev/null +++ b/tests/ragger/abis/erc1155.json @@ -0,0 +1,276 @@ +[ + { + "anonymous" : false, + "inputs" : [ + { + "indexed" : true, + "internalType" : "address", + "name" : "_owner", + "type" : "address" + }, + { + "indexed" : true, + "internalType" : "address", + "name" : "_operator", + "type" : "address" + }, + { + "indexed" : false, + "internalType" : "bool", + "name" : "_approved", + "type" : "bool" + } + ], + "name" : "ApprovalForAll", + "type" : "event" + }, + { + "anonymous" : false, + "inputs" : [ + { + "indexed" : true, + "internalType" : "address", + "name" : "_operator", + "type" : "address" + }, + { + "indexed" : true, + "internalType" : "address", + "name" : "_from", + "type" : "address" + }, + { + "indexed" : true, + "internalType" : "address", + "name" : "_to", + "type" : "address" + }, + { + "indexed" : false, + "internalType" : "uint256[]", + "name" : "_ids", + "type" : "uint256[]" + }, + { + "indexed" : false, + "internalType" : "uint256[]", + "name" : "_values", + "type" : "uint256[]" + } + ], + "name" : "TransferBatch", + "type" : "event" + }, + { + "anonymous" : false, + "inputs" : [ + { + "indexed" : true, + "internalType" : "address", + "name" : "_operator", + "type" : "address" + }, + { + "indexed" : true, + "internalType" : "address", + "name" : "_from", + "type" : "address" + }, + { + "indexed" : true, + "internalType" : "address", + "name" : "_to", + "type" : "address" + }, + { + "indexed" : false, + "internalType" : "uint256", + "name" : "_id", + "type" : "uint256" + }, + { + "indexed" : false, + "internalType" : "uint256", + "name" : "_value", + "type" : "uint256" + } + ], + "name" : "TransferSingle", + "type" : "event" + }, + { + "anonymous" : false, + "inputs" : [ + { + "indexed" : false, + "internalType" : "string", + "name" : "_value", + "type" : "string" + }, + { + "indexed" : true, + "internalType" : "uint256", + "name" : "_id", + "type" : "uint256" + } + ], + "name" : "URI", + "type" : "event" + }, + { + "inputs" : [ + { + "internalType" : "address", + "name" : "_owner", + "type" : "address" + }, + { + "internalType" : "uint256", + "name" : "_id", + "type" : "uint256" + } + ], + "name" : "balanceOf", + "outputs" : [ + { + "internalType" : "uint256", + "name" : "", + "type" : "uint256" + } + ], + "stateMutability" : "view", + "type" : "function" + }, + { + "inputs" : [ + { + "internalType" : "address[]", + "name" : "_owners", + "type" : "address[]" + }, + { + "internalType" : "uint256[]", + "name" : "_ids", + "type" : "uint256[]" + } + ], + "name" : "balanceOfBatch", + "outputs" : [ + { + "internalType" : "uint256[]", + "name" : "", + "type" : "uint256[]" + } + ], + "stateMutability" : "view", + "type" : "function" + }, + { + "inputs" : [ + { + "internalType" : "address", + "name" : "_owner", + "type" : "address" + }, + { + "internalType" : "address", + "name" : "_operator", + "type" : "address" + } + ], + "name" : "isApprovedForAll", + "outputs" : [ + { + "internalType" : "bool", + "name" : "", + "type" : "bool" + } + ], + "stateMutability" : "view", + "type" : "function" + }, + { + "inputs" : [ + { + "internalType" : "address", + "name" : "_from", + "type" : "address" + }, + { + "internalType" : "address", + "name" : "_to", + "type" : "address" + }, + { + "internalType" : "uint256[]", + "name" : "_ids", + "type" : "uint256[]" + }, + { + "internalType" : "uint256[]", + "name" : "_values", + "type" : "uint256[]" + }, + { + "internalType" : "bytes", + "name" : "_data", + "type" : "bytes" + } + ], + "name" : "safeBatchTransferFrom", + "outputs" : [], + "stateMutability" : "nonpayable", + "type" : "function" + }, + { + "inputs" : [ + { + "internalType" : "address", + "name" : "_from", + "type" : "address" + }, + { + "internalType" : "address", + "name" : "_to", + "type" : "address" + }, + { + "internalType" : "uint256", + "name" : "_id", + "type" : "uint256" + }, + { + "internalType" : "uint256", + "name" : "_value", + "type" : "uint256" + }, + { + "internalType" : "bytes", + "name" : "_data", + "type" : "bytes" + } + ], + "name" : "safeTransferFrom", + "outputs" : [], + "stateMutability" : "nonpayable", + "type" : "function" + }, + { + "inputs" : [ + { + "internalType" : "address", + "name" : "_operator", + "type" : "address" + }, + { + "internalType" : "bool", + "name" : "_approved", + "type" : "bool" + } + ], + "name" : "setApprovalForAll", + "outputs" : [], + "stateMutability" : "nonpayable", + "type" : "function" + } +] diff --git a/tests/ragger/abis/erc721.json b/tests/ragger/abis/erc721.json new file mode 100644 index 0000000..e00d5ca --- /dev/null +++ b/tests/ragger/abis/erc721.json @@ -0,0 +1,268 @@ +[ + { + "anonymous" : false, + "inputs" : [ + { + "indexed" : true, + "internalType" : "address", + "name" : "_owner", + "type" : "address" + }, + { + "indexed" : true, + "internalType" : "address", + "name" : "_approved", + "type" : "address" + }, + { + "indexed" : true, + "internalType" : "uint256", + "name" : "_tokenId", + "type" : "uint256" + } + ], + "name" : "Approval", + "type" : "event" + }, + { + "anonymous" : false, + "inputs" : [ + { + "indexed" : true, + "internalType" : "address", + "name" : "_owner", + "type" : "address" + }, + { + "indexed" : true, + "internalType" : "address", + "name" : "_operator", + "type" : "address" + }, + { + "indexed" : false, + "internalType" : "bool", + "name" : "_approved", + "type" : "bool" + } + ], + "name" : "ApprovalForAll", + "type" : "event" + }, + { + "anonymous" : false, + "inputs" : [ + { + "indexed" : true, + "internalType" : "address", + "name" : "_from", + "type" : "address" + }, + { + "indexed" : true, + "internalType" : "address", + "name" : "_to", + "type" : "address" + }, + { + "indexed" : true, + "internalType" : "uint256", + "name" : "_tokenId", + "type" : "uint256" + } + ], + "name" : "Transfer", + "type" : "event" + }, + { + "inputs" : [ + { + "internalType" : "address", + "name" : "_approved", + "type" : "address" + }, + { + "internalType" : "uint256", + "name" : "_tokenId", + "type" : "uint256" + } + ], + "name" : "approve", + "outputs" : [], + "stateMutability" : "payable", + "type" : "function" + }, + { + "inputs" : [ + { + "internalType" : "address", + "name" : "_owner", + "type" : "address" + } + ], + "name" : "balanceOf", + "outputs" : [ + { + "internalType" : "uint256", + "name" : "", + "type" : "uint256" + } + ], + "stateMutability" : "view", + "type" : "function" + }, + { + "inputs" : [ + { + "internalType" : "uint256", + "name" : "_tokenId", + "type" : "uint256" + } + ], + "name" : "getApproved", + "outputs" : [ + { + "internalType" : "address", + "name" : "", + "type" : "address" + } + ], + "stateMutability" : "view", + "type" : "function" + }, + { + "inputs" : [ + { + "internalType" : "address", + "name" : "_owner", + "type" : "address" + }, + { + "internalType" : "address", + "name" : "_operator", + "type" : "address" + } + ], + "name" : "isApprovedForAll", + "outputs" : [ + { + "internalType" : "bool", + "name" : "", + "type" : "bool" + } + ], + "stateMutability" : "view", + "type" : "function" + }, + { + "inputs" : [ + { + "internalType" : "uint256", + "name" : "_tokenId", + "type" : "uint256" + } + ], + "name" : "ownerOf", + "outputs" : [ + { + "internalType" : "address", + "name" : "", + "type" : "address" + } + ], + "stateMutability" : "view", + "type" : "function" + }, + { + "inputs" : [ + { + "internalType" : "address", + "name" : "_from", + "type" : "address" + }, + { + "internalType" : "address", + "name" : "_to", + "type" : "address" + }, + { + "internalType" : "uint256", + "name" : "_tokenId", + "type" : "uint256" + } + ], + "name" : "safeTransferFrom", + "outputs" : [], + "stateMutability" : "payable", + "type" : "function" + }, + { + "inputs" : [ + { + "internalType" : "address", + "name" : "_from", + "type" : "address" + }, + { + "internalType" : "address", + "name" : "_to", + "type" : "address" + }, + { + "internalType" : "uint256", + "name" : "_tokenId", + "type" : "uint256" + }, + { + "internalType" : "bytes", + "name" : "data", + "type" : "bytes" + } + ], + "name" : "safeTransferFrom", + "outputs" : [], + "stateMutability" : "payable", + "type" : "function" + }, + { + "inputs" : [ + { + "internalType" : "address", + "name" : "_operator", + "type" : "address" + }, + { + "internalType" : "bool", + "name" : "_approved", + "type" : "bool" + } + ], + "name" : "setApprovalForAll", + "outputs" : [], + "stateMutability" : "nonpayable", + "type" : "function" + }, + { + "inputs" : [ + { + "internalType" : "address", + "name" : "_from", + "type" : "address" + }, + { + "internalType" : "address", + "name" : "_to", + "type" : "address" + }, + { + "internalType" : "uint256", + "name" : "_tokenId", + "type" : "uint256" + } + ], + "name" : "transferFrom", + "outputs" : [], + "stateMutability" : "payable", + "type" : "function" + } +] diff --git a/tests/ragger/test_nft.py b/tests/ragger/test_nft.py index 1a96a53..be22144 100644 --- a/tests/ragger/test_nft.py +++ b/tests/ragger/test_nft.py @@ -1,17 +1,21 @@ import pytest +from typing import Optional, Any from pathlib import Path from typing import Callable from ragger.error import ExceptionRAPDU from ragger.firmware import Firmware from ragger.backend import BackendInterface from ragger.navigator import Navigator, NavInsID -from ledger_app_clients.ethereum.client import EthAppClient, TxData, StatusWord -from ledger_app_clients.ethereum.settings import SettingID, settings_toggle -from eth_utils import function_signature_to_4byte_selector -import struct +from ledger_app_clients.ethereum.client import EthAppClient, StatusWord +import ledger_app_clients.ethereum.response_parser as ResponseParser +from ledger_app_clients.ethereum.utils import get_selector_from_data, recover_transaction +from web3 import Web3 +import json +import os ROOT_SCREENSHOT_PATH = Path(__file__).parent +ABIS_FOLDER = "%s/abis" % (os.path.dirname(__file__)) BIP32_PATH = "m/44'/60'/0'/0/0" NONCE = 21 @@ -21,23 +25,25 @@ FROM = bytes.fromhex("1122334455667788990011223344556677889900") TO = bytes.fromhex("0099887766554433221100998877665544332211") NFTS = [ (1, 3), (5, 2), (7, 4) ] # tuples of (token_id, amount) DATA = "Some data".encode() +DEVICE_ADDR: Optional[bytes] = None class NFTCollection: addr: bytes name: str chain_id: int - def __init__(self, addr: bytes, name: str, chain_id: int): + def __init__(self, addr: bytes, name: str, chain_id: int, contract): self.addr = addr self.name = name self.chain_id = chain_id + self.contract = contract class Action: - fn: str - data_fn: Callable + fn_name: str + fn_args: list[Any] nav_fn: Callable - def __init__(self, fn: str, data_fn: Callable, nav_fn: Callable): - self.fn = fn - self.data_fn = data_fn + def __init__(self, fn_name: str, fn_args: list[Any], nav_fn: Callable): + self.fn_name = fn_name + self.fn_args = fn_args self.nav_fn = nav_fn def common_nav_nft(is_nano: bool, nano_steps: int, stax_steps: int, reject: bool) -> list[NavInsID]: @@ -59,7 +65,7 @@ def common_nav_nft(is_nano: bool, nano_steps: int, stax_steps: int, reject: bool return moves def snapshot_test_name(nft_type: str, fn: str, chain_id: int, reject: bool) -> str: - name = "%s_%s_%s" % (nft_type, fn.split("(")[0], str(chain_id)) + name = "%s_%s_%s" % (nft_type, fn, str(chain_id)) if reject: name += "-rejected" return name @@ -71,34 +77,48 @@ def common_test_nft(fw: Firmware, action: Action, reject: bool, plugin_name: str): + global DEVICE_ADDR app_client = EthAppClient(back) - selector = function_signature_to_4byte_selector(action.fn) if app_client._client.firmware.name == "nanos": pytest.skip("Not supported on LNS") + + if DEVICE_ADDR is None: # to only have to request it once + with app_client.get_public_addr(display=False): + pass + _, DEVICE_ADDR, _ = ResponseParser.pk_addr(app_client.response().data) + + data = collec.contract.encodeABI(action.fn_name, action.fn_args) with app_client.set_plugin(plugin_name, collec.addr, - selector, - 1): + get_selector_from_data(data), + collec.chain_id): pass with app_client.provide_nft_metadata(collec.name, collec.addr, collec.chain_id): pass - with app_client.sign_legacy(BIP32_PATH, - NONCE, - GAS_PRICE, - GAS_LIMIT, - collec.addr, - 0, - collec.chain_id, - action.data_fn(action)): + tx_params = { + "nonce": NONCE, + "gasPrice": Web3.to_wei(GAS_PRICE, "gwei"), + "gas": GAS_LIMIT, + "to": collec.addr, + "value": 0, + "chainId": collec.chain_id, + "data": data, + } + with app_client.sign(BIP32_PATH, tx_params): nav.navigate_and_compare(ROOT_SCREENSHOT_PATH, snapshot_test_name(plugin_name.lower(), - action.fn, + action.fn_name, collec.chain_id, reject), action.nav_fn(fw.is_nano, collec.chain_id, reject)) + # verify signature + vrs = ResponseParser.signature(app_client.response().data) + addr = recover_transaction(tx_params, vrs) + assert addr == DEVICE_ADDR + def common_test_nft_reject(test_fn: Callable, fw: Firmware, @@ -116,48 +136,14 @@ def common_test_nft_reject(test_fn: Callable, # ERC-721 ERC721_PLUGIN = "ERC721" -ERC721_SAFE_TRANSFER_FROM_DATA = "safeTransferFrom(address,address,uint256,bytes)" -ERC721_SAFE_TRANSFER_FROM = "safeTransferFrom(address,address,uint256)" -ERC721_TRANSFER_FROM = "transferFrom(address,address,uint256)" -ERC721_APPROVE = "approve(address,uint256)" -ERC721_SET_APPROVAL_FOR_ALL = "setApprovalForAll(address,bool)" -## data formatting functions - -def data_erc721_transfer_from(action: Action) -> TxData: - return TxData( - function_signature_to_4byte_selector(action.fn), - [ - FROM, - TO, - struct.pack(">H", NFTS[0][0]) - ] +with open("%s/erc721.json" % (ABIS_FOLDER)) as file: + contract_erc721 = Web3().eth.contract( + abi=json.load(file), + address=bytes(20) ) -def data_erc721_safe_transfer_from_data(action: Action) -> TxData: - txd = data_erc721_transfer_from(action) - txd.parameters += [ DATA ] - return txd - -def data_erc721_approve(action: Action) -> TxData: - return TxData( - function_signature_to_4byte_selector(action.fn), - [ - TO, - struct.pack(">H", NFTS[0][0]) - ] - ) - -def data_erc721_set_approval_for_all(action: Action) -> TxData: - return TxData( - function_signature_to_4byte_selector(action.fn), - [ - TO, - struct.pack("b", False) - ] - ) - -## ui nav functions +# ui nav functions def nav_erc721_transfer_from(is_nano: bool, chain_id: int, @@ -190,30 +176,33 @@ def nav_erc721_set_approval_for_all(is_nano: bool, collecs_721 = [ NFTCollection(bytes.fromhex("bc4ca0eda7647a8ab7c2061c2e118a18a936f13d"), "Bored Ape Yacht Club", - 1), + 1, + contract_erc721), NFTCollection(bytes.fromhex("670fd103b1a08628e9557cd66b87ded841115190"), "y00ts", - 137), + 137, + contract_erc721), NFTCollection(bytes.fromhex("2909cf13e458a576cdd9aab6bd6617051a92dacf"), "goerlirocks", - 5) + 5, + contract_erc721), ] actions_721 = [ - Action(ERC721_SAFE_TRANSFER_FROM_DATA, - data_erc721_safe_transfer_from_data, + Action("safeTransferFrom", + [FROM, TO, NFTS[0][0], DATA], nav_erc721_transfer_from), - Action(ERC721_SAFE_TRANSFER_FROM, - data_erc721_transfer_from, + Action("safeTransferFrom", + [FROM, TO, NFTS[0][0]], nav_erc721_transfer_from), - Action(ERC721_TRANSFER_FROM, - data_erc721_transfer_from, + Action("transferFrom", + [FROM, TO, NFTS[0][0]], nav_erc721_transfer_from), - Action(ERC721_APPROVE, - data_erc721_approve, + Action("approve", + [TO, NFTS[0][0]], nav_erc721_approve), - Action(ERC721_SET_APPROVAL_FOR_ALL, - data_erc721_set_approval_for_all, - nav_erc721_set_approval_for_all) + Action("setApprovalForAll", + [TO, False], + nav_erc721_set_approval_for_all), ] @@ -251,51 +240,15 @@ def test_erc721_reject(firmware: Firmware, # ERC-1155 ERC1155_PLUGIN = "ERC1155" -ERC1155_SAFE_TRANSFER_FROM = "safeTransferFrom(address,address,uint256,uint256,bytes)" -ERC1155_SAFE_BATCH_TRANSFER_FROM = "safeBatchTransferFrom(address,address,uint256[],uint256[],bytes)" -ERC1155_SET_APPROVAL_FOR_ALL = "setApprovalForAll(address,bool)" -## data formatting functions - -def data_erc1155_safe_transfer_from(action: Action) -> TxData: - return TxData( - function_signature_to_4byte_selector(action.fn), - [ - FROM, - TO, - struct.pack(">H", NFTS[0][0]), - struct.pack(">H", NFTS[0][1]), - DATA - ] +with open("%s/erc1155.json" % (ABIS_FOLDER)) as file: + contract_erc1155 = Web3().eth.contract( + abi=json.load(file), + address=bytes(20) ) -def data_erc1155_safe_batch_transfer_from(action: Action) -> TxData: - data = TxData( - function_signature_to_4byte_selector(action.fn), - [ - FROM, - TO - ]) - data.parameters += [ int(32 * 4).to_bytes(8, "big") ] # token_ids offset - data.parameters += [int(32 * (4 + len(NFTS) + 1)).to_bytes(8, "big") ] # amounts offset - data.parameters += [ int(len(NFTS)).to_bytes(8, "big") ] # token_ids length - for nft in NFTS: - data.parameters += [ struct.pack(">H", nft[0]) ] # token_id - data.parameters += [ int(len(NFTS)).to_bytes(8, "big") ] # amounts length - for nft in NFTS: - data.parameters += [ struct.pack(">H", nft[1]) ] # amount - return data -def data_erc1155_set_approval_for_all(action: Action) -> TxData: - return TxData( - function_signature_to_4byte_selector(action.fn), - [ - TO, - struct.pack("b", False) - ] - ) - -## ui nav functions +# ui nav functions def nav_erc1155_safe_transfer_from(is_nano: bool, chain_id: int, @@ -326,24 +279,33 @@ def nav_erc1155_set_approval_for_all(is_nano: bool, collecs_1155 = [ NFTCollection(bytes.fromhex("495f947276749ce646f68ac8c248420045cb7b5e"), "OpenSea Shared Storefront", - 1), + 1, + contract_erc1155), NFTCollection(bytes.fromhex("2953399124f0cbb46d2cbacd8a89cf0599974963"), "OpenSea Collections", - 137), + 137, + contract_erc1155), NFTCollection(bytes.fromhex("f4910c763ed4e47a585e2d34baa9a4b611ae448c"), "OpenSea Collections", - 5) + 5, + contract_erc1155), ] actions_1155 = [ - Action(ERC1155_SAFE_TRANSFER_FROM, - data_erc1155_safe_transfer_from, + Action("safeTransferFrom", + [FROM, TO, NFTS[0][0], NFTS[0][1], DATA], nav_erc1155_safe_transfer_from), - Action(ERC1155_SAFE_BATCH_TRANSFER_FROM, - data_erc1155_safe_batch_transfer_from, + Action("safeBatchTransferFrom", + [ + FROM, + TO, + list(map(lambda nft: nft[0], NFTS)), + list(map(lambda nft: nft[1], NFTS)), + DATA + ], nav_erc1155_safe_batch_transfer_from), - Action(ERC1155_SET_APPROVAL_FOR_ALL, - data_erc1155_set_approval_for_all, - nav_erc1155_set_approval_for_all) + Action("setApprovalForAll", + [TO, False], + nav_erc1155_set_approval_for_all), ] @pytest.fixture(params=collecs_1155) def collec_1155(request) -> bool: