Skip to content

Commit

Permalink
Remove use of MAPI callbacks for merkle proofs.
Browse files Browse the repository at this point in the history
- For both invoice and legacy payments both payer and payee now use
  output spends to detect entry of their transaction into the mempool
  or at least block.
- There is an unanswered question here that is for now consigned to
  technical debt. That we should not consider a broadcast transaction
  to be in the mempool until we get the output spend confirmation.
  • Loading branch information
rt121212121 authored and AustEcon committed Mar 24, 2023
1 parent 38d4884 commit e304116
Show file tree
Hide file tree
Showing 22 changed files with 255 additions and 974 deletions.
28 changes: 13 additions & 15 deletions electrumsv/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -572,13 +572,14 @@ class ServerPeerChannelFlag(IntFlag):
NONE = 0
# This gets set immediately before we create the actual peer channel remotely.
ALLOCATING = 1 << 0
DEACTIVATED = 1 << 19

# Bits 16-18: Isolated purposes that the channels are used for.
TIP_FILTER_DELIVERY = 0b001 << 16
MAPI_BROADCAST_CALLBACK = 0b010 << 16

DEACTIVATED = 0b010 << 18

PURPOSE_TIP_FILTER_DELIVERY = 0b001 << 16
# NOTE(rt12) This is not persisted and can be dropped when there is
# an alternative purpose that can be used in the tests (at this time
# there is only the tip filter option).
PURPOSE_TEST_ALTERNATIVE = 0b111 << 16
MASK_PURPOSE = 0b111 << 16


Expand All @@ -591,18 +592,16 @@ class PeerChannelMessageFlag(IntFlag):
class PeerChannelAccessTokenFlag(IntFlag):
NONE = 0

# Local vs third party are opposites.
# Third party tokens should not be used for marking
# messages as read because this will cause the third party to miss these messages.
# Only tokens that are `FOR_LOCAL_USAGE` should be used for reading / marking messages read
# This token is for use by this wallet.
FOR_LOCAL_USAGE = 1 << 0
# This token was given out to a third party for them to use accessing the given channel.
FOR_THIRD_PARTY_USAGE = 1 << 1

# Use cases
FOR_TIP_FILTER_SERVER = 1 << 2
FOR_MAPI_CALLBACK_USAGE = 1 << 3

USAGE_MASK = FOR_TIP_FILTER_SERVER | FOR_LOCAL_USAGE | FOR_MAPI_CALLBACK_USAGE
# This should include all use case masks.
MASK_FOR = FOR_TIP_FILTER_SERVER


class PushDataHashRegistrationFlag(IntFlag):
Expand Down Expand Up @@ -642,10 +641,9 @@ class PushDataMatchFlag(IntFlag):


class ChainWorkerToken(IntEnum):
MAPI_MESSAGE_CONSUMER = 1
CONNECT_PROOF_CONSUMER = 2
OBTAIN_PROOF_WORKER = 3
OBTAIN_TRANSACTION_WORKER = 4
CONNECT_PROOF_CONSUMER = 1
OBTAIN_PROOF_WORKER = 2
OBTAIN_TRANSACTION_WORKER = 3


class ChainManagementKind(IntEnum):
Expand Down
81 changes: 33 additions & 48 deletions electrumsv/dpp_messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@

import json
import time
from typing import Any, cast, Dict, List, Literal, Optional, TypedDict, Union
import types
from typing import Any, cast, Literal
from typing_extensions import NotRequired, TypedDict
import urllib.parse

from bitcoinx import Address, PublicKey, Script
Expand Down Expand Up @@ -58,24 +59,22 @@
# BIP 270 - Simplified Payment Protocol
# https://github.com/electrumsv/bips/blob/master/bip-0270.mediawiki

def has_expired(expiration_timestamp: Optional[int]=None) -> bool:
def has_expired(expiration_timestamp: int | None) -> bool:
return expiration_timestamp is not None and expiration_timestamp < time.time()


HYBRID_PAYMENT_MODE_BRFCID = "ef63d9775da5"


# DPP Message Types as per the TSC spec.
# NOTE(rt12) We keep this around in case we need to put a dummy entry with
# fake values to keep the DPP proxy working.
class PeerChannelDict(TypedDict):
host: str
token: str
channel_id: str


class PeerChannelsDict(TypedDict):
peerChannel: dict[str, Any]


class Policies(TypedDict):
fees: dict[str, int] | None
SPVRequired: bool
Expand All @@ -97,7 +96,7 @@ class DPPNativeOutput(TypedDict):


# HPM == "HybridPaymentMode"
class HPMTransactionTermsDict(TypedDict):
class HybridModeTransactionTermsDict(TypedDict):
outputs: dict[Literal["native"], list[DPPNativeOutput]] # {"native": list[Output]}
inputs: dict[Literal["native"], list[DPPNativeInput]] | None # {"native": list[DPPNativeInput]}
policies: Policies | None
Expand All @@ -113,15 +112,14 @@ class PaymentTermsModes(TypedDict):
# }
# }
# }
ef63d9775da5: dict[str, dict[str, list[HPMTransactionTermsDict]]]
ef63d9775da5: dict[str, dict[str, list[HybridModeTransactionTermsDict]]]


