mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-11-18 05:28:40 +00:00
fix(core/ethereum): ask before showing paginated data field
This commit is contained in:
parent
e6c42b7fa6
commit
3882b89be9
1
core/.changelog.d/1819.fixed
Normal file
1
core/.changelog.d/1819.fixed
Normal file
@ -0,0 +1 @@
|
||||
Ethereum: make it optional to view the entire data field when signing transaction.
|
@ -103,6 +103,7 @@ def require_confirm_data(ctx: Context, data: bytes, data_total: int) -> Awaitabl
|
||||
description=f"Size: {data_total} bytes",
|
||||
data=data,
|
||||
br_code=ButtonRequestType.SignTx,
|
||||
ask_pagination=True,
|
||||
)
|
||||
|
||||
|
||||
|
@ -1,21 +1,26 @@
|
||||
from trezor import loop, ui, wire
|
||||
|
||||
if False:
|
||||
from typing import Callable, Any, Awaitable
|
||||
from typing import Callable, Any, Awaitable, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
CONFIRMED = object()
|
||||
CANCELLED = object()
|
||||
INFO = object()
|
||||
GO_BACK = object()
|
||||
SHOW_PAGINATED = object()
|
||||
|
||||
|
||||
def is_confirmed(x: Any) -> bool:
|
||||
return x is CONFIRMED
|
||||
|
||||
|
||||
async def raise_if_cancelled(a: Awaitable, exc: Any = wire.ActionCancelled) -> None:
|
||||
async def raise_if_cancelled(a: Awaitable[T], exc: Any = wire.ActionCancelled) -> T:
|
||||
result = await a
|
||||
if result is CANCELLED:
|
||||
raise exc
|
||||
return result
|
||||
|
||||
|
||||
async def is_confirmed_info(
|
||||
|
@ -4,8 +4,9 @@ from trezor import loop, res, ui, utils, wire, workflow
|
||||
from trezor.enums import ButtonRequestType
|
||||
from trezor.messages import ButtonAck, ButtonRequest
|
||||
|
||||
from ..common.confirm import CANCELLED, CONFIRMED, GO_BACK, SHOW_PAGINATED
|
||||
from .button import Button, ButtonCancel, ButtonConfirm, ButtonDefault
|
||||
from .confirm import CANCELLED, CONFIRMED, Confirm
|
||||
from .confirm import Confirm
|
||||
from .swipe import SWIPE_DOWN, SWIPE_UP, SWIPE_VERTICAL, Swipe
|
||||
from .text import (
|
||||
LINE_WIDTH_PAGINATED,
|
||||
@ -47,7 +48,7 @@ def render_scrollbar(pages: int, page: int) -> None:
|
||||
ui.display.bar_radius(X, Y + i * padding, SIZE, SIZE, fg, ui.BG, 4)
|
||||
|
||||
|
||||
def render_swipe_icon() -> None:
|
||||
def render_swipe_icon(x_offset: int = 0) -> None:
|
||||
if utils.DISABLE_ANIMATION:
|
||||
c = ui.GREY
|
||||
else:
|
||||
@ -56,32 +57,46 @@ def render_swipe_icon() -> None:
|
||||
c = ui.blend(ui.GREY, ui.DARK_GREY, t)
|
||||
|
||||
icon = res.load(ui.ICON_SWIPE)
|
||||
ui.display.icon(70, 205, icon, c, ui.BG)
|
||||
ui.display.icon(70 + x_offset, 205, icon, c, ui.BG)
|
||||
|
||||
|
||||
def render_swipe_text() -> None:
|
||||
ui.display.text_center(130, 220, "Swipe", ui.BOLD, ui.GREY, ui.BG)
|
||||
def render_swipe_text(x_offset: int = 0) -> None:
|
||||
ui.display.text_center(130 + x_offset, 220, "Swipe", ui.BOLD, ui.GREY, ui.BG)
|
||||
|
||||
|
||||
class Paginated(ui.Layout):
|
||||
def __init__(self, pages: list[ui.Component], page: int = 0):
|
||||
def __init__(
|
||||
self, pages: list[ui.Component], page: int = 0, back_button: bool = False
|
||||
):
|
||||
super().__init__()
|
||||
self.pages = pages
|
||||
self.page = page
|
||||
self.back_button = None
|
||||
if back_button:
|
||||
area = ui.grid(16, n_x=4)
|
||||
icon = res.load(ui.ICON_BACK)
|
||||
self.back_button = Button(area, icon, ButtonDefault)
|
||||
self.back_button.on_click = self.on_back_click # type: ignore
|
||||
|
||||
def dispatch(self, event: int, x: int, y: int) -> None:
|
||||
pages = self.pages
|
||||
page = self.page
|
||||
length = len(pages)
|
||||
last_page = page >= length - 1
|
||||
x_offset = 0
|
||||
|
||||
pages[page].dispatch(event, x, y)
|
||||
if self.back_button is not None and not last_page:
|
||||
self.back_button.dispatch(event, x, y)
|
||||
x_offset = 30
|
||||
|
||||
if event is ui.REPAINT:
|
||||
self.repaint = True
|
||||
elif event is ui.RENDER:
|
||||
length = len(pages)
|
||||
if page < length - 1:
|
||||
render_swipe_icon()
|
||||
if not last_page:
|
||||
render_swipe_icon(x_offset=x_offset)
|
||||
if self.repaint:
|
||||
render_swipe_text()
|
||||
render_swipe_text(x_offset=x_offset)
|
||||
if self.repaint:
|
||||
render_scrollbar(length, page)
|
||||
self.repaint = False
|
||||
@ -143,12 +158,35 @@ class Paginated(ui.Layout):
|
||||
def on_change(self) -> None:
|
||||
pass
|
||||
|
||||
def on_back_click(self) -> None:
|
||||
raise ui.Result(GO_BACK)
|
||||
|
||||
if __debug__:
|
||||
|
||||
def read_content(self) -> list[str]:
|
||||
return self.pages[self.page].read_content()
|
||||
|
||||
|
||||
class AskPaginated(ui.Component):
|
||||
def __init__(self, content: ui.Component) -> None:
|
||||
super().__init__()
|
||||
self.content = content
|
||||
self.button = Button(ui.grid(3, n_x=1), "Show all", ButtonDefault)
|
||||
self.button.on_click = self.on_show_paginated_click # type: ignore
|
||||
|
||||
def dispatch(self, event: int, x: int, y: int) -> None:
|
||||
self.content.dispatch(event, x, y)
|
||||
self.button.dispatch(event, x, y)
|
||||
|
||||
def on_show_paginated_click(self) -> None:
|
||||
raise ui.Result(SHOW_PAGINATED)
|
||||
|
||||
if __debug__:
|
||||
|
||||
def read_content(self) -> list[str]:
|
||||
return self.content.read_content()
|
||||
|
||||
|
||||
class PageWithButtons(ui.Component):
|
||||
def __init__(
|
||||
self,
|
||||
@ -321,6 +359,7 @@ def paginate_paragraphs(
|
||||
icon_color: int = ui.ORANGE_ICON,
|
||||
break_words: bool = False,
|
||||
confirm: Callable[[ui.Component], ui.Layout] = Confirm,
|
||||
back_button: bool = False,
|
||||
) -> ui.Layout:
|
||||
span = Span("", 0, ui.NORMAL, break_words=break_words)
|
||||
lines = 0
|
||||
@ -391,4 +430,4 @@ def paginate_paragraphs(
|
||||
content_ctr += 1
|
||||
|
||||
pages[-1] = confirm(pages[-1])
|
||||
return Paginated(pages)
|
||||
return Paginated(pages, back_button=back_button)
|
||||
|
@ -10,12 +10,19 @@ from trezor.ui.qr import Qr
|
||||
from trezor.utils import chunks, chunks_intersperse
|
||||
|
||||
from ...components.common import break_path_to_lines
|
||||
from ...components.common.confirm import is_confirmed, raise_if_cancelled
|
||||
from ...components.common.confirm import (
|
||||
CONFIRMED,
|
||||
GO_BACK,
|
||||
SHOW_PAGINATED,
|
||||
is_confirmed,
|
||||
raise_if_cancelled,
|
||||
)
|
||||
from ...components.tt import passphrase, pin
|
||||
from ...components.tt.button import ButtonCancel, ButtonDefault
|
||||
from ...components.tt.confirm import Confirm, HoldToConfirm
|
||||
from ...components.tt.scroll import (
|
||||
PAGEBREAK,
|
||||
AskPaginated,
|
||||
Paginated,
|
||||
paginate_paragraphs,
|
||||
paginate_text,
|
||||
@ -32,7 +39,7 @@ from ...constants.tt import (
|
||||
from ..common import button_request, interact
|
||||
|
||||
if False:
|
||||
from typing import Awaitable, Iterator, NoReturn, Sequence
|
||||
from typing import Awaitable, Iterable, Iterator, NoReturn, Sequence
|
||||
|
||||
from ..common import PropertyType, ExceptionType
|
||||
|
||||
@ -503,6 +510,50 @@ async def confirm_output(
|
||||
await raise_if_cancelled(interact(ctx, content, "confirm_output", br_code))
|
||||
|
||||
|
||||
async def _confirm_ask_pagination(
|
||||
ctx: wire.GenericContext,
|
||||
br_type: str,
|
||||
title: str,
|
||||
para: Iterable[tuple[int, str]],
|
||||
para_truncated: Iterable[tuple[int, str]],
|
||||
br_code: ButtonRequestType,
|
||||
icon: str,
|
||||
icon_color: int,
|
||||
) -> None:
|
||||
paginated: ui.Layout | None = None
|
||||
|
||||
truncated = Text(
|
||||
title,
|
||||
header_icon=icon,
|
||||
icon_color=icon_color,
|
||||
new_lines=False,
|
||||
max_lines=TEXT_MAX_LINES - 2,
|
||||
)
|
||||
for font, text in para_truncated:
|
||||
truncated.content.extend((font, text, "\n"))
|
||||
ask_dialog = Confirm(AskPaginated(truncated))
|
||||
|
||||
while True:
|
||||
result = await raise_if_cancelled(interact(ctx, ask_dialog, br_type, br_code))
|
||||
if result is CONFIRMED:
|
||||
return
|
||||
assert result is SHOW_PAGINATED
|
||||
|
||||
if paginated is None:
|
||||
paginated = paginate_paragraphs(
|
||||
para,
|
||||
header=None,
|
||||
back_button=True,
|
||||
confirm=lambda content: Confirm(
|
||||
content, cancel=None, confirm="Close", confirm_style=ButtonDefault
|
||||
),
|
||||
)
|
||||
result = await interact(ctx, paginated, br_type, br_code)
|
||||
assert result in (CONFIRMED, GO_BACK)
|
||||
|
||||
assert False
|
||||
|
||||
|
||||
async def confirm_blob(
|
||||
ctx: wire.GenericContext,
|
||||
br_type: str,
|
||||
@ -512,6 +563,7 @@ async def confirm_blob(
|
||||
br_code: ButtonRequestType = ButtonRequestType.Other,
|
||||
icon: str = ui.ICON_SEND, # TODO cleanup @ redesign
|
||||
icon_color: int = ui.GREEN, # TODO cleanup @ redesign
|
||||
ask_pagination: bool = False,
|
||||
) -> None:
|
||||
"""Confirm data blob.
|
||||
|
||||
@ -556,14 +608,28 @@ async def confirm_blob(
|
||||
per_line = MONO_HEX_PER_LINE
|
||||
text.mono(ui.FG, *chunks_intersperse(data_str, per_line))
|
||||
content: ui.Layout = Confirm(text)
|
||||
return await raise_if_cancelled(interact(ctx, content, br_type, br_code))
|
||||
|
||||
elif ask_pagination:
|
||||
para = [(ui.MONO, line) for line in chunks(data_str, MONO_HEX_PER_LINE - 2)]
|
||||
|
||||
para_truncated = []
|
||||
if description is not None:
|
||||
para_truncated.append((ui.NORMAL, description))
|
||||
para_truncated.extend(para[:TEXT_MAX_LINES])
|
||||
|
||||
return await _confirm_ask_pagination(
|
||||
ctx, br_type, title, para, para_truncated, br_code, icon, icon_color
|
||||
)
|
||||
|
||||
else:
|
||||
para = []
|
||||
if description is not None:
|
||||
para.append((ui.NORMAL, description))
|
||||
para.extend((ui.MONO, line) for line in chunks(data_str, MONO_HEX_PER_LINE - 2))
|
||||
content = paginate_paragraphs(para, title, icon, icon_color)
|
||||
await raise_if_cancelled(interact(ctx, content, br_type, br_code))
|
||||
|
||||
paginated = paginate_paragraphs(para, title, icon, icon_color)
|
||||
return await raise_if_cancelled(interact(ctx, paginated, br_type, br_code))
|
||||
|
||||
|
||||
def confirm_address(
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from trezorlib import ethereum, messages
|
||||
from trezorlib import ethereum, exceptions, messages
|
||||
from trezorlib.debuglink import message_filters
|
||||
from trezorlib.exceptions import TrezorFailure
|
||||
from trezorlib.tools import parse_path
|
||||
@ -24,6 +24,8 @@ from trezorlib.tools import parse_path
|
||||
from ...common import parametrize_using_common_fixtures
|
||||
|
||||
TO_ADDR = "0x1d1c328764a41bda0492b66baa30c4a339ff85ef"
|
||||
SHOW_ALL = (143, 167)
|
||||
GO_BACK = (16, 220)
|
||||
|
||||
pytestmark = [pytest.mark.altcoin, pytest.mark.ethereum]
|
||||
|
||||
@ -325,3 +327,111 @@ def test_sanity_checks_eip1559(client):
|
||||
max_gas_fee=20,
|
||||
max_priority_fee=1,
|
||||
)
|
||||
|
||||
|
||||
def input_flow_skip(client, cancel=False):
|
||||
yield # confirm sending
|
||||
client.debug.press_yes()
|
||||
|
||||
yield # confirm data
|
||||
if cancel:
|
||||
client.debug.press_no()
|
||||
else:
|
||||
client.debug.press_yes()
|
||||
yield
|
||||
client.debug.press_yes()
|
||||
|
||||
|
||||
def input_flow_scroll_down(client, cancel=False):
|
||||
yield # confirm sending
|
||||
client.debug.wait_layout()
|
||||
client.debug.press_yes()
|
||||
|
||||
yield # confirm data
|
||||
client.debug.wait_layout()
|
||||
client.debug.click(SHOW_ALL)
|
||||
|
||||
br = yield # paginated data
|
||||
for i in range(br.pages):
|
||||
client.debug.wait_layout()
|
||||
if i < br.pages - 1:
|
||||
client.debug.swipe_up()
|
||||
|
||||
client.debug.press_yes()
|
||||
yield # confirm data
|
||||
if cancel:
|
||||
client.debug.press_no()
|
||||
else:
|
||||
client.debug.press_yes()
|
||||
yield # hold to confirm
|
||||
client.debug.press_yes()
|
||||
|
||||
|
||||
def input_flow_go_back(client, cancel=False):
|
||||
br = yield # confirm sending
|
||||
client.debug.wait_layout()
|
||||
client.debug.press_yes()
|
||||
|
||||
br = yield # confirm data
|
||||
client.debug.wait_layout()
|
||||
client.debug.click(SHOW_ALL)
|
||||
|
||||
br = yield # paginated data
|
||||
for i in range(br.pages):
|
||||
client.debug.wait_layout()
|
||||
if i == 2:
|
||||
client.debug.click(GO_BACK)
|
||||
yield # confirm data
|
||||
client.debug.wait_layout()
|
||||
if cancel:
|
||||
client.debug.press_no()
|
||||
else:
|
||||
client.debug.press_yes()
|
||||
yield # hold to confirm
|
||||
client.debug.wait_layout()
|
||||
client.debug.press_yes()
|
||||
return
|
||||
|
||||
elif i < br.pages - 1:
|
||||
client.debug.swipe_up()
|
||||
|
||||
|
||||
HEXDATA = "0123456789abcd000023456789abcd010003456789abcd020000456789abcd030000056789abcd040000006789abcd050000000789abcd060000000089abcd070000000009abcd080000000000abcd090000000001abcd0a0000000011abcd0b0000000111abcd0c0000001111abcd0d0000011111abcd0e0000111111abcd0f0000000002abcd100000000022abcd110000000222abcd120000002222abcd130000022222abcd140000222222abcd15"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"flow", (input_flow_skip, input_flow_scroll_down, input_flow_go_back)
|
||||
)
|
||||
@pytest.mark.skip_t1
|
||||
def test_signtx_data_pagination(client, flow):
|
||||
with client:
|
||||
client.watch_layout()
|
||||
client.set_input_flow(flow(client))
|
||||
ethereum.sign_tx(
|
||||
client,
|
||||
n=parse_path("m/44'/60'/0'/0/0"),
|
||||
nonce=0x0,
|
||||
gas_price=0x14,
|
||||
gas_limit=0x14,
|
||||
to="0x1d1c328764a41bda0492b66baa30c4a339ff85ef",
|
||||
chain_id=1,
|
||||
value=0xA,
|
||||
tx_type=None,
|
||||
data=bytes.fromhex(HEXDATA),
|
||||
)
|
||||
|
||||
with client, pytest.raises(exceptions.Cancelled):
|
||||
client.watch_layout()
|
||||
client.set_input_flow(flow(client, cancel=True))
|
||||
ethereum.sign_tx(
|
||||
client,
|
||||
n=parse_path("m/44'/60'/0'/0/0"),
|
||||
nonce=0x0,
|
||||
gas_price=0x14,
|
||||
gas_limit=0x14,
|
||||
to="0x1d1c328764a41bda0492b66baa30c4a339ff85ef",
|
||||
chain_id=1,
|
||||
value=0xA,
|
||||
tx_type=None,
|
||||
data=bytes.fromhex(HEXDATA),
|
||||
)
|
||||
|
@ -209,7 +209,7 @@
|
||||
"ethereum-test_sign_verify_message.py::test_verify[parameters6-result6]": "3a8312fc9f26f2bdf6569d44b4c6f103ea6300da84d4353678ec9a66b42aa05d",
|
||||
"ethereum-test_sign_verify_message.py::test_verify[parameters7-result7]": "8fb2aeb728da4fb973a8cf058d975a78214c3aea7cf09280155fb167077f8951",
|
||||
"ethereum-test_sign_verify_message.py::test_verify_invalid": "7e83f210ce98fee92e34bcc95d311701ec79702f8430239921efa72ff7759af6",
|
||||
"ethereum-test_signtx.py::test_data_streaming": "50db903970c264bddcc6e45fb2ef95aee0266fc9894613711d26be647b2eb7cd",
|
||||
"ethereum-test_signtx.py::test_data_streaming": "e0e6179a08c7a96958814d95ddfe09996a96aefeec3f538acfa58844c664d90f",
|
||||
"ethereum-test_signtx.py::test_sanity_checks": "c09de07fbbf1e047442180e2facb5482d06a1a428891b875b7dd93c9e4704ae1",
|
||||
"ethereum-test_signtx.py::test_sanity_checks_eip1559": "c09de07fbbf1e047442180e2facb5482d06a1a428891b875b7dd93c9e4704ae1",
|
||||
"ethereum-test_signtx.py::test_signtx[Auxilium]": "d2ba6424ba04e6db899ec33253ce7fd26bbb1850a5e548f1e5628bbb311cc2fc",
|
||||
@ -221,19 +221,22 @@
|
||||
"ethereum-test_signtx.py::test_signtx[Ropsten]": "fe817a91dbf529ef52f64de52ec137d809cb6ef337810e3e1da168bcda6d940b",
|
||||
"ethereum-test_signtx.py::test_signtx[Unknown_chain_id_eth_path]": "e1c268db1580ebbf957eb2912baa747133bf640d886b1339aee7e709494b46a7",
|
||||
"ethereum-test_signtx.py::test_signtx[Unknown_chain_id_testnet_path]": "e1c268db1580ebbf957eb2912baa747133bf640d886b1339aee7e709494b46a7",
|
||||
"ethereum-test_signtx.py::test_signtx[data_1]": "62e14aaedc636cacffc878a7f6f4045421683ef2be5105e771eaac7ba3ea2b93",
|
||||
"ethereum-test_signtx.py::test_signtx[data_2_bigdata]": "6feb271ba67066d3c60af76dcc64d9ebdf97f040934e95f7a63d5f1a78a48c8d",
|
||||
"ethereum-test_signtx.py::test_signtx[data_1]": "277686786f4dee54e641b15850b482480d1de4fd63401b76beb8da085ba27314",
|
||||
"ethereum-test_signtx.py::test_signtx[data_2_bigdata]": "a728a670736dc4b5a60e1e98ff294f20e0b3c0c97e32e3e032e19140aa040561",
|
||||
"ethereum-test_signtx.py::test_signtx[known_erc20_token]": "313cacddc8234c92ad18f4c94bbd9da366eb38e8f3a9345521cf4b4af491eac8",
|
||||
"ethereum-test_signtx.py::test_signtx[max_chain_id]": "e1c268db1580ebbf957eb2912baa747133bf640d886b1339aee7e709494b46a7",
|
||||
"ethereum-test_signtx.py::test_signtx[max_chain_plus_one]": "e1c268db1580ebbf957eb2912baa747133bf640d886b1339aee7e709494b46a7",
|
||||
"ethereum-test_signtx.py::test_signtx[max_uint64]": "e1c268db1580ebbf957eb2912baa747133bf640d886b1339aee7e709494b46a7",
|
||||
"ethereum-test_signtx.py::test_signtx[newcontract]": "9304b29803f1e85998127ae897a6d46457c8d6abd41f1daaa03eea90e5c8609f",
|
||||
"ethereum-test_signtx.py::test_signtx[newcontract]": "251aa9bf7d0febf372a3a9ba9c9a5b988ce02214cead9a768f994ad2a59ecfb3",
|
||||
"ethereum-test_signtx.py::test_signtx[nodata_1]": "0653c875f9e81fbef050769692c650a62f4d10973fbfc02f3dd7e32d80c89176",
|
||||
"ethereum-test_signtx.py::test_signtx[nodata_2_bigvalue]": "a5d66260785d02985106b12e21dd96db82b8579d8b09e4135d796d33e31356c1",
|
||||
"ethereum-test_signtx.py::test_signtx[unknown_erc20_token]": "9663070b464ef7bdd9f41bad540135a46564ee215e5d5d6448e4fa721a137bc8",
|
||||
"ethereum-test_signtx.py::test_signtx[wanchain]": "feb75d11291435a367479d0f55874a6a276aa760b57b804f2339d4fb4143f5c8",
|
||||
"ethereum-test_signtx.py::test_signtx_eip1559[data_1]": "a5e6df0f1fc2d96604f3b1af38b044b7426d8f0d8ab130e38afe160b317e3ed5",
|
||||
"ethereum-test_signtx.py::test_signtx_eip1559[data_2_bigdata]": "22524a38623b6c26dab77330e54c04fabe22ec9511dc24fa4044ec58868f2c79",
|
||||
"ethereum-test_signtx.py::test_signtx_data_pagination[input_flow_go_back]": "e0304501e6dd76d08052edd3ef518e4873f2029e8351bc1fb5ed5ba2a99e740a",
|
||||
"ethereum-test_signtx.py::test_signtx_data_pagination[input_flow_scroll_down]": "f707da6726a2ebad6888af490eed2a1441147c795a8b72fb805fd0d3a8b071f9",
|
||||
"ethereum-test_signtx.py::test_signtx_data_pagination[input_flow_skip]": "5d73d7f8366f4abd3eaf3e666f140ee0fcc0aca4f35483a6be27d96e2b64fd27",
|
||||
"ethereum-test_signtx.py::test_signtx_eip1559[data_1]": "bf7fe1457c4fd99e74b8b9a2090321edfb20bdf68713505d92dd011f44d88184",
|
||||
"ethereum-test_signtx.py::test_signtx_eip1559[data_2_bigdata]": "05cc77497ca4934f5472f4b8807208f54c734d1252e0eb186937a79e5ecec38b",
|
||||
"ethereum-test_signtx.py::test_signtx_eip1559[known_erc20]": "fd068a40fb0d4729c49316122711d36a92b09c04f60eed6d1fa5ecee57ae6f7a",
|
||||
"ethereum-test_signtx.py::test_signtx_eip1559[large_chainid]": "c8152a3f28ea1985d7c14844b3d8218a4a76e78fe0526e30c1d479f2cf200694",
|
||||
"ethereum-test_signtx.py::test_signtx_eip1559[nodata]": "55faae526aa63bc536114c70f29d5e12a6e973c565ae4247c439b755b589d0df",
|
||||
|
Loading…
Reference in New Issue
Block a user