feat(core/stellar): add type checking to Stellar app

pull/1825/head
matejcik 3 years ago committed by matejcik
parent 0a0b75378d
commit 98f0496b2c

@ -0,0 +1 @@
Type-checking enabled for apps.stellar

@ -115,6 +115,7 @@ mypy:
src/apps/ethereum \ src/apps/ethereum \
src/apps/management \ src/apps/management \
src/apps/misc \ src/apps/misc \
src/apps/stellar \
src/apps/webauthn \ src/apps/webauthn \
src/trezor/ui src/trezor/ui

@ -2,11 +2,44 @@ from micropython import const
from trezor.enums import MessageType from trezor.enums import MessageType
TX_TYPE = bytearray("\x00\x00\x00\x02") if False:
from typing import Union
from trezor import protobuf
from trezor.messages import (
StellarAccountMergeOp,
StellarAllowTrustOp,
StellarBumpSequenceOp,
StellarChangeTrustOp,
StellarCreateAccountOp,
StellarCreatePassiveOfferOp,
StellarManageDataOp,
StellarManageOfferOp,
StellarPathPaymentOp,
StellarPaymentOp,
StellarSetOptionsOp,
)
StellarMessageType = Union[
StellarAccountMergeOp,
StellarAllowTrustOp,
StellarBumpSequenceOp,
StellarChangeTrustOp,
StellarCreateAccountOp,
StellarCreatePassiveOfferOp,
StellarManageDataOp,
StellarManageOfferOp,
StellarPathPaymentOp,
StellarPaymentOp,
StellarSetOptionsOp,
]
TX_TYPE = b"\x00\x00\x00\x02"
# source: https://github.com/stellar/go/blob/3d2c1defe73dbfed00146ebe0e8d7e07ce4bb1b6/xdr/Stellar-transaction.x#L16 # source: https://github.com/stellar/go/blob/3d2c1defe73dbfed00146ebe0e8d7e07ce4bb1b6/xdr/Stellar-transaction.x#L16
# Inflation not supported see https://github.com/trezor/trezor-core/issues/202#issuecomment-393342089 # Inflation not supported see https://github.com/trezor/trezor-core/issues/202#issuecomment-393342089
op_codes = { op_codes: dict[int, int] = {
MessageType.StellarAccountMergeOp: 8, MessageType.StellarAccountMergeOp: 8,
MessageType.StellarAllowTrustOp: 7, MessageType.StellarAllowTrustOp: 7,
MessageType.StellarBumpSequenceOp: 11, MessageType.StellarBumpSequenceOp: 11,
@ -53,22 +86,8 @@ FLAG_AUTH_REVOCABLE = const(2)
FLAG_AUTH_IMMUTABLE = const(4) FLAG_AUTH_IMMUTABLE = const(4)
FLAGS_MAX_SIZE = const(7) FLAGS_MAX_SIZE = const(7)
# https://github.com/stellar/go/blob/e0ffe19f58879d3c31e2976b97a5bf10e13a337b/xdr/Stellar-transaction.x#L275
MEMO_TYPE_NONE = const(0)
MEMO_TYPE_TEXT = const(1)
MEMO_TYPE_ID = const(2)
MEMO_TYPE_HASH = const(3)
MEMO_TYPE_RETURN = const(4)
# https://github.com/stellar/go/blob/3d2c1defe73dbfed00146ebe0e8d7e07ce4bb1b6/xdr/xdr_generated.go#L156
SIGN_TYPE_ACCOUNT = const(0)
SIGN_TYPE_PRE_AUTH = const(1)
SIGN_TYPE_HASH = const(2)
SIGN_TYPES = (SIGN_TYPE_ACCOUNT, SIGN_TYPE_HASH, SIGN_TYPE_PRE_AUTH)
def get_op_code(msg) -> int: def get_op_code(msg: protobuf.MessageType) -> int:
wire = msg.MESSAGE_WIRE_TYPE wire = msg.MESSAGE_WIRE_TYPE
if wire not in op_codes: if wire not in op_codes:
raise ValueError("Stellar: op code unknown") raise ValueError("Stellar: op code unknown")

@ -6,9 +6,15 @@ from apps.common.keychain import auto_keychain
from . import helpers from . import helpers
if False:
from trezor.wire import Context
from apps.common.keychain import Keychain
@auto_keychain(__name__) @auto_keychain(__name__)
async def get_address(ctx, msg: StellarGetAddress, keychain): async def get_address(
ctx: Context, msg: StellarGetAddress, keychain: Keychain
) -> StellarAddress:
await paths.validate_path(ctx, keychain, msg.address_n) await paths.validate_path(ctx, keychain, msg.address_n)
node = keychain.derive(msg.address_n) node = keychain.derive(msg.address_n)

@ -14,7 +14,7 @@ def public_key_from_address(address: str) -> bytes:
return b[1:-2] return b[1:-2]
def address_from_public_key(pubkey: bytes): def address_from_public_key(pubkey: bytes) -> str:
"""Returns the base32-encoded version of public key bytes (G...)""" """Returns the base32-encoded version of public key bytes (G...)"""
address = bytearray() address = bytearray()
address.append(6 << 3) # version -> 'G' address.append(6 << 3) # version -> 'G'
@ -24,7 +24,7 @@ def address_from_public_key(pubkey: bytes):
return base32.encode(address) return base32.encode(address)
def _crc16_checksum_verify(data: bytes, checksum: bytes): def _crc16_checksum_verify(data: bytes, checksum: bytes) -> None:
if _crc16_checksum(data) != checksum: if _crc16_checksum(data) != checksum:
raise ProcessError("Invalid address checksum") raise ProcessError("Invalid address checksum")

@ -1,5 +1,5 @@
from trezor import strings, ui from trezor import strings, ui
from trezor.enums import ButtonRequestType from trezor.enums import ButtonRequestType, StellarAssetType, StellarMemoType
from trezor.ui.layouts import ( from trezor.ui.layouts import (
confirm_action, confirm_action,
confirm_address, confirm_address,
@ -7,16 +7,22 @@ from trezor.ui.layouts import (
confirm_metadata, confirm_metadata,
) )
from trezor.ui.layouts.tt.altcoin import confirm_timebounds_stellar from trezor.ui.layouts.tt.altcoin import confirm_timebounds_stellar
from trezor.wire import DataError
from . import consts from . import consts
if False: if False:
from trezor.messages import StellarAssetType from trezor.wire import Context
from trezor.messages import StellarAsset
async def require_confirm_init( async def require_confirm_init(
ctx, address: str, network_passphrase: str, accounts_match: bool ctx: Context,
): address: str,
network_passphrase: str,
accounts_match: bool,
) -> None:
if accounts_match: if accounts_match:
description = "Initialize signing with your account" description = "Initialize signing with your account"
else: else:
@ -44,18 +50,20 @@ async def require_confirm_init(
) )
async def require_confirm_timebounds(ctx, start: int, end: int): async def require_confirm_timebounds(ctx: Context, start: int, end: int) -> None:
await confirm_timebounds_stellar(ctx, start, end) await confirm_timebounds_stellar(ctx, start, end)
async def require_confirm_memo(ctx, memo_type: int, memo_text: str): async def require_confirm_memo(
if memo_type == consts.MEMO_TYPE_TEXT: ctx: Context, memo_type: StellarMemoType, memo_text: str
) -> None:
if memo_type == StellarMemoType.TEXT:
description = "Memo (TEXT)" description = "Memo (TEXT)"
elif memo_type == consts.MEMO_TYPE_ID: elif memo_type == StellarMemoType.ID:
description = "Memo (ID)" description = "Memo (ID)"
elif memo_type == consts.MEMO_TYPE_HASH: elif memo_type == StellarMemoType.HASH:
description = "Memo (HASH)" description = "Memo (HASH)"
elif memo_type == consts.MEMO_TYPE_RETURN: elif memo_type == StellarMemoType.RETURN:
description = "Memo (RETURN)" description = "Memo (RETURN)"
else: else:
return await confirm_action( return await confirm_action(
@ -78,7 +86,7 @@ async def require_confirm_memo(ctx, memo_type: int, memo_text: str):
) )
async def require_confirm_final(ctx, fee: int, num_operations: int): async def require_confirm_final(ctx: Context, fee: int, num_operations: int) -> None:
op_str = strings.format_plural("{count} {plural}", num_operations, "operation") op_str = strings.format_plural("{count} {plural}", num_operations, "operation")
await confirm_metadata( await confirm_metadata(
ctx, ctx,
@ -91,14 +99,16 @@ async def require_confirm_final(ctx, fee: int, num_operations: int):
) )
def format_asset(asset: StellarAssetType | None = None) -> str: def format_asset(asset: StellarAsset | None) -> str:
if asset is None or asset.type == consts.ASSET_TYPE_NATIVE: if asset is None or asset.type == StellarAssetType.NATIVE:
return "XLM" return "XLM"
else: else:
if asset.code is None:
raise DataError("Stellar asset code is missing")
return asset.code return asset.code
def format_amount(amount: int, asset: StellarAssetType | None = None) -> str: def format_amount(amount: int, asset: StellarAsset | None = None) -> str:
return ( return (
strings.format_amount(amount, consts.AMOUNT_DECIMALS) strings.format_amount(amount, consts.AMOUNT_DECIMALS)
+ " " + " "
@ -106,7 +116,7 @@ def format_amount(amount: int, asset: StellarAssetType | None = None) -> str:
) )
def get_network_warning(network_passphrase: str): def get_network_warning(network_passphrase: str) -> str | None:
if network_passphrase == consts.NETWORK_PASSPHRASE_PUBLIC: if network_passphrase == consts.NETWORK_PASSPHRASE_PUBLIC:
return None return None
if network_passphrase == consts.NETWORK_PASSPHRASE_TESTNET: if network_passphrase == consts.NETWORK_PASSPHRASE_TESTNET:

@ -1,8 +1,14 @@
from .. import consts, writers from .. import consts, writers
from . import layout, serialize from . import layout, serialize
if False:
from trezor.utils import Writer
from trezor.wire import Context
async def process_operation(ctx, w, op):
async def process_operation(
ctx: Context, w: Writer, op: consts.StellarMessageType
) -> None:
if op.source_account: if op.source_account:
await layout.confirm_source_account(ctx, op.source_account) await layout.confirm_source_account(ctx, op.source_account)
serialize.write_account(w, op.source_account) serialize.write_account(w, op.source_account)

@ -1,7 +1,8 @@
from trezor.enums import StellarAssetType, StellarSignerType
from trezor.messages import ( from trezor.messages import (
StellarAccountMergeOp, StellarAccountMergeOp,
StellarAllowTrustOp, StellarAllowTrustOp,
StellarAssetType, StellarAsset,
StellarBumpSequenceOp, StellarBumpSequenceOp,
StellarChangeTrustOp, StellarChangeTrustOp,
StellarCreateAccountOp, StellarCreateAccountOp,
@ -21,13 +22,16 @@ from trezor.ui.layouts import (
confirm_properties, confirm_properties,
confirm_text, confirm_text,
) )
from trezor.wire import ProcessError from trezor.wire import DataError, ProcessError
from .. import consts, helpers from .. import consts, helpers
from ..layout import format_amount, format_asset from ..layout import format_amount, format_asset
if False:
from trezor.wire import Context
async def confirm_source_account(ctx, source_account: str):
async def confirm_source_account(ctx: Context, source_account: str) -> None:
await confirm_address( await confirm_address(
ctx, ctx,
"Confirm operation", "Confirm operation",
@ -37,7 +41,7 @@ async def confirm_source_account(ctx, source_account: str):
) )
async def confirm_allow_trust_op(ctx, op: StellarAllowTrustOp): async def confirm_allow_trust_op(ctx: Context, op: StellarAllowTrustOp) -> None:
await confirm_properties( await confirm_properties(
ctx, ctx,
"op_allow_trust", "op_allow_trust",
@ -49,7 +53,7 @@ async def confirm_allow_trust_op(ctx, op: StellarAllowTrustOp):
) )
async def confirm_account_merge_op(ctx, op: StellarAccountMergeOp): async def confirm_account_merge_op(ctx: Context, op: StellarAccountMergeOp) -> None:
await confirm_address( await confirm_address(
ctx, ctx,
"Account Merge", "Account Merge",
@ -59,7 +63,7 @@ async def confirm_account_merge_op(ctx, op: StellarAccountMergeOp):
) )
async def confirm_bump_sequence_op(ctx, op: StellarBumpSequenceOp): async def confirm_bump_sequence_op(ctx: Context, op: StellarBumpSequenceOp) -> None:
await confirm_metadata( await confirm_metadata(
ctx, ctx,
"op_bump", "op_bump",
@ -69,7 +73,7 @@ async def confirm_bump_sequence_op(ctx, op: StellarBumpSequenceOp):
) )
async def confirm_change_trust_op(ctx, op: StellarChangeTrustOp): async def confirm_change_trust_op(ctx: Context, op: StellarChangeTrustOp) -> None:
await confirm_amount( await confirm_amount(
ctx, ctx,
title="Delete trust" if op.limit == 0 else "Add trust", title="Delete trust" if op.limit == 0 else "Add trust",
@ -80,7 +84,7 @@ async def confirm_change_trust_op(ctx, op: StellarChangeTrustOp):
await confirm_asset_issuer(ctx, op.asset) await confirm_asset_issuer(ctx, op.asset)
async def confirm_create_account_op(ctx, op: StellarCreateAccountOp): async def confirm_create_account_op(ctx: Context, op: StellarCreateAccountOp) -> None:
await confirm_properties( await confirm_properties(
ctx, ctx,
"op_create_account", "op_create_account",
@ -92,7 +96,9 @@ async def confirm_create_account_op(ctx, op: StellarCreateAccountOp):
) )
async def confirm_create_passive_offer_op(ctx, op: StellarCreatePassiveOfferOp): async def confirm_create_passive_offer_op(
ctx: Context, op: StellarCreatePassiveOfferOp
) -> None:
if op.amount == 0: if op.amount == 0:
text = "Delete Passive Offer" text = "Delete Passive Offer"
else: else:
@ -100,7 +106,7 @@ async def confirm_create_passive_offer_op(ctx, op: StellarCreatePassiveOfferOp):
await _confirm_offer(ctx, text, op) await _confirm_offer(ctx, text, op)
async def confirm_manage_offer_op(ctx, op: StellarManageOfferOp): async def confirm_manage_offer_op(ctx: Context, op: StellarManageOfferOp) -> None:
if op.offer_id == 0: if op.offer_id == 0:
text = "New Offer" text = "New Offer"
else: else:
@ -112,7 +118,11 @@ async def confirm_manage_offer_op(ctx, op: StellarManageOfferOp):
await _confirm_offer(ctx, text, op) await _confirm_offer(ctx, text, op)
async def _confirm_offer(ctx, title, op): async def _confirm_offer(
ctx: Context,
title: str,
op: StellarCreatePassiveOfferOp | StellarManageOfferOp,
) -> None:
await confirm_properties( await confirm_properties(
ctx, ctx,
"op_offer", "op_offer",
@ -130,7 +140,7 @@ async def _confirm_offer(ctx, title, op):
await confirm_asset_issuer(ctx, op.buying_asset) await confirm_asset_issuer(ctx, op.buying_asset)
async def confirm_manage_data_op(ctx, op: StellarManageDataOp): async def confirm_manage_data_op(ctx: Context, op: StellarManageDataOp) -> None:
from trezor.crypto.hashlib import sha256 from trezor.crypto.hashlib import sha256
if op.value: if op.value:
@ -151,7 +161,7 @@ async def confirm_manage_data_op(ctx, op: StellarManageDataOp):
) )
async def confirm_path_payment_op(ctx, op: StellarPathPaymentOp): async def confirm_path_payment_op(ctx: Context, op: StellarPathPaymentOp) -> None:
await confirm_output( await confirm_output(
ctx, ctx,
address=op.destination_account, address=op.destination_account,
@ -170,7 +180,7 @@ async def confirm_path_payment_op(ctx, op: StellarPathPaymentOp):
await confirm_asset_issuer(ctx, op.send_asset) await confirm_asset_issuer(ctx, op.send_asset)
async def confirm_payment_op(ctx, op: StellarPaymentOp): async def confirm_payment_op(ctx: Context, op: StellarPaymentOp) -> None:
await confirm_output( await confirm_output(
ctx, ctx,
address=op.destination_account, address=op.destination_account,
@ -179,7 +189,7 @@ async def confirm_payment_op(ctx, op: StellarPaymentOp):
await confirm_asset_issuer(ctx, op.asset) await confirm_asset_issuer(ctx, op.asset)
async def confirm_set_options_op(ctx, op: StellarSetOptionsOp): async def confirm_set_options_op(ctx: Context, op: StellarSetOptionsOp) -> None:
if op.inflation_destination_account: if op.inflation_destination_account:
await confirm_address( await confirm_address(
ctx, ctx,
@ -207,18 +217,21 @@ async def confirm_set_options_op(ctx, op: StellarSetOptionsOp):
await confirm_text(ctx, "op_home_domain", "Home Domain", op.home_domain) await confirm_text(ctx, "op_home_domain", "Home Domain", op.home_domain)
if op.signer_type is not None: if op.signer_type is not None:
if op.signer_key is None or op.signer_weight is None:
raise DataError("Stellar: invalid signer option data.")
if op.signer_weight > 0: if op.signer_weight > 0:
title = "Add Signer" title = "Add Signer"
else: else:
title = "Remove Signer" title = "Remove Signer"
data: str | bytes = "" data: str | bytes = ""
if op.signer_type == consts.SIGN_TYPE_ACCOUNT: if op.signer_type == StellarSignerType.ACCOUNT:
description = "Account:" description = "Account:"
data = helpers.address_from_public_key(op.signer_key) data = helpers.address_from_public_key(op.signer_key)
elif op.signer_type == consts.SIGN_TYPE_PRE_AUTH: elif op.signer_type == StellarSignerType.PRE_AUTH:
description = "Pre-auth transaction:" description = "Pre-auth transaction:"
data = op.signer_key data = op.signer_key
elif op.signer_type == consts.SIGN_TYPE_HASH: elif op.signer_type == StellarSignerType.HASH:
description = "Hash:" description = "Hash:"
data = op.signer_key data = op.signer_key
else: else:
@ -233,7 +246,7 @@ async def confirm_set_options_op(ctx, op: StellarSetOptionsOp):
) )
def _format_thresholds(op: StellarSetOptionsOp) -> list[(str, str)]: def _format_thresholds(op: StellarSetOptionsOp) -> list[tuple[str, str]]:
props = [] props = []
if op.master_weight is not None: if op.master_weight is not None:
props.append(("Master Weight:", str(op.master_weight))) props.append(("Master Weight:", str(op.master_weight)))
@ -246,7 +259,7 @@ def _format_thresholds(op: StellarSetOptionsOp) -> list[(str, str)]:
return props return props
def _format_flags(flags: int) -> tuple: def _format_flags(flags: int) -> str:
if flags > consts.FLAGS_MAX_SIZE: if flags > consts.FLAGS_MAX_SIZE:
raise ProcessError("Stellar: invalid flags") raise ProcessError("Stellar: invalid flags")
text = "{}{}{}".format( text = "{}{}{}".format(
@ -257,9 +270,11 @@ def _format_flags(flags: int) -> tuple:
return text return text
async def confirm_asset_issuer(ctx, asset: StellarAssetType): async def confirm_asset_issuer(ctx: Context, asset: StellarAsset) -> None:
if asset is None or asset.type == consts.ASSET_TYPE_NATIVE: if asset.type == StellarAssetType.NATIVE:
return return
if asset.issuer is None or asset.code is None:
raise DataError("Stellar: invalid asset definition")
await confirm_address( await confirm_address(
ctx, ctx,
"Confirm Issuer", "Confirm Issuer",

@ -1,7 +1,8 @@
from trezor.enums import StellarAssetType
from trezor.messages import ( from trezor.messages import (
StellarAccountMergeOp, StellarAccountMergeOp,
StellarAllowTrustOp, StellarAllowTrustOp,
StellarAssetType, StellarAsset,
StellarBumpSequenceOp, StellarBumpSequenceOp,
StellarChangeTrustOp, StellarChangeTrustOp,
StellarCreateAccountOp, StellarCreateAccountOp,
@ -14,14 +15,17 @@ from trezor.messages import (
) )
from trezor.wire import DataError, ProcessError from trezor.wire import DataError, ProcessError
from .. import consts, writers from .. import writers
if False:
from trezor.utils import Writer
def write_account_merge_op(w, msg: StellarAccountMergeOp):
def write_account_merge_op(w: Writer, msg: StellarAccountMergeOp) -> None:
writers.write_pubkey(w, msg.destination_account) writers.write_pubkey(w, msg.destination_account)
def write_allow_trust_op(w, msg: StellarAllowTrustOp): def write_allow_trust_op(w: Writer, msg: StellarAllowTrustOp) -> None:
# trustor account (the account being allowed to access the asset) # trustor account (the account being allowed to access the asset)
writers.write_pubkey(w, msg.trusted_account) writers.write_pubkey(w, msg.trusted_account)
writers.write_uint32(w, msg.asset_type) writers.write_uint32(w, msg.asset_type)
@ -30,21 +34,21 @@ def write_allow_trust_op(w, msg: StellarAllowTrustOp):
writers.write_bool(w, msg.is_authorized) writers.write_bool(w, msg.is_authorized)
def write_bump_sequence_op(w, msg: StellarBumpSequenceOp): def write_bump_sequence_op(w: Writer, msg: StellarBumpSequenceOp) -> None:
writers.write_uint64(w, msg.bump_to) writers.write_uint64(w, msg.bump_to)
def write_change_trust_op(w, msg: StellarChangeTrustOp): def write_change_trust_op(w: Writer, msg: StellarChangeTrustOp) -> None:
_write_asset(w, msg.asset) _write_asset(w, msg.asset)
writers.write_uint64(w, msg.limit) writers.write_uint64(w, msg.limit)
def write_create_account_op(w, msg: StellarCreateAccountOp): def write_create_account_op(w: Writer, msg: StellarCreateAccountOp) -> None:
writers.write_pubkey(w, msg.new_account) writers.write_pubkey(w, msg.new_account)
writers.write_uint64(w, msg.starting_balance) writers.write_uint64(w, msg.starting_balance)
def write_create_passive_offer_op(w, msg: StellarCreatePassiveOfferOp): def write_create_passive_offer_op(w: Writer, msg: StellarCreatePassiveOfferOp) -> None:
_write_asset(w, msg.selling_asset) _write_asset(w, msg.selling_asset)
_write_asset(w, msg.buying_asset) _write_asset(w, msg.buying_asset)
writers.write_uint64(w, msg.amount) writers.write_uint64(w, msg.amount)
@ -52,7 +56,7 @@ def write_create_passive_offer_op(w, msg: StellarCreatePassiveOfferOp):
writers.write_uint32(w, msg.price_d) writers.write_uint32(w, msg.price_d)
def write_manage_data_op(w, msg: StellarManageDataOp): def write_manage_data_op(w: Writer, msg: StellarManageDataOp) -> None:
if len(msg.key) > 64: if len(msg.key) > 64:
raise ProcessError("Stellar: max length of a key is 64 bytes") raise ProcessError("Stellar: max length of a key is 64 bytes")
writers.write_string(w, msg.key) writers.write_string(w, msg.key)
@ -61,7 +65,7 @@ def write_manage_data_op(w, msg: StellarManageDataOp):
writers.write_string(w, msg.value) writers.write_string(w, msg.value)
def write_manage_offer_op(w, msg: StellarManageOfferOp): def write_manage_offer_op(w: Writer, msg: StellarManageOfferOp) -> None:
_write_asset(w, msg.selling_asset) _write_asset(w, msg.selling_asset)
_write_asset(w, msg.buying_asset) _write_asset(w, msg.buying_asset)
writers.write_uint64(w, msg.amount) # amount to sell writers.write_uint64(w, msg.amount) # amount to sell
@ -70,7 +74,7 @@ def write_manage_offer_op(w, msg: StellarManageOfferOp):
writers.write_uint64(w, msg.offer_id) writers.write_uint64(w, msg.offer_id)
def write_path_payment_op(w, msg: StellarPathPaymentOp): def write_path_payment_op(w: Writer, msg: StellarPathPaymentOp) -> None:
_write_asset(w, msg.send_asset) _write_asset(w, msg.send_asset)
writers.write_uint64(w, msg.send_max) writers.write_uint64(w, msg.send_max)
writers.write_pubkey(w, msg.destination_account) writers.write_pubkey(w, msg.destination_account)
@ -82,13 +86,13 @@ def write_path_payment_op(w, msg: StellarPathPaymentOp):
_write_asset(w, p) _write_asset(w, p)
def write_payment_op(w, msg: StellarPaymentOp): def write_payment_op(w: Writer, msg: StellarPaymentOp) -> None:
writers.write_pubkey(w, msg.destination_account) writers.write_pubkey(w, msg.destination_account)
_write_asset(w, msg.asset) _write_asset(w, msg.asset)
writers.write_uint64(w, msg.amount) writers.write_uint64(w, msg.amount)
def write_set_options_op(w, msg: StellarSetOptionsOp): def write_set_options_op(w: Writer, msg: StellarSetOptionsOp) -> None:
# inflation destination # inflation destination
if msg.inflation_destination_account is None: if msg.inflation_destination_account is None:
writers.write_bool(w, False) writers.write_bool(w, False)
@ -118,16 +122,18 @@ def write_set_options_op(w, msg: StellarSetOptionsOp):
# signer # signer
if msg.signer_type is None: if msg.signer_type is None:
writers.write_bool(w, False) writers.write_bool(w, False)
elif msg.signer_type in consts.SIGN_TYPES: else:
if msg.signer_key is None or msg.signer_weight is None:
raise DataError(
"Stellar: signer_type, signer_key, signer_weight must be set together"
)
writers.write_bool(w, True) writers.write_bool(w, True)
writers.write_uint32(w, msg.signer_type) writers.write_uint32(w, msg.signer_type)
writers.write_bytes_fixed(w, msg.signer_key, 32) writers.write_bytes_fixed(w, msg.signer_key, 32)
writers.write_uint32(w, msg.signer_weight) writers.write_uint32(w, msg.signer_weight)
else:
raise ProcessError("Stellar: unknown signer type")
def _write_set_options_int(w, value: int): def _write_set_options_int(w: Writer, value: int | None) -> None:
if value is None: if value is None:
writers.write_bool(w, False) writers.write_bool(w, False)
else: else:
@ -135,7 +141,7 @@ def _write_set_options_int(w, value: int):
writers.write_uint32(w, value) writers.write_uint32(w, value)
def write_account(w, source_account: str): def write_account(w: Writer, source_account: str | None) -> None:
if source_account is None: if source_account is None:
writers.write_bool(w, False) writers.write_bool(w, False)
else: else:
@ -143,28 +149,36 @@ def write_account(w, source_account: str):
writers.write_pubkey(w, source_account) writers.write_pubkey(w, source_account)
def _write_asset_code(w, asset_type: int, asset_code: str): def _write_asset_code(
code = bytearray(asset_code) w: Writer, asset_type: StellarAssetType, asset_code: str | None
if asset_type == consts.ASSET_TYPE_NATIVE: ) -> None:
if asset_type == StellarAssetType.NATIVE:
return # nothing is needed return # nothing is needed
elif asset_type == consts.ASSET_TYPE_ALPHANUM4:
if asset_code is None:
raise DataError("Stellar: invalid asset")
code = asset_code.encode()
if asset_type == StellarAssetType.ALPHANUM4:
if len(code) > 4: if len(code) > 4:
raise DataError("Stellar: asset code too long for ALPHANUM4") raise DataError("Stellar: asset code too long for ALPHANUM4")
# pad with zeros to 4 chars # pad with zeros to 4 chars
writers.write_bytes_fixed(w, code + bytearray([0] * (4 - len(code))), 4) writers.write_bytes_fixed(w, code + bytes([0] * (4 - len(code))), 4)
elif asset_type == consts.ASSET_TYPE_ALPHANUM12: elif asset_type == StellarAssetType.ALPHANUM12:
if len(code) > 12: if len(code) > 12:
raise DataError("Stellar: asset code too long for ALPHANUM12") raise DataError("Stellar: asset code too long for ALPHANUM12")
# pad with zeros to 12 chars # pad with zeros to 12 chars
writers.write_bytes_fixed(w, code + bytearray([0] * (12 - len(code))), 12) writers.write_bytes_fixed(w, code + bytes([0] * (12 - len(code))), 12)
else: else:
raise ProcessError("Stellar: invalid asset type") raise ProcessError("Stellar: invalid asset type")
def _write_asset(w, asset: StellarAssetType): def _write_asset(w: Writer, asset: StellarAsset) -> None:
if asset is None or asset.type == consts.ASSET_TYPE_NATIVE: if asset.type == StellarAssetType.NATIVE:
writers.write_uint32(w, 0) writers.write_uint32(w, 0)
return return
if asset.code is None or asset.issuer is None:
raise DataError("Stellar: invalid asset")
writers.write_uint32(w, asset.type) writers.write_uint32(w, asset.type)
_write_asset_code(w, asset.type, asset.code) _write_asset_code(w, asset.type, asset.code)
writers.write_pubkey(w, asset.issuer) writers.write_pubkey(w, asset.issuer)

@ -2,8 +2,9 @@ from ubinascii import hexlify
from trezor.crypto.curve import ed25519 from trezor.crypto.curve import ed25519
from trezor.crypto.hashlib import sha256 from trezor.crypto.hashlib import sha256
from trezor.enums import StellarMemoType
from trezor.messages import StellarSignedTx, StellarSignTx, StellarTxOpRequest from trezor.messages import StellarSignedTx, StellarSignTx, StellarTxOpRequest
from trezor.wire import ProcessError from trezor.wire import DataError, ProcessError
from apps.common import paths, seed from apps.common import paths, seed
from apps.common.keychain import auto_keychain from apps.common.keychain import auto_keychain
@ -11,9 +12,16 @@ from apps.common.keychain import auto_keychain
from . import consts, helpers, layout, writers from . import consts, helpers, layout, writers
from .operations import process_operation from .operations import process_operation
if False:
from trezor.wire import Context
from apps.common.keychain import Keychain
@auto_keychain(__name__) @auto_keychain(__name__)
async def sign_tx(ctx, msg: StellarSignTx, keychain): async def sign_tx(
ctx: Context, msg: StellarSignTx, keychain: Keychain
) -> StellarSignedTx:
await paths.validate_path(ctx, keychain, msg.address_n) await paths.validate_path(ctx, keychain, msg.address_n)
node = keychain.derive(msg.address_n) node = keychain.derive(msg.address_n)
@ -37,15 +45,15 @@ async def sign_tx(ctx, msg: StellarSignTx, keychain):
return StellarSignedTx(public_key=pubkey, signature=signature) return StellarSignedTx(public_key=pubkey, signature=signature)
async def _final(ctx, w: bytearray, msg: StellarSignTx): async def _final(ctx: Context, w: bytearray, msg: StellarSignTx) -> None:
# 4 null bytes representing a (currently unused) empty union # 4 null bytes representing a (currently unused) empty union
writers.write_uint32(w, 0) writers.write_uint32(w, 0)
# final confirm # final confirm
await layout.require_confirm_final(ctx, msg.fee, msg.num_operations) await layout.require_confirm_final(ctx, msg.fee, msg.num_operations)
async def _init(ctx, w: bytearray, pubkey: bytes, msg: StellarSignTx): async def _init(ctx: Context, w: bytearray, pubkey: bytes, msg: StellarSignTx) -> None:
network_passphrase_hash = sha256(msg.network_passphrase).digest() network_passphrase_hash = sha256(msg.network_passphrase.encode()).digest()
writers.write_bytes_fixed(w, network_passphrase_hash, 32) writers.write_bytes_fixed(w, network_passphrase_hash, 32)
writers.write_bytes_fixed(w, consts.TX_TYPE, 4) writers.write_bytes_fixed(w, consts.TX_TYPE, 4)
@ -62,46 +70,52 @@ async def _init(ctx, w: bytearray, pubkey: bytes, msg: StellarSignTx):
) )
async def _timebounds(ctx, w: bytearray, start: int, end: int): async def _timebounds(
ctx: Context, w: bytearray, start: int | None, end: int | None
) -> None:
# timebounds are only present if timebounds_start or timebounds_end is non-zero # timebounds are only present if timebounds_start or timebounds_end is non-zero
if start or end: if start is not None and end is not None:
# confirm dialog # confirm dialog
await layout.require_confirm_timebounds(ctx, start, end) await layout.require_confirm_timebounds(ctx, start, end)
writers.write_bool(w, True) writers.write_bool(w, True)
# timebounds are sent as uint32s since that's all we can display, but they must be hashed as 64bit # timebounds are sent as uint32s since that's all we can display, but they must be hashed as 64bit
writers.write_uint64(w, start or 0) writers.write_uint64(w, start)
writers.write_uint64(w, end or 0) writers.write_uint64(w, end)
else: else:
writers.write_bool(w, False) writers.write_bool(w, False)
async def _operations(ctx, w: bytearray, num_operations: int): async def _operations(ctx: Context, w: bytearray, num_operations: int) -> None:
writers.write_uint32(w, num_operations) writers.write_uint32(w, num_operations)
for i in range(num_operations): for i in range(num_operations):
op = await ctx.call_any(StellarTxOpRequest(), *consts.op_wire_types) op = await ctx.call_any(StellarTxOpRequest(), *consts.op_wire_types)
await process_operation(ctx, w, op) await process_operation(ctx, w, op) # type: ignore
async def _memo(ctx, w: bytearray, msg: StellarSignTx): async def _memo(ctx: Context, w: bytearray, msg: StellarSignTx) -> None:
if msg.memo_type is None:
msg.memo_type = consts.MEMO_TYPE_NONE
writers.write_uint32(w, msg.memo_type) writers.write_uint32(w, msg.memo_type)
if msg.memo_type == consts.MEMO_TYPE_NONE: if msg.memo_type == StellarMemoType.NONE:
# nothing is serialized # nothing is serialized
memo_confirm_text = "" memo_confirm_text = ""
elif msg.memo_type == consts.MEMO_TYPE_TEXT: elif msg.memo_type == StellarMemoType.TEXT:
# Text: 4 bytes (size) + up to 28 bytes # Text: 4 bytes (size) + up to 28 bytes
if msg.memo_text is None:
raise DataError("Stellar: Missing memo text")
if len(msg.memo_text) > 28: if len(msg.memo_text) > 28:
raise ProcessError("Stellar: max length of a memo text is 28 bytes") raise ProcessError("Stellar: max length of a memo text is 28 bytes")
writers.write_string(w, msg.memo_text) writers.write_string(w, msg.memo_text)
memo_confirm_text = msg.memo_text memo_confirm_text = msg.memo_text
elif msg.memo_type == consts.MEMO_TYPE_ID: elif msg.memo_type == StellarMemoType.ID:
# ID: 64 bit unsigned integer # ID: 64 bit unsigned integer
if msg.memo_id is None:
raise DataError("Stellar: Missing memo id")
writers.write_uint64(w, msg.memo_id) writers.write_uint64(w, msg.memo_id)
memo_confirm_text = str(msg.memo_id) memo_confirm_text = str(msg.memo_id)
elif msg.memo_type in (consts.MEMO_TYPE_HASH, consts.MEMO_TYPE_RETURN): elif msg.memo_type in (StellarMemoType.HASH, StellarMemoType.RETURN):
# Hash/Return: 32 byte hash # Hash/Return: 32 byte hash
if msg.memo_hash is None:
raise DataError("Stellar: Missing memo hash")
writers.write_bytes_fixed(w, bytearray(msg.memo_hash), 32) writers.write_bytes_fixed(w, bytearray(msg.memo_hash), 32)
memo_confirm_text = hexlify(msg.memo_hash).decode() memo_confirm_text = hexlify(msg.memo_hash).decode()
else: else:

@ -13,8 +13,10 @@ write_uint64 = write_uint64_be
if False: if False:
from typing import AnyStr from typing import AnyStr
from trezor.utils import Writer
def write_string(w, s: AnyStr) -> None:
def write_string(w: Writer, s: AnyStr) -> None:
"""Write XDR string padded to a multiple of 4 bytes.""" """Write XDR string padded to a multiple of 4 bytes."""
if isinstance(s, str): if isinstance(s, str):
buf = s.encode() buf = s.encode()
@ -28,14 +30,14 @@ def write_string(w, s: AnyStr) -> None:
write_bytes_unchecked(w, bytes([0] * (4 - remainder))) write_bytes_unchecked(w, bytes([0] * (4 - remainder)))
def write_bool(w, val: bool): def write_bool(w: Writer, val: bool) -> None:
if val: if val:
write_uint32(w, 1) write_uint32(w, 1)
else: else:
write_uint32(w, 0) write_uint32(w, 0)
def write_pubkey(w, address: str): def write_pubkey(w: Writer, address: str) -> None:
# first 4 bytes of an address are the type, there's only one type (0) # first 4 bytes of an address are the type, there's only one type (0)
write_uint32(w, 0) write_uint32(w, 0)
write_bytes_fixed(w, public_key_from_address(address), 32) write_bytes_fixed(w, public_key_from_address(address), 32)

@ -53,17 +53,17 @@ def encode(s: bytes) -> str:
def decode(s: str) -> bytes: def decode(s: str) -> bytes:
s = s.encode() data = s.encode()
quanta, leftover = divmod(len(s), 8) quanta, leftover = divmod(len(data), 8)
if leftover: if leftover:
raise ValueError("Incorrect padding") raise ValueError("Incorrect padding")
# Strip off pad characters from the right. We need to count the pad # Strip off pad characters from the right. We need to count the pad
# characters because this will tell us how many null bytes to remove from # characters because this will tell us how many null bytes to remove from
# the end of the decoded string. # the end of the decoded string.
padchars = s.find(b"=") padchars = data.find(b"=")
if padchars > 0: if padchars > 0:
padchars = len(s) - padchars padchars = len(data) - padchars
s = s[:-padchars] data = data[:-padchars]
else: else:
padchars = 0 padchars = 0
@ -71,7 +71,7 @@ def decode(s: str) -> bytes:
parts = [] parts = []
acc = 0 acc = 0
shift = 35 shift = 35
for c in s: for c in data:
val = _b32rev.get(c) val = _b32rev.get(c)
if val is None: if val is None:
raise ValueError("Non-base32 digit found") raise ValueError("Non-base32 digit found")

@ -68,9 +68,9 @@ NETWORK_PASSPHRASE = "Test SDF Network ; September 2015"
def _create_msg(memo=False) -> messages.StellarSignTx: def _create_msg(memo=False) -> messages.StellarSignTx:
kwargs = {"memo_type": 0} kwargs = {"memo_type": messages.StellarMemoType.NONE}
if memo: if memo:
kwargs = {"memo_type": 1, "memo_text": "hi"} kwargs = {"memo_type": messages.StellarMemoType.TEXT, "memo_text": "hi"}
return messages.StellarSignTx( return messages.StellarSignTx(
source_account="GAK5MSF74TJW6GLM7NLTL76YZJKM2S4CGP3UH4REJHPHZ4YBZW2GSBPW", source_account="GAK5MSF74TJW6GLM7NLTL76YZJKM2S4CGP3UH4REJHPHZ4YBZW2GSBPW",
fee=100, fee=100,
@ -135,25 +135,7 @@ def test_sign_tx_payment_op_native(client):
op = messages.StellarPaymentOp() op = messages.StellarPaymentOp()
op.amount = 500111000 op.amount = 500111000
op.destination_account = "GBOVKZBEM2YYLOCDCUXJ4IMRKHN4LCJAE7WEAEA2KF562XFAGDBOB64V" op.destination_account = "GBOVKZBEM2YYLOCDCUXJ4IMRKHN4LCJAE7WEAEA2KF562XFAGDBOB64V"
op.asset = messages.StellarAsset(type=messages.StellarAssetType.NATIVE)
tx = _create_msg()
response = stellar.sign_tx(client, tx, [op], ADDRESS_N, NETWORK_PASSPHRASE)
assert (
b64encode(response.signature)
== b"pDc6ghKCLNoYbt3h4eBw+533237m0BB0Jp/d/TxJCA83mF3o5Fr4l5vwAWBR62hdTWAP9MhVluY0cd5i54UwDg=="
# a4373a8212822cda186edde1e1e070fb9df7db7ee6d01074269fddfd3c49080f37985de8e45af8979bf0016051eb685d4d600ff4c85596e63471de62e785300e
)
def test_sign_tx_payment_op_native_explicit_asset(client):
"""Native payment of 50.0111 XLM to GBOVKZBEM2YYLOCDCUXJ4IMRKHN4LCJAE7WEAEA2KF562XFAGDBOB64V"""
op = messages.StellarPaymentOp()
op.amount = 500111000
op.destination_account = "GBOVKZBEM2YYLOCDCUXJ4IMRKHN4LCJAE7WEAEA2KF562XFAGDBOB64V"
op.asset = messages.StellarAssetType(type=0)
tx = _create_msg() tx = _create_msg()
@ -173,8 +155,8 @@ def test_sign_tx_payment_op_custom_asset1(client):
op.amount = 500111000 op.amount = 500111000
op.destination_account = "GBOVKZBEM2YYLOCDCUXJ4IMRKHN4LCJAE7WEAEA2KF562XFAGDBOB64V" op.destination_account = "GBOVKZBEM2YYLOCDCUXJ4IMRKHN4LCJAE7WEAEA2KF562XFAGDBOB64V"
op.asset = messages.StellarAssetType( op.asset = messages.StellarAsset(
type=1, type=messages.StellarAssetType.ALPHANUM4,
code="X", code="X",
issuer="GAUYJFQCYIHFQNS7CI6BFWD2DSSFKDIQZUQ3BLQODDKE4PSW7VVBKENC", issuer="GAUYJFQCYIHFQNS7CI6BFWD2DSSFKDIQZUQ3BLQODDKE4PSW7VVBKENC",
) )
@ -196,8 +178,8 @@ def test_sign_tx_payment_op_custom_asset12(client):
op.amount = 500111000 op.amount = 500111000
op.destination_account = "GBOVKZBEM2YYLOCDCUXJ4IMRKHN4LCJAE7WEAEA2KF562XFAGDBOB64V" op.destination_account = "GBOVKZBEM2YYLOCDCUXJ4IMRKHN4LCJAE7WEAEA2KF562XFAGDBOB64V"
op.asset = messages.StellarAssetType( op.asset = messages.StellarAsset(
type=2, type=messages.StellarAssetType.ALPHANUM12,
code="ABCDEFGHIJKL", code="ABCDEFGHIJKL",
issuer="GAUYJFQCYIHFQNS7CI6BFWD2DSSFKDIQZUQ3BLQODDKE4PSW7VVBKENC", issuer="GAUYJFQCYIHFQNS7CI6BFWD2DSSFKDIQZUQ3BLQODDKE4PSW7VVBKENC",
) )
@ -218,7 +200,7 @@ def test_sign_tx_allow_trust_op(client):
op = messages.StellarAllowTrustOp() op = messages.StellarAllowTrustOp()
op.is_authorized = True op.is_authorized = True
op.trusted_account = "GBOVKZBEM2YYLOCDCUXJ4IMRKHN4LCJAE7WEAEA2KF562XFAGDBOB64V" op.trusted_account = "GBOVKZBEM2YYLOCDCUXJ4IMRKHN4LCJAE7WEAEA2KF562XFAGDBOB64V"
op.asset_type = 1 op.asset_type = messages.StellarAssetType.ALPHANUM4
op.asset_code = "X" op.asset_code = "X"
tx = _create_msg(memo=True) tx = _create_msg(memo=True)
@ -237,8 +219,8 @@ def test_sign_tx_change_trust_op(client):
op.limit = 5000000000 op.limit = 5000000000
op.source_account = "GBOVKZBEM2YYLOCDCUXJ4IMRKHN4LCJAE7WEAEA2KF562XFAGDBOB64V" op.source_account = "GBOVKZBEM2YYLOCDCUXJ4IMRKHN4LCJAE7WEAEA2KF562XFAGDBOB64V"
op.asset = messages.StellarAssetType( op.asset = messages.StellarAsset(
type=2, type=messages.StellarAssetType.ALPHANUM12,
code="ABCDEFGHIJKL", code="ABCDEFGHIJKL",
issuer="GAUYJFQCYIHFQNS7CI6BFWD2DSSFKDIQZUQ3BLQODDKE4PSW7VVBKENC", issuer="GAUYJFQCYIHFQNS7CI6BFWD2DSSFKDIQZUQ3BLQODDKE4PSW7VVBKENC",
) )
@ -256,13 +238,13 @@ def test_sign_tx_change_trust_op(client):
def test_sign_tx_passive_offer_op(client): def test_sign_tx_passive_offer_op(client):
op = messages.StellarCreatePassiveOfferOp() op = messages.StellarCreatePassiveOfferOp()
op.selling_asset = messages.StellarAssetType( op.selling_asset = messages.StellarAsset(
type=2, type=messages.StellarAssetType.ALPHANUM12,
code="ABCDEFGHIJKL", code="ABCDEFGHIJKL",
issuer="GAUYJFQCYIHFQNS7CI6BFWD2DSSFKDIQZUQ3BLQODDKE4PSW7VVBKENC", issuer="GAUYJFQCYIHFQNS7CI6BFWD2DSSFKDIQZUQ3BLQODDKE4PSW7VVBKENC",
) )
op.buing_asset = messages.StellarAssetType( op.buying_asset = messages.StellarAsset(
type=1, type=messages.StellarAssetType.ALPHANUM4,
code="X", code="X",
issuer="GAUYJFQCYIHFQNS7CI6BFWD2DSSFKDIQZUQ3BLQODDKE4PSW7VVBKENC", issuer="GAUYJFQCYIHFQNS7CI6BFWD2DSSFKDIQZUQ3BLQODDKE4PSW7VVBKENC",
) )
@ -284,13 +266,13 @@ def test_sign_tx_passive_offer_op(client):
def test_sign_tx_manage_offer_op(client): def test_sign_tx_manage_offer_op(client):
op = messages.StellarManageOfferOp() op = messages.StellarManageOfferOp()
op.selling_asset = messages.StellarAssetType( op.selling_asset = messages.StellarAsset(
type=2, type=messages.StellarAssetType.ALPHANUM12,
code="ABCDEFGHIJKL", code="ABCDEFGHIJKL",
issuer="GAUYJFQCYIHFQNS7CI6BFWD2DSSFKDIQZUQ3BLQODDKE4PSW7VVBKENC", issuer="GAUYJFQCYIHFQNS7CI6BFWD2DSSFKDIQZUQ3BLQODDKE4PSW7VVBKENC",
) )
op.buing_asset = messages.StellarAssetType( op.buying_asset = messages.StellarAsset(
type=1, type=messages.StellarAssetType.ALPHANUM4,
code="X", code="X",
issuer="GAUYJFQCYIHFQNS7CI6BFWD2DSSFKDIQZUQ3BLQODDKE4PSW7VVBKENC", issuer="GAUYJFQCYIHFQNS7CI6BFWD2DSSFKDIQZUQ3BLQODDKE4PSW7VVBKENC",
) )
@ -313,15 +295,15 @@ def test_sign_tx_manage_offer_op(client):
def test_sign_tx_path_payment_op(client): def test_sign_tx_path_payment_op(client):
op = messages.StellarPathPaymentOp() op = messages.StellarPathPaymentOp()
op.send = messages.StellarAssetType( op.send_asset = messages.StellarAsset(
type=1, type=messages.StellarAssetType.ALPHANUM4,
code="X", code="X",
issuer="GAUYJFQCYIHFQNS7CI6BFWD2DSSFKDIQZUQ3BLQODDKE4PSW7VVBKENC", issuer="GAUYJFQCYIHFQNS7CI6BFWD2DSSFKDIQZUQ3BLQODDKE4PSW7VVBKENC",
) )
op.send_max = 50000 op.send_max = 50000
op.destination_account = "GAUYJFQCYIHFQNS7CI6BFWD2DSSFKDIQZUQ3BLQODDKE4PSW7VVBKENC" op.destination_account = "GAUYJFQCYIHFQNS7CI6BFWD2DSSFKDIQZUQ3BLQODDKE4PSW7VVBKENC"
op.destination_asset = messages.StellarAssetType( op.destination_asset = messages.StellarAsset(
type=1, type=messages.StellarAssetType.ALPHANUM4,
code="X", code="X",
issuer="GAUYJFQCYIHFQNS7CI6BFWD2DSSFKDIQZUQ3BLQODDKE4PSW7VVBKENC", issuer="GAUYJFQCYIHFQNS7CI6BFWD2DSSFKDIQZUQ3BLQODDKE4PSW7VVBKENC",
) )
@ -355,7 +337,7 @@ def test_sign_tx_set_options(client):
) )
op = messages.StellarSetOptionsOp() op = messages.StellarSetOptionsOp()
op.signer_type = 0 op.signer_type = messages.StellarSignerType.ACCOUNT
op.signer_key = bytes.fromhex( op.signer_key = bytes.fromhex(
"72187adb879c414346d77c71af8cce7b6eaa57b528e999fd91feae6b6418628e" "72187adb879c414346d77c71af8cce7b6eaa57b528e999fd91feae6b6418628e"
) )
@ -407,7 +389,7 @@ def test_sign_tx_set_options(client):
) )
op = messages.StellarSetOptionsOp() op = messages.StellarSetOptionsOp()
op.signer_type = 1 op.signer_type = messages.StellarSignerType.PRE_AUTH
op.signer_key = bytes.fromhex( op.signer_key = bytes.fromhex(
"72187adb879c414346d77c71af8cce7b6eaa57b528e999fd91feae6b6418628e" "72187adb879c414346d77c71af8cce7b6eaa57b528e999fd91feae6b6418628e"
) )

Loading…
Cancel
Save