Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exchange address support #53

Merged
merged 7 commits into from
Jul 3, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 35 additions & 4 deletions electrum_dash/bitcoin.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,8 @@ class opcodes(IntEnum):

OP_INVALIDOPCODE = 0xff

OP_EXCHANGEADDR = 0xe0

def hex(self) -> str:
return bytes([self]).hex()

Expand Down Expand Up @@ -350,6 +352,8 @@ def hash_decode(x: str) -> bytes:

def hash160_to_b58_address(h160: bytes, addrtype: int) -> str:
s = bytes([addrtype]) + h160
if addrtype == 185:
s = bytes([1, addrtype, 187]) + h160
s = s + sha256d(s)[0:4]
res = base_encode(s, base=58)
return res
Expand All @@ -358,15 +362,22 @@ def hash160_to_b58_address(h160: bytes, addrtype: int) -> str:
def b58_address_to_hash160(addr: str) -> Tuple[int, bytes]:
addr = to_bytes(addr, 'ascii')
_bytes = DecodeBase58Check(addr)
if len(_bytes) != 21:
if len(_bytes) != 21 and len(_bytes) != 23:
raise Exception(f'expected 21 payload bytes in base58 address. got: {len(_bytes)}')
return _bytes[0], _bytes[1:21]

if len(_bytes) == 21:
return _bytes[0], _bytes[1:21]
elif len(_bytes) == 23:
return _bytes[1], _bytes[3:23]
return None

def hash160_to_p2pkh(h160: bytes, *, net=None) -> str:
if net is None: net = constants.net
return hash160_to_b58_address(h160, net.ADDRTYPE_P2PKH)

def hash160_to_exp2pkh(h160: bytes, *, net=None) -> str:
if net is None: net = constants.net
return hash160_to_b58_address(h160, net.ADDRTYPE_EXP2PKH)

def hash160_to_p2sh(h160: bytes, *, net=None) -> str:
if net is None: net = constants.net
return hash160_to_b58_address(h160, net.ADDRTYPE_P2SH)
Expand All @@ -375,10 +386,16 @@ def public_key_to_p2pkh(public_key: bytes, *, net=None) -> str:
if net is None: net = constants.net
return hash160_to_p2pkh(hash_160(public_key), net=net)

def public_key_to_exp2pkh(public_key: bytes, *, net=None) -> str:
if net is None: net = constants.net
return hash160_to_exp2pkh(hash_160(public_key), net=net)

def pubkey_to_address(txin_type: str, pubkey: str, *, net=None) -> str:
if net is None: net = constants.net
if txin_type == 'p2pkh':
return public_key_to_p2pkh(bfh(pubkey), net=net)
elif txin_type == 'exp2pkh':
return public_key_to_exp2pkh(bfh(pubkey), net=net)
else:
raise NotImplementedError(txin_type)

