mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-04-25 11:39:02 +00:00
feat(core): Implement Bitcoin payment requests.
This commit is contained in:
parent
d0c3a6a2fa
commit
0e58218f5f
1
core/.changelog.d/1430.added
Normal file
1
core/.changelog.d/1430.added
Normal file
@ -0,0 +1 @@
|
||||
Support Bitcoin payment requests.
|
@ -276,6 +276,8 @@ apps.bitcoin.sign_tx.matchcheck
|
||||
import apps.bitcoin.sign_tx.matchcheck
|
||||
apps.bitcoin.sign_tx.omni
|
||||
import apps.bitcoin.sign_tx.omni
|
||||
apps.bitcoin.sign_tx.payment_request
|
||||
import apps.bitcoin.sign_tx.payment_request
|
||||
apps.bitcoin.sign_tx.progress
|
||||
import apps.bitcoin.sign_tx.progress
|
||||
apps.bitcoin.sign_tx.sig_hasher
|
||||
|
@ -3,20 +3,24 @@ from typing import TYPE_CHECKING
|
||||
|
||||
from trezor import wire
|
||||
from trezor.enums import OutputScriptType
|
||||
from trezor.ui.components.common.confirm import INFO
|
||||
|
||||
from apps.common import safety_checks
|
||||
|
||||
from ..authorization import FEE_PER_ANONYMITY_DECIMALS
|
||||
from ..keychain import validate_path_against_script_type
|
||||
from . import helpers, tx_weight
|
||||
from .payment_request import PaymentRequestVerifier
|
||||
from .tx_info import OriginalTxInfo, TxInfo
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from trezor.messages import SignTx
|
||||
from trezor.messages import TxInput
|
||||
from trezor.messages import TxOutput
|
||||
from trezor.messages import TxAckPaymentRequest
|
||||
|
||||
from apps.common.coininfo import CoinInfo
|
||||
from apps.common.keychain import Keychain
|
||||
|
||||
from ..authorization import CoinJoinAuthorization
|
||||
|
||||
@ -29,6 +33,8 @@ class Approver:
|
||||
def __init__(self, tx: SignTx, coin: CoinInfo) -> None:
|
||||
self.coin = coin
|
||||
self.weight = tx_weight.TxWeightCalculator()
|
||||
self.payment_req_verifier: PaymentRequestVerifier | None = None
|
||||
self.show_payment_req_details = False
|
||||
|
||||
# amounts in the current transaction
|
||||
self.total_in = 0 # sum of input amounts
|
||||
@ -67,10 +73,27 @@ class Approver:
|
||||
self.orig_total_in += txi.amount
|
||||
self.orig_external_in += txi.amount
|
||||
|
||||
def add_change_output(self, txo: TxOutput, script_pubkey: bytes) -> None:
|
||||
def _add_output(self, txo: TxOutput, script_pubkey: bytes) -> None:
|
||||
self.weight.add_output(script_pubkey)
|
||||
self.total_out += txo.amount
|
||||
|
||||
async def add_payment_request(
|
||||
self, msg: TxAckPaymentRequest, keychain: Keychain
|
||||
) -> None:
|
||||
self.finish_payment_request()
|
||||
self.payment_req_verifier = PaymentRequestVerifier(msg, self.coin, keychain)
|
||||
|
||||
def finish_payment_request(self) -> None:
|
||||
if self.payment_req_verifier:
|
||||
self.payment_req_verifier.verify()
|
||||
self.payment_req_verifier = None
|
||||
self.show_payment_req_details = False
|
||||
|
||||
def add_change_output(self, txo: TxOutput, script_pubkey: bytes) -> None:
|
||||
self._add_output(txo, script_pubkey)
|
||||
self.change_out += txo.amount
|
||||
if self.payment_req_verifier:
|
||||
self.payment_req_verifier.add_change_output(txo)
|
||||
|
||||
def add_orig_change_output(self, txo: TxOutput) -> None:
|
||||
self.orig_total_out += txo.amount
|
||||
@ -82,8 +105,9 @@ class Approver:
|
||||
script_pubkey: bytes,
|
||||
orig_txo: TxOutput | None = None,
|
||||
) -> None:
|
||||
self.weight.add_output(script_pubkey)
|
||||
self.total_out += txo.amount
|
||||
self._add_output(txo, script_pubkey)
|
||||
if self.payment_req_verifier:
|
||||
self.payment_req_verifier.add_external_output(txo)
|
||||
|
||||
def add_orig_external_output(self, txo: TxOutput) -> None:
|
||||
self.orig_total_out += txo.amount
|
||||
@ -94,7 +118,7 @@ class Approver:
|
||||
raise NotImplementedError
|
||||
|
||||
async def approve_tx(self, tx_info: TxInfo, orig_txs: list[OriginalTxInfo]) -> None:
|
||||
raise NotImplementedError
|
||||
self.finish_payment_request()
|
||||
|
||||
|
||||
class BasicApprover(Approver):
|
||||
@ -164,9 +188,21 @@ class BasicApprover(Approver):
|
||||
raise wire.ProcessError(
|
||||
"Adding new OP_RETURN outputs in replacement transactions is not supported."
|
||||
)
|
||||
else:
|
||||
elif txo.payment_req_index is None or self.show_payment_req_details:
|
||||
# Ask user to confirm output, unless it is part of a payment
|
||||
# request, which gets confirmed separately.
|
||||
await helpers.confirm_output(txo, self.coin, self.amount_unit)
|
||||
|
||||
async def add_payment_request(
|
||||
self, msg: TxAckPaymentRequest, keychain: Keychain
|
||||
) -> None:
|
||||
await super().add_payment_request(msg, keychain)
|
||||
if msg.amount is None:
|
||||
raise wire.DataError("Missing payment request amount.")
|
||||
|
||||
result = await helpers.confirm_payment_request(msg, self.coin, self.amount_unit)
|
||||
self.show_payment_req_details = result is INFO
|
||||
|
||||
async def approve_orig_txids(
|
||||
self, tx_info: TxInfo, orig_txs: list[OriginalTxInfo]
|
||||
) -> None:
|
||||
@ -188,6 +224,8 @@ class BasicApprover(Approver):
|
||||
await helpers.confirm_replacement(description, orig.orig_hash)
|
||||
|
||||
async def approve_tx(self, tx_info: TxInfo, orig_txs: list[OriginalTxInfo]) -> None:
|
||||
await super().approve_tx(tx_info, orig_txs)
|
||||
|
||||
fee = self.total_in - self.total_out
|
||||
|
||||
# some coins require negative fees for reward TX
|
||||
@ -315,18 +353,19 @@ class CoinJoinApprover(Approver):
|
||||
|
||||
def add_change_output(self, txo: TxOutput, script_pubkey: bytes) -> None:
|
||||
super().add_change_output(txo, script_pubkey)
|
||||
self._add_output(txo, script_pubkey)
|
||||
self.our_weight.add_output(script_pubkey)
|
||||
self.group_our_count += 1
|
||||
|
||||
async def add_external_output(
|
||||
self,
|
||||
txo: TxOutput,
|
||||
script_pubkey: bytes,
|
||||
orig_txo: TxOutput | None = None,
|
||||
async def add_payment_request(
|
||||
self, msg: TxAckPaymentRequest, keychain: Keychain
|
||||
) -> None:
|
||||
await super().add_external_output(txo, script_pubkey, orig_txo)
|
||||
self._add_output(txo, script_pubkey)
|
||||
await super().add_payment_request(msg, keychain)
|
||||
|
||||
if msg.recipient_name != self.authorization.params.coordinator:
|
||||
raise wire.DataError("CoinJoin coordinator mismatch in payment request.")
|
||||
|
||||
if msg.memos:
|
||||
raise wire.DataError("Memos not allowed in CoinJoin payment request.")
|
||||
|
||||
async def approve_orig_txids(
|
||||
self, tx_info: TxInfo, orig_txs: list[OriginalTxInfo]
|
||||
@ -334,6 +373,8 @@ class CoinJoinApprover(Approver):
|
||||
pass
|
||||
|
||||
async def approve_tx(self, tx_info: TxInfo, orig_txs: list[OriginalTxInfo]) -> None:
|
||||
await super().approve_tx(tx_info, orig_txs)
|
||||
|
||||
# The mining fee of the transaction as a whole.
|
||||
mining_fee = self.total_in - self.total_out
|
||||
|
||||
@ -383,6 +424,12 @@ class CoinJoinApprover(Approver):
|
||||
)
|
||||
|
||||
def _add_output(self, txo: TxOutput, script_pubkey: bytes) -> None:
|
||||
super()._add_output(txo, script_pubkey)
|
||||
|
||||
# All CoinJoin outputs must be accompanied by a signed payment request.
|
||||
if txo.payment_req_index is None:
|
||||
raise wire.DataError("Missing payment request.")
|
||||
|
||||
# Assumption: CoinJoin outputs are grouped by amount. (If this assumption is
|
||||
# not satisfied, then we will compute a lower coordinator fee, which may lead
|
||||
# us to wrongfully decline the transaction.)
|
||||
|
@ -126,6 +126,9 @@ class Bitcoin:
|
||||
self.h_inputs: bytes | None = None
|
||||
self.h_external_inputs: bytes | None = None
|
||||
|
||||
# The index of the payment request being processed.
|
||||
self.payment_req_index: int | None = None
|
||||
|
||||
progress.init(tx.inputs_count, tx.outputs_count)
|
||||
|
||||
def create_hash_writer(self) -> HashWriter:
|
||||
@ -427,6 +430,19 @@ class Bitcoin:
|
||||
script_pubkey: bytes,
|
||||
orig_txo: TxOutput | None,
|
||||
) -> None:
|
||||
if txo.payment_req_index != self.payment_req_index:
|
||||
if txo.payment_req_index is None:
|
||||
# TODO not needed
|
||||
self.approver.finish_payment_request()
|
||||
else:
|
||||
tx_ack_payment_req = await helpers.request_payment_req(
|
||||
self.tx_req, txo.payment_req_index
|
||||
)
|
||||
await self.approver.add_payment_request(
|
||||
tx_ack_payment_req, self.keychain
|
||||
)
|
||||
self.payment_req_index = txo.payment_req_index
|
||||
|
||||
if self.tx_info.output_is_change(txo):
|
||||
# Output is change and does not need approval.
|
||||
self.approver.add_change_output(txo, script_pubkey)
|
||||
|
@ -9,6 +9,7 @@ from trezor.messages import (
|
||||
SignTx,
|
||||
TxAckInput,
|
||||
TxAckOutput,
|
||||
TxAckPaymentRequest,
|
||||
TxAckPrevExtraData,
|
||||
TxAckPrevInput,
|
||||
TxAckPrevMeta,
|
||||
@ -63,6 +64,25 @@ class UiConfirmDecredSSTXSubmission(UiConfirm):
|
||||
)
|
||||
|
||||
|
||||
class UiConfirmPaymentRequest(UiConfirm):
|
||||
def __init__(
|
||||
self,
|
||||
payment_req: TxAckPaymentRequest,
|
||||
coin: CoinInfo,
|
||||
amount_unit: AmountUnit,
|
||||
):
|
||||
self.payment_req = payment_req
|
||||
self.amount_unit = amount_unit
|
||||
self.coin = coin
|
||||
|
||||
def confirm_dialog(self, ctx: wire.Context) -> Awaitable[Any]:
|
||||
return layout.confirm_payment_request(
|
||||
ctx, self.payment_req, self.coin, self.amount_unit
|
||||
)
|
||||
|
||||
__eq__ = utils.obj_eq
|
||||
|
||||
|
||||
class UiConfirmReplacement(UiConfirm):
|
||||
def __init__(self, description: str, txid: bytes):
|
||||
self.description = description
|
||||
@ -187,6 +207,10 @@ def confirm_decred_sstx_submission(output: TxOutput, coin: CoinInfo, amount_unit
|
||||
return (yield UiConfirmDecredSSTXSubmission(output, coin, amount_unit))
|
||||
|
||||
|
||||
def confirm_payment_request(payment_req: TxAckPaymentRequest, coin: CoinInfo, amount_unit: AmountUnit) -> Awaitable[Any]: # type: ignore
|
||||
return (yield UiConfirmPaymentRequest(payment_req, coin, amount_unit))
|
||||
|
||||
|
||||
def confirm_replacement(description: str, txid: bytes) -> Awaitable[Any]: # type: ignore
|
||||
return (yield UiConfirmReplacement(description, txid))
|
||||
|
||||
@ -292,6 +316,15 @@ def request_tx_prev_output(tx_req: TxRequest, i: int, coin: CoinInfo, tx_hash: b
|
||||
return ack.tx.output
|
||||
|
||||
|
||||
def request_payment_req(tx_req: TxRequest, i: int) -> Awaitable[TxAckPaymentRequest]: # type: ignore
|
||||
assert tx_req.details is not None
|
||||
tx_req.request_type = RequestType.TXPAYMENTREQ
|
||||
tx_req.details.request_index = i
|
||||
ack = yield TxAckPaymentRequest, tx_req
|
||||
_clear_tx_request(tx_req)
|
||||
return sanitize_payment_req(ack)
|
||||
|
||||
|
||||
def request_tx_finish(tx_req: TxRequest) -> Awaitable[None]: # type: ignore
|
||||
tx_req.request_type = RequestType.TXFINISHED
|
||||
yield None, tx_req
|
||||
@ -459,3 +492,13 @@ def sanitize_tx_output(txo: TxOutput, coin: CoinInfo) -> TxOutput:
|
||||
raise wire.DataError("Missing orig_index field.")
|
||||
|
||||
return txo
|
||||
|
||||
|
||||
def sanitize_payment_req(payment_req: TxAckPaymentRequest) -> TxAckPaymentRequest:
|
||||
for memo in payment_req.memos:
|
||||
if (memo.text_memo, memo.refund_memo, memo.coin_purchase_memo).count(None) != 2:
|
||||
raise wire.DataError(
|
||||
"Exactly one memo type must be specified in each PaymentRequestMemo."
|
||||
)
|
||||
|
||||
return payment_req
|
||||
|
@ -2,7 +2,7 @@ from micropython import const
|
||||
from typing import TYPE_CHECKING
|
||||
from ubinascii import hexlify
|
||||
|
||||
from trezor import utils
|
||||
from trezor import ui, utils, wire
|
||||
from trezor.enums import AmountUnit, ButtonRequestType, OutputScriptType
|
||||
from trezor.strings import format_amount, format_timestamp
|
||||
from trezor.ui import layouts
|
||||
@ -15,8 +15,9 @@ if not utils.BITCOIN_ONLY:
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from trezor import wire
|
||||
from trezor.messages import TxOutput
|
||||
from typing import Any
|
||||
|
||||
from trezor.messages import TxAckPaymentRequest, TxOutput
|
||||
from trezor.ui.layouts import LayoutType
|
||||
|
||||
from apps.common.coininfo import CoinInfo
|
||||
@ -66,8 +67,19 @@ async def confirm_output(
|
||||
else:
|
||||
assert output.address is not None
|
||||
address_short = addresses.address_short(coin, output.address)
|
||||
if output.payment_req_index is not None:
|
||||
title = "Confirm details"
|
||||
icon = ui.ICON_CONFIRM
|
||||
else:
|
||||
title = "Confirm sending"
|
||||
icon = ui.ICON_SEND
|
||||
|
||||
layout = layouts.confirm_output(
|
||||
ctx, address_short, format_coin_amount(output.amount, coin, amount_unit)
|
||||
ctx,
|
||||
address_short,
|
||||
format_coin_amount(output.amount, coin, amount_unit),
|
||||
title=title,
|
||||
icon=icon,
|
||||
)
|
||||
|
||||
await layout
|
||||
@ -84,6 +96,33 @@ async def confirm_decred_sstx_submission(
|
||||
)
|
||||
|
||||
|
||||
async def confirm_payment_request(
|
||||
ctx: wire.Context,
|
||||
msg: TxAckPaymentRequest,
|
||||
coin: CoinInfo,
|
||||
amount_unit: AmountUnit,
|
||||
) -> Any:
|
||||
memo_texts = []
|
||||
for m in msg.memos:
|
||||
if m.text_memo is not None:
|
||||
memo_texts.append(m.text_memo.text)
|
||||
elif m.refund_memo is not None:
|
||||
pass
|
||||
elif m.coin_purchase_memo is not None:
|
||||
memo_texts.append(f"Buying {m.coin_purchase_memo.amount}.")
|
||||
else:
|
||||
raise wire.DataError("Unrecognized memo type in payment request memo.")
|
||||
|
||||
assert msg.amount is not None
|
||||
|
||||
return await layouts.confirm_payment_request(
|
||||
ctx,
|
||||
msg.recipient_name,
|
||||
format_coin_amount(msg.amount, coin, amount_unit),
|
||||
memo_texts,
|
||||
)
|
||||
|
||||
|
||||
async def confirm_replacement(ctx: wire.Context, description: str, txid: bytes) -> None:
|
||||
await layouts.confirm_replacement(
|
||||
ctx,
|
||||
|
98
core/src/apps/bitcoin/sign_tx/payment_request.py
Normal file
98
core/src/apps/bitcoin/sign_tx/payment_request.py
Normal file
@ -0,0 +1,98 @@
|
||||
from micropython import const
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from storage import cache
|
||||
from trezor import wire
|
||||
from trezor.crypto.curve import secp256k1
|
||||
from trezor.crypto.hashlib import sha256
|
||||
from trezor.utils import HashWriter
|
||||
|
||||
from apps.common import coininfo
|
||||
from apps.common.address_mac import check_address_mac
|
||||
from apps.common.keychain import Keychain
|
||||
|
||||
from .. import writers
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from trezor.messages import TxAckPaymentRequest, TxOutput
|
||||
|
||||
_MEMO_TYPE_TEXT = const(1)
|
||||
_MEMO_TYPE_REFUND = const(2)
|
||||
_MEMO_TYPE_COIN_PURCHASE = const(3)
|
||||
|
||||
|
||||
class PaymentRequestVerifier:
|
||||
if __debug__:
|
||||
# secp256k1 public key of m/0h for "all all ... all" seed.
|
||||
PUBLIC_KEY = b"\x03\x0f\xdf^(\x9bZ\xefSb\x90\x95:\xe8\x1c\xe6\x0e\x84\x1f\xf9V\xf3f\xac\x12?\xa6\x9d\xb3\xc7\x9f!\xb0"
|
||||
else:
|
||||
PUBLIC_KEY = b""
|
||||
|
||||
def __init__(
|
||||
self, msg: TxAckPaymentRequest, coin: coininfo.CoinInfo, keychain: Keychain
|
||||
) -> None:
|
||||
self.h_outputs = HashWriter(sha256())
|
||||
self.amount = 0
|
||||
self.expected_amount = msg.amount
|
||||
self.signature = msg.signature
|
||||
self.h_pr = HashWriter(sha256())
|
||||
|
||||
if msg.nonce:
|
||||
nonce = bytes(msg.nonce)
|
||||
if cache.get(cache.APP_COMMON_NONCE) != nonce:
|
||||
raise wire.DataError("Invalid nonce in payment request.")
|
||||
cache.delete(cache.APP_COMMON_NONCE)
|
||||
else:
|
||||
nonce = b""
|
||||
if msg.memos:
|
||||
wire.DataError("Missing nonce in payment request.")
|
||||
|
||||
writers.write_bytes_fixed(self.h_pr, b"SL\x00\x24", 4)
|
||||
writers.write_bytes_prefixed(self.h_pr, nonce)
|
||||
writers.write_bytes_prefixed(self.h_pr, msg.recipient_name.encode())
|
||||
writers.write_bitcoin_varint(self.h_pr, len(msg.memos))
|
||||
for m in msg.memos:
|
||||
if m.text_memo is not None:
|
||||
memo = m.text_memo
|
||||
writers.write_uint32(self.h_pr, _MEMO_TYPE_TEXT)
|
||||
writers.write_bytes_prefixed(self.h_pr, memo.text.encode())
|
||||
elif m.refund_memo is not None:
|
||||
memo = m.refund_memo
|
||||
# Unlike in a coin purchase memo, the coin type is implied by the payment request.
|
||||
check_address_mac(memo.address, memo.mac, coin.slip44, keychain)
|
||||
writers.write_uint32(self.h_pr, _MEMO_TYPE_REFUND)
|
||||
writers.write_bytes_prefixed(self.h_pr, memo.address.encode())
|
||||
elif m.coin_purchase_memo is not None:
|
||||
memo = m.coin_purchase_memo
|
||||
check_address_mac(memo.address, memo.mac, memo.coin_type, keychain)
|
||||
writers.write_uint32(self.h_pr, _MEMO_TYPE_COIN_PURCHASE)
|
||||
writers.write_uint32(self.h_pr, memo.coin_type)
|
||||
writers.write_bytes_prefixed(self.h_pr, memo.amount.encode())
|
||||
writers.write_bytes_prefixed(self.h_pr, memo.address.encode())
|
||||
|
||||
writers.write_uint32(self.h_pr, coin.slip44)
|
||||
|
||||
def verify(self) -> None:
|
||||
if self.expected_amount is not None and self.amount != self.expected_amount:
|
||||
raise wire.DataError("Invalid amount in payment request.")
|
||||
|
||||
hash_outputs = writers.get_tx_hash(self.h_outputs)
|
||||
writers.write_bytes_fixed(self.h_pr, hash_outputs, 32)
|
||||
|
||||
if not secp256k1.verify(
|
||||
self.PUBLIC_KEY, self.signature, self.h_pr.get_digest()
|
||||
):
|
||||
raise wire.DataError("Invalid signature in payment request.")
|
||||
|
||||
def _add_output(self, txo: TxOutput) -> None:
|
||||
# For change outputs txo.address filled in by output_derive_script().
|
||||
assert txo.address is not None
|
||||
writers.write_uint64(self.h_outputs, txo.amount)
|
||||
writers.write_bytes_prefixed(self.h_outputs, txo.address.encode())
|
||||
|
||||
def add_external_output(self, txo: TxOutput) -> None:
|
||||
self._add_output(txo)
|
||||
self.amount += txo.amount
|
||||
|
||||
def add_change_output(self, txo: TxOutput) -> None:
|
||||
self._add_output(txo)
|
@ -47,6 +47,8 @@ def find_message_handler_module(msg_type: int) -> str:
|
||||
return "apps.management.change_pin"
|
||||
if msg_type == MessageType.ChangeWipeCode:
|
||||
return "apps.management.change_wipe_code"
|
||||
elif msg_type == MessageType.GetNonce:
|
||||
return "apps.management.get_nonce"
|
||||
|
||||
if utils.MODEL == "T" and msg_type == MessageType.SdProtect:
|
||||
return "apps.management.sd_protect"
|
||||
|
@ -20,7 +20,7 @@ from ...components.common.confirm import (
|
||||
)
|
||||
from ...components.tt import passphrase, pin
|
||||
from ...components.tt.button import ButtonCancel, ButtonDefault
|
||||
from ...components.tt.confirm import Confirm, HoldToConfirm
|
||||
from ...components.tt.confirm import Confirm, HoldToConfirm, InfoConfirm
|
||||
from ...components.tt.scroll import (
|
||||
PAGEBREAK,
|
||||
AskPaginated,
|
||||
@ -39,7 +39,7 @@ from ...constants.tt import (
|
||||
from ..common import button_request, interact
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Awaitable, Iterable, Iterator, NoReturn, Sequence
|
||||
from typing import Any, Awaitable, Iterable, Iterator, NoReturn, Sequence
|
||||
|
||||
from ..common import PropertyType, ExceptionType
|
||||
from ...components.tt.button import ButtonContent
|
||||
@ -62,6 +62,7 @@ __all__ = (
|
||||
"show_xpub",
|
||||
"show_warning",
|
||||
"confirm_output",
|
||||
"confirm_payment_request",
|
||||
"confirm_blob",
|
||||
"confirm_properties",
|
||||
"confirm_total",
|
||||
@ -496,6 +497,7 @@ async def confirm_output(
|
||||
width: int = MONO_ADDR_PER_LINE,
|
||||
width_paginated: int = MONO_ADDR_PER_LINE - 1,
|
||||
br_code: ButtonRequestType = ButtonRequestType.ConfirmOutput,
|
||||
icon: str = ui.ICON_SEND,
|
||||
) -> None:
|
||||
header_lines = to_str.count("\n") + int(subtitle is not None)
|
||||
if len(address) > (TEXT_MAX_LINES - header_lines) * width:
|
||||
@ -506,9 +508,9 @@ async def confirm_output(
|
||||
if to_paginated:
|
||||
para.append((ui.NORMAL, "to"))
|
||||
para.extend((ui.MONO, line) for line in chunks(address, width_paginated))
|
||||
content: ui.Layout = paginate_paragraphs(para, title, ui.ICON_SEND, ui.GREEN)
|
||||
content: ui.Layout = paginate_paragraphs(para, title, icon, ui.GREEN)
|
||||
else:
|
||||
text = Text(title, ui.ICON_SEND, ui.GREEN, new_lines=False)
|
||||
text = Text(title, icon, ui.GREEN, new_lines=False)
|
||||
if subtitle is not None:
|
||||
text.normal(subtitle, "\n")
|
||||
text.content = [font_amount, amount, ui.NORMAL, color_to, to_str, ui.FG]
|
||||
@ -518,6 +520,28 @@ async def confirm_output(
|
||||
await raise_if_cancelled(interact(ctx, content, "confirm_output", br_code))
|
||||
|
||||
|
||||
async def confirm_payment_request(
|
||||
ctx: wire.GenericContext,
|
||||
recipient_name: str,
|
||||
amount: str,
|
||||
memos: list[str],
|
||||
) -> Any:
|
||||
para = [(ui.NORMAL, f"{amount} to\n{recipient_name}")]
|
||||
para.extend((ui.NORMAL, memo) for memo in memos)
|
||||
content = paginate_paragraphs(
|
||||
para,
|
||||
"Confirm sending",
|
||||
ui.ICON_SEND,
|
||||
ui.GREEN,
|
||||
confirm=lambda text: InfoConfirm(text, info="Details"),
|
||||
)
|
||||
return await raise_if_cancelled(
|
||||
interact(
|
||||
ctx, content, "confirm_payment_request", ButtonRequestType.ConfirmOutput
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def should_show_more(
|
||||
ctx: wire.GenericContext,
|
||||
title: str,
|
||||
|
@ -2,17 +2,22 @@ from common import unittest, await_result, H_
|
||||
|
||||
import storage.cache
|
||||
from trezor import wire
|
||||
from trezor.crypto.curve import secp256k1
|
||||
from trezor.crypto.hashlib import sha256
|
||||
from trezor.messages import AuthorizeCoinJoin
|
||||
from trezor.messages import TxInput
|
||||
from trezor.messages import TxOutput
|
||||
from trezor.messages import SignTx
|
||||
from trezor.messages import TxAckPaymentRequest
|
||||
from trezor.enums import InputScriptType, OutputScriptType
|
||||
from trezor.utils import HashWriter
|
||||
|
||||
from apps.common import coins
|
||||
from apps.bitcoin.authorization import CoinJoinAuthorization
|
||||
from apps.bitcoin.sign_tx.approvers import CoinJoinApprover
|
||||
from apps.bitcoin.sign_tx.bitcoin import Bitcoin
|
||||
from apps.bitcoin.sign_tx.tx_info import TxInfo
|
||||
from apps.bitcoin import writers
|
||||
|
||||
|
||||
class TestApprover(unittest.TestCase):
|
||||
@ -20,9 +25,10 @@ class TestApprover(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.coin = coins.by_name('Bitcoin')
|
||||
self.fee_per_anonymity_percent = 0.003
|
||||
self.coordinator_name = "www.example.com"
|
||||
|
||||
self.msg_auth = AuthorizeCoinJoin(
|
||||
coordinator="www.example.com",
|
||||
coordinator=self.coordinator_name,
|
||||
max_total_fee=40000,
|
||||
fee_per_anonymity=int(self.fee_per_anonymity_percent * 10**9),
|
||||
address_n=[H_(84), H_(0), H_(0)],
|
||||
@ -62,8 +68,10 @@ class TestApprover(unittest.TestCase):
|
||||
# Other's CoinJoined outputs.
|
||||
outputs = [
|
||||
TxOutput(
|
||||
address="",
|
||||
amount=denomination,
|
||||
script_type=OutputScriptType.PAYTOWITNESS,
|
||||
payment_req_index=0,
|
||||
) for i in range(99)
|
||||
]
|
||||
|
||||
@ -71,9 +79,11 @@ class TestApprover(unittest.TestCase):
|
||||
outputs.insert(
|
||||
40,
|
||||
TxOutput(
|
||||
address="",
|
||||
address_n=[H_(84), H_(0), H_(0), 0, 2],
|
||||
amount=denomination,
|
||||
script_type=OutputScriptType.PAYTOWITNESS,
|
||||
payment_req_index=0,
|
||||
)
|
||||
)
|
||||
|
||||
@ -84,25 +94,31 @@ class TestApprover(unittest.TestCase):
|
||||
# Other's change-outputs.
|
||||
outputs.extend(
|
||||
TxOutput(
|
||||
address="",
|
||||
amount=1000000 * (i + 1) - fees,
|
||||
script_type=OutputScriptType.PAYTOWITNESS,
|
||||
payment_req_index=0,
|
||||
) for i in range(99)
|
||||
)
|
||||
|
||||
# Our change-output.
|
||||
outputs.append(
|
||||
TxOutput(
|
||||
address="",
|
||||
address_n=[H_(84), H_(0), H_(0), 1, 1],
|
||||
amount=1000000 - fees,
|
||||
script_type=OutputScriptType.PAYTOWITNESS,
|
||||
payment_req_index=0,
|
||||
)
|
||||
)
|
||||
|
||||
# Coordinator's output.
|
||||
outputs.append(
|
||||
TxOutput(
|
||||
address="",
|
||||
amount=total_coordinator_fee,
|
||||
script_type=OutputScriptType.PAYTOWITNESS,
|
||||
payment_req_index=0,
|
||||
)
|
||||
)
|
||||
|
||||
@ -111,12 +127,34 @@ class TestApprover(unittest.TestCase):
|
||||
approver = CoinJoinApprover(tx, self.coin, authorization)
|
||||
signer = Bitcoin(tx, None, self.coin, approver)
|
||||
|
||||
# Compute payment request signature.
|
||||
# Private key of m/0h for "all all ... all" seed.
|
||||
private_key = b'?S\ti\x8b\xc5o{,\xab\x03\x194\xea\xa8[_:\xeb\xdf\xce\xef\xe50\xf17D\x98`\xb9dj'
|
||||
h_pr = HashWriter(sha256())
|
||||
writers.write_bytes_fixed(h_pr, b"SL\x00\x24", 4)
|
||||
writers.write_bytes_prefixed(h_pr, b"") # Empty nonce.
|
||||
writers.write_bytes_prefixed(h_pr, self.coordinator_name.encode())
|
||||
writers.write_bitcoin_varint(h_pr, 0) # No memos.
|
||||
writers.write_uint32(h_pr, self.coin.slip44)
|
||||
h_outputs = HashWriter(sha256())
|
||||
for txo in outputs:
|
||||
writers.write_uint64(h_outputs, txo.amount)
|
||||
writers.write_bytes_prefixed(h_outputs, txo.address.encode())
|
||||
writers.write_bytes_fixed(h_pr, h_outputs.get_digest(), 32)
|
||||
signature = secp256k1.sign(private_key, h_pr.get_digest())
|
||||
|
||||
tx_ack_payment_req = TxAckPaymentRequest(
|
||||
recipient_name=self.coordinator_name,
|
||||
signature=signature,
|
||||
)
|
||||
|
||||
for txi in inputs:
|
||||
if txi.script_type == InputScriptType.EXTERNAL:
|
||||
approver.add_external_input(txi)
|
||||
else:
|
||||
await_result(approver.add_internal_input(txi))
|
||||
|
||||
await_result(approver.add_payment_request(tx_ack_payment_req, None))
|
||||
for txo in outputs:
|
||||
if txo.address_n:
|
||||
approver.add_change_output(txo, script_pubkey=bytes(22))
|
||||
|
@ -288,6 +288,14 @@ Host must respond with a `TxAckOutput` message. All relevant data must be set in
|
||||
`tx.output`. The derivation path and script type are mandatory for all original
|
||||
change-outputs.
|
||||
|
||||
### Payment request
|
||||
|
||||
Trezor sets `request_type` to `TXPAYMENTREQ`, and `request_details.tx_hash` is unset.
|
||||
`request_details.request_index` is the index of the payment request in the transaction:
|
||||
0 is the first payment request, 1 is second, etc.
|
||||
|
||||
The host must respond with a `TxAckPaymentRequest` message.
|
||||
|
||||
## Replacement transactions
|
||||
|
||||
A replacement transaction is a transaction that uses the same inputs as one or more
|
||||
@ -334,6 +342,22 @@ So the replacement transaction is, for example, allowed to:
|
||||
original external outputs or even to increase the user's change outputs so as to
|
||||
decrease the amount that the user is spending.
|
||||
|
||||
## Payment requests
|
||||
|
||||
In Trezor T a set of transaction outputs can be accompanied by a payment request.
|
||||
Multiple payment requests per transaction are also possible. A payment request is a
|
||||
message signed by a trusted party requesting payment of certain amounts to a set of
|
||||
outputs as specified in [SLIP-24](https://github.com/satoshilabs/slips/blob/master/slip-0024.md).
|
||||
The user then does not have to verify the output addresses, but only confirms the
|
||||
payment of the requested amount to the recipient.
|
||||
|
||||
The host signals that an output belongs to a payment request by setting the
|
||||
`payment_req_index` field in the `TxOutput` message. When Trezor encounters the first
|
||||
output that has this field set to a particular index, it will ask for the payment request
|
||||
that has the given index.
|
||||
|
||||
All outputs belonging to one payment request must be consecutive in the transaction.
|
||||
|
||||
## Implementation notes
|
||||
|
||||
### Pseudo-code
|
||||
|
Loading…
Reference in New Issue
Block a user