mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-01-11 16:00:57 +00:00
all: modify passphrase source to always on device
This commit is contained in:
parent
eafd57c301
commit
cd09f9ce94
@ -89,6 +89,7 @@ message Features {
|
||||
optional bool sd_protection = 33; // is SD Protect enabled
|
||||
optional bool wipe_code_protection = 34; // is wipe code protection enabled
|
||||
optional bytes session_id = 35;
|
||||
optional bool passphrase_always_on_device = 36; // device enforces passphrase entry on Trezor
|
||||
}
|
||||
|
||||
/**
|
||||
@ -110,17 +111,10 @@ message ApplySettings {
|
||||
optional string label = 2;
|
||||
optional bool use_passphrase = 3;
|
||||
optional bytes homescreen = 4;
|
||||
optional PassphraseSourceType passphrase_source = 5;
|
||||
// optional PassphraseSourceType passphrase_source = 5; DEPRECATED
|
||||
optional uint32 auto_lock_delay_ms = 6;
|
||||
optional uint32 display_rotation = 7; // in degrees from North
|
||||
/**
|
||||
* Structure representing passphrase source
|
||||
*/
|
||||
enum PassphraseSourceType {
|
||||
ASK = 0;
|
||||
DEVICE = 1;
|
||||
HOST = 2;
|
||||
}
|
||||
optional bool passphrase_always_on_device = 8; // do not prompt for passphrase, enforce device entry
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -74,6 +74,7 @@ def get_features() -> Features:
|
||||
f.sd_protection = storage.sd_salt.is_enabled()
|
||||
f.wipe_code_protection = config.has_wipe_code()
|
||||
f.session_id = cache.get_session_id()
|
||||
f.passphrase_always_on_device = storage.device.get_passphrase_always_on_device()
|
||||
return f
|
||||
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import storage.device
|
||||
from trezor import ui, wire
|
||||
from trezor.messages import ButtonRequestType, PassphraseSourceType
|
||||
from trezor.messages import ButtonRequestType
|
||||
from trezor.messages.Success import Success
|
||||
from trezor.ui.text import Text
|
||||
|
||||
@ -12,7 +12,7 @@ async def apply_settings(ctx, msg):
|
||||
msg.homescreen is None
|
||||
and msg.label is None
|
||||
and msg.use_passphrase is None
|
||||
and msg.passphrase_source is None
|
||||
and msg.passphrase_always_on_device is None
|
||||
and msg.display_rotation is None
|
||||
):
|
||||
raise wire.ProcessError("No setting provided")
|
||||
@ -28,8 +28,10 @@ async def apply_settings(ctx, msg):
|
||||
if msg.use_passphrase is not None:
|
||||
await require_confirm_change_passphrase(ctx, msg.use_passphrase)
|
||||
|
||||
if msg.passphrase_source is not None:
|
||||
await require_confirm_change_passphrase_source(ctx, msg.passphrase_source)
|
||||
if msg.passphrase_always_on_device is not None:
|
||||
await require_confirm_change_passphrase_source(
|
||||
ctx, msg.passphrase_always_on_device
|
||||
)
|
||||
|
||||
if msg.display_rotation is not None:
|
||||
await require_confirm_change_display_rotation(ctx, msg.display_rotation)
|
||||
@ -38,7 +40,7 @@ async def apply_settings(ctx, msg):
|
||||
label=msg.label,
|
||||
use_passphrase=msg.use_passphrase,
|
||||
homescreen=msg.homescreen,
|
||||
passphrase_source=msg.passphrase_source,
|
||||
passphrase_always_on_device=msg.passphrase_always_on_device,
|
||||
display_rotation=msg.display_rotation,
|
||||
)
|
||||
|
||||
@ -69,16 +71,16 @@ async def require_confirm_change_passphrase(ctx, use):
|
||||
await require_confirm(ctx, text, ButtonRequestType.ProtectCall)
|
||||
|
||||
|
||||
async def require_confirm_change_passphrase_source(ctx, source):
|
||||
if source == PassphraseSourceType.DEVICE:
|
||||
desc = "ON DEVICE"
|
||||
elif source == PassphraseSourceType.HOST:
|
||||
desc = "ON HOST"
|
||||
else:
|
||||
desc = "ASK"
|
||||
async def require_confirm_change_passphrase_source(
|
||||
ctx, passphrase_always_on_device: bool
|
||||
):
|
||||
text = Text("Passphrase source", ui.ICON_CONFIG)
|
||||
text.normal("Do you really want to", "change the passphrase", "source to")
|
||||
text.bold("ALWAYS %s?" % desc)
|
||||
if passphrase_always_on_device:
|
||||
text.normal(
|
||||
"Do you really want to", "entry passphrase always", "on the device?"
|
||||
)
|
||||
else:
|
||||
text.normal("Do you want to revoke", "the passphrase on device", "setting?")
|
||||
await require_confirm(ctx, text, ButtonRequestType.ProtectCall)
|
||||
|
||||
|
||||
|
@ -24,7 +24,7 @@ _HOMESCREEN = const(0x06) # bytes
|
||||
_NEEDS_BACKUP = const(0x07) # bool (0x01 or empty)
|
||||
_FLAGS = const(0x08) # int
|
||||
U2F_COUNTER = const(0x09) # int
|
||||
_PASSPHRASE_SOURCE = const(0x0A) # int
|
||||
_PASSPHRASE_ALWAYS_ON_DEVICE = const(0x0A) # bool (0x01 or empty)
|
||||
_UNFINISHED_BACKUP = const(0x0B) # bool (0x01 or empty)
|
||||
_AUTOLOCK_DELAY_MS = const(0x0C) # int
|
||||
_NO_BACKUP = const(0x0D) # bool (0x01 or empty)
|
||||
@ -143,21 +143,24 @@ def no_backup() -> bool:
|
||||
return common.get_bool(_NAMESPACE, _NO_BACKUP)
|
||||
|
||||
|
||||
def get_passphrase_source() -> int:
|
||||
b = common.get(_NAMESPACE, _PASSPHRASE_SOURCE)
|
||||
if b == b"\x01":
|
||||
return 1
|
||||
elif b == b"\x02":
|
||||
return 2
|
||||
def get_passphrase_always_on_device() -> bool:
|
||||
b = common.get(_NAMESPACE, _PASSPHRASE_ALWAYS_ON_DEVICE)
|
||||
# backwards compatible for _PASSPHRASE_SOURCE.HOST = 2
|
||||
if b == b"\x02":
|
||||
return False
|
||||
# backwards compatible for _PASSPHRASE_SOURCE.DEVICE = 1
|
||||
# and also \x01 is TRUE_BYTE so it is future compatible as well
|
||||
elif b == b"\x01":
|
||||
return True
|
||||
else:
|
||||
return 0
|
||||
return False
|
||||
|
||||
|
||||
def load_settings(
|
||||
label: str = None,
|
||||
use_passphrase: bool = None,
|
||||
homescreen: bytes = None,
|
||||
passphrase_source: int = None,
|
||||
passphrase_always_on_device: bool = None,
|
||||
display_rotation: int = None,
|
||||
) -> None:
|
||||
if label is not None:
|
||||
@ -170,9 +173,10 @@ def load_settings(
|
||||
common.set(_NAMESPACE, _HOMESCREEN, homescreen, True) # public
|
||||
else:
|
||||
common.set(_NAMESPACE, _HOMESCREEN, b"", True) # public
|
||||
if passphrase_source is not None:
|
||||
if passphrase_source in (0, 1, 2):
|
||||
common.set(_NAMESPACE, _PASSPHRASE_SOURCE, bytes([passphrase_source]))
|
||||
if passphrase_always_on_device is not None:
|
||||
common.set_bool(
|
||||
_NAMESPACE, _PASSPHRASE_ALWAYS_ON_DEVICE, passphrase_always_on_device
|
||||
)
|
||||
if display_rotation is not None:
|
||||
if display_rotation not in (0, 90, 180, 270):
|
||||
raise ValueError(
|
||||
|
@ -6,7 +6,6 @@ if __debug__:
|
||||
try:
|
||||
from typing import Dict, List # noqa: F401
|
||||
from typing_extensions import Literal # noqa: F401
|
||||
EnumTypePassphraseSourceType = Literal[0, 1, 2]
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
@ -20,17 +19,17 @@ class ApplySettings(p.MessageType):
|
||||
label: str = None,
|
||||
use_passphrase: bool = None,
|
||||
homescreen: bytes = None,
|
||||
passphrase_source: EnumTypePassphraseSourceType = None,
|
||||
auto_lock_delay_ms: int = None,
|
||||
display_rotation: int = None,
|
||||
passphrase_always_on_device: bool = None,
|
||||
) -> None:
|
||||
self.language = language
|
||||
self.label = label
|
||||
self.use_passphrase = use_passphrase
|
||||
self.homescreen = homescreen
|
||||
self.passphrase_source = passphrase_source
|
||||
self.auto_lock_delay_ms = auto_lock_delay_ms
|
||||
self.display_rotation = display_rotation
|
||||
self.passphrase_always_on_device = passphrase_always_on_device
|
||||
|
||||
@classmethod
|
||||
def get_fields(cls) -> Dict:
|
||||
@ -39,7 +38,7 @@ class ApplySettings(p.MessageType):
|
||||
2: ('label', p.UnicodeType, 0),
|
||||
3: ('use_passphrase', p.BoolType, 0),
|
||||
4: ('homescreen', p.BytesType, 0),
|
||||
5: ('passphrase_source', p.EnumType("PassphraseSourceType", (0, 1, 2)), 0),
|
||||
6: ('auto_lock_delay_ms', p.UVarintType, 0),
|
||||
7: ('display_rotation', p.UVarintType, 0),
|
||||
8: ('passphrase_always_on_device', p.BoolType, 0),
|
||||
}
|
||||
|
@ -51,6 +51,7 @@ class Features(p.MessageType):
|
||||
sd_protection: bool = None,
|
||||
wipe_code_protection: bool = None,
|
||||
session_id: bytes = None,
|
||||
passphrase_always_on_device: bool = None,
|
||||
) -> None:
|
||||
self.vendor = vendor
|
||||
self.major_version = major_version
|
||||
@ -86,6 +87,7 @@ class Features(p.MessageType):
|
||||
self.sd_protection = sd_protection
|
||||
self.wipe_code_protection = wipe_code_protection
|
||||
self.session_id = session_id
|
||||
self.passphrase_always_on_device = passphrase_always_on_device
|
||||
|
||||
@classmethod
|
||||
def get_fields(cls) -> Dict:
|
||||
@ -124,4 +126,5 @@ class Features(p.MessageType):
|
||||
33: ('sd_protection', p.BoolType, 0),
|
||||
34: ('wipe_code_protection', p.BoolType, 0),
|
||||
35: ('session_id', p.BytesType, 0),
|
||||
36: ('passphrase_always_on_device', p.BoolType, 0),
|
||||
}
|
||||
|
@ -1,8 +0,0 @@
|
||||
# Automatically generated by pb2py
|
||||
# fmt: off
|
||||
if False:
|
||||
from typing_extensions import Literal
|
||||
|
||||
ASK = 0 # type: Literal[0]
|
||||
DEVICE = 1 # type: Literal[1]
|
||||
HOST = 2 # type: Literal[2]
|
@ -1,7 +1,6 @@
|
||||
from micropython import const
|
||||
|
||||
from trezor import io, loop, res, ui
|
||||
from trezor.messages import PassphraseSourceType
|
||||
from trezor.ui import display
|
||||
from trezor.ui.button import Button, ButtonClear, ButtonConfirm
|
||||
from trezor.ui.swipe import SWIPE_HORIZONTAL, SWIPE_LEFT, Swipe
|
||||
@ -246,25 +245,3 @@ class PassphraseKeyboard(ui.Layout):
|
||||
|
||||
def create_tasks(self) -> Tuple[loop.Task, ...]:
|
||||
return self.handle_input(), self.handle_rendering(), self.handle_paging()
|
||||
|
||||
|
||||
class PassphraseSource(ui.Layout):
|
||||
def __init__(self, content: ui.Component) -> None:
|
||||
self.content = content
|
||||
|
||||
self.device = Button(ui.grid(8, n_y=4, n_x=4, cells_x=4), "Device")
|
||||
self.device.on_click = self.on_device # type: ignore
|
||||
|
||||
self.host = Button(ui.grid(12, n_y=4, n_x=4, cells_x=4), "Host")
|
||||
self.host.on_click = self.on_host # type: ignore
|
||||
|
||||
def dispatch(self, event: int, x: int, y: int) -> None:
|
||||
self.content.dispatch(event, x, y)
|
||||
self.device.dispatch(event, x, y)
|
||||
self.host.dispatch(event, x, y)
|
||||
|
||||
def on_device(self) -> None:
|
||||
raise ui.Result(PassphraseSourceType.DEVICE)
|
||||
|
||||
def on_host(self) -> None:
|
||||
raise ui.Result(PassphraseSourceType.HOST)
|
||||
|
@ -16,15 +16,9 @@
|
||||
|
||||
import click
|
||||
|
||||
from .. import device, messages
|
||||
from .. import device
|
||||
from . import ChoiceType
|
||||
|
||||
PASSPHRASE_SOURCE = {
|
||||
"ask": messages.PassphraseSourceType.ASK,
|
||||
"device": messages.PassphraseSourceType.DEVICE,
|
||||
"host": messages.PassphraseSourceType.HOST,
|
||||
}
|
||||
|
||||
ROTATION = {"north": 0, "east": 90, "south": 180, "west": 270}
|
||||
|
||||
|
||||
@ -145,10 +139,24 @@ def passphrase():
|
||||
|
||||
|
||||
@passphrase.command(name="enabled")
|
||||
@click.option("-f", "--force-on-device", is_flag=True)
|
||||
@click.option("-F", "--no-force-on-device", is_flag=True)
|
||||
@click.pass_obj
|
||||
def passphrase_enable(connect):
|
||||
def passphrase_enable(connect, force_on_device: bool, no_force_on_device: bool):
|
||||
"""Enable passphrase."""
|
||||
return device.apply_settings(connect(), use_passphrase=True)
|
||||
if force_on_device and no_force_on_device:
|
||||
raise ValueError(
|
||||
"Only one option of --force-on-device/-no-force-on-device makes sense."
|
||||
)
|
||||
on_device = None
|
||||
if force_on_device:
|
||||
on_device = True
|
||||
if no_force_on_device:
|
||||
on_device = False
|
||||
|
||||
return device.apply_settings(
|
||||
connect(), use_passphrase=True, passphrase_always_on_device=on_device
|
||||
)
|
||||
|
||||
|
||||
@passphrase.command(name="disabled")
|
||||
@ -156,19 +164,3 @@ def passphrase_enable(connect):
|
||||
def passphrase_disable(connect):
|
||||
"""Disable passphrase."""
|
||||
return device.apply_settings(connect(), use_passphrase=False)
|
||||
|
||||
|
||||
@passphrase.command(name="source")
|
||||
@click.argument("source", type=ChoiceType(PASSPHRASE_SOURCE))
|
||||
@click.pass_obj
|
||||
def passphrase_source(connect, source):
|
||||
"""Set passphrase source.
|
||||
|
||||
Configure how to enter passphrase on Trezor Model T. The options are:
|
||||
|
||||
\b
|
||||
ask - always ask where to enter passphrase
|
||||
device - always enter passphrase on device
|
||||
host - always enter passphrase on host
|
||||
"""
|
||||
return device.apply_settings(connect(), passphrase_source=source)
|
||||
|
@ -51,7 +51,6 @@ COMMAND_ALIASES = {
|
||||
"change-pin": settings.pin,
|
||||
"enable-passphrase": settings.passphrase_enable,
|
||||
"disable-passphrase": settings.passphrase_disable,
|
||||
"set-passphrase-source": settings.passphrase_source,
|
||||
"wipe-device": device.wipe,
|
||||
"reset-device": device.setup,
|
||||
"recovery-device": device.recover,
|
||||
|
@ -51,7 +51,7 @@ def apply_settings(
|
||||
language=None,
|
||||
use_passphrase=None,
|
||||
homescreen=None,
|
||||
passphrase_source=None,
|
||||
passphrase_always_on_device=None,
|
||||
auto_lock_delay_ms=None,
|
||||
display_rotation=None,
|
||||
):
|
||||
@ -64,8 +64,8 @@ def apply_settings(
|
||||
settings.use_passphrase = use_passphrase
|
||||
if homescreen is not None:
|
||||
settings.homescreen = homescreen
|
||||
if passphrase_source is not None:
|
||||
settings.passphrase_source = passphrase_source
|
||||
if passphrase_always_on_device is not None:
|
||||
settings.passphrase_always_on_device = passphrase_always_on_device
|
||||
if auto_lock_delay_ms is not None:
|
||||
settings.auto_lock_delay_ms = auto_lock_delay_ms
|
||||
if display_rotation is not None:
|
||||
|
@ -6,7 +6,6 @@ if __debug__:
|
||||
try:
|
||||
from typing import Dict, List # noqa: F401
|
||||
from typing_extensions import Literal # noqa: F401
|
||||
EnumTypePassphraseSourceType = Literal[0, 1, 2]
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
@ -20,17 +19,17 @@ class ApplySettings(p.MessageType):
|
||||
label: str = None,
|
||||
use_passphrase: bool = None,
|
||||
homescreen: bytes = None,
|
||||
passphrase_source: EnumTypePassphraseSourceType = None,
|
||||
auto_lock_delay_ms: int = None,
|
||||
display_rotation: int = None,
|
||||
passphrase_always_on_device: bool = None,
|
||||
) -> None:
|
||||
self.language = language
|
||||
self.label = label
|
||||
self.use_passphrase = use_passphrase
|
||||
self.homescreen = homescreen
|
||||
self.passphrase_source = passphrase_source
|
||||
self.auto_lock_delay_ms = auto_lock_delay_ms
|
||||
self.display_rotation = display_rotation
|
||||
self.passphrase_always_on_device = passphrase_always_on_device
|
||||
|
||||
@classmethod
|
||||
def get_fields(cls) -> Dict:
|
||||
@ -39,7 +38,7 @@ class ApplySettings(p.MessageType):
|
||||
2: ('label', p.UnicodeType, 0),
|
||||
3: ('use_passphrase', p.BoolType, 0),
|
||||
4: ('homescreen', p.BytesType, 0),
|
||||
5: ('passphrase_source', p.EnumType("PassphraseSourceType", (0, 1, 2)), 0),
|
||||
6: ('auto_lock_delay_ms', p.UVarintType, 0),
|
||||
7: ('display_rotation', p.UVarintType, 0),
|
||||
8: ('passphrase_always_on_device', p.BoolType, 0),
|
||||
}
|
||||
|
@ -51,6 +51,7 @@ class Features(p.MessageType):
|
||||
sd_protection: bool = None,
|
||||
wipe_code_protection: bool = None,
|
||||
session_id: bytes = None,
|
||||
passphrase_always_on_device: bool = None,
|
||||
) -> None:
|
||||
self.vendor = vendor
|
||||
self.major_version = major_version
|
||||
@ -86,6 +87,7 @@ class Features(p.MessageType):
|
||||
self.sd_protection = sd_protection
|
||||
self.wipe_code_protection = wipe_code_protection
|
||||
self.session_id = session_id
|
||||
self.passphrase_always_on_device = passphrase_always_on_device
|
||||
|
||||
@classmethod
|
||||
def get_fields(cls) -> Dict:
|
||||
@ -124,4 +126,5 @@ class Features(p.MessageType):
|
||||
33: ('sd_protection', p.BoolType, 0),
|
||||
34: ('wipe_code_protection', p.BoolType, 0),
|
||||
35: ('session_id', p.BytesType, 0),
|
||||
36: ('passphrase_always_on_device', p.BoolType, 0),
|
||||
}
|
||||
|
@ -1,8 +0,0 @@
|
||||
# Automatically generated by pb2py
|
||||
# fmt: off
|
||||
if False:
|
||||
from typing_extensions import Literal
|
||||
|
||||
ASK = 0 # type: Literal[0]
|
||||
DEVICE = 1 # type: Literal[1]
|
||||
HOST = 2 # type: Literal[2]
|
@ -278,7 +278,6 @@ from . import NEMModificationType
|
||||
from . import NEMMosaicLevy
|
||||
from . import NEMSupplyChangeType
|
||||
from . import OutputScriptType
|
||||
from . import PassphraseSourceType
|
||||
from . import PinMatrixRequestType
|
||||
from . import RecoveryDeviceType
|
||||
from . import RequestType
|
||||
|
@ -20,8 +20,7 @@ import pytest
|
||||
|
||||
from trezorlib import debuglink, log
|
||||
from trezorlib.debuglink import TrezorClientDebugLink
|
||||
from trezorlib.device import apply_settings, wipe as wipe_device
|
||||
from trezorlib.messages.PassphraseSourceType import HOST as PASSPHRASE_ON_HOST
|
||||
from trezorlib.device import wipe as wipe_device
|
||||
from trezorlib.transport import enumerate_devices, get_transport
|
||||
|
||||
from . import ui_tests
|
||||
@ -130,8 +129,8 @@ def client(request):
|
||||
needs_backup=setup_params["needs_backup"],
|
||||
no_backup=setup_params["no_backup"],
|
||||
)
|
||||
if setup_params["passphrase"] and client.features.model != "1":
|
||||
apply_settings(client, passphrase_source=PASSPHRASE_ON_HOST)
|
||||
if setup_params["passphrase"]:
|
||||
client.passphrase_on_host = True
|
||||
|
||||
if setup_params["pin"]:
|
||||
# ClearSession locks the device. We only do that if the PIN is set.
|
||||
|
@ -18,7 +18,6 @@ import pytest
|
||||
|
||||
from trezorlib import btc, debuglink, device
|
||||
from trezorlib.messages import BackupType
|
||||
from trezorlib.messages.PassphraseSourceType import HOST as PASSPHRASE_ON_HOST
|
||||
|
||||
from ..common import (
|
||||
MNEMONIC12,
|
||||
@ -53,8 +52,7 @@ class TestDeviceLoad:
|
||||
passphrase_protection=True,
|
||||
label="test",
|
||||
)
|
||||
if client.features.model == "T":
|
||||
device.apply_settings(client, passphrase_source=PASSPHRASE_ON_HOST)
|
||||
client.passphrase_on_host = True
|
||||
client.set_passphrase("passphrase")
|
||||
state = client.debug.state()
|
||||
assert state.mnemonic_secret == MNEMONIC12.encode()
|
||||
@ -108,7 +106,7 @@ class TestDeviceLoad:
|
||||
u"Neuve\u030cr\u030citelne\u030c bezpec\u030cne\u0301 hesli\u0301c\u030cko"
|
||||
)
|
||||
|
||||
device.wipe(client)
|
||||
client.passphrase_on_host = True
|
||||
debuglink.load_device(
|
||||
client,
|
||||
mnemonic=words_nfkd,
|
||||
@ -118,8 +116,6 @@ class TestDeviceLoad:
|
||||
language="en-US",
|
||||
skip_checksum=True,
|
||||
)
|
||||
if client.features.model == "T":
|
||||
device.apply_settings(client, passphrase_source=PASSPHRASE_ON_HOST)
|
||||
client.set_passphrase(passphrase_nfkd)
|
||||
address_nfkd = btc.get_address(client, "Bitcoin", [])
|
||||
|
||||
@ -133,8 +129,6 @@ class TestDeviceLoad:
|
||||
language="en-US",
|
||||
skip_checksum=True,
|
||||
)
|
||||
if client.features.model == "T":
|
||||
device.apply_settings(client, passphrase_source=PASSPHRASE_ON_HOST)
|
||||
client.set_passphrase(passphrase_nfc)
|
||||
address_nfc = btc.get_address(client, "Bitcoin", [])
|
||||
|
||||
@ -148,8 +142,6 @@ class TestDeviceLoad:
|
||||
language="en-US",
|
||||
skip_checksum=True,
|
||||
)
|
||||
if client.features.model == "T":
|
||||
device.apply_settings(client, passphrase_source=PASSPHRASE_ON_HOST)
|
||||
client.set_passphrase(passphrase_nfkc)
|
||||
address_nfkc = btc.get_address(client, "Bitcoin", [])
|
||||
|
||||
@ -163,8 +155,6 @@ class TestDeviceLoad:
|
||||
language="en-US",
|
||||
skip_checksum=True,
|
||||
)
|
||||
if client.features.model == "T":
|
||||
device.apply_settings(client, passphrase_source=PASSPHRASE_ON_HOST)
|
||||
client.set_passphrase(passphrase_nfd)
|
||||
address_nfd = btc.get_address(client, "Bitcoin", [])
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user