parent
168ab2944c
commit
c2c0900c5d
@ -0,0 +1 @@
|
||||
Signed Ethereum network and token definitions from host
|
@ -0,0 +1,154 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from trezor import protobuf, utils
|
||||
from trezor.crypto.curve import ed25519
|
||||
from trezor.crypto.hashlib import sha256
|
||||
from trezor.enums import EthereumDefinitionType
|
||||
from trezor.messages import EthereumNetworkInfo, EthereumTokenInfo
|
||||
from trezor.wire import DataError
|
||||
|
||||
from apps.common import readers
|
||||
|
||||
from . import definitions_constants as consts, networks, tokens
|
||||
from .networks import UNKNOWN_NETWORK
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import TypeVar
|
||||
from typing_extensions import Self
|
||||
|
||||
DefType = TypeVar("DefType", EthereumNetworkInfo, EthereumTokenInfo)
|
||||
|
||||
|
||||
def decode_definition(definition: bytes, expected_type: type[DefType]) -> DefType:
|
||||
# check network definition
|
||||
r = utils.BufferReader(definition)
|
||||
expected_type_number = EthereumDefinitionType.NETWORK
|
||||
# TODO: can't check equality of MsgDefObjs now, so we check the name
|
||||
if expected_type.MESSAGE_NAME == EthereumTokenInfo.MESSAGE_NAME:
|
||||
expected_type_number = EthereumDefinitionType.TOKEN
|
||||
|
||||
try:
|
||||
# first check format version
|
||||
if r.read_memoryview(len(consts.FORMAT_VERSION)) != consts.FORMAT_VERSION:
|
||||
raise DataError("Invalid Ethereum definition")
|
||||
|
||||
# second check the type of the data
|
||||
if r.get() != expected_type_number:
|
||||
raise DataError("Definition type mismatch")
|
||||
|
||||
# third check data version
|
||||
if readers.read_uint32_le(r) < consts.MIN_DATA_VERSION:
|
||||
raise DataError("Definition is outdated")
|
||||
|
||||
# get payload
|
||||
payload_length = readers.read_uint16_le(r)
|
||||
payload = r.read_memoryview(payload_length)
|
||||
|
||||
# at the end compute Merkle tree root hash using
|
||||
# provided leaf data (payload with prefix) and proof
|
||||
hasher = sha256(b"\x00")
|
||||
hasher.update(memoryview(definition)[: r.offset])
|
||||
hash = hasher.digest()
|
||||
proof_length = r.get()
|
||||
for _ in range(proof_length):
|
||||
proof_entry = r.read_memoryview(32)
|
||||
hash_a = min(hash, proof_entry)
|
||||
hash_b = max(hash, proof_entry)
|
||||
hasher = sha256(b"\x01")
|
||||
hasher.update(hash_a)
|
||||
hasher.update(hash_b)
|
||||
hash = hasher.digest()
|
||||
|
||||
signed_tree_root = r.read_memoryview(64)
|
||||
|
||||
if r.remaining_count():
|
||||
raise DataError("Invalid Ethereum definition")
|
||||
|
||||
except EOFError:
|
||||
raise DataError("Invalid Ethereum definition")
|
||||
|
||||
# verify signature
|
||||
if not ed25519.verify(consts.DEFINITIONS_PUBLIC_KEY, signed_tree_root, hash):
|
||||
error_msg = DataError("Invalid definition signature")
|
||||
if __debug__:
|
||||
# check against dev key
|
||||
if not ed25519.verify(
|
||||
consts.DEFINITIONS_DEV_PUBLIC_KEY,
|
||||
signed_tree_root,
|
||||
hash,
|
||||
):
|
||||
raise error_msg
|
||||
else:
|
||||
raise error_msg
|
||||
|
||||
# decode it if it's OK
|
||||
try:
|
||||
return protobuf.decode(payload, expected_type, True)
|
||||
except ValueError:
|
||||
raise DataError("Invalid Ethereum definition")
|
||||
|
||||
|
||||
class Definitions:
|
||||
"""Class that holds Ethereum definitions - network and tokens.
|
||||
Prefers built-in definitions over encoded ones.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, network: EthereumNetworkInfo, tokens: dict[bytes, EthereumTokenInfo]
|
||||
) -> None:
|
||||
self.network = network
|
||||
self._tokens = tokens
|
||||
|
||||
@classmethod
|
||||
def from_encoded(
|
||||
cls,
|
||||
encoded_network: bytes | None,
|
||||
encoded_token: bytes | None,
|
||||
chain_id: int | None = None,
|
||||
slip44: int | None = None,
|
||||
) -> Self:
|
||||
network = UNKNOWN_NETWORK
|
||||
tokens: dict[bytes, EthereumTokenInfo] = {}
|
||||
|
||||
# if we have a built-in definition, use it
|
||||
if chain_id is not None:
|
||||
network = networks.by_chain_id(chain_id)
|
||||
elif slip44 is not None:
|
||||
network = networks.by_slip44(slip44)
|
||||
else:
|
||||
# ignore encoded definitions if we can't match them to request details
|
||||
return cls(UNKNOWN_NETWORK, {})
|
||||
|
||||
if network is UNKNOWN_NETWORK and encoded_network is not None:
|
||||
network = decode_definition(encoded_network, EthereumNetworkInfo)
|
||||
|
||||
if network is UNKNOWN_NETWORK:
|
||||
# ignore tokens if we don't have a network
|
||||
return cls(UNKNOWN_NETWORK, {})
|
||||
|
||||
if chain_id is not None and network.chain_id != chain_id:
|
||||
raise DataError("Network definition mismatch")
|
||||
if slip44 is not None and network.slip44 != slip44:
|
||||
raise DataError("Network definition mismatch")
|
||||
|
||||
# get token definition
|
||||
if encoded_token is not None:
|
||||
token = decode_definition(encoded_token, EthereumTokenInfo)
|
||||
# Ignore token if it doesn't match the network instead of raising an error.
|
||||
# This might help us in the future if we allow multiple networks/tokens
|
||||
# in the same message.
|
||||
if token.chain_id == network.chain_id:
|
||||
tokens[token.address] = token
|
||||
|
||||
return cls(network, tokens)
|
||||
|
||||
def get_token(self, address: bytes) -> EthereumTokenInfo:
|
||||
# if we have a built-in definition, use it
|
||||
token = tokens.token_by_chain_address(self.network.chain_id, address)
|
||||
if token is not None:
|
||||
return token
|
||||
|
||||
if address in self._tokens:
|
||||
return self._tokens[address]
|
||||
|
||||
return tokens.UNKNOWN_TOKEN
|
@ -0,0 +1,14 @@
|
||||
# generated from definitions_constants.py.mako
|
||||
# (by running `make templates` in `core`)
|
||||
# do not edit manually!
|
||||
|
||||
from ubinascii import unhexlify
|
||||
|
||||
DEFINITIONS_PUBLIC_KEY = b""
|
||||
MIN_DATA_VERSION = 1669892465
|
||||
FORMAT_VERSION = b"trzd1"
|
||||
|
||||
if __debug__:
|
||||
DEFINITIONS_DEV_PUBLIC_KEY = unhexlify(
|
||||
"db995fe25169d141cab9bbba92baa01f9f2e1ece7df4cb2ac05190f37fcc1f9d"
|
||||
)
|
@ -0,0 +1,14 @@
|
||||
# generated from definitions_constants.py.mako
|
||||
# (by running `make templates` in `core`)
|
||||
# do not edit manually!
|
||||
|
||||
from ubinascii import unhexlify
|
||||
|
||||
DEFINITIONS_PUBLIC_KEY = b""
|
||||
MIN_DATA_VERSION = ${ethereum_defs_timestamp}
|
||||
FORMAT_VERSION = b"trzd1"
|
||||
|
||||
if __debug__:
|
||||
DEFINITIONS_DEV_PUBLIC_KEY = unhexlify(
|
||||
"db995fe25169d141cab9bbba92baa01f9f2e1ece7df4cb2ac05190f37fcc1f9d"
|
||||
)
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,6 @@
|
||||
# Automatically generated by pb2py
|
||||
# fmt: off
|
||||
# isort:skip_file
|
||||
|
||||
NETWORK = 0
|
||||
TOKEN = 1
|
@ -0,0 +1,102 @@
|
||||
from ubinascii import unhexlify # noqa: F401
|
||||
|
||||
from trezor import messages, protobuf
|
||||
from trezor.enums import EthereumDefinitionType
|
||||
from trezor.crypto.curve import ed25519
|
||||
from trezor.crypto.hashlib import sha256
|
||||
|
||||
DEFINITIONS_DEV_PRIVATE_KEY = unhexlify(
|
||||
"4141414141414141414141414141414141414141414141414141414141414141"
|
||||
)
|
||||
|
||||
|
||||
def make_network(
|
||||
chain_id: int = 0,
|
||||
slip44: int = 0,
|
||||
symbol: str = "FAKE",
|
||||
name: str = "Fake network",
|
||||
) -> messages.EthereumNetworkInfo:
|
||||
return messages.EthereumNetworkInfo(
|
||||
chain_id=chain_id,
|
||||
slip44=slip44,
|
||||
symbol=symbol,
|
||||
name=name,
|
||||
)
|
||||
|
||||
|
||||
def make_token(
|
||||
symbol: str = "FAKE",
|
||||
decimals: int = 18,
|
||||
address: bytes = b"",
|
||||
chain_id: int = 0,
|
||||
name: str = "Fake token",
|
||||
) -> messages.EthereumTokenInfo:
|
||||
return messages.EthereumTokenInfo(
|
||||
symbol=symbol,
|
||||
decimals=decimals,
|
||||
address=address,
|
||||
chain_id=chain_id,
|
||||
name=name,
|
||||
)
|
||||
|
||||
|
||||
def make_payload(
|
||||
prefix: bytes = b"trzd1",
|
||||
data_type: EthereumDefinitionType = EthereumDefinitionType.NETWORK,
|
||||
timestamp: int = 0xFFFF_FFFF,
|
||||
message: messages.EthereumNetworkInfo
|
||||
| messages.EthereumTokenInfo
|
||||
| bytes = make_network(),
|
||||
) -> bytes:
|
||||
payload = prefix
|
||||
payload += data_type.to_bytes(1, "little")
|
||||
payload += timestamp.to_bytes(4, "little")
|
||||
if isinstance(message, bytes):
|
||||
message_bytes = message
|
||||
else:
|
||||
message_bytes = protobuf.dump_message_buffer(message)
|
||||
payload += len(message_bytes).to_bytes(2, "little")
|
||||
payload += message_bytes
|
||||
return payload
|
||||
|
||||
|
||||
def sign_payload(payload: bytes, merkle_neighbors: list[bytes]) -> tuple[bytes, bytes]:
|
||||
digest = sha256(b"\x00" + payload).digest()
|
||||
merkle_proof = []
|
||||
for item in merkle_neighbors:
|
||||
left, right = min(digest, item), max(digest, item)
|
||||
digest = sha256(b"\x01" + left + right).digest()
|
||||
merkle_proof.append(digest)
|
||||
|
||||
merkle_proof = len(merkle_proof).to_bytes(1, "little") + b"".join(merkle_proof)
|
||||
signature = ed25519.sign(DEFINITIONS_DEV_PRIVATE_KEY, digest)
|
||||
return merkle_proof, signature
|
||||
|
||||
|
||||
def encode_network(
|
||||
network: messages.EthereumNetworkInfo | None = None,
|
||||
chain_id: int = 0,
|
||||
slip44: int = 0,
|
||||
symbol: str = "FAKE",
|
||||
name: str = "Fake network",
|
||||
) -> bytes:
|
||||
if network is None:
|
||||
network = make_network(chain_id, slip44, symbol, name)
|
||||
payload = make_payload(data_type=EthereumDefinitionType.NETWORK, message=network)
|
||||
proof, signature = sign_payload(payload, [])
|
||||
return payload + proof + signature
|
||||
|
||||
|
||||
def encode_token(
|
||||
token: messages.EthereumTokenInfo | None = None,
|
||||
symbol: str = "FAKE",
|
||||
decimals: int = 18,
|
||||
address: bytes = b"",
|
||||
chain_id: int = 0,
|
||||
name: str = "Fake token",
|
||||
) -> bytes:
|
||||
if token is None:
|
||||
token = make_token(symbol, decimals, address, chain_id, name)
|
||||
payload = make_payload(data_type=EthereumDefinitionType.TOKEN, message=token)
|
||||
proof, signature = sign_payload(payload, [])
|
||||
return payload + proof + signature
|
@ -0,0 +1,250 @@
|
||||
from common import *
|
||||
import unittest
|
||||
import typing as t
|
||||
from trezor import utils, wire
|
||||
from ubinascii import hexlify # noqa: F401
|
||||
|
||||
if not utils.BITCOIN_ONLY:
|
||||
|
||||
from apps.ethereum import networks, tokens
|
||||
from apps.ethereum.definitions import decode_definition, Definitions
|
||||
from ethereum_common import *
|
||||
from trezor import protobuf
|
||||
from trezor.enums import EthereumDefinitionType
|
||||
from trezor.messages import (
|
||||
EthereumDefinitions,
|
||||
EthereumNetworkInfo,
|
||||
EthereumTokenInfo,
|
||||
EthereumSignTx,
|
||||
EthereumSignTxEIP1559,
|
||||
EthereumSignTypedData,
|
||||
)
|
||||
|
||||
TETHER_ADDRESS = b"\xda\xc1\x7f\x95\x8d\x2e\xe5\x23\xa2\x20\x62\x06\x99\x45\x97\xc1\x3d\x83\x1e\xc7"
|
||||
|
||||
|
||||
@unittest.skipUnless(not utils.BITCOIN_ONLY, "altcoin")
|
||||
class TestDecodeDefinition(unittest.TestCase):
|
||||
def test_short_message(self):
|
||||
with self.assertRaises(wire.DataError):
|
||||
decode_definition(b"\x00", EthereumNetworkInfo)
|
||||
with self.assertRaises(wire.DataError):
|
||||
decode_definition(b"\x00", EthereumTokenInfo)
|
||||
|
||||
# successful decode network
|
||||
def test_network_definition(self):
|
||||
network = make_network(chain_id=42, slip44=69, symbol="FAKE", name="Fakenet")
|
||||
encoded = encode_network(network)
|
||||
try:
|
||||
self.assertEqual(decode_definition(encoded, EthereumNetworkInfo), network)
|
||||
except Exception as e:
|
||||
print(e.message)
|
||||
|
||||
# successful decode token
|
||||
def test_token_definition(self):
|
||||
token = make_token("FAKE", decimals=33, address=b"abcd" * 5, chain_id=42)
|
||||
encoded = encode_token(token)
|
||||
self.assertEqual(decode_definition(encoded, EthereumTokenInfo), token)
|
||||
|
||||
def assertFailed(self, data: bytes) -> None:
|
||||
with self.assertRaises(wire.DataError):
|
||||
decode_definition(data, EthereumNetworkInfo)
|
||||
|
||||
def test_mangled_signature(self):
|
||||
payload = make_payload()
|
||||
proof, signature = sign_payload(payload, [])
|
||||
bad_signature = signature[:-1] + b"\xff"
|
||||
self.assertFailed(payload + proof + bad_signature)
|
||||
|
||||
def test_missing_signature(self):
|
||||
payload = make_payload()
|
||||
proof, signature = sign_payload(payload, [])
|
||||
self.assertFailed(payload + proof)
|
||||
|
||||
def test_mangled_payload(self):
|
||||
payload = make_payload()
|
||||
proof, signature = sign_payload(payload, [])
|
||||
bad_payload = payload[:-1] + b"\xff"
|
||||
self.assertFailed(bad_payload + proof + signature)
|
||||
|
||||
def test_proof_length_mismatch(self):
|
||||
payload = make_payload()
|
||||
proof, signature = sign_payload(payload, [])
|
||||
bad_proof = b"\x01"
|
||||
self.assertFailed(payload + bad_proof + signature)
|
||||
|
||||
def test_bad_proof(self):
|
||||
payload = make_payload()
|
||||
proof, signature = sign_payload(payload, [sha256(b"x").digest()])
|
||||
bad_proof = proof[:-1] + b"\xff"
|
||||
self.assertFailed(payload + bad_proof + signature)
|
||||
|
||||
def test_trimmed_proof(self):
|
||||
payload = make_payload()
|
||||
proof, signature = sign_payload(payload, [])
|
||||
bad_proof = proof[:-1]
|
||||
self.assertFailed(payload + bad_proof + signature)
|
||||
|
||||
def test_bad_prefix(self):
|
||||
payload = make_payload(prefix=b"trzd2")
|
||||
proof, signature = sign_payload(payload, [])
|
||||
self.assertFailed(payload + proof + signature)
|
||||
|
||||
def test_bad_type(self):
|
||||
payload = make_payload(
|
||||
data_type=EthereumDefinitionType.TOKEN, message=make_token()
|
||||
)
|
||||
proof, signature = sign_payload(payload, [])
|
||||
self.assertFailed(payload + proof + signature)
|
||||
|
||||
def test_outdated(self):
|
||||
payload = make_payload(timestamp=0)
|
||||
proof, signature = sign_payload(payload, [])
|
||||
self.assertFailed(payload + proof + signature)
|
||||
|
||||
def test_malformed_protobuf(self):
|
||||
payload = make_payload(message=b"\x00")
|
||||
proof, signature = sign_payload(payload, [])
|
||||
self.assertFailed(payload + proof + signature)
|
||||
|
||||
def test_protobuf_mismatch(self):
|
||||
payload = make_payload(
|
||||
data_type=EthereumDefinitionType.NETWORK, message=make_token()
|
||||
)
|
||||
proof, signature = sign_payload(payload, [])
|
||||
self.assertFailed(payload + proof + signature)
|
||||
|
||||
payload = make_payload(
|
||||
data_type=EthereumDefinitionType.TOKEN, message=make_network()
|
||||
)
|
||||
proof, signature = sign_payload(payload, [])
|
||||
self.assertFailed(payload + proof + signature)
|
||||
|
||||
def test_trailing_garbage(self):
|
||||
payload = make_payload()
|
||||
proof, signature = sign_payload(payload, [])
|
||||
self.assertFailed(payload + proof + signature + b"\x00")
|
||||
|
||||
|
||||
@unittest.skipUnless(not utils.BITCOIN_ONLY, "altcoin")
|
||||
class TestEthereumDefinitions(unittest.TestCase):
|
||||
def assertUnknown(self, what: t.Any) -> None:
|
||||
if what is networks.UNKNOWN_NETWORK:
|
||||
return
|
||||
if what is tokens.UNKNOWN_TOKEN:
|
||||
return
|
||||
self.fail("Expected UNKNOWN_*, got %r" % what)
|
||||
|
||||
def assertKnown(self, what: t.Any) -> None:
|
||||
if not EthereumNetworkInfo.is_type_of(
|
||||
what
|
||||
) and not EthereumTokenInfo.is_type_of(what):
|
||||
self.fail("Expected network / token info, got %r" % what)
|
||||
if what is networks.UNKNOWN_NETWORK:
|
||||
self.fail("Expected known network, got UNKNOWN_NETWORK")
|
||||
if what is tokens.UNKNOWN_TOKEN:
|
||||
self.fail("Expected known token, got UNKNOWN_TOKEN")
|
||||
|
||||
def test_empty(self) -> None:
|
||||
# no slip44 nor chain_id -- should short-circuit and always be unknown
|
||||
defs = Definitions.from_encoded(None, None)
|
||||
self.assertUnknown(defs.network)
|
||||
self.assertFalse(defs._tokens)
|
||||
self.assertUnknown(defs.get_token(TETHER_ADDRESS))
|
||||
|
||||
# chain_id provided, no definition
|
||||
defs = Definitions.from_encoded(None, None, chain_id=100_000)
|
||||
self.assertUnknown(defs.network)
|
||||
self.assertFalse(defs._tokens)
|
||||
self.assertUnknown(defs.get_token(TETHER_ADDRESS))
|
||||
|
||||
def test_builtin(self) -> None:
|
||||
defs = Definitions.from_encoded(None, None, chain_id=1)
|
||||
self.assertKnown(defs.network)
|
||||
self.assertFalse(defs._tokens)
|
||||
self.assertKnown(defs.get_token(TETHER_ADDRESS))
|
||||
self.assertUnknown(defs.get_token(b"\x00" * 20))
|
||||
|
||||
defs = Definitions.from_encoded(None, None, slip44=60)
|
||||
self.assertKnown(defs.network)
|
||||
self.assertFalse(defs._tokens)
|
||||
self.assertKnown(defs.get_token(TETHER_ADDRESS))
|
||||
self.assertUnknown(defs.get_token(b"\x00" * 20))
|
||||
|
||||
def test_external(self) -> None:
|
||||
network = make_network(chain_id=42)
|
||||
defs = Definitions.from_encoded(encode_network(network), None, chain_id=42)
|
||||
self.assertEqual(defs.network, network)
|
||||
self.assertUnknown(defs.get_token(b"\x00" * 20))
|
||||
|
||||
token = make_token(chain_id=42, address=b"\x00" * 20)
|
||||
defs = Definitions.from_encoded(
|
||||
encode_network(network), encode_token(token), chain_id=42
|
||||
)
|
||||
self.assertEqual(defs.network, network)
|
||||
self.assertEqual(defs.get_token(b"\x00" * 20), token)
|
||||
|
||||
token = make_token(chain_id=1, address=b"\x00" * 20)
|
||||
defs = Definitions.from_encoded(None, encode_token(token), chain_id=1)
|
||||
self.assertKnown(defs.network)
|
||||
self.assertEqual(defs.get_token(b"\x00" * 20), token)
|
||||
|
||||
def test_external_token_mismatch(self) -> None:
|
||||
network = make_network(chain_id=42)
|
||||
token = make_token(chain_id=43, address=b"\x00" * 20)
|
||||
defs = Definitions.from_encoded(encode_network(network), encode_token(token))
|
||||
self.assertUnknown(defs.get_token(b"\x00" * 20))
|
||||
|
||||
def test_external_chain_match(self) -> None:
|
||||
network = make_network(chain_id=42)
|
||||
token = make_token(chain_id=42, address=b"\x00" * 20)
|
||||
defs = Definitions.from_encoded(
|
||||
encode_network(network), encode_token(token), chain_id=42
|
||||
)
|
||||
self.assertEqual(defs.network, network)
|
||||
self.assertEqual(defs.get_token(b"\x00" * 20), token)
|
||||
|
||||
with self.assertRaises(wire.DataError):
|
||||
Definitions.from_encoded(
|
||||
encode_network(network), encode_token(token), chain_id=333
|
||||
)
|
||||
|
||||
def test_external_slip44_mismatch(self) -> None:
|
||||
network = make_network(chain_id=42, slip44=1999)
|
||||
token = make_token(chain_id=42, address=b"\x00" * 20)
|
||||
defs = Definitions.from_encoded(
|
||||
encode_network(network), encode_token(token), slip44=1999
|
||||
)
|
||||
self.assertEqual(defs.network, network)
|
||||
self.assertEqual(defs.get_token(b"\x00" * 20), token)
|
||||
|
||||
with self.assertRaises(wire.DataError):
|
||||
Definitions.from_encoded(
|
||||
encode_network(network), encode_token(token), slip44=333
|
||||
)
|
||||
|
||||
def test_ignore_encoded_network(self) -> None:
|
||||
# when network is builtin, ignore the encoded one
|
||||
network = encode_network(chain_id=1, symbol="BAD")
|
||||
defs = Definitions.from_encoded(network, None, chain_id=1)
|
||||
self.assertNotEqual(defs.network, network)
|
||||
|
||||
def test_ignore_encoded_token(self) -> None:
|
||||
# when token is builtin, ignore the encoded one
|
||||
token = encode_token(chain_id=1, address=TETHER_ADDRESS, symbol="BAD")
|
||||
defs = Definitions.from_encoded(None, token, chain_id=1)
|
||||
self.assertNotEqual(defs.get_token(TETHER_ADDRESS), token)
|
||||
|
||||
def test_ignore_with_no_match(self) -> None:
|
||||
network = encode_network(chain_id=100_000, symbol="BAD")
|
||||
# smoke test: definition is accepted
|
||||
defs = Definitions.from_encoded(network, None, chain_id=100_000)
|
||||
self.assertKnown(defs.network)
|
||||
|
||||
# same definition but nothing to match it to
|
||||
defs = Definitions.from_encoded(network, None)
|
||||
self.assertUnknown(defs.network)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
@ -1,104 +1,109 @@
|
||||
from common import *
|
||||
|
||||
if not utils.BITCOIN_ONLY:
|
||||
from apps.ethereum import networks
|
||||
from apps.ethereum.layout import format_ethereum_amount
|
||||
from apps.ethereum.tokens import token_by_chain_address
|
||||
from apps.ethereum.tokens import UNKNOWN_TOKEN
|
||||
|
||||
from ethereum_common import make_network, make_token
|
||||
|
||||
ETH = networks.by_chain_id(1)
|
||||
|
||||
|
||||
@unittest.skipUnless(not utils.BITCOIN_ONLY, "altcoin")
|
||||
class TestFormatEthereumAmount(unittest.TestCase):
|
||||
def test_denominations(self):
|
||||
text = format_ethereum_amount(1, None, ETH)
|
||||
self.assertEqual(text, "1 Wei ETH")
|
||||
text = format_ethereum_amount(1000, None, ETH)
|
||||
self.assertEqual(text, "1,000 Wei ETH")
|
||||
text = format_ethereum_amount(1000000, None, ETH)
|
||||
self.assertEqual(text, "1,000,000 Wei ETH")
|
||||
text = format_ethereum_amount(10000000, None, ETH)
|
||||
self.assertEqual(text, "10,000,000 Wei ETH")
|
||||
text = format_ethereum_amount(100000000, None, ETH)
|
||||
self.assertEqual(text, "100,000,000 Wei ETH")
|
||||
text = format_ethereum_amount(1000000000, None, ETH)
|
||||
self.assertEqual(text, "0.000000001 ETH")
|
||||
text = format_ethereum_amount(10000000000, None, ETH)
|
||||
self.assertEqual(text, "0.00000001 ETH")
|
||||
text = format_ethereum_amount(100000000000, None, ETH)
|
||||
self.assertEqual(text, "0.0000001 ETH")
|
||||
text = format_ethereum_amount(1000000000000, None, ETH)
|
||||
self.assertEqual(text, "0.000001 ETH")
|
||||
text = format_ethereum_amount(10000000000000, None, ETH)
|
||||
self.assertEqual(text, "0.00001 ETH")
|
||||
text = format_ethereum_amount(100000000000000, None, ETH)
|
||||
self.assertEqual(text, "0.0001 ETH")
|
||||
text = format_ethereum_amount(1000000000000000, None, ETH)
|
||||
self.assertEqual(text, "0.001 ETH")
|
||||
text = format_ethereum_amount(10000000000000000, None, ETH)
|
||||
self.assertEqual(text, "0.01 ETH")
|
||||
text = format_ethereum_amount(100000000000000000, None, ETH)
|
||||
self.assertEqual(text, "0.1 ETH")
|
||||
text = format_ethereum_amount(1000000000000000000, None, ETH)
|
||||
self.assertEqual(text, "1 ETH")
|
||||
text = format_ethereum_amount(10000000000000000000, None, ETH)
|
||||
self.assertEqual(text, "10 ETH")
|
||||
text = format_ethereum_amount(100000000000000000000, None, ETH)
|
||||
self.assertEqual(text, "100 ETH")
|
||||
text = format_ethereum_amount(1000000000000000000000, None, ETH)
|
||||
self.assertEqual(text, "1,000 ETH")
|
||||
|
||||
def test_format(self):
|
||||
text = format_ethereum_amount(1, None, 1)
|
||||
self.assertEqual(text, '1 Wei ETH')
|
||||
text = format_ethereum_amount(1000, None, 1)
|
||||
self.assertEqual(text, '1,000 Wei ETH')
|
||||
text = format_ethereum_amount(1000000, None, 1)
|
||||
self.assertEqual(text, '1,000,000 Wei ETH')
|
||||
text = format_ethereum_amount(10000000, None, 1)
|
||||
self.assertEqual(text, '10,000,000 Wei ETH')
|
||||
text = format_ethereum_amount(100000000, None, 1)
|
||||
self.assertEqual(text, '100,000,000 Wei ETH')
|
||||
text = format_ethereum_amount(1000000000, None, 1)
|
||||
self.assertEqual(text, '0.000000001 ETH')
|
||||
text = format_ethereum_amount(10000000000, None, 1)
|
||||
self.assertEqual(text, '0.00000001 ETH')
|
||||
text = format_ethereum_amount(100000000000, None, 1)
|
||||
self.assertEqual(text, '0.0000001 ETH')
|
||||
text = format_ethereum_amount(1000000000000, None, 1)
|
||||
self.assertEqual(text, '0.000001 ETH')
|
||||
text = format_ethereum_amount(10000000000000, None, 1)
|
||||
self.assertEqual(text, '0.00001 ETH')
|
||||
text = format_ethereum_amount(100000000000000, None, 1)
|
||||
self.assertEqual(text, '0.0001 ETH')
|
||||
text = format_ethereum_amount(1000000000000000, None, 1)
|
||||
self.assertEqual(text, '0.001 ETH')
|
||||
text = format_ethereum_amount(10000000000000000, None, 1)
|
||||
self.assertEqual(text, '0.01 ETH')
|
||||
text = format_ethereum_amount(100000000000000000, None, 1)
|
||||
self.assertEqual(text, '0.1 ETH')
|
||||
text = format_ethereum_amount(1000000000000000000, None, 1)
|
||||
self.assertEqual(text, '1 ETH')
|
||||
text = format_ethereum_amount(10000000000000000000, None, 1)
|
||||
self.assertEqual(text, '10 ETH')
|
||||
text = format_ethereum_amount(100000000000000000000, None, 1)
|
||||
self.assertEqual(text, '100 ETH')
|
||||
text = format_ethereum_amount(1000000000000000000000, None, 1)
|
||||
self.assertEqual(text, '1,000 ETH')
|
||||
|
||||
text = format_ethereum_amount(1000000000000000000, None, 61)
|
||||
self.assertEqual(text, '1 ETC')
|
||||
text = format_ethereum_amount(1000000000000000000, None, 31)
|
||||
self.assertEqual(text, '1 tRBTC')
|
||||
def test_precision(self):
|
||||
text = format_ethereum_amount(1000000000000000001, None, ETH)
|
||||
self.assertEqual(text, "1.000000000000000001 ETH")
|
||||
text = format_ethereum_amount(10000000000000000001, None, ETH)
|
||||
self.assertEqual(text, "10.000000000000000001 ETH")
|
||||
|
||||
text = format_ethereum_amount(1000000000000000001, None, 1)
|
||||
self.assertEqual(text, '1.000000000000000001 ETH')
|
||||
text = format_ethereum_amount(10000000000000000001, None, 1)
|
||||
self.assertEqual(text, '10.000000000000000001 ETH')
|
||||
text = format_ethereum_amount(10000000000000000001, None, 61)
|
||||
self.assertEqual(text, '10.000000000000000001 ETC')
|
||||
text = format_ethereum_amount(1000000000000000001, None, 31)
|
||||
self.assertEqual(text, '1.000000000000000001 tRBTC')
|
||||
def test_symbols(self):
|
||||
fake_network = make_network(symbol="FAKE")
|
||||
text = format_ethereum_amount(1, None, fake_network)
|
||||
self.assertEqual(text, "1 Wei FAKE")
|
||||
text = format_ethereum_amount(1000000000000000000, None, fake_network)
|
||||
self.assertEqual(text, "1 FAKE")
|
||||
text = format_ethereum_amount(1000000000000000001, None, fake_network)
|
||||
self.assertEqual(text, "1.000000000000000001 FAKE")
|
||||
|
||||
def test_unknown_chain(self):
|
||||
# unknown chain
|
||||
text = format_ethereum_amount(1, None, 9999)
|
||||
self.assertEqual(text, '1 Wei UNKN')
|
||||
text = format_ethereum_amount(10000000000000000001, None, 9999)
|
||||
self.assertEqual(text, '10.000000000000000001 UNKN')
|
||||
text = format_ethereum_amount(1, None, networks.UNKNOWN_NETWORK)
|
||||
self.assertEqual(text, "1 Wei UNKN")
|
||||
text = format_ethereum_amount(
|
||||
10000000000000000001, None, networks.UNKNOWN_NETWORK
|
||||
)
|
||||
self.assertEqual(text, "10.000000000000000001 UNKN")
|
||||
|
||||
def test_tokens(self):
|
||||
# tokens with low decimal values
|
||||
# USDC has 6 decimals
|
||||
usdc_token = token_by_chain_address(1, unhexlify("a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"))
|
||||
# ICO has 10 decimals
|
||||
ico_token = token_by_chain_address(1, unhexlify("a33e729bf4fdeb868b534e1f20523463d9c46bee"))
|
||||
|
||||
usdc_token = make_token(symbol="USDC", decimals=6)
|
||||
# when decimals < 10, should never display 'Wei' format
|
||||
text = format_ethereum_amount(1, usdc_token, 1)
|
||||
self.assertEqual(text, '0.000001 USDC')
|
||||
text = format_ethereum_amount(0, usdc_token, 1)
|
||||
self.assertEqual(text, '0 USDC')
|
||||
text = format_ethereum_amount(1, usdc_token, ETH)
|
||||
self.assertEqual(text, "0.000001 USDC")
|
||||
text = format_ethereum_amount(0, usdc_token, ETH)
|
||||
self.assertEqual(text, "0 USDC")
|
||||
|
||||
text = format_ethereum_amount(1, ico_token, 1)
|
||||
self.assertEqual(text, '1 Wei ICO')
|
||||
text = format_ethereum_amount(9, ico_token, 1)
|
||||
self.assertEqual(text, '9 Wei ICO')
|
||||
text = format_ethereum_amount(10, ico_token, 1)
|
||||
self.assertEqual(text, '0.000000001 ICO')
|
||||
text = format_ethereum_amount(11, ico_token, 1)
|
||||
self.assertEqual(text, '0.0000000011 ICO')
|
||||
# ICO has 10 decimals
|
||||
ico_token = make_token(symbol="ICO", decimals=10)
|
||||
text = format_ethereum_amount(1, ico_token, ETH)
|
||||
self.assertEqual(text, "1 Wei ICO")
|
||||
text = format_ethereum_amount(9, ico_token, ETH)
|
||||
self.assertEqual(text, "9 Wei ICO")
|
||||
text = format_ethereum_amount(10, ico_token, ETH)
|
||||
self.assertEqual(text, "0.000000001 ICO")
|
||||
text = format_ethereum_amount(11, ico_token, ETH)
|
||||
self.assertEqual(text, "0.0000000011 ICO")
|
||||
|
||||
def test_unknown_token(self):
|
||||
unknown_token = token_by_chain_address(1, b"hello")
|
||||
text = format_ethereum_amount(1, unknown_token, 1)
|
||||
self.assertEqual(text, '1 Wei UNKN')
|
||||
text = format_ethereum_amount(0, unknown_token, 1)
|
||||
self.assertEqual(text, '0 Wei UNKN')
|
||||
text = format_ethereum_amount(1, UNKNOWN_TOKEN, ETH)
|
||||
self.assertEqual(text, "1 Wei UNKN")
|
||||
text = format_ethereum_amount(0, UNKNOWN_TOKEN, ETH)
|
||||
self.assertEqual(text, "0 Wei UNKN")
|
||||
# unknown token has 0 decimals so is always wei
|
||||
text = format_ethereum_amount(1000000000000000000, unknown_token, 1)
|
||||
self.assertEqual(text, '1,000,000,000,000,000,000 Wei UNKN')
|
||||
text = format_ethereum_amount(1000000000000000000, UNKNOWN_TOKEN, ETH)
|
||||
self.assertEqual(text, "1,000,000,000,000,000,000 Wei UNKN")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
Loading…
Reference in new issue