Expand All @@ -405,6 +422,8 @@ def address_to_script(addr: str, *, net=None) -> str:
addrtype, hash_160_ = b58_address_to_hash160(addr)
if addrtype == net.ADDRTYPE_P2PKH:
script = pubkeyhash_to_p2pkh_script(bh2u(hash_160_))
elif addrtype == net.ADDRTYPE_EXP2PKH:
script = pubkeyhash_to_exp2pkh_script(bh2u(hash_160_))
elif addrtype == net.ADDRTYPE_P2SH:
script = construct_script([opcodes.OP_HASH160, hash_160_, opcodes.OP_EQUAL])
else:
Expand All @@ -417,6 +436,7 @@ class OnchainOutputType(Enum):
In case of p2sh, p2wsh and similar, no knowledge of redeem script, etc.
"""
P2PKH = enum.auto()
EXP2PKH = enum.auto()
P2SH = enum.auto()


Expand All @@ -428,6 +448,8 @@ def address_to_hash(addr: str, *, net=None) -> Tuple[OnchainOutputType, bytes]:
addrtype, hash_160_ = b58_address_to_hash160(addr)
if addrtype == net.ADDRTYPE_P2PKH:
return OnchainOutputType.P2PKH, hash_160_
elif addrtype == net.ADDRTYPE_EXP2PKH:
return OnchainOutputType.EXP2PKH, hash_160_
elif addrtype == net.ADDRTYPE_P2SH:
return OnchainOutputType.P2SH, hash_160_
raise BitcoinException(f"unknown address type: {addrtype}")
Expand All @@ -454,6 +476,15 @@ def pubkeyhash_to_p2pkh_script(pubkey_hash160: str) -> str:
opcodes.OP_CHECKSIG
])

def pubkeyhash_to_exp2pkh_script(pubkey_hash160: str) -> str:
return construct_script([
opcodes.OP_EXCHANGEADDR,
opcodes.OP_DUP,
opcodes.OP_HASH160,
pubkey_hash160,
opcodes.OP_EQUALVERIFY,
opcodes.OP_CHECKSIG
])

__b58chars = b'123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
assert len(__b58chars) == 58
Expand Down Expand Up @@ -641,7 +672,7 @@ def is_b58_address(addr: str, *, net=None) -> bool:
addrtype, h = b58_address_to_hash160(addr)
except Exception as e:
return False
if addrtype not in [net.ADDRTYPE_P2PKH, net.ADDRTYPE_P2SH]:
if addrtype not in [net.ADDRTYPE_P2PKH, net.ADDRTYPE_P2SH, net.ADDRTYPE_EXP2PKH]:
return False
return True

Expand Down
4 changes: 4 additions & 0 deletions electrum_dash/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ class AbstractNet:
ADDRTYPE_P2SH: int
GENESIS: str
BIP44_COIN_TYPE: int
ADDRTYPE_EXP2PKH: int

@classmethod
def max_checkpoint(cls) -> int:
Expand All @@ -92,6 +93,7 @@ class BitcoinMainnet(AbstractNet):
TESTNET = False
WIF_PREFIX = 210
ADDRTYPE_P2PKH = 82 # 0x52
ADDRTYPE_EXP2PKH = 185 # 0xb9
ADDRTYPE_P2SH = 7 # 0x07
SEGWIT_HRP = "xzc"
GENESIS = "4381deb85b1b2c9843c222944b616d997516dcbd6a964e1eaf0def0830695233"
Expand Down Expand Up @@ -129,6 +131,7 @@ class BitcoinTestnet(AbstractNet):
TESTNET = True
WIF_PREFIX = 185
ADDRTYPE_P2PKH = 65
ADDRTYPE_EXP2PKH = 185 # 0xb9
ADDRTYPE_P2SH = 178
SEGWIT_HRP = "txzc"
GENESIS = "aa22adcc12becaf436027ffe62a8fb21b234c58c23865291e5dc52cf53f64fca"
Expand Down Expand Up @@ -166,6 +169,7 @@ class BitcoinRegtest(AbstractNet):
TESTNET = False
WIF_PREFIX = 239
ADDRTYPE_P2PKH = 65
ADDRTYPE_EXP2PKH = 185 # 0xb9
ADDRTYPE_P2SH = 178
SEGWIT_HRP = "txzc"
GENESIS = "a42b98f04cc2916e8adfb5d9db8a2227c4629bc205748ed2f33180b636ee885b"
Expand Down
3 changes: 2 additions & 1 deletion electrum_dash/plugins/ledger/ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ def perform_hw1_preflight(self):
self.dongleObject.verifyPin(pin)
if self.canAlternateCoinVersions:
self.dongleObject.setAlternateCoinVersions(constants.net.ADDRTYPE_P2PKH,
constants.net.ADDRTYPE_EXP2PKH,
constants.net.ADDRTYPE_P2SH)
except BTChipException as e:
if (e.sw == 0x6faa):
Expand Down Expand Up @@ -531,7 +532,7 @@ def sign_transaction(self, tx, password):
output = txout.address
if not self.get_client_electrum().canAlternateCoinVersions:
v, h = b58_address_to_hash160(output)
if v == constants.net.ADDRTYPE_P2PKH:
if v == constants.net.ADDRTYPE_P2PKH or v == constants.net.ADDRTYPE_EXP2PKH:
output = hash160_to_b58_address(h, 0)

self.handler.show_message(_("Confirm Transaction on your Ledger device..."))
Expand Down
4 changes: 2 additions & 2 deletions electrum_dash/plugins/trezor/trezor.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,14 +320,14 @@ def get_xpub(self, device_id, derivation, xtype, wizard):
return xpub

def get_trezor_input_script_type(self, electrum_txin_type: str):
if electrum_txin_type in ('p2pkh',):
if electrum_txin_type in ('p2pkh', 'exp2pkh'):
return InputScriptType.SPENDADDRESS
if electrum_txin_type in ('p2sh',):
return InputScriptType.SPENDMULTISIG
raise ValueError('unexpected txin type: {}'.format(electrum_txin_type))

def get_trezor_output_script_type(self, electrum_txin_type: str):
if electrum_txin_type in ('p2pkh',):
if electrum_txin_type in ('p2pkh', 'exp2pkh'):
return OutputScriptType.PAYTOADDRESS
if electrum_txin_type in ('p2sh',):
return OutputScriptType.PAYTOMULTISIG
Expand Down
27 changes: 22 additions & 5 deletions electrum_dash/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,12 @@

from . import ecc, bitcoin, constants, bip32
from .bip32 import BIP32Node
from .util import profiler, to_bytes, bh2u, bfh, chunks, is_hex_str
from .util import to_bytes, bh2u, bfh, chunks, is_hex_str
from .bitcoin import (TYPE_ADDRESS, TYPE_SCRIPT, hash_160,
hash160_to_p2sh, hash160_to_p2pkh,
hash160_to_p2sh, hash160_to_p2pkh, hash160_to_exp2pkh,
var_int, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC, COIN,
int_to_hex, push_script, b58_address_to_hash160,
opcodes, add_number_to_script, base_decode, base_encode,
int_to_hex, b58_address_to_hash160,
opcodes, base_decode, base_encode,
construct_script)
from .crypto import sha256d
from .dash_tx import (ProTxBase, read_extra_payload, serialize_extra_payload,
Expand Down Expand Up @@ -421,6 +421,9 @@ def is_instance(cls, item):
SCRIPTPUBKEY_TEMPLATE_P2PKH = [opcodes.OP_DUP, opcodes.OP_HASH160,
OPPushDataGeneric(lambda x: x == 20),
opcodes.OP_EQUALVERIFY, opcodes.OP_CHECKSIG]
SCRIPTPUBKEY_TEMPLATE_EXP2PKH = [opcodes.OP_EXCHANGEADDR, opcodes.OP_DUP, opcodes.OP_HASH160,
OPPushDataGeneric(lambda x: x == 20),
opcodes.OP_EQUALVERIFY, opcodes.OP_CHECKSIG]
SCRIPTPUBKEY_TEMPLATE_P2SH = [opcodes.OP_HASH160, OPPushDataGeneric(lambda x: x == 20), opcodes.OP_EQUAL]


Expand Down Expand Up @@ -454,6 +457,8 @@ def get_script_type_from_output_script(_bytes: bytes) -> Optional[str]:
return None
if match_script_against_template(decoded, SCRIPTPUBKEY_TEMPLATE_P2PKH):
return 'p2pkh'
if match_script_against_template(decoded, SCRIPTPUBKEY_TEMPLATE_EXP2PKH):
return 'exp2pkh'
if match_script_against_template(decoded, SCRIPTPUBKEY_TEMPLATE_P2SH):
return 'p2sh'
return None
Expand All @@ -468,6 +473,10 @@ def get_address_from_output_script(_bytes: bytes, *, net=None) -> Optional[str]:
if match_script_against_template(decoded, SCRIPTPUBKEY_TEMPLATE_P2PKH):
return hash160_to_p2pkh(decoded[2][1], net=net)

# exp2pkh
if match_script_against_template(decoded, SCRIPTPUBKEY_TEMPLATE_EXP2PKH):
return hash160_to_exp2pkh(decoded[3][1], net=net)

# p2sh
if match_script_against_template(decoded, SCRIPTPUBKEY_TEMPLATE_P2SH):
return hash160_to_p2sh(decoded[1][1], net=net)
Expand Down Expand Up @@ -675,6 +684,8 @@ def guess_txintype_from_address(cls, addr: Optional[str]) -> str:
addrtype, hash_160_ = b58_address_to_hash160(addr)
if addrtype == constants.net.ADDRTYPE_P2PKH:
return 'p2pkh'
if addrtype == constants.net.ADDRTYPE_EXP2PKH:
return 'exp2pkh'
elif addrtype == constants.net.ADDRTYPE_P2SH:
return 'p2sh'
raise Exception(f'unrecognized address: {repr(addr)}')
Expand Down Expand Up @@ -721,6 +732,10 @@ def get_preimage_script(cls, txin: 'PartialTxInput') -> str:
pubkey = pubkeys[0]
pkh = bh2u(hash_160(bfh(pubkey)))
return bitcoin.pubkeyhash_to_p2pkh_script(pkh)
elif txin.script_type in ['exp2pkh']:
pubkey = pubkeys[0]
pkh = bh2u(hash_160(bfh(pubkey)))
return bitcoin.pubkeyhash_to_exp2pkh_script(pkh)
elif txin.script_type == 'p2pk':
pubkey = pubkeys[0]
return bitcoin.public_key_to_p2pk_script(pubkey)
Expand Down Expand Up @@ -1242,6 +1257,8 @@ def set_script_type(self) -> None:
type = inner_type + '-' + type
if type in ('p2pkh', ):
self.script_type = type
if type in ('exp2pkh', ):
self.script_type = type
return

def is_complete(self) -> bool:
Expand All @@ -1256,7 +1273,7 @@ def is_complete(self) -> bool:
# that are related to the wallet.
# The 'fix' would be adding extra logic that matches on templates,
# and figures out the script_type from available fields.
if self.script_type in ('p2pk', 'p2pkh'):
if self.script_type in ('p2pk', 'p2pkh', "exp2pkh"):
return s >= 1
if self.script_type in ('p2sh', ):
return s >= self.num_sig
Expand Down