Skip to content

Commit d8d3aa8

Browse files
committed
Merge branch 'dev' into master
commit 152fc497a543af1c41cec7a529ec736fde80b5f7 Author: OPReturnCode <mask.em@protonmail.com> Date: Tue Jan 7 16:54:27 2025 +0000 Modify the on_connect behaviour to prevent connection attempt in watchin-only wallets commit d6489a17480701c9dc00a1684caa9fbf3dab7fde Author: OPReturnCode <mask.em@protonmail.com> Date: Tue Jan 7 16:41:02 2025 +0000 Add support for more complex contracts that need user pubkey and signature commit 02dc0f7270458fc1c31ed46e136583df6c363ba6 Author: OPReturnCode <mask.em@protonmail.com> Date: Wed Jan 1 22:00:12 2025 +0000 Fix incorrect address storage location
1 parent 47f81eb commit d8d3aa8

4 files changed

Lines changed: 186 additions & 55 deletions

File tree

transaction.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from electroncash import token
2+
from electroncash.util import *
3+
from electroncash.bitcoin import *
4+
from electroncash.transaction import Transaction, InputValueMissing
5+
6+
7+
# The EC Transaction class does not support SIGHASH_UTXOS
8+
class TransactionWithSighashUTXOS(Transaction):
9+
10+
def get_hash_utxos(self):
11+
buf = b''
12+
for i in self.inputs():
13+
script_bytes = bfh(i['walletconnect_locking_bytecode'])
14+
buf += int_to_bytes(i['value'], 8)
15+
buf += var_int_bytes(len(script_bytes))
16+
buf += script_bytes
17+
return Hash(buf)
18+
19+
# This method is taken and edited from electroncash/transaction.py from the official Electron Cash project repo at
20+
# https://github.com/Electron-Cash/Electron-Cash
21+
def serialize_preimage_bytes(self, i, nHashType=0x00000041, use_cache=False) -> bytes:
22+
""" See `.calc_common_sighash` for explanation of use_cache feature """
23+
24+
if (nHashType & 0xff) != 0x61:
25+
raise ValueError("other hashtypes not supported.")
26+
27+
nVersion = int_to_bytes(self.version, 4)
28+
nHashType = int_to_bytes(nHashType, 4)
29+
nLocktime = int_to_bytes(self.locktime, 4)
30+
31+
txin = self.inputs()[i]
32+
outpoint = self.serialize_outpoint_bytes(txin)
33+
preimage_script = bfh(self.get_preimage_script(txin))
34+
input_token = txin.get('token_data')
35+
if input_token is not None:
36+
serInputToken = token.PREFIX_BYTE + input_token.serialize()
37+
else:
38+
serInputToken = b''
39+
scriptCode = var_int_bytes(len(preimage_script)) + preimage_script
40+
try:
41+
amount = int_to_bytes(txin['value'], 8)
42+
except KeyError:
43+
raise InputValueMissing
44+
nSequence = int_to_bytes(txin.get('sequence', 0xffffffff - 1), 4)
45+
46+
hashPrevouts, hashSequence, hashOutputs = self.calc_common_sighash(use_cache=use_cache)
47+
hashUtxos = self.get_hash_utxos()
48+
49+
preimage = (nVersion + hashPrevouts + hashUtxos + hashSequence + outpoint + serInputToken + scriptCode + amount + nSequence
50+
+ hashOutputs + nLocktime + nHashType)
51+
print("preimage: ", nVersion.hex(), hashPrevouts.hex(), hashUtxos.hex(), hashSequence.hex(), outpoint.hex(),
52+
serInputToken.hex(), scriptCode.hex(), amount.hex(), nSequence.hex(), hashOutputs.hex(), nLocktime.hex(),
53+
nHashType.hex())
54+
return preimage

ui.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ def __init__(self, parent, plugin):
2424
self.parent = parent
2525
self.wallet = self.parent.wallet
2626
self.address = None
27+
self.password = None
2728

2829
self.wallet_connect = None
2930
self.proxy_opts = None
@@ -120,9 +121,23 @@ def on_connect_btn(self):
120121
self.wallet_connect.close_session()
121122
self.active_sessions.clear()
122123

124+
if self.wallet.is_watching_only():
125+
self.parent.show_warning(_(
126+
"This wallet is watching-only."
127+
"This means you are not able to sign transactions or messages. Or spend BCH in any way."))
128+
return
129+
123130
try:
131+
if self.wallet.has_password():
132+
pd = PasswordDialog()
133+
password = pd.run()
134+
self.wallet.check_password(password)
135+
self.password = password
136+
del pd
137+
124138
self.wallet_connect = w_connect2.WalletConnect(
125-
self.wallet_connect_uri_text_edit.text(), self.address.to_token_string(), self.wallet, self.proxy_opts)
139+
self.wallet_connect_uri_text_edit.text(), self.address.to_token_string(),
140+
self.wallet, self.password, self.proxy_opts)
126141