class HPMPaymentACK(TypedDict):
class HybridModePaymentACKDict(TypedDict):
transactionIds: list[str]
peerChannel: PeerChannelDict | None


class HPMPayment(TypedDict):
class HybridModePaymentDict(TypedDict):
optionId: str
transactions: list[str] # hex raw transactions
ancestors: dict[str, Any] | None
Expand All @@ -143,16 +141,16 @@ class PaymentTermsDict(TypedDict):

class PaymentDict(TypedDict):
modeId: str # i.e. HYBRID_PAYMENT_MODE_BRFCID
mode: HPMPayment
mode: HybridModePaymentDict
originator: dict[str, Any] | None
transaction: Optional[str] # DEPRECATED as per TSC spec.
memo: Optional[str] # Optional
transaction: str | None # DEPRECATED as per TSC spec.
memo: str | None


class PaymentACKDict(TypedDict):
modeId: str
mode: HPMPaymentACK
peerChannel: PeerChannelDict
mode: HybridModePaymentACKDict
peerChannel: NotRequired[PeerChannelDict | None]
redirectUrl: str | None


Expand Down Expand Up @@ -233,14 +231,14 @@ def get_dpp_network_string() -> NETWORK_NAMES:
class PaymentTerms:
MAXIMUM_JSON_LENGTH = 10 * 1000 * 1000

def __init__(self, outputs: List[Output], network: str, version: str,
creation_timestamp: Optional[int]=None, expiration_timestamp: int | None=None,
def __init__(self, outputs: list[Output], network: str, version: str, *,
creation_timestamp: int | None, expiration_timestamp: int | None=None,
memo: str | None=None, beneficiary: dict[str, Any] | None=None,
payment_url: str | None=None, merchant_data: str | None=None,
hybrid_payment_data: dict[str, dict[str, list[HPMTransactionTermsDict]]] | None=None) \
-> None:
hybrid_payment_data: \
dict[str, dict[str, list[HybridModeTransactionTermsDict]]] | None=None) -> None:
# This is only used if there is a requestor identity (old openalias, needs rewrite).
self._id: Optional[int] = None
self._id: int | None = None
self.tx = None

self.network = network
Expand Down Expand Up @@ -273,7 +271,7 @@ def from_wallet_entry(cls, request_row: PaymentRequestRow,
expiration_timestamp=request_row.date_expires, memo=request_row.merchant_reference)

@classmethod
def from_json(cls, s: Union[bytes, str]) -> PaymentTerms:
def from_json(cls, s: bytes | str) -> PaymentTerms:
if len(s) > cls.MAXIMUM_JSON_LENGTH:
raise Bip270Exception(_("Payment request oversized"))

Expand Down Expand Up @@ -333,38 +331,34 @@ def from_json(cls, s: Union[bytes, str]) -> PaymentTerms:
for ui_dict in transactions[0]['outputs']['native']:
outputs.append(Output.from_dict(ui_dict))

pr = cls(outputs=outputs, version=payment_terms['version'], network=network,
hybrid_payment_data=hybrid_payment_mode)

if 'creationTimestamp' not in payment_terms:
raise Bip270Exception(_("Creation time missing"))
creation_timestamp = payment_terms['creationTimestamp']
if type(creation_timestamp) is not int:
raise Bip270Exception(_("Corrupt creation time"))
pr.creation_timestamp = creation_timestamp

expiration_timestamp = payment_terms.get('expirationTimestamp')
if expiration_timestamp is not None and type(expiration_timestamp) is not int:
raise Bip270Exception(_("Corrupt expiration time"))
pr.expiration_timestamp = expiration_timestamp

memo = payment_terms.get('memo')
if memo is not None and type(memo) is not str:
raise Bip270Exception(_("Corrupt memo"))
pr.memo = memo

payment_url = payment_terms.get('paymentUrl')
if payment_url is not None and type(payment_url) is not str:
raise Bip270Exception(_("Corrupt payment URL"))
pr.payment_url = payment_url

# NOTE: payd wallet returns a nested json dictionary but technically the BIP270 spec.
# states this must be a string up to 10000 characters long.
merchant_data = payment_terms.get('merchantData')
if not isinstance(merchant_data, (str, types.NoneType)):
raise Bip270Exception(_("Corrupt merchant data"))
pr.merchant_data = merchant_data

return pr
return cls(outputs, network, payment_terms['version'],
creation_timestamp=creation_timestamp, expiration_timestamp=expiration_timestamp,
memo=memo, payment_url=payment_url, hybrid_payment_data=hybrid_payment_mode,
merchant_data=merchant_data)

def to_json(self) -> str:
# TODO: This should be a TypedDict.
Expand All @@ -391,7 +385,7 @@ def is_pr(self) -> bool:
def has_expired(self) -> bool:
return has_expired(self.expiration_timestamp)

def get_expiration_date(self) -> Optional[int]:
def get_expiration_date(self) -> int | None:
return self.expiration_timestamp

