mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-11-18 05:28:40 +00:00
refactor(core): convert most of apps.monero to layouts
Progress popups are not ported yet as they're unlike anything else. Introduces paginate_paragraphs.
This commit is contained in:
parent
6c926ad82e
commit
16094df0c5
@ -514,10 +514,8 @@ if utils.BITCOIN_ONLY:
|
|||||||
import apps.monero.get_watch_only
|
import apps.monero.get_watch_only
|
||||||
apps.monero.key_image_sync
|
apps.monero.key_image_sync
|
||||||
import apps.monero.key_image_sync
|
import apps.monero.key_image_sync
|
||||||
apps.monero.layout.common
|
apps.monero.layout
|
||||||
import apps.monero.layout.common
|
import apps.monero.layout
|
||||||
apps.monero.layout.confirms
|
|
||||||
import apps.monero.layout.confirms
|
|
||||||
apps.monero.live_refresh
|
apps.monero.live_refresh
|
||||||
import apps.monero.live_refresh
|
import apps.monero.live_refresh
|
||||||
apps.monero.misc
|
apps.monero.misc
|
||||||
|
@ -78,6 +78,7 @@ async def get_ownership_proof(
|
|||||||
data=hexlify(msg.commitment_data).decode(),
|
data=hexlify(msg.commitment_data).decode(),
|
||||||
icon=ui.ICON_CONFIG,
|
icon=ui.ICON_CONFIG,
|
||||||
icon_color=ui.ORANGE_ICON,
|
icon_color=ui.ORANGE_ICON,
|
||||||
|
truncate=True, # commitment data, probably should show all
|
||||||
truncate_middle=True,
|
truncate_middle=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -56,6 +56,7 @@ async def confirm_output(
|
|||||||
title="OP_RETURN",
|
title="OP_RETURN",
|
||||||
data=hexlify(data).decode(),
|
data=hexlify(data).decode(),
|
||||||
br_code=ButtonRequestType.ConfirmOutput,
|
br_code=ButtonRequestType.ConfirmOutput,
|
||||||
|
truncate=True, # 80 bytes - not truncated 2 screens max
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
assert output.address is not None
|
assert output.address is not None
|
||||||
|
@ -336,20 +336,3 @@ def is_hardened(i: int) -> bool:
|
|||||||
|
|
||||||
def path_is_hardened(address_n: Bip32Path) -> bool:
|
def path_is_hardened(address_n: Bip32Path) -> bool:
|
||||||
return all(is_hardened(n) for n in address_n)
|
return all(is_hardened(n) for n in address_n)
|
||||||
|
|
||||||
|
|
||||||
def break_address_n_to_lines(address_n: Bip32Path) -> list[str]:
|
|
||||||
from trezor.ui.constants import MONO_CHARS_PER_LINE
|
|
||||||
from .layout import address_n_to_str
|
|
||||||
|
|
||||||
lines = []
|
|
||||||
path_str = address_n_to_str(address_n)
|
|
||||||
|
|
||||||
per_line = MONO_CHARS_PER_LINE
|
|
||||||
while len(path_str) > per_line:
|
|
||||||
i = path_str[:per_line].rfind("/")
|
|
||||||
lines.append(path_str[:i])
|
|
||||||
path_str = path_str[i:]
|
|
||||||
lines.append(path_str)
|
|
||||||
|
|
||||||
return lines
|
|
||||||
|
@ -30,6 +30,7 @@ async def show_internal_entropy(ctx, entropy: bytes):
|
|||||||
icon_color=ui.ORANGE_ICON,
|
icon_color=ui.ORANGE_ICON,
|
||||||
width=16,
|
width=16,
|
||||||
br_code=ButtonRequestType.ResetDevice,
|
br_code=ButtonRequestType.ResetDevice,
|
||||||
|
truncate=True, # 32 bytes always fits
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -53,6 +53,7 @@ async def require_confirm_ecdh_session_key(
|
|||||||
serialize_identity_without_proto(identity),
|
serialize_identity_without_proto(identity),
|
||||||
icon=ui.ICON_DEFAULT,
|
icon=ui.ICON_DEFAULT,
|
||||||
icon_color=ui.ORANGE_ICON,
|
icon_color=ui.ORANGE_ICON,
|
||||||
|
truncate=True, # uri without protocol, probably should show entire
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
from trezor.messages import MoneroAddress
|
from trezor.messages import MoneroAddress
|
||||||
|
from trezor.ui.layouts import show_address
|
||||||
|
|
||||||
from apps.common import paths
|
from apps.common import paths
|
||||||
from apps.common.keychain import auto_keychain
|
from apps.common.keychain import auto_keychain
|
||||||
from apps.common.layout import address_n_to_str, show_qr
|
from apps.common.layout import address_n_to_str
|
||||||
from apps.monero import misc
|
from apps.monero import misc
|
||||||
from apps.monero.layout import confirms
|
|
||||||
from apps.monero.xmr import addresses, crypto, monero
|
from apps.monero.xmr import addresses, crypto, monero
|
||||||
from apps.monero.xmr.networks import net_version
|
from apps.monero.xmr.networks import net_version
|
||||||
|
|
||||||
@ -42,10 +42,11 @@ async def get_address(ctx, msg, keychain):
|
|||||||
|
|
||||||
if msg.show_display:
|
if msg.show_display:
|
||||||
desc = address_n_to_str(msg.address_n)
|
desc = address_n_to_str(msg.address_n)
|
||||||
while True:
|
await show_address(
|
||||||
if await confirms.show_address(ctx, addr.decode(), desc=desc):
|
ctx,
|
||||||
break
|
address=addr.decode(),
|
||||||
if await show_qr(ctx, "monero:" + addr.decode(), desc=desc):
|
address_qr="monero:" + addr.decode(),
|
||||||
break
|
desc=desc,
|
||||||
|
)
|
||||||
|
|
||||||
return MoneroAddress(address=addr)
|
return MoneroAddress(address=addr)
|
||||||
|
@ -20,8 +20,7 @@ from trezor.messages import MoneroGetTxKeyAck, MoneroGetTxKeyRequest
|
|||||||
|
|
||||||
from apps.common import paths
|
from apps.common import paths
|
||||||
from apps.common.keychain import auto_keychain
|
from apps.common.keychain import auto_keychain
|
||||||
from apps.monero import misc
|
from apps.monero import layout, misc
|
||||||
from apps.monero.layout import confirms
|
|
||||||
from apps.monero.xmr import crypto
|
from apps.monero.xmr import crypto
|
||||||
from apps.monero.xmr.crypto import chacha_poly
|
from apps.monero.xmr.crypto import chacha_poly
|
||||||
|
|
||||||
@ -34,7 +33,7 @@ async def get_tx_keys(ctx, msg: MoneroGetTxKeyRequest, keychain):
|
|||||||
await paths.validate_path(ctx, keychain, msg.address_n)
|
await paths.validate_path(ctx, keychain, msg.address_n)
|
||||||
|
|
||||||
do_deriv = msg.reason == _GET_TX_KEY_REASON_TX_DERIVATION
|
do_deriv = msg.reason == _GET_TX_KEY_REASON_TX_DERIVATION
|
||||||
await confirms.require_confirm_tx_key(ctx, export_key=not do_deriv)
|
await layout.require_confirm_tx_key(ctx, export_key=not do_deriv)
|
||||||
|
|
||||||
creds = misc.get_creds(keychain, msg.address_n, msg.network_type)
|
creds = misc.get_creds(keychain, msg.address_n, msg.network_type)
|
||||||
|
|
||||||
|
@ -2,8 +2,7 @@ from trezor.messages import MoneroGetWatchKey, MoneroWatchKey
|
|||||||
|
|
||||||
from apps.common import paths
|
from apps.common import paths
|
||||||
from apps.common.keychain import auto_keychain
|
from apps.common.keychain import auto_keychain
|
||||||
from apps.monero import misc
|
from apps.monero import layout, misc
|
||||||
from apps.monero.layout import confirms
|
|
||||||
from apps.monero.xmr import crypto
|
from apps.monero.xmr import crypto
|
||||||
|
|
||||||
|
|
||||||
@ -11,7 +10,7 @@ from apps.monero.xmr import crypto
|
|||||||
async def get_watch_only(ctx, msg: MoneroGetWatchKey, keychain):
|
async def get_watch_only(ctx, msg: MoneroGetWatchKey, keychain):
|
||||||
await paths.validate_path(ctx, keychain, msg.address_n)
|
await paths.validate_path(ctx, keychain, msg.address_n)
|
||||||
|
|
||||||
await confirms.require_confirm_watchkey(ctx)
|
await layout.require_confirm_watchkey(ctx)
|
||||||
|
|
||||||
creds = misc.get_creds(keychain, msg.address_n, msg.network_type)
|
creds = misc.get_creds(keychain, msg.address_n, msg.network_type)
|
||||||
address = creds.address
|
address = creds.address
|
||||||
|
@ -12,8 +12,7 @@ from trezor.messages import (
|
|||||||
|
|
||||||
from apps.common import paths
|
from apps.common import paths
|
||||||
from apps.common.keychain import auto_keychain
|
from apps.common.keychain import auto_keychain
|
||||||
from apps.monero import misc
|
from apps.monero import layout, misc
|
||||||
from apps.monero.layout import confirms
|
|
||||||
from apps.monero.xmr import crypto, key_image, monero
|
from apps.monero.xmr import crypto, key_image, monero
|
||||||
from apps.monero.xmr.crypto import chacha_poly
|
from apps.monero.xmr.crypto import chacha_poly
|
||||||
|
|
||||||
@ -49,7 +48,7 @@ async def _init_step(s, ctx, msg, keychain):
|
|||||||
|
|
||||||
s.creds = misc.get_creds(keychain, msg.address_n, msg.network_type)
|
s.creds = misc.get_creds(keychain, msg.address_n, msg.network_type)
|
||||||
|
|
||||||
await confirms.require_confirm_keyimage_sync(ctx)
|
await layout.require_confirm_keyimage_sync(ctx)
|
||||||
|
|
||||||
s.num_outputs = msg.num
|
s.num_outputs = msg.num
|
||||||
s.expected_hash = msg.hash
|
s.expected_hash = msg.hash
|
||||||
@ -71,7 +70,7 @@ async def _sync_step(s, ctx, tds):
|
|||||||
buff = bytearray(32 * 3)
|
buff = bytearray(32 * 3)
|
||||||
buff_mv = memoryview(buff)
|
buff_mv = memoryview(buff)
|
||||||
|
|
||||||
await confirms.keyimage_sync_step(ctx, s.current_output, s.num_outputs)
|
await layout.keyimage_sync_step(ctx, s.current_output, s.num_outputs)
|
||||||
|
|
||||||
for td in tds.tdis:
|
for td in tds.tdis:
|
||||||
s.current_output += 1
|
s.current_output += 1
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
from ubinascii import hexlify
|
from ubinascii import hexlify
|
||||||
|
|
||||||
from trezor import ui, wire
|
from trezor import strings, ui
|
||||||
from trezor.enums import ButtonRequestType
|
from trezor.enums import ButtonRequestType
|
||||||
from trezor.ui.components.tt.text import Text
|
from trezor.ui.layouts import (
|
||||||
from trezor.ui.layouts import confirm_action
|
confirm_action,
|
||||||
|
confirm_hex,
|
||||||
|
confirm_metadata,
|
||||||
|
confirm_output,
|
||||||
|
)
|
||||||
from trezor.ui.popup import Popup
|
from trezor.ui.popup import Popup
|
||||||
from trezor.utils import chunks
|
|
||||||
|
|
||||||
from apps.common.confirm import require_confirm, require_hold_to_confirm
|
|
||||||
from apps.monero.layout import common
|
|
||||||
|
|
||||||
DUMMY_PAYMENT_ID = b"\x00\x00\x00\x00\x00\x00\x00\x00"
|
DUMMY_PAYMENT_ID = b"\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||||
|
|
||||||
@ -21,6 +21,10 @@ if False:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _format_amount(value):
|
||||||
|
return "%s XMR" % strings.format_amount(value, 12)
|
||||||
|
|
||||||
|
|
||||||
async def require_confirm_watchkey(ctx):
|
async def require_confirm_watchkey(ctx):
|
||||||
await confirm_action(
|
await confirm_action(
|
||||||
ctx,
|
ctx,
|
||||||
@ -123,43 +127,46 @@ async def _require_confirm_output(
|
|||||||
version, dst.addr.spend_public_key, dst.addr.view_public_key, payment_id
|
version, dst.addr.spend_public_key, dst.addr.view_public_key, payment_id
|
||||||
)
|
)
|
||||||
|
|
||||||
text_addr = common.split_address(addr.decode())
|
await confirm_output(
|
||||||
text_amount = common.format_amount(dst.amount)
|
|
||||||
|
|
||||||
if not await common.naive_pagination(
|
|
||||||
ctx,
|
ctx,
|
||||||
[ui.BOLD, text_amount, ui.MONO] + list(text_addr),
|
address=addr.decode(),
|
||||||
"Confirm send",
|
amount=_format_amount(dst.amount),
|
||||||
ui.ICON_SEND,
|
font_amount=ui.BOLD,
|
||||||
ui.GREEN,
|
br_code=ButtonRequestType.SignTx,
|
||||||
4,
|
)
|
||||||
):
|
|
||||||
raise wire.ActionCancelled
|
|
||||||
|
|
||||||
|
|
||||||
async def _require_confirm_payment_id(ctx, payment_id: bytes):
|
async def _require_confirm_payment_id(ctx, payment_id: bytes):
|
||||||
if not await common.naive_pagination(
|
await confirm_hex(
|
||||||
ctx,
|
ctx,
|
||||||
[ui.MONO] + list(chunks(hexlify(payment_id).decode(), 16)),
|
"confirm_payment_id",
|
||||||
"Payment ID",
|
title="Payment ID",
|
||||||
ui.ICON_SEND,
|
data=hexlify(payment_id).decode(),
|
||||||
ui.GREEN,
|
br_code=ButtonRequestType.SignTx,
|
||||||
):
|
)
|
||||||
raise wire.ActionCancelled
|
|
||||||
|
|
||||||
|
|
||||||
async def _require_confirm_fee(ctx, fee):
|
async def _require_confirm_fee(ctx, fee):
|
||||||
content = Text("Confirm fee", ui.ICON_SEND, ui.GREEN)
|
await confirm_metadata(
|
||||||
content.bold(common.format_amount(fee))
|
ctx,
|
||||||
await require_hold_to_confirm(ctx, content, ButtonRequestType.ConfirmOutput)
|
"confirm_final",
|
||||||
|
title="Confirm fee",
|
||||||
|
content="{}",
|
||||||
|
param=_format_amount(fee),
|
||||||
|
hide_continue=True,
|
||||||
|
hold=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def _require_confirm_unlock_time(ctx, unlock_time):
|
async def _require_confirm_unlock_time(ctx, unlock_time):
|
||||||
content = Text("Confirm unlock time", ui.ICON_SEND, ui.GREEN)
|
await confirm_metadata(
|
||||||
content.normal("Unlock time for this transaction is set to")
|
ctx,
|
||||||
content.bold(str(unlock_time))
|
"confirm_locktime",
|
||||||
content.normal("Continue?")
|
"Confirm unlock time",
|
||||||
await require_confirm(ctx, content, ButtonRequestType.SignTx)
|
"Unlock time for this transaction is set to {}",
|
||||||
|
str(unlock_time),
|
||||||
|
br_code=ButtonRequestType.SignTx,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TransactionStep(ui.Component):
|
class TransactionStep(ui.Component):
|
||||||
@ -242,28 +249,3 @@ async def live_refresh_step(ctx, current):
|
|||||||
if current is None:
|
if current is None:
|
||||||
return
|
return
|
||||||
await Popup(LiveRefreshStep(current))
|
await Popup(LiveRefreshStep(current))
|
||||||
|
|
||||||
|
|
||||||
async def show_address(
|
|
||||||
ctx, address: str, desc: str = "Confirm address", network: str = None
|
|
||||||
):
|
|
||||||
from apps.common.confirm import confirm
|
|
||||||
from trezor.enums import ButtonRequestType
|
|
||||||
from trezor.ui.components.tt.button import ButtonDefault
|
|
||||||
from trezor.ui.components.tt.scroll import Paginated
|
|
||||||
|
|
||||||
pages = []
|
|
||||||
for lines in common.paginate_lines(common.split_address(address), 5):
|
|
||||||
text = Text(desc, ui.ICON_RECEIVE, ui.GREEN)
|
|
||||||
if network is not None:
|
|
||||||
text.normal("%s network" % network)
|
|
||||||
text.mono(*lines)
|
|
||||||
pages.append(text)
|
|
||||||
|
|
||||||
return await confirm(
|
|
||||||
ctx,
|
|
||||||
Paginated(pages),
|
|
||||||
code=ButtonRequestType.Address,
|
|
||||||
cancel="QR",
|
|
||||||
cancel_style=ButtonDefault,
|
|
||||||
)
|
|
@ -1,70 +0,0 @@
|
|||||||
from trezor import strings, ui, utils
|
|
||||||
from trezor.enums import ButtonRequestType
|
|
||||||
from trezor.ui.components.tt.text import Text
|
|
||||||
|
|
||||||
from apps.common import button_request
|
|
||||||
|
|
||||||
|
|
||||||
async def naive_pagination(
|
|
||||||
ctx, lines, title, icon=ui.ICON_RESET, icon_color=ui.ORANGE, per_page=5
|
|
||||||
):
|
|
||||||
from trezor.ui.components.tt.scroll import (
|
|
||||||
CANCELLED,
|
|
||||||
CONFIRMED,
|
|
||||||
PaginatedWithButtons,
|
|
||||||
)
|
|
||||||
|
|
||||||
pages = []
|
|
||||||
page_lines = paginate_lines(lines, per_page)
|
|
||||||
|
|
||||||
for i, lines in enumerate(page_lines):
|
|
||||||
if len(page_lines) > 1:
|
|
||||||
paging = "%s/%s" % (i + 1, len(page_lines))
|
|
||||||
else:
|
|
||||||
paging = ""
|
|
||||||
text = Text("%s %s" % (title, paging), icon, icon_color)
|
|
||||||
text.normal(*lines)
|
|
||||||
pages.append(text)
|
|
||||||
|
|
||||||
paginated = PaginatedWithButtons(pages, one_by_one=True)
|
|
||||||
|
|
||||||
while True:
|
|
||||||
await button_request(ctx, code=ButtonRequestType.SignTx)
|
|
||||||
result = await ctx.wait(paginated)
|
|
||||||
if result is CONFIRMED:
|
|
||||||
return True
|
|
||||||
if result is CANCELLED:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def paginate_lines(lines, lines_per_page=5):
|
|
||||||
"""Paginates lines across pages with preserving formatting modifiers (e.g., mono)"""
|
|
||||||
pages = []
|
|
||||||
cpage = []
|
|
||||||
nlines = 0
|
|
||||||
last_modifier = None
|
|
||||||
for line in lines:
|
|
||||||
cpage.append(line)
|
|
||||||
if not isinstance(line, int):
|
|
||||||
nlines += 1
|
|
||||||
else:
|
|
||||||
last_modifier = line
|
|
||||||
|
|
||||||
if nlines >= lines_per_page:
|
|
||||||
pages.append(cpage)
|
|
||||||
cpage = []
|
|
||||||
nlines = 0
|
|
||||||
if last_modifier is not None:
|
|
||||||
cpage.append(last_modifier)
|
|
||||||
|
|
||||||
if nlines > 0:
|
|
||||||
pages.append(cpage)
|
|
||||||
return pages
|
|
||||||
|
|
||||||
|
|
||||||
def format_amount(value):
|
|
||||||
return "%s XMR" % strings.format_amount(value, 12)
|
|
||||||
|
|
||||||
|
|
||||||
def split_address(address):
|
|
||||||
return utils.chunks(address, 16)
|
|
@ -13,8 +13,7 @@ from trezor.messages import (
|
|||||||
|
|
||||||
from apps.common import paths
|
from apps.common import paths
|
||||||
from apps.common.keychain import auto_keychain
|
from apps.common.keychain import auto_keychain
|
||||||
from apps.monero import misc
|
from apps.monero import layout, misc
|
||||||
from apps.monero.layout import confirms
|
|
||||||
from apps.monero.xmr import crypto, key_image, monero
|
from apps.monero.xmr import crypto, key_image, monero
|
||||||
from apps.monero.xmr.crypto import chacha_poly
|
from apps.monero.xmr.crypto import chacha_poly
|
||||||
|
|
||||||
@ -52,7 +51,7 @@ async def _init_step(
|
|||||||
await paths.validate_path(ctx, keychain, msg.address_n)
|
await paths.validate_path(ctx, keychain, msg.address_n)
|
||||||
|
|
||||||
if not storage.cache.get(storage.cache.APP_MONERO_LIVE_REFRESH):
|
if not storage.cache.get(storage.cache.APP_MONERO_LIVE_REFRESH):
|
||||||
await confirms.require_confirm_live_refresh(ctx)
|
await layout.require_confirm_live_refresh(ctx)
|
||||||
storage.cache.set(storage.cache.APP_MONERO_LIVE_REFRESH, b"\x01")
|
storage.cache.set(storage.cache.APP_MONERO_LIVE_REFRESH, b"\x01")
|
||||||
|
|
||||||
s.creds = misc.get_creds(keychain, msg.address_n, msg.network_type)
|
s.creds = misc.get_creds(keychain, msg.address_n, msg.network_type)
|
||||||
@ -64,7 +63,7 @@ async def _refresh_step(s: LiveRefreshState, ctx, msg: MoneroLiveRefreshStepRequ
|
|||||||
buff = bytearray(32 * 3)
|
buff = bytearray(32 * 3)
|
||||||
buff_mv = memoryview(buff)
|
buff_mv = memoryview(buff)
|
||||||
|
|
||||||
await confirms.live_refresh_step(ctx, s.current_output)
|
await layout.live_refresh_step(ctx, s.current_output)
|
||||||
s.current_output += 1
|
s.current_output += 1
|
||||||
|
|
||||||
if __debug__:
|
if __debug__:
|
||||||
|
@ -4,8 +4,7 @@ Initializes a new transaction.
|
|||||||
|
|
||||||
import gc
|
import gc
|
||||||
|
|
||||||
from apps.monero import misc, signing
|
from apps.monero import layout, misc, signing
|
||||||
from apps.monero.layout import confirms
|
|
||||||
from apps.monero.signing.state import State
|
from apps.monero.signing.state import State
|
||||||
from apps.monero.xmr import crypto, monero
|
from apps.monero.xmr import crypto, monero
|
||||||
|
|
||||||
@ -48,7 +47,7 @@ async def init_transaction(
|
|||||||
state.progress_cur = 0
|
state.progress_cur = 0
|
||||||
|
|
||||||
# Ask for confirmation
|
# Ask for confirmation
|
||||||
await confirms.require_confirm_transaction(
|
await layout.require_confirm_transaction(
|
||||||
state.ctx, state, tsx_data, state.creds.network_type
|
state.ctx, state, tsx_data, state.creds.network_type
|
||||||
)
|
)
|
||||||
state.creds.address = None
|
state.creds.address = None
|
||||||
|
@ -11,7 +11,7 @@ If number of inputs is small, in-memory mode is used = alpha, pseudo_outs are ke
|
|||||||
Otherwise pseudo_outs are offloaded with HMAC, alpha is offloaded encrypted under chacha_poly with
|
Otherwise pseudo_outs are offloaded with HMAC, alpha is offloaded encrypted under chacha_poly with
|
||||||
key derived for exactly this purpose.
|
key derived for exactly this purpose.
|
||||||
"""
|
"""
|
||||||
from apps.monero.layout import confirms
|
from apps.monero import layout
|
||||||
from apps.monero.xmr import crypto, monero, serialize
|
from apps.monero.xmr import crypto, monero, serialize
|
||||||
|
|
||||||
from .state import State
|
from .state import State
|
||||||
@ -32,7 +32,7 @@ async def set_input(
|
|||||||
|
|
||||||
state.current_input_index += 1
|
state.current_input_index += 1
|
||||||
|
|
||||||
await confirms.transaction_step(state, state.STEP_INP, state.current_input_index)
|
await layout.transaction_step(state, state.STEP_INP, state.current_input_index)
|
||||||
|
|
||||||
if state.last_step > state.STEP_INP:
|
if state.last_step > state.STEP_INP:
|
||||||
raise ValueError("Invalid state transition")
|
raise ValueError("Invalid state transition")
|
||||||
|
@ -16,7 +16,7 @@ HMAC correctness (host sends original sort idx) and ordering check
|
|||||||
on the key images. This step is skipped.
|
on the key images. This step is skipped.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from apps.monero.layout.confirms import transaction_step
|
from apps.monero.layout import transaction_step
|
||||||
|
|
||||||
from .state import State
|
from .state import State
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ This step serves for an incremental hashing of tx.vin[i] to the tx_prefix_hasher
|
|||||||
after the sorting on tx.vin[i].ki. The sorting order was received in the previous step.
|
after the sorting on tx.vin[i].ki. The sorting order was received in the previous step.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from apps.monero.layout import confirms
|
from apps.monero import layout
|
||||||
from apps.monero.signing import offloading_keys
|
from apps.monero.signing import offloading_keys
|
||||||
from apps.monero.xmr import crypto
|
from apps.monero.xmr import crypto
|
||||||
|
|
||||||
@ -25,9 +25,7 @@ async def input_vini(
|
|||||||
) -> MoneroTransactionInputViniAck:
|
) -> MoneroTransactionInputViniAck:
|
||||||
from trezor.messages import MoneroTransactionInputViniAck
|
from trezor.messages import MoneroTransactionInputViniAck
|
||||||
|
|
||||||
await confirms.transaction_step(
|
await layout.transaction_step(state, state.STEP_VINI, state.current_input_index + 1)
|
||||||
state, state.STEP_VINI, state.current_input_index + 1
|
|
||||||
)
|
|
||||||
if state.last_step not in (state.STEP_INP, state.STEP_PERM, state.STEP_VINI):
|
if state.last_step not in (state.STEP_INP, state.STEP_PERM, state.STEP_VINI):
|
||||||
raise ValueError("Invalid state transition")
|
raise ValueError("Invalid state transition")
|
||||||
if state.current_input_index >= state.input_count:
|
if state.current_input_index >= state.input_count:
|
||||||
|
@ -3,7 +3,7 @@ All inputs set. Defining range signature parameters.
|
|||||||
If in the applicable offloading mode, generate commitment masks.
|
If in the applicable offloading mode, generate commitment masks.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from apps.monero.layout import confirms
|
from apps.monero import layout
|
||||||
from apps.monero.xmr import crypto
|
from apps.monero.xmr import crypto
|
||||||
|
|
||||||
from .state import State
|
from .state import State
|
||||||
@ -15,7 +15,7 @@ if False:
|
|||||||
async def all_inputs_set(state: State) -> MoneroTransactionAllInputsSetAck:
|
async def all_inputs_set(state: State) -> MoneroTransactionAllInputsSetAck:
|
||||||
state.mem_trace(0)
|
state.mem_trace(0)
|
||||||
|
|
||||||
await confirms.transaction_step(state, state.STEP_ALL_IN)
|
await layout.transaction_step(state, state.STEP_ALL_IN)
|
||||||
|
|
||||||
from trezor.messages import MoneroTransactionAllInputsSetAck
|
from trezor.messages import MoneroTransactionAllInputsSetAck
|
||||||
|
|
||||||
|
@ -6,8 +6,7 @@ import gc
|
|||||||
|
|
||||||
from trezor import utils
|
from trezor import utils
|
||||||
|
|
||||||
from apps.monero import signing
|
from apps.monero import layout, signing
|
||||||
from apps.monero.layout import confirms
|
|
||||||
from apps.monero.signing import offloading_keys
|
from apps.monero.signing import offloading_keys
|
||||||
from apps.monero.xmr import crypto, serialize
|
from apps.monero.xmr import crypto, serialize
|
||||||
|
|
||||||
@ -36,7 +35,7 @@ async def set_output(
|
|||||||
|
|
||||||
# Progress update only for master message (skip for offloaded BP msg)
|
# Progress update only for master message (skip for offloaded BP msg)
|
||||||
if not is_offloaded_bp:
|
if not is_offloaded_bp:
|
||||||
await confirms.transaction_step(
|
await layout.transaction_step(
|
||||||
state, state.STEP_OUT, state.current_output_index + 1
|
state, state.STEP_OUT, state.current_output_index + 1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ import gc
|
|||||||
|
|
||||||
from trezor import utils
|
from trezor import utils
|
||||||
|
|
||||||
from apps.monero.layout import confirms
|
from apps.monero import layout
|
||||||
from apps.monero.xmr import crypto
|
from apps.monero.xmr import crypto
|
||||||
|
|
||||||
from .state import State
|
from .state import State
|
||||||
@ -20,7 +20,7 @@ if False:
|
|||||||
async def all_outputs_set(state: State) -> MoneroTransactionAllOutSetAck:
|
async def all_outputs_set(state: State) -> MoneroTransactionAllOutSetAck:
|
||||||
state.mem_trace(0)
|
state.mem_trace(0)
|
||||||
|
|
||||||
await confirms.transaction_step(state, state.STEP_ALL_OUT)
|
await layout.transaction_step(state, state.STEP_ALL_OUT)
|
||||||
state.mem_trace(1)
|
state.mem_trace(1)
|
||||||
|
|
||||||
_validate(state)
|
_validate(state)
|
||||||
|
@ -14,7 +14,7 @@ import gc
|
|||||||
|
|
||||||
from trezor import utils
|
from trezor import utils
|
||||||
|
|
||||||
from apps.monero.layout import confirms
|
from apps.monero import layout
|
||||||
from apps.monero.xmr import crypto
|
from apps.monero.xmr import crypto
|
||||||
|
|
||||||
from .state import State
|
from .state import State
|
||||||
@ -48,9 +48,7 @@ async def sign_input(
|
|||||||
:param orig_idx: original index of the src_entr before sorting (HMAC check)
|
:param orig_idx: original index of the src_entr before sorting (HMAC check)
|
||||||
:return: Generated signature MGs[i]
|
:return: Generated signature MGs[i]
|
||||||
"""
|
"""
|
||||||
await confirms.transaction_step(
|
await layout.transaction_step(state, state.STEP_SIGN, state.current_input_index + 1)
|
||||||
state, state.STEP_SIGN, state.current_input_index + 1
|
|
||||||
)
|
|
||||||
|
|
||||||
state.current_input_index += 1
|
state.current_input_index += 1
|
||||||
if state.last_step not in (state.STEP_ALL_OUT, state.STEP_SIGN):
|
if state.last_step not in (state.STEP_ALL_OUT, state.STEP_SIGN):
|
||||||
|
@ -9,6 +9,12 @@ from .confirm import CANCELLED, CONFIRMED, Confirm
|
|||||||
from .swipe import SWIPE_DOWN, SWIPE_UP, SWIPE_VERTICAL, Swipe
|
from .swipe import SWIPE_DOWN, SWIPE_UP, SWIPE_VERTICAL, Swipe
|
||||||
from .text import TEXT_MAX_LINES, Span, Text
|
from .text import TEXT_MAX_LINES, Span, Text
|
||||||
|
|
||||||
|
if False:
|
||||||
|
from typing import Iterable, Any
|
||||||
|
|
||||||
|
from ..common.text import TextContent
|
||||||
|
|
||||||
|
|
||||||
_PAGINATED_LINE_WIDTH = const(204)
|
_PAGINATED_LINE_WIDTH = const(204)
|
||||||
|
|
||||||
WAS_PAGED = object()
|
WAS_PAGED = object()
|
||||||
@ -301,3 +307,71 @@ def paginate_text(
|
|||||||
|
|
||||||
pages[-1] = Confirm(pages[-1])
|
pages[-1] = Confirm(pages[-1])
|
||||||
return Paginated(pages)
|
return Paginated(pages)
|
||||||
|
|
||||||
|
|
||||||
|
def paginate_paragraphs(
|
||||||
|
para: Iterable[tuple[int, str]],
|
||||||
|
header: str,
|
||||||
|
header_icon: str = ui.ICON_DEFAULT,
|
||||||
|
icon_color: int = ui.ORANGE_ICON,
|
||||||
|
break_words: bool = False,
|
||||||
|
confirm_kwargs: Dict[str, Any] = {},
|
||||||
|
) -> Union[Confirm, Paginated]:
|
||||||
|
span = Span("", 0, ui.NORMAL, break_words=break_words)
|
||||||
|
lines = 0
|
||||||
|
content: list[TextContent] = []
|
||||||
|
for font, text in para:
|
||||||
|
span.reset(text, 0, font, break_words=break_words)
|
||||||
|
lines += span.count_lines()
|
||||||
|
|
||||||
|
# we'll need this for multipage too
|
||||||
|
if content:
|
||||||
|
content.append("\n")
|
||||||
|
content.append(font)
|
||||||
|
content.append(text)
|
||||||
|
|
||||||
|
if lines <= TEXT_MAX_LINES:
|
||||||
|
result = Text(
|
||||||
|
header,
|
||||||
|
header_icon=header_icon,
|
||||||
|
icon_color=icon_color,
|
||||||
|
new_lines=False,
|
||||||
|
break_words=break_words,
|
||||||
|
)
|
||||||
|
for font, text in para:
|
||||||
|
if len(result.content) != 0:
|
||||||
|
result.content.append("\n")
|
||||||
|
result.content.append(font)
|
||||||
|
result.content.append(text)
|
||||||
|
return Confirm(result, **confirm_kwargs)
|
||||||
|
|
||||||
|
else:
|
||||||
|
pages: list[ui.Component] = []
|
||||||
|
lines_left = 0
|
||||||
|
for i, (font, text) in enumerate(para):
|
||||||
|
span.reset(
|
||||||
|
text, 0, font, break_words=break_words, line_width=_PAGINATED_LINE_WIDTH
|
||||||
|
)
|
||||||
|
|
||||||
|
while span.has_more_content():
|
||||||
|
span.next_line()
|
||||||
|
if lines_left <= 0:
|
||||||
|
page = Text(
|
||||||
|
header,
|
||||||
|
header_icon=header_icon,
|
||||||
|
icon_color=icon_color,
|
||||||
|
new_lines=False,
|
||||||
|
content_offset=i * 3 + 1, # font, _text_, newline
|
||||||
|
char_offset=span.start,
|
||||||
|
line_width=_PAGINATED_LINE_WIDTH,
|
||||||
|
render_page_overflow=False,
|
||||||
|
break_words=break_words,
|
||||||
|
)
|
||||||
|
page.content = content
|
||||||
|
pages.append(page)
|
||||||
|
lines_left = TEXT_MAX_LINES - 1
|
||||||
|
else:
|
||||||
|
lines_left -= 1
|
||||||
|
|
||||||
|
pages[-1] = Confirm(pages[-1], **confirm_kwargs)
|
||||||
|
return Paginated(pages)
|
||||||
|
@ -6,7 +6,7 @@ TEXT_LINE_HEIGHT_HALF = const(13)
|
|||||||
TEXT_MARGIN_LEFT = const(14)
|
TEXT_MARGIN_LEFT = const(14)
|
||||||
TEXT_MAX_LINES = const(5)
|
TEXT_MAX_LINES = const(5)
|
||||||
|
|
||||||
MONO_CHARS_PER_LINE = const(17)
|
MONO_ADDR_PER_LINE = const(17)
|
||||||
MONO_HEX_PER_LINE = const(18)
|
MONO_HEX_PER_LINE = const(18)
|
||||||
|
|
||||||
QR_X = const(120)
|
QR_X = const(120)
|
||||||
|
@ -11,10 +11,10 @@ from ..components.common import break_path_to_lines
|
|||||||
from ..components.common.confirm import is_confirmed, raise_if_cancelled
|
from ..components.common.confirm import is_confirmed, raise_if_cancelled
|
||||||
from ..components.tt.button import ButtonCancel, ButtonDefault
|
from ..components.tt.button import ButtonCancel, ButtonDefault
|
||||||
from ..components.tt.confirm import Confirm, HoldToConfirm
|
from ..components.tt.confirm import Confirm, HoldToConfirm
|
||||||
from ..components.tt.scroll import Paginated, paginate_text
|
from ..components.tt.scroll import Paginated, paginate_paragraphs, paginate_text
|
||||||
from ..components.tt.text import Span, Text
|
from ..components.tt.text import Span, Text
|
||||||
from ..constants.tt import (
|
from ..constants.tt import (
|
||||||
MONO_CHARS_PER_LINE,
|
MONO_ADDR_PER_LINE,
|
||||||
MONO_HEX_PER_LINE,
|
MONO_HEX_PER_LINE,
|
||||||
QR_SIZE_THRESHOLD,
|
QR_SIZE_THRESHOLD,
|
||||||
QR_X,
|
QR_X,
|
||||||
@ -182,7 +182,7 @@ async def confirm_backup(ctx: wire.GenericContext) -> bool:
|
|||||||
async def confirm_path_warning(ctx: wire.GenericContext, path: str) -> None:
|
async def confirm_path_warning(ctx: wire.GenericContext, path: str) -> None:
|
||||||
text = Text("Confirm path", ui.ICON_WRONG, ui.RED)
|
text = Text("Confirm path", ui.ICON_WRONG, ui.RED)
|
||||||
text.normal("Path")
|
text.normal("Path")
|
||||||
text.mono(*break_path_to_lines(path, MONO_CHARS_PER_LINE))
|
text.mono(*break_path_to_lines(path, MONO_ADDR_PER_LINE))
|
||||||
text.normal("is unknown.", "Are you sure?")
|
text.normal("is unknown.", "Are you sure?")
|
||||||
await raise_if_cancelled(
|
await raise_if_cancelled(
|
||||||
interact(
|
interact(
|
||||||
@ -207,7 +207,7 @@ def _show_qr(
|
|||||||
|
|
||||||
|
|
||||||
def _split_address(address: str) -> Iterator[str]:
|
def _split_address(address: str) -> Iterator[str]:
|
||||||
return chunks_intersperse(address, MONO_CHARS_PER_LINE)
|
return chunks_intersperse(address, MONO_ADDR_PER_LINE)
|
||||||
|
|
||||||
|
|
||||||
def _truncate_hex(
|
def _truncate_hex(
|
||||||
@ -232,13 +232,18 @@ def _show_address(
|
|||||||
address: str,
|
address: str,
|
||||||
desc: str,
|
desc: str,
|
||||||
network: str | None = None,
|
network: str | None = None,
|
||||||
) -> Confirm:
|
) -> Confirm | Paginated:
|
||||||
text = Text(desc, ui.ICON_RECEIVE, ui.GREEN, new_lines=False)
|
para = [(ui.NORMAL, "%s network" % network)] if network is not None else []
|
||||||
if network is not None:
|
para.extend(
|
||||||
text.normal("%s network\n" % network)
|
(ui.MONO, address_line) for address_line in chunks(address, MONO_ADDR_PER_LINE)
|
||||||
text.mono(*_split_address(address))
|
)
|
||||||
|
return paginate_paragraphs(
|
||||||
return Confirm(text, cancel="QR", cancel_style=ButtonDefault)
|
para,
|
||||||
|
header=desc,
|
||||||
|
header_icon=ui.ICON_RECEIVE,
|
||||||
|
icon_color=ui.GREEN,
|
||||||
|
confirm_kwargs={"cancel": "QR", "cancel_style": ButtonDefault},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _show_xpub(xpub: str, desc: str, cancel: str) -> Paginated:
|
def _show_xpub(xpub: str, desc: str, cancel: str) -> Paginated:
|
||||||
@ -332,6 +337,7 @@ def show_pubkey(
|
|||||||
data=pubkey,
|
data=pubkey,
|
||||||
br_code=ButtonRequestType.PublicKey,
|
br_code=ButtonRequestType.PublicKey,
|
||||||
icon=ui.ICON_RECEIVE,
|
icon=ui.ICON_RECEIVE,
|
||||||
|
truncate=True, # should fit?
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -441,14 +447,22 @@ async def confirm_output(
|
|||||||
amount: str,
|
amount: str,
|
||||||
font_amount: int = ui.NORMAL, # TODO cleanup @ redesign
|
font_amount: int = ui.NORMAL, # TODO cleanup @ redesign
|
||||||
color_to: int = ui.FG, # TODO cleanup @ redesign
|
color_to: int = ui.FG, # TODO cleanup @ redesign
|
||||||
|
width: int = MONO_ADDR_PER_LINE,
|
||||||
|
width_paginated: int = MONO_ADDR_PER_LINE - 1,
|
||||||
br_code: ButtonRequestType = ButtonRequestType.ConfirmOutput,
|
br_code: ButtonRequestType = ButtonRequestType.ConfirmOutput,
|
||||||
) -> None:
|
) -> None:
|
||||||
text = Text("Confirm sending", ui.ICON_SEND, ui.GREEN, new_lines=False)
|
title = "Confirm sending"
|
||||||
text.content = [font_amount, amount, ui.NORMAL, color_to, " to\n", ui.FG]
|
if len(address) > (TEXT_MAX_LINES - 1) * width:
|
||||||
text.mono(*_split_address(address))
|
para = [(font_amount, amount)]
|
||||||
await raise_if_cancelled(
|
para.extend((ui.MONO, line) for line in chunks(address, width_paginated))
|
||||||
interact(ctx, Confirm(text), "confirm_output", br_code)
|
content: ui.Layout = paginate_paragraphs(para, title, ui.ICON_SEND, ui.GREEN)
|
||||||
)
|
else:
|
||||||
|
text = Text(title, ui.ICON_SEND, ui.GREEN, new_lines=False)
|
||||||
|
text.content = [font_amount, amount, ui.NORMAL, color_to, " to\n", ui.FG]
|
||||||
|
text.mono(*chunks_intersperse(address, width))
|
||||||
|
content = Confirm(text)
|
||||||
|
|
||||||
|
await raise_if_cancelled(interact(ctx, content, "confirm_output", br_code))
|
||||||
|
|
||||||
|
|
||||||
async def confirm_decred_sstx_submission(
|
async def confirm_decred_sstx_submission(
|
||||||
@ -482,25 +496,34 @@ async def confirm_hex(
|
|||||||
font_description: int = ui.NORMAL, # TODO cleanup @ redesign
|
font_description: int = ui.NORMAL, # TODO cleanup @ redesign
|
||||||
color_description: int = ui.FG, # TODO cleanup @ redesign
|
color_description: int = ui.FG, # TODO cleanup @ redesign
|
||||||
width: int = MONO_HEX_PER_LINE,
|
width: int = MONO_HEX_PER_LINE,
|
||||||
|
width_paginated: int = MONO_HEX_PER_LINE - 2,
|
||||||
|
truncate: bool = False,
|
||||||
truncate_middle: bool = False,
|
truncate_middle: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
text = Text(title, icon, icon_color, new_lines=False)
|
if truncate:
|
||||||
description_lines = 0
|
text = Text(title, icon, icon_color, new_lines=False)
|
||||||
if description is not None:
|
description_lines = 0
|
||||||
description_lines = Span(description, 0, font_description).count_lines()
|
if description is not None:
|
||||||
text.content.extend(
|
description_lines = Span(description, 0, font_description).count_lines()
|
||||||
(font_description, color_description, description, ui.FG)
|
text.content.extend(
|
||||||
|
(font_description, color_description, description, ui.FG)
|
||||||
|
)
|
||||||
|
text.br()
|
||||||
|
text.mono(
|
||||||
|
*_truncate_hex(
|
||||||
|
data,
|
||||||
|
lines=TEXT_MAX_LINES - description_lines,
|
||||||
|
width=width,
|
||||||
|
middle=truncate_middle,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
text.br()
|
content: ui.Layout = Confirm(text)
|
||||||
text.mono(
|
else:
|
||||||
*_truncate_hex(
|
width_paginated = min(width, MONO_HEX_PER_LINE - 2)
|
||||||
data,
|
assert color_description == ui.FG # only ethereum uses this and it truncates
|
||||||
lines=TEXT_MAX_LINES - description_lines,
|
para = [(font_description, description)] if description is not None else []
|
||||||
width=width,
|
para.extend((ui.MONO, line) for line in chunks(data, width_paginated))
|
||||||
middle=truncate_middle,
|
content = paginate_paragraphs(para, title, icon, icon_color)
|
||||||
)
|
|
||||||
)
|
|
||||||
content: ui.Layout = Confirm(text)
|
|
||||||
await raise_if_cancelled(interact(ctx, content, br_type, br_code))
|
await raise_if_cancelled(interact(ctx, content, br_type, br_code))
|
||||||
|
|
||||||
|
|
||||||
@ -554,14 +577,19 @@ async def confirm_metadata(
|
|||||||
content: str,
|
content: str,
|
||||||
param: str | None = None,
|
param: str | None = None,
|
||||||
br_code: ButtonRequestType = ButtonRequestType.SignTx,
|
br_code: ButtonRequestType = ButtonRequestType.SignTx,
|
||||||
|
hide_continue: bool = False,
|
||||||
|
hold: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
text = Text(title, ui.ICON_SEND, ui.GREEN, new_lines=False)
|
text = Text(title, ui.ICON_SEND, ui.GREEN, new_lines=False)
|
||||||
text.format_parametrized(content, param if param is not None else "")
|
text.format_parametrized(content, param if param is not None else "")
|
||||||
text.br()
|
|
||||||
|
|
||||||
text.normal("Continue?")
|
if not hide_continue:
|
||||||
|
text.br()
|
||||||
|
text.normal("Continue?")
|
||||||
|
|
||||||
await raise_if_cancelled(interact(ctx, Confirm(text), br_type, br_code))
|
cls = HoldToConfirm if hold else Confirm
|
||||||
|
|
||||||
|
await raise_if_cancelled(interact(ctx, cls(text), br_type, br_code))
|
||||||
|
|
||||||
|
|
||||||
async def confirm_replacement(
|
async def confirm_replacement(
|
||||||
|
@ -29,14 +29,14 @@ class TestMsgMoneroGetaddress:
|
|||||||
@pytest.mark.setup_client(mnemonic=MNEMONIC12)
|
@pytest.mark.setup_client(mnemonic=MNEMONIC12)
|
||||||
def test_monero_getaddress(self, client):
|
def test_monero_getaddress(self, client):
|
||||||
assert (
|
assert (
|
||||||
monero.get_address(client, parse_path("m/44h/128h/0h"))
|
monero.get_address(client, parse_path("m/44h/128h/0h"), show_display=True)
|
||||||
== b"4Ahp23WfMrMFK3wYL2hLWQFGt87ZTeRkufS6JoQZu6MEFDokAQeGWmu9MA3GFq1yVLSJQbKJqVAn9F9DLYGpRzRAEXqAXKM"
|
== b"4Ahp23WfMrMFK3wYL2hLWQFGt87ZTeRkufS6JoQZu6MEFDokAQeGWmu9MA3GFq1yVLSJQbKJqVAn9F9DLYGpRzRAEXqAXKM"
|
||||||
)
|
)
|
||||||
assert (
|
assert (
|
||||||
monero.get_address(client, parse_path("m/44h/128h/1h"))
|
monero.get_address(client, parse_path("m/44h/128h/1h"), show_display=True)
|
||||||
== b"44iAazhoAkv5a5RqLNVyh82a1n3ceNggmN4Ho7bUBJ14WkEVR8uFTe9f7v5rNnJ2kEbVXxfXiRzsD5Jtc6NvBi4D6WNHPie"
|
== b"44iAazhoAkv5a5RqLNVyh82a1n3ceNggmN4Ho7bUBJ14WkEVR8uFTe9f7v5rNnJ2kEbVXxfXiRzsD5Jtc6NvBi4D6WNHPie"
|
||||||
)
|
)
|
||||||
assert (
|
assert (
|
||||||
monero.get_address(client, parse_path("m/44h/128h/2h"))
|
monero.get_address(client, parse_path("m/44h/128h/2h"), show_display=True)
|
||||||
== b"47ejhmbZ4wHUhXaqA4b7PN667oPMkokf4ZkNdWrMSPy9TNaLVr7vLqVUQHh2MnmaAEiyrvLsX8xUf99q3j1iAeMV8YvSFcH"
|
== b"47ejhmbZ4wHUhXaqA4b7PN667oPMkokf4ZkNdWrMSPy9TNaLVr7vLqVUQHh2MnmaAEiyrvLsX8xUf99q3j1iAeMV8YvSFcH"
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user