parent
c3cd1871ef
commit
ee2f462ee9
27 changed files with 1251 additions and 88 deletions
@ -0,0 +1,577 @@ |
||||
#!/usr/bin/env python |
||||
# -*- coding: utf-8 -*- |
||||
|
||||
# Copyright (c) 2020-2024 tecnovert |
||||
# Distributed under the MIT software license, see the accompanying |
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. |
||||
|
||||
import json |
||||
import logging |
||||
|
||||
import basicswap.contrib.ed25519_fast as edf |
||||
import basicswap.ed25519_fast_util as edu |
||||
import basicswap.util_xmr as wow_util |
||||
from coincurve.ed25519 import ( |
||||
ed25519_add, |
||||
ed25519_get_pubkey, |
||||
ed25519_scalar_add, |
||||
) |
||||
from coincurve.keys import PrivateKey |
||||
from coincurve.dleag import ( |
||||
dleag_prove, |
||||
dleag_verify, |
||||
dleag_proof_len, |
||||
verify_ed25519_point, |
||||
) |
||||
|
||||
from basicswap.interface import ( |
||||
Curves) |
||||
from basicswap.util import ( |
||||
i2b, b2i, b2h, |
||||
dumpj, |
||||
ensure, |
||||
make_int, |
||||
TemporaryError) |
||||
from basicswap.util.network import ( |
||||
is_private_ip_address) |
||||
from basicswap.rpc_wow import ( |
||||
make_wow_rpc_func, |
||||
make_wow_rpc2_func) |
||||
from basicswap.chainparams import WOW_COIN, CoinInterface, Coins |
||||
|
||||
|
||||
class WOWInterface(CoinInterface): |
||||
@staticmethod |
||||
def curve_type(): |
||||
return Curves.ed25519 |
||||
|
||||
@staticmethod |
||||
def coin_type(): |
||||
return Coins.WOW |
||||
|
||||
@staticmethod |
||||
def COIN(): |
||||
return WOW_COIN |
||||
|
||||
@staticmethod |
||||
def exp() -> int: |
||||
return 11 |
||||
|
||||
@staticmethod |
||||
def nbk() -> int: |
||||
return 32 |
||||
|
||||
@staticmethod |
||||
def nbK() -> int: # No. of bytes requires to encode a public key |
||||
return 32 |
||||
|
||||
@staticmethod |
||||
def depth_spendable() -> int: |
||||
return 10 |
||||
|
||||
@staticmethod |
||||
def xmr_swap_a_lock_spend_tx_vsize() -> int: |
||||
raise ValueError('Not possible') |
||||
|
||||
@staticmethod |
||||
def xmr_swap_b_lock_spend_tx_vsize() -> int: |
||||
# TODO: Estimate with ringsize |
||||
return 1604 |
||||
|
||||
def __init__(self, coin_settings, network, swap_client=None): |
||||
super().__init__(network) |
||||
|
||||
self._addr_prefix = self.chainparams_network()['address_prefix'] |
||||
|
||||
self.blocks_confirmed = coin_settings['blocks_confirmed'] |
||||
self._restore_height = coin_settings.get('restore_height', 0) |
||||
self.setFeePriority(coin_settings.get('fee_priority', 0)) |
||||
self._sc = swap_client |
||||
self._log = self._sc.log if self._sc and self._sc.log else logging |
||||
self._wallet_password = None |
||||
self._have_checked_seed = False |
||||
|
||||
daemon_login = None |
||||
if coin_settings.get('rpcuser', '') != '': |
||||
daemon_login = (coin_settings.get('rpcuser', ''), coin_settings.get('rpcpassword', '')) |
||||
|
||||
rpchost = coin_settings.get('rpchost', '127.0.0.1') |
||||
proxy_host = None |
||||
proxy_port = None |
||||
# Connect to the daemon over a proxy if not running locally |
||||
if swap_client: |
||||
chain_client_settings = swap_client.getChainClientSettings(self.coin_type()) |
||||
manage_daemon: bool = chain_client_settings['manage_daemon'] |
||||
if swap_client.use_tor_proxy: |
||||
if manage_daemon is False: |
||||
log_str: str = '' |
||||
have_cc_tor_opt = 'use_tor' in chain_client_settings |
||||
if have_cc_tor_opt and chain_client_settings['use_tor'] is False: |
||||
log_str = ' bypassing proxy (use_tor false for WOW)' |
||||
elif have_cc_tor_opt is False and is_private_ip_address(rpchost): |
||||
log_str = ' bypassing proxy (private ip address)' |
||||
else: |
||||
proxy_host = swap_client.tor_proxy_host |
||||
proxy_port = swap_client.tor_proxy_port |
||||
log_str = f' through proxy at {proxy_host}' |
||||
self._log.info(f'Connecting to remote {self.coin_name()} daemon at {rpchost}{log_str}.') |
||||
else: |
||||
self._log.info(f'Not connecting to local {self.coin_name()} daemon through proxy.') |
||||
elif manage_daemon is False: |
||||
self._log.info(f'Connecting to remote {self.coin_name()} daemon at {rpchost}.') |
||||
|
||||
self._rpctimeout = coin_settings.get('rpctimeout', 60) |
||||
self._walletrpctimeout = coin_settings.get('walletrpctimeout', 120) |
||||
self._walletrpctimeoutlong = coin_settings.get('walletrpctimeoutlong', 600) |
||||
|
||||
self.rpc = make_wow_rpc_func(coin_settings['rpcport'], daemon_login, host=rpchost, proxy_host=proxy_host, proxy_port=proxy_port, default_timeout=self._rpctimeout, tag='Node(j) ') |
||||
self.rpc2 = make_wow_rpc2_func(coin_settings['rpcport'], daemon_login, host=rpchost, proxy_host=proxy_host, proxy_port=proxy_port, default_timeout=self._rpctimeout, tag='Node ') # non-json endpoint |
||||
self.rpc_wallet = make_wow_rpc_func(coin_settings['walletrpcport'], coin_settings['walletrpcauth'], host=coin_settings.get('walletrpchost', '127.0.0.1'), default_timeout=self._walletrpctimeout, tag='Wallet ') |
||||
|
||||
def checkWallets(self) -> int: |
||||
return 1 |
||||
|
||||
def setFeePriority(self, new_priority): |
||||
ensure(new_priority >= 0 and new_priority < 4, 'Invalid fee_priority value') |
||||
self._fee_priority = new_priority |
||||
|
||||
def setWalletFilename(self, wallet_filename): |
||||
self._wallet_filename = wallet_filename |
||||
|
||||
def createWallet(self, params): |
||||
if self._wallet_password is not None: |
||||
params['password'] = self._wallet_password |
||||
rv = self.rpc_wallet('generate_from_keys', params) |
||||
self._log.info('generate_from_keys %s', dumpj(rv)) |
||||
|
||||
def openWallet(self, filename): |
||||
params = {'filename': filename} |
||||
if self._wallet_password is not None: |
||||
params['password'] = self._wallet_password |
||||
|
||||
try: |
||||
# Can't reopen the same wallet in windows, !is_keys_file_locked() |
||||
self.rpc_wallet('close_wallet') |
||||
except Exception: |
||||
pass |
||||
self.rpc_wallet('open_wallet', params) |
||||
|
||||
def initialiseWallet(self, key_view, key_spend, restore_height=None): |
||||
with self._mx_wallet: |
||||
try: |
||||
self.openWallet(self._wallet_filename) |
||||
# TODO: Check address |
||||
return # Wallet exists |
||||
except Exception as e: |
||||
pass |
||||
|
||||
Kbv = self.getPubkey(key_view) |
||||
Kbs = self.getPubkey(key_spend) |
||||
address_b58 = wow_util.encode_address(Kbv, Kbs, self._addr_prefix) |
||||
|
||||
params = { |
||||
'filename': self._wallet_filename, |
||||
'address': address_b58, |
||||
'viewkey': b2h(key_view[::-1]), |
||||
'spendkey': b2h(key_spend[::-1]), |
||||
'restore_height': self._restore_height, |
||||
} |
||||
self.createWallet(params) |
||||
self.openWallet(self._wallet_filename) |
||||
|
||||
def ensureWalletExists(self) -> None: |
||||
with self._mx_wallet: |
||||
self.openWallet(self._wallet_filename) |
||||
|
||||
def testDaemonRPC(self, with_wallet=True) -> None: |
||||
self.rpc_wallet('get_languages') |
||||
|
||||
def getDaemonVersion(self): |
||||
return self.rpc_wallet('get_version')['version'] |
||||
|
||||
def getBlockchainInfo(self): |
||||
get_height = self.rpc2('get_height', timeout=self._rpctimeout) |
||||
rv = { |
||||
'blocks': get_height['height'], |
||||
'verificationprogress': 0.0, |
||||
} |
||||
|
||||
try: |
||||
# get_block_count.block_count is how many blocks are in the longest chain known to the node. |
||||
# get_block_count returns "Internal error" if bootstrap-daemon is active |
||||
if get_height['untrusted'] is True: |
||||
rv['bootstrapping'] = True |
||||
get_info = self.rpc2('get_info', timeout=self._rpctimeout) |
||||
if 'height_without_bootstrap' in get_info: |
||||
rv['blocks'] = get_info['height_without_bootstrap'] |
||||
|
||||
rv['known_block_count'] = get_info['height'] |
||||
if rv['known_block_count'] > rv['blocks']: |
||||
rv['verificationprogress'] = rv['blocks'] / rv['known_block_count'] |
||||
else: |
||||
rv['known_block_count'] = self.rpc('get_block_count', timeout=self._rpctimeout)['count'] |
||||
rv['verificationprogress'] = rv['blocks'] / rv['known_block_count'] |
||||
except Exception as e: |
||||
self._log.warning('WOW get_block_count failed with: %s', str(e)) |
||||
rv['verificationprogress'] = 0.0 |
||||
|
||||
return rv |
||||
|
||||
def getChainHeight(self): |
||||
return self.rpc2('get_height', timeout=self._rpctimeout)['height'] |
||||
|
||||
def getWalletInfo(self): |
||||
with self._mx_wallet: |
||||
try: |
||||
self.openWallet(self._wallet_filename) |
||||
except Exception as e: |
||||
if 'Failed to open wallet' in str(e): |
||||
rv = {'encrypted': True, 'locked': True, 'balance': 0, 'unconfirmed_balance': 0} |
||||
return rv |
||||
raise e |
||||
|
||||
rv = {} |
||||
self.rpc_wallet('refresh') |
||||
balance_info = self.rpc_wallet('get_balance') |
||||
|
||||
rv['balance'] = self.format_amount(balance_info['unlocked_balance']) |
||||
rv['unconfirmed_balance'] = self.format_amount(balance_info['balance'] - balance_info['unlocked_balance']) |
||||
rv['encrypted'] = False if self._wallet_password is None else True |
||||
rv['locked'] = False |
||||
return rv |
||||
|
||||
def walletRestoreHeight(self): |
||||
return self._restore_height |
||||
|
||||
def getMainWalletAddress(self) -> str: |
||||
with self._mx_wallet: |
||||
self.openWallet(self._wallet_filename) |
||||
return self.rpc_wallet('get_address')['address'] |
||||
|
||||
def getNewAddress(self, placeholder) -> str: |
||||
with self._mx_wallet: |
||||
self.openWallet(self._wallet_filename) |
||||
new_address = self.rpc_wallet('create_address', {'account_index': 0})['address'] |
||||
self.rpc_wallet('store') |
||||
return new_address |
||||
|
||||
def get_fee_rate(self, conf_target: int = 2): |
||||
# fees - array of unsigned int; Represents the base fees at different priorities [slow, normal, fast, fastest]. |
||||
fee_est = self.rpc('get_fee_estimate') |
||||
if conf_target <= 1: |
||||
conf_target = 1 # normal |
||||
else: |
||||
conf_target = 0 # slow |
||||
fee_per_k_bytes = fee_est['fees'][conf_target] * 1000 |
||||
|
||||
return float(self.format_amount(fee_per_k_bytes)), 'get_fee_estimate' |
||||
|
||||
def getNewSecretKey(self) -> bytes: |
||||
# Note: Returned bytes are in big endian order |
||||
return i2b(edu.get_secret()) |
||||
|
||||
def pubkey(self, key: bytes) -> bytes: |
||||
return edf.scalarmult_B(key) |
||||
|
||||
def encodeKey(self, vk: bytes) -> str: |
||||
return vk[::-1].hex() |
||||
|
||||
def decodeKey(self, k_hex: str) -> bytes: |
||||
return bytes.fromhex(k_hex)[::-1] |
||||
|
||||
def encodePubkey(self, pk: bytes) -> str: |
||||
return edu.encodepoint(pk) |
||||
|
||||
def decodePubkey(self, pke): |
||||
return edf.decodepoint(pke) |
||||
|
||||
def getPubkey(self, privkey): |
||||
return ed25519_get_pubkey(privkey) |
||||
|
||||
def getAddressFromKeys(self, key_view: bytes, key_spend: bytes) -> str: |
||||
pk_view = self.getPubkey(key_view) |
||||
pk_spend = self.getPubkey(key_spend) |
||||
return wow_util.encode_address(pk_view, pk_spend, self._addr_prefix) |
||||
|
||||
def verifyKey(self, k: int) -> bool: |
||||
i = b2i(k) |
||||
return (i < edf.l and i > 8) |
||||
|
||||
def verifyPubkey(self, pubkey_bytes): |
||||
# Calls ed25519_decode_check_point() in secp256k1 |
||||
# Checks for small order |
||||
return verify_ed25519_point(pubkey_bytes) |
||||
|
||||
def proveDLEAG(self, key: bytes) -> bytes: |
||||
privkey = PrivateKey(key) |
||||
return dleag_prove(privkey) |
||||
|
||||
def verifyDLEAG(self, dleag_bytes: bytes) -> bool: |
||||
return dleag_verify(dleag_bytes) |
||||
|
||||
def lengthDLEAG(self) -> int: |
||||
return dleag_proof_len() |
||||
|
||||
def sumKeys(self, ka: bytes, kb: bytes) -> bytes: |
||||
return ed25519_scalar_add(ka, kb) |
||||
|
||||
def sumPubkeys(self, Ka: bytes, Kb: bytes) -> bytes: |
||||
return ed25519_add(Ka, Kb) |
||||
|
||||
def encodeSharedAddress(self, Kbv: bytes, Kbs: bytes) -> str: |
||||
return wow_util.encode_address(Kbv, Kbs, self._addr_prefix) |
||||
|
||||
def publishBLockTx(self, kbv: bytes, Kbs: bytes, output_amount: int, feerate: int, unlock_time: int = 0) -> bytes: |
||||
with self._mx_wallet: |
||||
self.openWallet(self._wallet_filename) |
||||
self.rpc_wallet('refresh') |
||||
|
||||
Kbv = self.getPubkey(kbv) |
||||
shared_addr = wow_util.encode_address(Kbv, Kbs, self._addr_prefix) |
||||
|
||||
params = {'destinations': [{'amount': output_amount, 'address': shared_addr}], 'unlock_time': unlock_time} |
||||
if self._fee_priority > 0: |
||||
params['priority'] = self._fee_priority |
||||
rv = self.rpc_wallet('transfer', params) |
||||
self._log.info('publishBLockTx %s to address_b58 %s', rv['tx_hash'], shared_addr) |
||||
tx_hash = bytes.fromhex(rv['tx_hash']) |
||||
|
||||
return tx_hash |
||||
|
||||
def findTxB(self, kbv, Kbs, cb_swap_value, cb_block_confirmed, restore_height, bid_sender): |
||||
with self._mx_wallet: |
||||
Kbv = self.getPubkey(kbv) |
||||
address_b58 = wow_util.encode_address(Kbv, Kbs, self._addr_prefix) |
||||
|
||||
kbv_le = kbv[::-1] |
||||
params = { |
||||
'restore_height': restore_height, |
||||
'filename': address_b58, |
||||
'address': address_b58, |
||||
'viewkey': b2h(kbv_le), |
||||
} |
||||
|
||||
try: |
||||
self.openWallet(address_b58) |
||||
except Exception as e: |
||||
self.createWallet(params) |
||||
self.openWallet(address_b58) |
||||
|
||||
self.rpc_wallet('refresh', timeout=self._walletrpctimeoutlong) |
||||
|
||||
''' |
||||
# Debug |
||||
try: |
||||
current_height = self.rpc_wallet('get_height')['height'] |
||||
self._log.info('findTxB WOW current_height %d\nAddress: %s', current_height, address_b58) |
||||
except Exception as e: |
||||
self._log.info('rpc failed %s', str(e)) |
||||
current_height = None # If the transfer is available it will be deep enough |
||||
# and (current_height is None or current_height - transfer['block_height'] > cb_block_confirmed): |
||||
''' |
||||
params = {'transfer_type': 'available'} |
||||
transfers = self.rpc_wallet('incoming_transfers', params) |
||||
rv = None |
||||
if 'transfers' in transfers: |
||||
for transfer in transfers['transfers']: |
||||
# unlocked <- wallet->is_transfer_unlocked() checks unlock_time and CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE |
||||
if not transfer['unlocked']: |
||||
full_tx = self.rpc_wallet('get_transfer_by_txid', {'txid': transfer['tx_hash']}) |
||||
unlock_time = full_tx['transfer']['unlock_time'] |
||||
if unlock_time != 0: |
||||
self._log.warning('Coin b lock txn is locked: {}, unlock_time {}'.format(transfer['tx_hash'], unlock_time)) |
||||
rv = -1 |
||||
continue |
||||
if transfer['amount'] == cb_swap_value: |
||||
return {'txid': transfer['tx_hash'], 'amount': transfer['amount'], 'height': 0 if 'block_height' not in transfer else transfer['block_height']} |
||||
else: |
||||
self._log.warning('Incorrect amount detected for coin b lock txn: {}'.format(transfer['tx_hash'])) |
||||
rv = -1 |
||||
return rv |
||||
|
||||
def findTxnByHash(self, txid): |
||||
with self._mx_wallet: |
||||
self.openWallet(self._wallet_filename) |
||||
self.rpc_wallet('refresh', timeout=self._walletrpctimeoutlong) |
||||
|
||||
try: |
||||
current_height = self.rpc2('get_height', timeout=self._rpctimeout)['height'] |
||||
self._log.info('findTxnByHash WOW current_height %d\nhash: %s', current_height, txid) |
||||
except Exception as e: |
||||
self._log.info('rpc failed %s', str(e)) |
||||
current_height = None # If the transfer is available it will be deep enough |
||||
|
||||
params = {'transfer_type': 'available'} |
||||
rv = self.rpc_wallet('incoming_transfers', params) |
||||
if 'transfers' in rv: |
||||
for transfer in rv['transfers']: |
||||
if transfer['tx_hash'] == txid \ |
||||
and (current_height is None or current_height - transfer['block_height'] > self.blocks_confirmed): |
||||
return {'txid': transfer['tx_hash'], 'amount': transfer['amount'], 'height': transfer['block_height']} |
||||
|
||||
return None |
||||
|
||||
def spendBLockTx(self, chain_b_lock_txid: bytes, address_to: str, kbv: bytes, kbs: bytes, cb_swap_value: int, b_fee_rate: int, restore_height: int, spend_actual_balance: bool = False) -> bytes: |
||||
''' |
||||
Notes: |
||||
"Error: No unlocked balance in the specified subaddress(es)" can mean not enough funds after tx fee. |
||||
''' |
||||
with self._mx_wallet: |
||||
Kbv = self.getPubkey(kbv) |
||||
Kbs = self.getPubkey(kbs) |
||||
address_b58 = wow_util.encode_address(Kbv, Kbs, self._addr_prefix) |
||||
|
||||
wallet_filename = address_b58 + '_spend' |
||||
|
||||
params = { |
||||
'filename': wallet_filename, |
||||
'address': address_b58, |
||||
'viewkey': b2h(kbv[::-1]), |
||||
'spendkey': b2h(kbs[::-1]), |
||||
'restore_height': restore_height, |
||||
} |
||||
|
||||
try: |
||||
self.openWallet(wallet_filename) |
||||
except Exception as e: |
||||
self.createWallet(params) |
||||
self.openWallet(wallet_filename) |
||||
|
||||
self.rpc_wallet('refresh') |
||||
rv = self.rpc_wallet('get_balance') |
||||
if rv['balance'] < cb_swap_value: |
||||
self._log.warning('Balance is too low, checking for existing spend.') |
||||
txns = self.rpc_wallet('get_transfers', {'out': True}) |
||||
if 'out' in txns: |
||||
txns = txns['out'] |
||||
if len(txns) > 0: |
||||
txid = txns[0]['txid'] |
||||
self._log.warning(f'spendBLockTx detected spending tx: {txid}.') |
||||
if txns[0]['address'] == address_b58: |
||||
return bytes.fromhex(txid) |
||||
|
||||
self._log.error('wallet {} balance {}, expected {}'.format(wallet_filename, rv['balance'], cb_swap_value)) |
||||
|
||||
if not spend_actual_balance: |
||||
raise TemporaryError('Invalid balance') |
||||
|
||||
if spend_actual_balance and rv['balance'] != cb_swap_value: |
||||
self._log.warning('Spending actual balance {}, not swap value {}.'.format(rv['balance'], cb_swap_value)) |
||||
cb_swap_value = rv['balance'] |
||||
if rv['unlocked_balance'] < cb_swap_value: |
||||
self._log.error('wallet {} balance {}, expected {}, blocks_to_unlock {}'.format(wallet_filename, rv['unlocked_balance'], cb_swap_value, rv['blocks_to_unlock'])) |
||||
raise TemporaryError('Invalid unlocked_balance') |
||||
|
||||
params = {'address': address_to} |
||||
if self._fee_priority > 0: |
||||
params['priority'] = self._fee_priority |
||||
|
||||
rv = self.rpc_wallet('sweep_all', params) |
||||
self._log.debug('sweep_all {}'.format(json.dumps(rv))) |
||||
|
||||
return bytes.fromhex(rv['tx_hash_list'][0]) |
||||
|
||||
def withdrawCoin(self, value, addr_to: str, sweepall: bool, estimate_fee: bool = False) -> str: |
||||
with self._mx_wallet: |
||||
self.openWallet(self._wallet_filename) |
||||
self.rpc_wallet('refresh') |
||||
|
||||
if sweepall: |
||||
balance = self.rpc_wallet('get_balance') |
||||
if balance['balance'] != balance['unlocked_balance']: |
||||
raise ValueError('Balance must be fully confirmed to use sweep all.') |
||||
self._log.info('WOW {} sweep_all.'.format('estimate fee' if estimate_fee else 'withdraw')) |
||||
self._log.debug('WOW balance: {}'.format(balance['balance'])) |
||||
params = {'address': addr_to, 'do_not_relay': estimate_fee} |
||||
if self._fee_priority > 0: |
||||
params['priority'] = self._fee_priority |
||||
rv = self.rpc_wallet('sweep_all', params) |
||||
if estimate_fee: |
||||
return {'num_txns': len(rv['fee_list']), 'sum_amount': sum(rv['amount_list']), 'sum_fee': sum(rv['fee_list']), 'sum_weight': sum(rv['weight_list'])} |
||||
return rv['tx_hash_list'][0] |
||||
|
||||
value_sats: int = make_int(value, self.exp()) |
||||
params = {'destinations': [{'amount': value_sats, 'address': addr_to}], 'do_not_relay': estimate_fee} |
||||
if self._fee_priority > 0: |
||||
params['priority'] = self._fee_priority |
||||
rv = self.rpc_wallet('transfer', params) |
||||
if estimate_fee: |
||||
return {'num_txns': 1, 'sum_amount': rv['amount'], 'sum_fee': rv['fee'], 'sum_weight': rv['weight']} |
||||
return rv['tx_hash'] |
||||
|
||||
def estimateFee(self, value: int, addr_to: str, sweepall: bool) -> str: |
||||
return self.withdrawCoin(value, addr_to, sweepall, estimate_fee=True) |
||||
|
||||
def showLockTransfers(self, kbv, Kbs, restore_height): |
||||
with self._mx_wallet: |
||||
try: |
||||
Kbv = self.getPubkey(kbv) |
||||
address_b58 = wow_util.encode_address(Kbv, Kbs, self._addr_prefix) |
||||
wallet_file = address_b58 + '_spend' |
||||
try: |
||||
self.openWallet(wallet_file) |
||||
except Exception: |
||||
wallet_file = address_b58 |
||||
try: |
||||
self.openWallet(wallet_file) |
||||
except Exception: |
||||
self._log.info(f'showLockTransfers trying to create wallet for address {address_b58}.') |
||||
kbv_le = kbv[::-1] |
||||
params = { |
||||
'restore_height': restore_height, |
||||
'filename': address_b58, |
||||
'address': address_b58, |
||||
'viewkey': b2h(kbv_le), |
||||
} |
||||
self.createWallet(params) |
||||
self.openWallet(address_b58) |
||||
|
||||
self.rpc_wallet('refresh') |
||||
|
||||
rv = self.rpc_wallet('get_transfers', {'in': True, 'out': True, 'pending': True, 'failed': True}) |
||||
rv['filename'] = wallet_file |
||||
return rv |
||||
except Exception as e: |
||||
return {'error': str(e)} |
||||
|
||||
def getSpendableBalance(self) -> int: |
||||
with self._mx_wallet: |
||||
self.openWallet(self._wallet_filename) |
||||
|
||||
self.rpc_wallet('refresh') |
||||
balance_info = self.rpc_wallet('get_balance') |
||||
return balance_info['unlocked_balance'] |
||||
|
||||
def changeWalletPassword(self, old_password, new_password): |
||||
self._log.info('changeWalletPassword - {}'.format(self.ticker())) |
||||
orig_password = self._wallet_password |
||||
if old_password != '': |
||||
self._wallet_password = old_password |
||||
try: |
||||
self.openWallet(self._wallet_filename) |
||||
self.rpc_wallet('change_wallet_password', {'old_password': old_password, 'new_password': new_password}) |
||||
except Exception as e: |
||||
self._wallet_password = orig_password |
||||
raise e |
||||
|
||||
def unlockWallet(self, password: str) -> None: |
||||
self._log.info('unlockWallet - {}'.format(self.ticker())) |
||||
self._wallet_password = password |
||||
|
||||
if not self._have_checked_seed: |
||||
self._sc.checkWalletSeed(self.coin_type()) |
||||
|
||||
def lockWallet(self) -> None: |
||||
self._log.info('lockWallet - {}'.format(self.ticker())) |
||||
self._wallet_password = None |
||||
|
||||
def isAddressMine(self, address): |
||||
# TODO |
||||
return True |
||||
|
||||
def ensureFunds(self, amount: int) -> None: |
||||
if self.getSpendableBalance() < amount: |
||||
raise ValueError('Balance too low') |
||||
|
||||
def getTransaction(self, txid: bytes): |
||||
return self.rpc2('get_transactions', {'txs_hashes': [txid.hex(), ]}) |
@ -0,0 +1,258 @@ |
||||
# -*- coding: utf-8 -*- |
||||
|
||||
import os |
||||
import json |
||||
import socks |
||||
import time |
||||
import urllib |
||||
import hashlib |
||||
from xmlrpc.client import ( |
||||
Fault, |
||||
Transport, |
||||
SafeTransport, |
||||
) |
||||
from sockshandler import SocksiPyConnection |
||||
from .util import jsonDecimal |
||||
|
||||
|
||||
class SocksTransport(Transport): |
||||
|
||||
def set_proxy(self, proxy_host, proxy_port): |
||||
self.proxy_host = proxy_host |
||||
self.proxy_port = proxy_port |
||||
|
||||
self.proxy_type = socks.PROXY_TYPE_SOCKS5 |
||||
self.proxy_rdns = True |
||||
self.proxy_username = None |
||||
self.proxy_password = None |
||||
|
||||
def make_connection(self, host): |
||||
# return an existing connection if possible. This allows |
||||
# HTTP/1.1 keep-alive. |
||||
if self._connection and host == self._connection[0]: |
||||
return self._connection[1] |
||||
# create a HTTP connection object from a host descriptor |
||||
chost, self._extra_headers, x509 = self.get_host_info(host) |
||||
self._connection = host, SocksiPyConnection(self.proxy_type, self.proxy_host, self.proxy_port, self.proxy_rdns, self.proxy_username, self.proxy_password, chost) |
||||
return self._connection[1] |
||||
|
||||
|
||||
class JsonrpcDigest(): |
||||
# __getattr__ complicates extending ServerProxy |
||||
def __init__(self, uri, transport=None, encoding=None, verbose=False, |
||||
allow_none=False, use_datetime=False, use_builtin_types=False, |
||||
*, context=None): |
||||
|
||||
parsed = urllib.parse.urlparse(uri) |
||||
if parsed.scheme not in ('http', 'https'): |
||||
raise OSError('unsupported XML-RPC protocol') |
||||
self.__host = parsed.netloc |
||||
self.__handler = parsed.path |
||||
|
||||
if transport is None: |
||||
handler = SafeTransport if parsed.scheme == 'https' else Transport |
||||
extra_kwargs = {} |
||||
transport = handler(use_datetime=use_datetime, |
||||
use_builtin_types=use_builtin_types, |
||||
**extra_kwargs) |
||||
self.__transport = transport |
||||
|
||||
self.__encoding = encoding or 'utf-8' |
||||
self.__verbose = verbose |
||||
self.__allow_none = allow_none |
||||
|
||||
self.__request_id = 0 |
||||
|
||||
def close(self): |
||||
if self.__transport is not None: |
||||
self.__transport.close() |
||||
|
||||
def request_id(self): |
||||
return self.__request_id |
||||
|
||||
def post_request(self, method, params, timeout=None): |
||||
try: |
||||
connection = self.__transport.make_connection(self.__host) |
||||
if timeout: |
||||
connection.timeout = timeout |
||||
headers = self.__transport._extra_headers[:] |
||||
|
||||
connection.putrequest('POST', self.__handler) |
||||
headers.append(('Content-Type', 'application/json')) |
||||
headers.append(('User-Agent', 'jsonrpc')) |
||||
self.__transport.send_headers(connection, headers) |
||||
self.__transport.send_content(connection, '' if params is None else json.dumps(params, default=jsonDecimal).encode('utf-8')) |
||||
self.__request_id += 1 |
||||
|
||||
resp = connection.getresponse() |
||||
return resp.read() |
||||
|
||||
except Fault: |
||||
raise |
||||
except Exception: |
||||
self.__transport.close() |
||||
raise |
||||
|
||||
def json_request(self, request_body, username='', password='', timeout=None): |
||||
try: |
||||
connection = self.__transport.make_connection(self.__host) |
||||
if timeout: |
||||
connection.timeout = timeout |
||||
|
||||
headers = self.__transport._extra_headers[:] |
||||
|
||||
connection.putrequest('POST', self.__handler) |
||||
headers.append(('Content-Type', 'application/json')) |
||||
headers.append(('Connection', 'keep-alive')) |
||||
self.__transport.send_headers(connection, headers) |
||||
self.__transport.send_content(connection, json.dumps(request_body, default=jsonDecimal).encode('utf-8') if request_body else '') |
||||
resp = connection.getresponse() |
||||
|
||||
if resp.status == 401: |
||||
resp_headers = resp.getheaders() |
||||
v = resp.read() |
||||
|
||||
algorithm = '' |
||||
realm = '' |
||||
nonce = '' |
||||
for h in resp_headers: |
||||
if h[0] != 'WWW-authenticate': |
||||
continue |
||||
fields = h[1].split(',') |
||||
for f in fields: |
||||
key, value = f.split('=', 1) |
||||
if key == 'algorithm' and value != 'MD5': |
||||
break |
||||
if key == 'realm': |
||||
realm = value.strip('"') |
||||
if key == 'nonce': |
||||
nonce = value.strip('"') |
||||
if realm != '' and nonce != '': |
||||
break |
||||
|
||||
if realm == '' or nonce == '': |
||||
raise ValueError('Authenticate header not found.') |
||||
|
||||
path = self.__handler |
||||
HA1 = hashlib.md5(f'{username}:{realm}:{password}'.encode('utf-8')).hexdigest() |
||||
|
||||
http_method = 'POST' |
||||
HA2 = hashlib.md5(f'{http_method}:{path}'.encode('utf-8')).hexdigest() |
||||
|
||||
ncvalue = '{:08x}'.format(1) |
||||
s = ncvalue.encode('utf-8') |
||||
s += nonce.encode('utf-8') |
||||
s += time.ctime().encode('utf-8') |
||||
s += os.urandom(8) |
||||
cnonce = (hashlib.sha1(s).hexdigest()[:16]) |
||||
|
||||
# MD5-SESS |
||||
HA1 = hashlib.md5(f'{HA1}:{nonce}:{cnonce}'.encode('utf-8')).hexdigest() |
||||
|
||||
respdig = hashlib.md5(f'{HA1}:{nonce}:{ncvalue}:{cnonce}:auth:{HA2}'.encode('utf-8')).hexdigest() |
||||
|
||||
header_value = f'Digest username="{username}", realm="{realm}", nonce="{nonce}", uri="{path}", response="{respdig}", algorithm="MD5-sess", qop="auth", nc={ncvalue}, cnonce="{cnonce}"' |
||||
headers = self.__transport._extra_headers[:] |
||||
headers.append(('Authorization', header_value)) |
||||
|
||||
connection.putrequest('POST', self.__handler) |
||||
headers.append(('Content-Type', 'application/json')) |
||||
headers.append(('Connection', 'keep-alive')) |
||||
self.__transport.send_headers(connection, headers) |
||||
self.__transport.send_content(connection, json.dumps(request_body, default=jsonDecimal).encode('utf-8') if request_body else '') |
||||
resp = connection.getresponse() |
||||
|
||||
self.__request_id += 1 |
||||
return resp.read() |
||||
|
||||
except Fault: |
||||
raise |
||||
except Exception: |
||||
self.__transport.close() |
||||
raise |
||||
|
||||
|
||||
def callrpc_wow(rpc_port, method, params=[], rpc_host='127.0.0.1', path='json_rpc', auth=None, timeout=120, transport=None, tag=''): |
||||
# auth is a tuple: (username, password) |
||||
try: |
||||
if rpc_host.count('://') > 0: |
||||
url = '{}:{}/{}'.format(rpc_host, rpc_port, path) |
||||
else: |
||||
url = 'http://{}:{}/{}'.format(rpc_host, rpc_port, path) |
||||
|
||||
x = JsonrpcDigest(url, transport=transport) |
||||
request_body = { |
||||
'method': method, |
||||
'params': params, |
||||
'jsonrpc': '2.0', |
||||
'id': x.request_id() |
||||
} |
||||
if auth: |
||||
v = x.json_request(request_body, username=auth[0], password=auth[1], timeout=timeout) |
||||
else: |
||||
v = x.json_request(request_body, timeout=timeout) |
||||
x.close() |
||||
r = json.loads(v.decode('utf-8')) |
||||
except Exception as ex: |
||||
raise ValueError('{}RPC Server Error: {}'.format(tag, str(ex))) |
||||
|
||||
if 'error' in r and r['error'] is not None: |
||||
raise ValueError(tag + 'RPC error ' + str(r['error'])) |
||||
|
||||
return r['result'] |
||||
|
||||
|
||||
def callrpc_wow2(rpc_port: int, method: str, params=None, auth=None, rpc_host='127.0.0.1', timeout=120, transport=None, tag=''): |
||||
try: |
||||
if rpc_host.count('://') > 0: |
||||
url = '{}:{}/{}'.format(rpc_host, rpc_port, method) |
||||
else: |
||||
url = 'http://{}:{}/{}'.format(rpc_host, rpc_port, method) |
||||
|
||||
x = JsonrpcDigest(url, transport=transport) |
||||
if auth: |
||||
v = x.json_request(params, username=auth[0], password=auth[1], timeout=timeout) |
||||
else: |
||||
v = x.json_request(params, timeout=timeout) |
||||
x.close() |
||||
r = json.loads(v.decode('utf-8')) |
||||
except Exception as ex: |
||||
raise ValueError('{}RPC Server Error: {}'.format(tag, str(ex))) |
||||
|
||||
return r |
||||
|
||||
|
||||
def make_wow_rpc2_func(port, auth, host='127.0.0.1', proxy_host=None, proxy_port=None, default_timeout=120, tag=''): |
||||
port = port |
||||
auth = auth |
||||
host = host |
||||
transport = None |
||||
default_timeout = default_timeout |
||||
tag = tag |
||||
|
||||
if proxy_host: |
||||
transport = SocksTransport() |
||||
transport.set_proxy(proxy_host, proxy_port) |
||||
|
||||
def rpc_func(method, params=None, wallet=None, timeout=default_timeout): |
||||
nonlocal port, auth, host, transport, tag |
||||
return callrpc_wow2(port, method, params, auth=auth, rpc_host=host, timeout=timeout, transport=transport, tag=tag) |
||||
return rpc_func |
||||
|
||||
|
||||
def make_wow_rpc_func(port, auth, host='127.0.0.1', proxy_host=None, proxy_port=None, default_timeout=120, tag=''): |
||||
port = port |
||||
auth = auth |
||||
host = host |
||||
transport = None |
||||
default_timeout = default_timeout |
||||
tag = tag |
||||
|
||||
if proxy_host: |
||||
transport = SocksTransport() |
||||
transport.set_proxy(proxy_host, proxy_port) |
||||
|
||||
def rpc_func(method, params=None, wallet=None, timeout=default_timeout): |
||||
nonlocal port, auth, host, transport, tag |
||||
return callrpc_wow(port, method, params, rpc_host=host, auth=auth, timeout=timeout, transport=transport, tag=tag) |
||||
return rpc_func |
@ -0,0 +1,16 @@ |
||||
wownero_wallet: |
||||
image: i_wownero_wallet |
||||
build: |
||||
context: wownero_wallet |
||||
dockerfile: Dockerfile |
||||
container_name: wownero_wallet |
||||
volumes: |
||||
- ${DATA_PATH}/wownero_wallet:/data |
||||
expose: |
||||
- ${BASE_WOW_WALLET_PORT} |
||||
logging: |
||||
driver: "json-file" |
||||
options: |
||||
max-size: "10m" |
||||
max-file: "3" |
||||
restart: unless-stopped |
@ -0,0 +1,16 @@ |
||||
wownero_daemon: |
||||
image: i_wownero_daemon |
||||
build: |
||||
context: wownero_daemon |
||||
dockerfile: Dockerfile |
||||
container_name: wownero_daemon |
||||
volumes: |
||||
- ${DATA_PATH}/wownero_daemon:/data |
||||
expose: |
||||
- ${BASE_WOW_RPC_PORT} |
||||
logging: |
||||
driver: "json-file" |
||||
options: |
||||
max-size: "10m" |
||||
max-file: "3" |
||||
restart: unless-stopped |
@ -0,0 +1,24 @@ |
||||
FROM i_swapclient as install_stage |
||||
|
||||
RUN basicswap-prepare --preparebinonly --bindir=/coin_bin --withcoin=wownero --withoutcoins=particl |
||||
|
||||
FROM debian:bullseye-slim |
||||
|
||||
COPY --from=install_stage /coin_bin . |
||||
|
||||
ENV WOWNERO_DATA /data |
||||
|
||||
RUN groupadd -r wownero && useradd -r -m -g wownero wownero \ |
||||
&& apt-get update \ |
||||
&& apt-get install -qq --no-install-recommends gosu \ |
||||
&& rm -rf /var/lib/apt/lists/* \ |
||||
&& mkdir -p "$WOWNERO_DATA" \ |
||||
&& chown -R wownero:wownero "$WOWNERO_DATA" \ |
||||
&& ln -sfn "$WOWNERO_DATA" /home/wownero/.wownero \ |
||||
&& chown -h wownero:wownero /home/wownero/.wownero |
||||
VOLUME $WOWNERO_DATA |
||||
|
||||
COPY entrypoint.sh /entrypoint.sh |
||||
ENTRYPOINT ["/entrypoint.sh"] |
||||
|
||||
CMD ["/wownero/wownerod", "--non-interactive", "--config-file=/home/wownero/.wownero/wownerod.conf", "--confirm-external-bind"] |
@ -0,0 +1,11 @@ |
||||
#!/bin/bash |
||||
set -e |
||||
|
||||
if [[ "$1" == "wownerod" ]]; then |
||||
mkdir -p "$WOWNERO_DATA" |
||||
|
||||
chown -h wownero:wownero /home/wownero/.wownero |
||||
exec gosu wownero "$@" |
||||
else |
||||
exec "$@" |
||||
fi |
@ -0,0 +1,16 @@ |
||||
FROM i_wownero_daemon |
||||
|
||||
ENV WOWNERO_DATA /data |
||||
|
||||
RUN groupadd -r wownero_wallet && useradd -r -m -g wownero_wallet wownero_wallet \ |
||||
&& apt-get update \ |
||||
&& apt-get install -qq --no-install-recommends gosu \ |
||||
&& rm -rf /var/lib/apt/lists/* \ |
||||
&& mkdir -p "$WOWNERO_DATA" \ |
||||
&& chown -R wownero_wallet:wownero_wallet "$WOWNERO_DATA" |
||||
VOLUME $WOWNERO_DATA |
||||
|
||||
COPY entrypoint.sh /entrypoint.sh |
||||
ENTRYPOINT ["/entrypoint.sh"] |
||||
|
||||
CMD ["/wownero/wownero-wallet-rpc", "--non-interactive", "--config-file=/data/wownero-wallet-rpc.conf", "--confirm-external-bind"] |
@ -0,0 +1,11 @@ |
||||
#!/bin/bash |
||||
set -e |
||||
|
||||
if [[ "$1" == "wownero-wallet-rpc" ]]; then |
||||
mkdir -p "$WOWNERO_DATA" |
||||
|
||||
chown -h wownero_wallet:wownero_wallet /data |
||||
exec gosu wownero_wallet "$@" |
||||
else |
||||
exec "$@" |
||||
fi |
Loading…
Reference in new issue