diff --git a/src/apps/tezos/helpers.py b/src/apps/tezos/helpers.py index 0050b93fd..5669cd1c5 100644 --- a/src/apps/tezos/helpers.py +++ b/src/apps/tezos/helpers.py @@ -22,6 +22,8 @@ TEZOS_PREFIX_BYTES = { "edsig": [9, 245, 205, 134, 18], # operation hash "o": [5, 116], + # protocola hash + "P": [2, 170], } diff --git a/src/apps/tezos/layout.py b/src/apps/tezos/layout.py index 4b1528efe..14d474e31 100644 --- a/src/apps/tezos/layout.py +++ b/src/apps/tezos/layout.py @@ -1,5 +1,9 @@ -from trezor import ui +from micropython import const + +from trezor import ui, wire from trezor.messages import ButtonRequestType +from trezor.ui.confirm import CANCELLED, CONFIRMED, ConfirmDialog +from trezor.ui.scroll import Scrollpage, animate_swipe, paginate from trezor.ui.text import Text from trezor.utils import chunks, format_amount @@ -66,6 +70,51 @@ def split_address(address): return chunks(address, 18) +def split_proposal(proposal): + return chunks(proposal, 17) + + def format_tezos_amount(value): formatted_value = format_amount(value, TEZOS_AMOUNT_DIVISIBILITY) return formatted_value + " XTZ" + + +async def require_confirm_proposal(ctx, proposals): + text = Text("Submit proposal", ui.ICON_SEND, icon_color=ui.PURPLE) + text.bold("Proposal:") + text.mono(*split_proposal(proposals[0])) + await require_confirm(ctx, text, ButtonRequestType.SignTx) + + +async def require_confirm_ballot(ctx, proposal, ballot): + text = Text("Submit ballot", ui.ICON_SEND, icon_color=ui.PURPLE) + text.bold("Ballot: {}".format(ballot)) + text.bold("Proposal:") + text.mono(*split_proposal(proposal[0])) + await require_confirm(ctx, text, ButtonRequestType.SignTx) + + +# use, when there are more then one proposals in one operation +async def show_proposals(ctx, proposals): + first_page = const(0) + pages = proposals + + paginator = paginate(show_proposal_page, len(pages), first_page, pages) + return await ctx.wait(paginator) + + +@ui.layout +async def show_proposal_page(page: int, page_count: int, pages: list): + + text = Text("Submit proposals", ui.ICON_SEND, icon_color=ui.PURPLE) + text.bold("Proposal {}: ".format(page + 1)) + text.mono(*split_proposal(pages[page])) + content = Scrollpage(text, page, page_count) + + if page + 1 >= page_count: + confirm = await ConfirmDialog(content) + if confirm == CANCELLED: + raise wire.ActionCancelled("Cancelled") + else: + content.render() + await animate_swipe() diff --git a/src/apps/tezos/sign_tx.py b/src/apps/tezos/sign_tx.py index 5cf0e11d3..ffd665ed9 100644 --- a/src/apps/tezos/sign_tx.py +++ b/src/apps/tezos/sign_tx.py @@ -1,3 +1,5 @@ +from micropython import const + from trezor import wire from trezor.crypto import hashlib from trezor.crypto.curve import ed25519 @@ -5,8 +7,19 @@ from trezor.messages import TezosContractType from trezor.messages.TezosSignedTx import TezosSignedTx from apps.common import paths -from apps.common.writers import write_bytes, write_uint8 + +# from apps.common.writers import write_bytes, write_uint8 from apps.tezos import helpers, layout +from apps.tezos.writers import ( + write_bool, + write_bytes, + write_uint8, + write_uint16, + write_uint32, + write_uint64, +) + +PROPOSAL_LENGTH = const(32) async def sign_tx(ctx, msg, keychain): @@ -50,6 +63,20 @@ async def sign_tx(ctx, msg, keychain): ctx, source, msg.delegation.fee ) + elif msg.proposal is not None: + proposed_protocols = _get_protocol_hash_from_msg(msg.proposal.proposals) + + # byte count larger than PROPOSAL_LENGTH indicates more than 1 proposal, use paginated screen + if msg.proposal.bytes_in_next_field > PROPOSAL_LENGTH: + await layout.show_proposals(ctx, proposed_protocols) + else: + await layout.require_confirm_proposal(ctx, proposed_protocols) + + elif msg.ballot is not None: + proposed_protocol = _get_protocol_hash_from_msg(msg.ballot.proposal) + submitted_ballot = _get_ballot(msg.ballot.ballot) + await layout.require_confirm_ballot(ctx, proposed_protocol, submitted_ballot) + else: raise wire.DataError("Invalid operation") @@ -99,11 +126,34 @@ def _get_address_from_contract(address): raise wire.DataError("Invalid contract type") +def _get_protocol_hash_from_msg(proposals): + # split the proposals + proposal_list = list( + [ + proposals[i : i + PROPOSAL_LENGTH] + for i in range(0, len(proposals), PROPOSAL_LENGTH) + ] + ) + return [ + helpers.base58_encode_check(proposal, prefix="P") for proposal in proposal_list + ] + + +def _get_ballot(encoded_ballot): + encoded_ballot = int(encoded_ballot[0]) + if encoded_ballot == 0: + return "yay" + elif encoded_ballot == 1: + return "nay" + elif encoded_ballot == 2: + return "pass" + + def _get_operation_bytes(w: bytearray, msg): write_bytes(w, msg.branch) # when the account sends first operation in lifetime, - # we need to reveal its publickey + # we need to reveal its public key if msg.reveal is not None: _encode_common(w, msg.reveal, "reveal") write_bytes(w, msg.reveal.public_key) @@ -127,6 +177,10 @@ def _get_operation_bytes(w: bytearray, msg): elif msg.delegation is not None: _encode_common(w, msg.delegation, "delegation") _encode_data_with_bool_prefix(w, msg.delegation.delegate) + elif msg.proposal is not None: + _encode_proposal(w, msg.proposal) + elif msg.ballot is not None: + _encode_ballot(w, msg.ballot) def _encode_common(w: bytearray, operation, str_operation): @@ -169,3 +223,23 @@ def _encode_zarith(w: bytearray, num): break write_uint8(w, 128 | byte) + + +def _encode_proposal(w: bytearray, proposal): + proposal_tag = 5 + + write_uint8(w, proposal_tag) + write_bytes(w, proposal.source) + write_uint32(w, proposal.period) + write_uint32(w, proposal.bytes_in_next_field) + write_bytes(w, proposal.proposals) + + +def _encode_ballot(w: bytearray, ballot): + ballot_tag = 6 + + write_uint8(w, ballot_tag) + write_bytes(w, ballot.source) + write_uint32(w, ballot.period) + write_bytes(w, ballot.proposal) + write_bytes(w, ballot.ballot) diff --git a/src/apps/tezos/writers.py b/src/apps/tezos/writers.py new file mode 100644 index 000000000..0c10054c8 --- /dev/null +++ b/src/apps/tezos/writers.py @@ -0,0 +1,24 @@ +from apps.common.writers import ( + write_bytes, + write_uint8, + write_uint32_be, + write_uint64_be, +) + +write_uint8 = write_uint8 +write_uint32 = write_uint32_be +write_uint64 = write_uint64_be +write_bytes = write_bytes + + +def write_bool(w: bytearray, boolean: bool): + if boolean: + write_uint8(w, 255) + else: + write_uint8(w, 0) + + +# write uint16 in be +def write_uint16(w: bytearray, n: int): + w.append((n >> 8) & 0xFF) + w.append(n & 0xFF) diff --git a/src/trezor/messages/TezosBallotOp.py b/src/trezor/messages/TezosBallotOp.py new file mode 100644 index 000000000..260bf2a4b --- /dev/null +++ b/src/trezor/messages/TezosBallotOp.py @@ -0,0 +1,27 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + + +class TezosBallotOp(p.MessageType): + + def __init__( + self, + source: bytes = None, + period: int = None, + proposal: bytes = None, + ballot: bytes = None, + ) -> None: + self.source = source + self.period = period + self.proposal = proposal + self.ballot = ballot + + @classmethod + def get_fields(cls): + return { + 1: ('source', p.BytesType, 0), + 2: ('period', p.UVarintType, 0), + 3: ('proposal', p.BytesType, 0), + 4: ('ballot', p.BytesType, 0), + } diff --git a/src/trezor/messages/TezosProposalOp.py b/src/trezor/messages/TezosProposalOp.py new file mode 100644 index 000000000..94f0dd532 --- /dev/null +++ b/src/trezor/messages/TezosProposalOp.py @@ -0,0 +1,27 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + + +class TezosProposalOp(p.MessageType): + + def __init__( + self, + source: bytes = None, + period: int = None, + bytes_in_next_field: int = None, + proposals: bytes = None, + ) -> None: + self.source = source + self.period = period + self.bytes_in_next_field = bytes_in_next_field + self.proposals = proposals + + @classmethod + def get_fields(cls): + return { + 1: ('source', p.BytesType, 0), + 2: ('period', p.UVarintType, 0), + 3: ('bytes_in_next_field', p.UVarintType, 0), + 4: ('proposals', p.BytesType, 0), + } diff --git a/src/trezor/messages/TezosSignTx.py b/src/trezor/messages/TezosSignTx.py index 812760378..cbba27e04 100644 --- a/src/trezor/messages/TezosSignTx.py +++ b/src/trezor/messages/TezosSignTx.py @@ -2,8 +2,10 @@ # fmt: off import protobuf as p +from .TezosBallotOp import TezosBallotOp from .TezosDelegationOp import TezosDelegationOp from .TezosOriginationOp import TezosOriginationOp +from .TezosProposalOp import TezosProposalOp from .TezosRevealOp import TezosRevealOp from .TezosTransactionOp import TezosTransactionOp @@ -25,6 +27,8 @@ class TezosSignTx(p.MessageType): transaction: TezosTransactionOp = None, origination: TezosOriginationOp = None, delegation: TezosDelegationOp = None, + proposal: TezosProposalOp = None, + ballot: TezosBallotOp = None, ) -> None: self.address_n = address_n if address_n is not None else [] self.branch = branch @@ -32,6 +36,8 @@ class TezosSignTx(p.MessageType): self.transaction = transaction self.origination = origination self.delegation = delegation + self.proposal = proposal + self.ballot = ballot @classmethod def get_fields(cls): @@ -42,4 +48,6 @@ class TezosSignTx(p.MessageType): 4: ('transaction', TezosTransactionOp, 0), 5: ('origination', TezosOriginationOp, 0), 6: ('delegation', TezosDelegationOp, 0), + 7: ('proposal', TezosProposalOp, 0), + 8: ('ballot', TezosBallotOp, 0), }