127142
session_data = self.wallet_connect.open_session()
128143

@@ -159,12 +174,23 @@ def approve_transaction_sign(self):
159174
pass
160175

161176
def get_wallet_address(self):
177+
178+
# For backward compatibility, check if an address is stored in the incorrect location and fix it.
179+
# This section can be removed in future iterations, as it is only relevant to users of the initial version.
162180
address_str = self.parent.config.get("wallet_connect_address")
181+
if address_str:
182+
self.wallet.is_mine(address.Address.from_string(address_str))
183+
self.wallet.storage.put("wallet_connect_address", address_str)
184+
self.wallet.storage.write()
185+
self.parent.config.set_key("wallet_connect_address", None)
186+
187+
address_str = self.wallet.storage.get("wallet_connect_address")
163188
if address_str:
164189
self.address = address.Address.from_string(address_str)
165190
else:
166191
self.address = self.wallet.get_unused_address(frozen_ok=False)
167-
self.parent.config.set_key("wallet_connect_address", self.address.to_cashaddr())
192+
self.wallet.storage.put("wallet_connect_address", self.address.to_cashaddr())
193+
self.wallet.storage.write()
168194
self.wallet.set_frozen_state([self.address], True)
169195

170196
return self.address

utils.py

Lines changed: 100 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,30 @@
11
import re
22

3-
from electroncash.address import UnknownAddress
4-
from electroncash import transaction, token, bitcoin
3+
from electroncash.address import UnknownAddress, Address
4+
from electroncash import transaction, token, bitcoin, util
5+
6+
from .transaction import TransactionWithSighashUTXOS
57

68

79
def extract_hex_from_libauth_extended_json_string(s):
810
hex_regex = r'0x[0-9a-f-A-F]+'
911

1012
match = re.search(hex_regex, s)
1113
if match:
12-
return match.group(0)
14+
group = match.group(0)
15+
group = group[2:] if group.startswith('0x') else group
16+
return group
1317

1418
def extract_bigint_from_libauth_extended_json_string(s):
1519
bigint_regex = r'[0-9]+'
1620

1721
match = re.search(bigint_regex, s)
1822
if match:
19-
return match.group(0)
20-
23+
group = match.group(0)
24+
group = group[:-1] if group.endswith('n') else group
25+
return group
2126

22-
def generate_electron_cash_tx_from_libauth_format(tx_data, wallet):
27+
def generate_electron_cash_tx_from_libauth_format(tx_data, wallet, wallet_connect_address, password):
2328

2429
transaction_data = tx_data['transaction']
2530
source_outputs_data = tx_data['sourceOutputs']
@@ -28,22 +33,21 @@ def generate_electron_cash_tx_from_libauth_format(tx_data, wallet):
2833
ec_inputs_bytes = list()
2934
for index, item in enumerate(transaction_data['inputs']):
3035
prevout_hash = extract_hex_from_libauth_extended_json_string(item['outpointTransactionHash'])
31-
prevout_hash = prevout_hash[2:] if prevout_hash.startswith('0x') else prevout_hash
3236
script_sig = extract_hex_from_libauth_extended_json_string(item['unlockingBytecode'])
3337

38+
# in case of empty script_sig, first check if it's a contract and the contract contains the script_sig
39+
if not script_sig and ('contract' in source_outputs_data[index].keys() and
40+
source_outputs_data[index]['contract']['artifact']['contractName']):
41+
script_sig = extract_hex_from_libauth_extended_json_string(source_outputs_data[index]['unlockingBytecode'])
42+
3443