def get_amount(self) -> int:
Expand All @@ -414,16 +408,16 @@ def get_payment_uri(self) -> str:
assert self.payment_url is not None
return self.payment_url

def get_memo(self) -> Optional[str]:
def get_memo(self) -> str | None:
return self.memo

def get_id(self) -> Optional[int]:
def get_id(self) -> int | None:
return self._id

def set_id(self, invoice_id: int) -> None:
self._id = invoice_id

def get_outputs(self) -> List[XTxOutput]:
def get_outputs(self) -> list[XTxOutput]:
return [output.to_tx_output() for output in self.outputs]


Expand All @@ -439,7 +433,7 @@ def __init__(self, transaction_hex: str, memo: str | None=None) -> None:
self.memo = memo

@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'Payment':
def from_dict(cls, data: dict[str, Any]) -> 'Payment':
if "modeId" in data:
mode_id = data['modeId']
if type(mode_id) is not str:
Expand Down Expand Up @@ -506,18 +500,16 @@ def to_json(self) -> str:
class PaymentACK:
MAXIMUM_JSON_LENGTH = 11 * 1000 * 1000

def __init__(self, mode_id: str, mode: HPMPaymentACK, peer_channel_info: PeerChannelDict,
def __init__(self, mode_id: str, mode: HybridModePaymentACKDict,
redirect_url: str | None = None) -> None:
self.mode_id = mode_id
self.mode = mode
self.peer_channel_info = peer_channel_info
self.redirect_url = redirect_url

def to_dict(self) -> PaymentACKDict:
return PaymentACKDict(
modeId=self.mode_id,
mode=self.mode,
peerChannel=self.peer_channel_info,
redirectUrl=self.redirect_url
)

Expand All @@ -537,27 +529,20 @@ def from_dict(cls, data: PaymentACKDict) -> PaymentACK:
if mode is not None and type(mode) is not dict:
raise Bip270Exception("Invalid json 'mode' field")

peer_channel_info = data.get('peerChannel')
if peer_channel_info is None:
raise Bip270Exception("'peerChannel' field is required")
if mode_id is not None and type(peer_channel_info) is not dict:
raise Bip270Exception("Invalid json 'peerChannel' field")

redirect_url = data.get('redirectUrl')
if redirect_url is not None and type(redirect_url) is not str:
raise Bip270Exception("Invalid json 'redirectUrl' field")

assert mode_id is not None
assert mode is not None
assert peer_channel_info is not None
return cls(mode_id, mode, peer_channel_info, redirect_url=redirect_url)
return cls(mode_id, mode, redirect_url=redirect_url)

def to_json(self) -> str:
data = self.to_dict()
return json.dumps(data)

@classmethod
def from_json(cls, s: Union[bytes, str]) -> PaymentACK:
def from_json(cls, s: bytes | str) -> PaymentACK:
if len(s) > cls.MAXIMUM_JSON_LENGTH:
raise Bip270Exception("Invalid payment ACK, too large")
data = cast(PaymentACKDict, json.loads(s))
Expand Down
7 changes: 1 addition & 6 deletions electrumsv/gui/qt/dialogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,12 +177,7 @@ def show_named(name: str, *, parent: Optional[QWidget]=None, wallet: Optional["W
WarningBox("think-before-sending",
_("Avoid Coin Loss"),
take_care_notice,
frequency=DisplayFrequency.OncePerRun),
YesNoBox('mapi-broadcast-servers', _("This broadcast uses a MAPI server, and in order to "
"be notified when your transaction is mined or double-spent, you need to provide it "
"with a way to notify you. This is done through the use of a message box server, and "
"you do not currently have one selected."), _("Do you wish to select a message box "
"server to use before you broadcast this transaction?"), _("Yes"), _("No"), True),
frequency=DisplayFrequency.OncePerRun)
]

all_boxes_by_name: Dict[str, BoxBase] = {box.name: box for box in all_boxes}
Expand Down
6 changes: 3 additions & 3 deletions electrumsv/gui/qt/receive_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,8 @@ def _event_action_triggered_invoice(self) -> None:
"payment. Please complete that one first."))
return

# The message box service is required to get mAPI merkle proof callbacks.
required_flags = NetworkServerFlag.USE_MESSAGE_BOX
# The blockchain service is required to get output spend notifications.
required_flags = NetworkServerFlag.USE_BLOCKCHAIN
if self._main_window_proxy._wallet.have_wallet_servers(required_flags):
self.show_dialog(None, PaymentFlag.TYPE_INVOICE)
return
Expand All @@ -139,7 +139,7 @@ def _event_action_triggered_invoice(self) -> None:
reload(server_required_dialog)

dialog = server_required_dialog.ServerRequiredDialog(self, self._main_window_proxy._wallet,
NetworkServerFlag.USE_MESSAGE_BOX, dialog_text)
NetworkServerFlag.USE_BLOCKCHAIN, dialog_text)
# There are two paths to the user accepting this dialog:
# - They checked "select servers on my behalf" then the OK buton and then servers were
# selected and connected to.
Expand Down

0 comments on commit e304116

Please sign in to comment.