3544
address = None
36-
locking_byte_code = extract_hex_from_libauth_extended_json_string(
45+
locking_bytecode = extract_hex_from_libauth_extended_json_string(
3746
source_outputs_data[index]['lockingBytecode'])
38-
locking_byte_code = locking_byte_code[2:] if locking_byte_code.startswith('0x') else locking_byte_code
39-
type, possible_address = transaction.get_address_from_output_script(bytes.fromhex(locking_byte_code))
47+
type, possible_address = transaction.get_address_from_output_script(bytes.fromhex(locking_bytecode))
4048
if type in [bitcoin.TYPE_ADDRESS, bitcoin.TYPE_PUBKEY]:
4149
address = possible_address
4250

43-
if script_sig:
44-
script_sig = script_sig[2:] if script_sig.startswith('0x') else script_sig
45-
else:
46-
script_sig = None
4751
ec_input = {
4852
'prevout_n': item['outpointIndex'],
4953
'prevout_hash': prevout_hash,
@@ -54,8 +58,20 @@ def generate_electron_cash_tx_from_libauth_format(tx_data, wallet):
5458
'x_pubkeys': [],
5559
'pubkeys': [],
5660
'signatures': {},
61+
'walletconnect_locking_bytecode': locking_bytecode
5762
}
5863

64+
token_output_data = None
65+
if 'token' in source_outputs_data[index].keys():
66+
token_output_data = generate_token_data_output_from_token_dict(source_outputs_data[index]['token'])
67+
ec_input['token_data'] = token_output_data
68+
ec_input['walletconnect_locking_bytecode'] = token.wrap_spk(
69+
token_output_data, bytes.fromhex(locking_bytecode)).hex()
70+
71+
value = extract_bigint_from_libauth_extended_json_string(source_outputs_data[index]['valueSatoshis'])
72+
value = int(value)
73+
ec_input['value'] = value
74+
5975
if script_sig:
6076
try:
6177
script_sig_data = {}
@@ -94,10 +110,9 @@ def generate_electron_cash_tx_from_libauth_format(tx_data, wallet):
94110
token_data_list = list()
95111
for item in transaction_data['outputs']:
96112
locking_bytecode = extract_hex_from_libauth_extended_json_string(item['lockingBytecode'])
97-
locking_bytecode = locking_bytecode[2:] if locking_bytecode.startswith('0x') else locking_bytecode
98113

99114
item['valueSatoshis'] = extract_bigint_from_libauth_extended_json_string(item['valueSatoshis'])
100-
value = int(item['valueSatoshis'][:-1]) if item['valueSatoshis'].endswith('n') else int(item['valueSatoshis'])
115+
value = int(item['valueSatoshis'])
101116

102117
address = transaction.get_address_from_output_script(bytes.fromhex(locking_bytecode))
103118
ec_output = address + (value,)
@@ -106,42 +121,8 @@ def generate_electron_cash_tx_from_libauth_format(tx_data, wallet):
106121
output_bytes = b''
107122
output_bytes += bitcoin.int_to_bytes(value, 8)
108123

109-
amount = None
110124
if 'token' in item.keys():
111-
item['token']['category'] = extract_hex_from_libauth_extended_json_string(item['token']['category'])
112-
category_id = item['token']['category']
113-
category_id = category_id[2:] if category_id.startswith('0x') else category_id
114-
category_id_arr = bytearray.fromhex(category_id)
115-
category_id_arr.reverse()
116-
category_id = bytes(category_id_arr)
117-
118-
bitfield = 0
119-
if 'amount' in item['token'].keys():
120-
item['token']['amount'] = extract_bigint_from_libauth_extended_json_string(item['token']['amount'])
121-
amount = item['token']['amount']
122-
amount = int(amount) if amount.endswith('n') else int(amount)
123-
if amount and amount > 0:
124-
bitfield |= token.Structure.HasAmount
125-
126-
commitment = b''
127-
if 'nft' in item['token'].keys():
128-
bitfield |= token.Structure.HasNFT
129-
commitment = item['token']['nft'].get('commitment')
130-
commitment = commitment.encode() if commitment else b''
131-
132-
capability = item['token']['nft'].get('capability')
133-
134-
if not capability or capability == 'none':
135-
bitfield |= token.Capability.NoCapability
136-
elif capability == 'mutable':
137-
bitfield |= token.Capability.Mutable
138-
elif capability == 'minting':
139-
bitfield |= token.Capability.Minting
140-
141-
if commitment and len(commitment) > 0:
142-
bitfield |= token.Structure.HasCommitmentLength
143-
144-
token_output_data = token.OutputData(category_id, amount, commitment, bitfield)
125+
token_output_data = generate_token_data_output_from_token_dict(item['token'])
145126
token_data_list.append(token_output_data)
146127

147128
wrapped_locking_bytecode = token.wrap_spk(token_output_data, bytes.fromhex(locking_bytecode))
@@ -187,4 +168,72 @@ def generate_electron_cash_tx_from_libauth_format(tx_data, wallet):
187168
ec_tx = transaction.Transaction.from_io(
188169
inputs=ec_inputs, outputs=ec_outputs, locktime=locktime, token_datas=token_data_list, version=version)
189170
ec_tx._sign_schnorr = True
171+
172+
for index, input in enumerate(ec_tx.inputs()):
173+
sig_placeholder = "41" + bytearray(65).hex()
174+
pubkey_placeholder = "21" + bytearray(33).hex()
175+
176+
address = Address.from_string(wallet_connect_address)
177+
priv_key = wallet.export_private_key(address, password)
178+
if input['scriptSig'] and input['scriptSig'].find(sig_placeholder) != -1:
179+
source_output = source_outputs_data[index]
180+
assert "contract" in source_output.keys()
181+
assert "redeemScript" in source_output['contract'].keys()
182+
redeem_script = source_output['contract']['redeemScript']
183+
redeem_script = extract_hex_from_libauth_extended_json_string(redeem_script)
184+
input['scriptCode'] = redeem_script
185+
186+
ec_tx.__class__ = TransactionWithSighashUTXOS
187+
hash_type = 0x1 | 0x40 | 0x20 # SIGHASH_ALL | SIGHASH_FORKID | SIGHASH_UTXOS # Consistency
188+
type, priv_key, _ = bitcoin.deserialize_privkey(priv_key)
189+
pre_hash = bitcoin.Hash(ec_tx.serialize_preimage_bytes(index, hash_type))
190+
sig = ec_tx._schnorr_sign(bitcoin.public_key_from_private_key(priv_key, True), priv_key, pre_hash)
191+
sig = '41' + util.bh2u(sig + bytes((hash_type & 0xff,)))
192+
input['scriptSig'] = input['scriptSig'].replace(sig_placeholder, sig)
193+
ec_tx.__class__ = transaction.Transaction
194+
195+
if input['scriptSig'] and input['scriptSig'].find(pubkey_placeholder) != -1:
196+
pubkey = '21' + bitcoin.public_key_from_private_key(priv_key, True)
197+
input['scriptSig'] = input['scriptSig'].replace(pubkey_placeholder, pubkey)
198+
190199
return ec_tx
200+
201+
202+
203+
def generate_token_data_output_from_token_dict(token_dict):
204+
token_dict['category'] = extract_hex_from_libauth_extended_json_string(token_dict['category'])
205+
category_id = token_dict['category']
206+
category_id_arr = bytearray.fromhex(category_id)
207+
category_id_arr.reverse()
208+
category_id = bytes(category_id_arr)
209+
210+
amount = None
211+
bitfield = 0
212+
if 'amount' in token_dict.keys():
213+
token_dict['amount'] = extract_bigint_from_libauth_extended_json_string(token_dict['amount'])
214+
amount = int(token_dict['amount'])
215+
if amount and amount > 0:
216+
bitfield |= token.Structure.HasAmount
217+
218+
commitment = b''
219+
if 'nft' in token_dict.keys():
220+
bitfield |= token.Structure.HasNFT
221+
commitment = token_dict['nft'].get('commitment')
222+
commitment = extract_hex_from_libauth_extended_json_string(commitment) if commitment else None
223+
commitment = bytes.fromhex(commitment) if commitment else b''
224+
225+
capability = token_dict['nft'].get('capability')
226+
227+
if not capability or capability == 'none':
228+
bitfield |= token.Capability.NoCapability
229+
elif capability == 'mutable':
230+
bitfield |= token.Capability.Mutable
231+
elif capability == 'minting':
232+
bitfield |= token.Capability.Minting
233+
234+
if commitment and len(commitment) > 0:
235+
bitfield |= token.Structure.HasCommitmentLength
236+
237+
token_output_data = token.OutputData(category_id, amount, commitment, bitfield)
238+
239+
return token_output_data

w_connect2.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434

3535

3636
class WalletConnect:
37-
def __init__(self, wallet_connect_uri, wallet_address, wallet, proxy_opts):
37+
def __init__(self, wallet_connect_uri, wallet_address, wallet, password, proxy_opts):
3838
# assert proxy_opts is not None
3939

4040
self.session_data = None
@@ -43,6 +43,7 @@ def __init__(self, wallet_connect_uri, wallet_address, wallet, proxy_opts):
4343
self.wallet_connect_uri = wallet_connect_uri
4444
self.wallet_address = wallet_address
4545
self.wallet = wallet
46+
self.password = password
4647
self.proxy_opts = proxy_opts
4748

4849
self.wc_client = WCClient.from_wc_uri(self.wallet_connect_uri, socks_opts=self.proxy_opts)
@@ -90,7 +91,8 @@ def get_message(self):
9091

9192
elif params["request"]["method"] == WALLET_CONNECT_SIGN_TRANSACTION:
9293
tx_data = params['request']['params']
93-
ec_tx = utils.generate_electron_cash_tx_from_libauth_format(tx_data, self.wallet)
94+
ec_tx = utils.generate_electron_cash_tx_from_libauth_format(
95+
tx_data, self.wallet, self.wallet_address, self.password)
9496
task_data = {
9597
"type": WALLET_CONNECT_SIGN_TRANSACTION,
9698
"message_id": message_id,

0 commit comments

Comments
 (0)