Host-customized fork of https://github.com/tecnovert/basicswap/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

5061 lines
225 KiB

5 years ago
# -*- coding: utf-8 -*-
# Copyright (c) 2019-2021 tecnovert
5 years ago
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
5 years ago
import os
import re
import zmq
import json
4 years ago
import time
import base64
5 years ago
import random
import shutil
import struct
import hashlib
5 years ago
import secrets
4 years ago
import datetime as dt
import threading
4 years ago
import traceback
import sqlalchemy as sa
import collections
4 years ago
5 years ago
from enum import IntEnum, auto
4 years ago
from sqlalchemy.orm import sessionmaker, scoped_session
from sqlalchemy.orm.session import close_all_sessions
from .interface_part import PARTInterface
from .interface_btc import BTCInterface
from .interface_ltc import LTCInterface
from .interface_nmc import NMCInterface
from .interface_xmr import XMRInterface
from .interface_passthrough_btc import PassthroughBTCInterface
5 years ago
from . import __version__
from .util import (
pubkeyToAddress,
format_amount,
format_timestamp,
5 years ago
encodeAddress,
decodeAddress,
DeserialiseNum,
decodeWif,
toWIF,
getKeyID,
make_int,
getP2SHScriptForHash,
getP2WSH,
5 years ago
)
from .chainparams import (
chainparams,
Coins,
)
from .script import (
OpCodes,
)
5 years ago
from .messages_pb2 import (
OfferMessage,
BidMessage,
BidAcceptMessage,
4 years ago
XmrBidMessage,
XmrBidAcceptMessage,
XmrSplitMessage,
XmrBidLockTxSigsMessage,
XmrBidLockSpendTxMessage,
XmrBidLockReleaseMessage,
OfferRevokeMessage,
5 years ago
)
from .db import (
CURRENT_DB_VERSION,
TableTypes,
Base,
DBKVInt,
DBKVString,
Offer,
Bid,
SwapTx,
PooledAddress,
SentOffer,
SmsgAddress,
5 years ago
EventQueue,
EventLog,
4 years ago
XmrOffer,
XmrSwap,
XmrSplitData,
)
from .base import BaseApp
from .explorers import (
ExplorerInsight,
ExplorerBitAps,
ExplorerChainz,
)
from .types import (
SEQUENCE_LOCK_BLOCKS,
SEQUENCE_LOCK_TIME,
ABS_LOCK_BLOCKS,
ABS_LOCK_TIME)
import basicswap.config as cfg
import basicswap.network as bsn
import basicswap.protocols.atomic_swap_1 as atomic_swap_1
5 years ago
MIN_OFFER_VALID_TIME = 60 * 10
MAX_OFFER_VALID_TIME = 60 * 60 * 48
MIN_BID_VALID_TIME = 60 * 10
MAX_BID_VALID_TIME = 60 * 60 * 48
class MessageTypes(IntEnum):
OFFER = auto()
BID = auto()
BID_ACCEPT = auto()
XMR_OFFER = auto()
XMR_BID_FL = auto()
4 years ago
XMR_BID_SPLIT = auto()
XMR_BID_ACCEPT_LF = auto()
XMR_BID_TXN_SIGS_FL = auto()
XMR_BID_LOCK_SPEND_TX_LF = auto()
XMR_BID_LOCK_RELEASE_LF = auto()
OFFER_REVOKE = auto()
5 years ago
class SwapTypes(IntEnum):
SELLER_FIRST = auto()
BUYER_FIRST = auto()
5 years ago
SELLER_FIRST_2MSG = auto()
BUYER_FIRST_2MSG = auto()
XMR_SWAP = auto()
5 years ago
class OfferStates(IntEnum):
OFFER_SENT = auto()
OFFER_RECEIVED = auto()
OFFER_ABANDONED = auto()
class BidStates(IntEnum):
BID_SENT = auto()
4 years ago
BID_RECEIVING = auto() # Partially received
5 years ago
BID_RECEIVED = auto()
4 years ago
BID_RECEIVING_ACC = auto() # Partially received accept message
5 years ago
BID_ACCEPTED = auto() # BidAcceptMessage received/sent
SWAP_INITIATED = auto() # Initiate txn validated
SWAP_PARTICIPATING = auto() # Participate txn validated
SWAP_COMPLETED = auto() # All swap txns spent
XMR_SWAP_SCRIPT_COIN_LOCKED = auto()
XMR_SWAP_HAVE_SCRIPT_COIN_SPEND_TX = auto()
XMR_SWAP_NOSCRIPT_COIN_LOCKED = auto()
XMR_SWAP_LOCK_RELEASED = auto()
XMR_SWAP_SCRIPT_TX_REDEEMED = auto()
XMR_SWAP_NOSCRIPT_TX_REDEEMED = auto()
XMR_SWAP_NOSCRIPT_TX_RECOVERED = auto()
XMR_SWAP_FAILED_REFUNDED = auto()
XMR_SWAP_FAILED_SWIPED = auto()
XMR_SWAP_FAILED = auto()
SWAP_DELAYING = auto()
5 years ago
SWAP_TIMEDOUT = auto()
BID_ABANDONED = auto() # Bid will no longer be processed
BID_ERROR = auto() # An error occurred
BID_STALLED_FOR_TEST = auto()
5 years ago
class TxStates(IntEnum):
TX_NONE = auto()
TX_SENT = auto()
TX_CONFIRMED = auto()
TX_REDEEMED = auto()
TX_REFUNDED = auto()
class TxTypes(IntEnum):
ITX = auto()
PTX = auto()
ITX_REDEEM = auto()
ITX_REFUND = auto()
PTX_REDEEM = auto()
PTX_REFUND = auto()
XMR_SWAP_A_LOCK = auto()
XMR_SWAP_A_LOCK_SPEND = auto()
XMR_SWAP_A_LOCK_REFUND = auto()
XMR_SWAP_A_LOCK_REFUND_SPEND = auto()
XMR_SWAP_A_LOCK_REFUND_SWIPE = auto()
XMR_SWAP_B_LOCK = auto()
5 years ago
class EventTypes(IntEnum):
ACCEPT_BID = auto()
ACCEPT_XMR_BID = auto()
SIGN_XMR_SWAP_LOCK_TX_A = auto()
SEND_XMR_SWAP_LOCK_TX_A = auto()
SEND_XMR_SWAP_LOCK_TX_B = auto()
SEND_XMR_LOCK_RELEASE = auto()
REDEEM_XMR_SWAP_LOCK_TX_A = auto() # Follower
REDEEM_XMR_SWAP_LOCK_TX_B = auto() # Leader
RECOVER_XMR_SWAP_LOCK_TX_B = auto()
5 years ago
class EventLogTypes(IntEnum):
FAILED_TX_B_LOCK_PUBLISH = auto()
LOCK_TX_A_PUBLISHED = auto()
LOCK_TX_B_PUBLISHED = auto()
FAILED_TX_B_SPEND = auto()
LOCK_TX_A_SEEN = auto()
LOCK_TX_A_CONFIRMED = auto()
LOCK_TX_B_SEEN = auto()
LOCK_TX_B_CONFIRMED = auto()
DEBUG_TWEAK_APPLIED = auto()
FAILED_TX_B_REFUND = auto()
LOCK_TX_B_INVALID = auto()
LOCK_TX_A_REFUND_TX_PUBLISHED = auto()
LOCK_TX_A_REFUND_SPEND_TX_PUBLISHED = auto()
LOCK_TX_A_REFUND_SWIPE_TX_PUBLISHED = auto()
LOCK_TX_B_REFUND_TX_PUBLISHED = auto()
4 years ago
class XmrSplitMsgTypes(IntEnum):
BID = auto()
BID_ACCEPT = auto()
class DebugTypes(IntEnum):
BID_STOP_AFTER_COIN_A_LOCK = auto()
BID_DONT_SPEND_COIN_A_LOCK_REFUND = auto()
CREATE_INVALID_COIN_B_LOCK = auto()
BUYER_STOP_AFTER_ITX = auto()
MAKE_INVALID_PTX = auto()
INITIATE_TX_TIMEOUT = 40 * 60 # TODO: make variable per coin
5 years ago
def strOfferState(state):
5 years ago
if state == OfferStates.OFFER_SENT:
return 'Sent'
if state == OfferStates.OFFER_RECEIVED:
return 'Received'
if state == OfferStates.OFFER_ABANDONED:
return 'Abandoned'
return 'Unknown'
def strBidState(state):
5 years ago
if state == BidStates.BID_SENT:
return 'Sent'
if state == BidStates.BID_RECEIVING:
return 'Receiving'
if state == BidStates.BID_RECEIVING_ACC:
return 'Receiving accept'
5 years ago
if state == BidStates.BID_RECEIVED:
return 'Received'
if state == BidStates.BID_ACCEPTED:
return 'Accepted'
if state == BidStates.SWAP_INITIATED:
return 'Initiated'
if state == BidStates.SWAP_PARTICIPATING:
return 'Participating'
if state == BidStates.SWAP_COMPLETED:
return 'Completed'
if state == BidStates.SWAP_TIMEDOUT:
return 'Timed-out'
if state == BidStates.BID_ABANDONED:
return 'Abandoned'
if state == BidStates.BID_STALLED_FOR_TEST:
return 'Stalled (debug)'
if state == BidStates.BID_ERROR:
return 'Error'
if state == BidStates.XMR_SWAP_SCRIPT_COIN_LOCKED:
return 'Script coin locked'
if state == BidStates.XMR_SWAP_HAVE_SCRIPT_COIN_SPEND_TX:
return 'Script coin spend tx valid'
if state == BidStates.XMR_SWAP_NOSCRIPT_COIN_LOCKED:
return 'Scriptless coin locked'
if state == BidStates.XMR_SWAP_LOCK_RELEASED:
return 'Script coin lock released'
if state == BidStates.XMR_SWAP_SCRIPT_TX_REDEEMED:
return 'Script tx redeemed'
if state == BidStates.XMR_SWAP_NOSCRIPT_TX_REDEEMED:
return 'Scriptless tx redeemed'
if state == BidStates.XMR_SWAP_NOSCRIPT_TX_RECOVERED:
return 'Scriptless tx recovered'
if state == BidStates.XMR_SWAP_FAILED_REFUNDED:
return 'Failed, refunded'
if state == BidStates.XMR_SWAP_FAILED_SWIPED:
return 'Failed, swiped'
if state == BidStates.XMR_SWAP_FAILED:
return 'Failed'
if state == BidStates.SWAP_DELAYING:
return 'Delaying'
5 years ago
return 'Unknown'
def strTxState(state):
5 years ago
if state == TxStates.TX_NONE:
return 'None'
if state == TxStates.TX_SENT:
return 'Sent'
if state == TxStates.TX_CONFIRMED:
return 'Confirmed'
if state == TxStates.TX_REDEEMED:
return 'Redeemed'
if state == TxStates.TX_REFUNDED:
return 'Refunded'
return 'Unknown'
def strTxType(tx_type):
if tx_type == TxTypes.XMR_SWAP_A_LOCK:
return 'Chain A Lock Tx'
if tx_type == TxTypes.XMR_SWAP_A_LOCK_SPEND:
return 'Chain A Lock Spend Tx'
if tx_type == TxTypes.XMR_SWAP_A_LOCK_REFUND:
return 'Chain A Lock Refund Tx'
if tx_type == TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND:
return 'Chain A Lock Refund Spend Tx'
if tx_type == TxTypes.XMR_SWAP_A_LOCK_REFUND_SWIPE:
return 'Chain A Lock Refund Swipe Tx'
if tx_type == TxTypes.XMR_SWAP_B_LOCK:
return 'Chain B Lock Tx'
return 'Unknown'
5 years ago
def getLockName(lock_type):
if lock_type == SEQUENCE_LOCK_BLOCKS:
return 'Sequence lock, blocks'
if lock_type == SEQUENCE_LOCK_TIME:
return 'Sequence lock, time'
if lock_type == ABS_LOCK_BLOCKS:
return 'blocks'
if lock_type == ABS_LOCK_TIME:
return 'time'
5 years ago
def describeEventEntry(event_type, event_msg):
if event_type == EventLogTypes.FAILED_TX_B_LOCK_PUBLISH:
return 'Failed to publish lock tx B'
if event_type == EventLogTypes.FAILED_TX_B_LOCK_PUBLISH:
return 'Failed to publish lock tx B'
if event_type == EventLogTypes.LOCK_TX_A_PUBLISHED:
return 'Lock tx A published'
if event_type == EventLogTypes.LOCK_TX_B_PUBLISHED:
return 'Lock tx B published'
if event_type == EventLogTypes.FAILED_TX_B_SPEND:
return 'Failed to publish lock tx B spend'
if event_type == EventLogTypes.LOCK_TX_A_SEEN:
return 'Lock tx A seen in chain'
if event_type == EventLogTypes.LOCK_TX_A_CONFIRMED:
return 'Lock tx A confirmed in chain'
if event_type == EventLogTypes.LOCK_TX_B_SEEN:
return 'Lock tx B seen in chain'
if event_type == EventLogTypes.LOCK_TX_B_CONFIRMED:
return 'Lock tx B confirmed in chain'
if event_type == EventLogTypes.DEBUG_TWEAK_APPLIED:
return 'Debug tweak applied ' + event_msg
if event_type == EventLogTypes.FAILED_TX_B_REFUND:
return 'Failed to publish lock tx B refund'
if event_type == EventLogTypes.LOCK_TX_B_INVALID:
return 'Detected invalid lock Tx B'
if event_type == EventLogTypes.LOCK_TX_A_REFUND_TX_PUBLISHED:
return 'Lock tx A refund tx published'
if event_type == EventLogTypes.LOCK_TX_A_REFUND_SPEND_TX_PUBLISHED:
return 'Lock tx A refund spend tx published'
if event_type == EventLogTypes.LOCK_TX_A_REFUND_SWIPE_TX_PUBLISHED:
return 'Lock tx A refund swipe tx published'
if event_type == EventLogTypes.LOCK_TX_B_REFUND_TX_PUBLISHED:
return 'Lock tx B refund tx published'
5 years ago
def getVoutByAddress(txjs, p2sh):
for o in txjs['vout']:
try:
if p2sh in o['scriptPubKey']['addresses']:
return o['n']
except Exception:
pass
raise ValueError('Address output not found in txn')
def getVoutByP2WSH(txjs, p2wsh_hex):
for o in txjs['vout']:
try:
if p2wsh_hex == o['scriptPubKey']['hex']:
return o['n']
except Exception:
pass
raise ValueError('P2WSH output not found in txn')
def replaceAddrPrefix(addr, coin_type, chain_name, addr_type='pubkey_address'):
return encodeAddress(bytes((chainparams[coin_type][chain_name][addr_type],)) + decodeAddress(addr)[1:])
def getOfferProofOfFundsHash(offer_msg, offer_addr):
# TODO: Hash must not include proof_of_funds sig if it exists in offer_msg
h = hashlib.sha256()
h.update(offer_addr.encode('utf-8'))
offer_bytes = offer_msg.SerializeToString()
h.update(offer_bytes)
return h.digest()
def threadPollChainState(swap_client, coin_type):
while not swap_client.delay_event.is_set():
try:
ci = swap_client.ci(coin_type)
if coin_type == Coins.XMR:
new_height = ci.getChainHeight()
if new_height != swap_client.coin_clients[coin_type]['chain_height']:
swap_client.log.debug('New {} block at height: {}'.format(str(coin_type), new_height))
with swap_client.mxDB:
swap_client.coin_clients[coin_type]['chain_height'] = new_height
else:
chain_state = ci.getBlockchainInfo()
if chain_state['bestblockhash'] != swap_client.coin_clients[coin_type]['chain_best_block']:
swap_client.log.debug('New {} block at height: {}'.format(str(coin_type), chain_state['blocks']))
with swap_client.mxDB:
swap_client.coin_clients[coin_type]['chain_height'] = chain_state['blocks']
swap_client.coin_clients[coin_type]['chain_best_block'] = chain_state['bestblockhash']
swap_client.coin_clients[coin_type]['chain_median_time'] = chain_state['mediantime']
except Exception as e:
swap_client.log.warning('threadPollChainState error: {}'.format(str(e)))
swap_client.delay_event.wait(random.randrange(20, 30)) # random to stagger updates
class WatchedOutput(): # Watch for spends
__slots__ = ('bid_id', 'txid_hex', 'vout', 'tx_type', 'swap_type')
def __init__(self, bid_id, txid_hex, vout, tx_type, swap_type):
self.bid_id = bid_id
self.txid_hex = txid_hex
self.vout = vout
self.tx_type = tx_type
self.swap_type = swap_type
class WatchedTransaction():
# TODO
# Watch for presence in mempool (getrawtransaction)
def __init__(self, bid_id, txid_hex, tx_type, swap_type):
self.bid_id = bid_id
self.txid_hex = txid_hex
self.tx_type = tx_type
self.swap_type = swap_type
class BasicSwap(BaseApp):
5 years ago
def __init__(self, fp, data_dir, settings, chain, log_name='BasicSwap'):
super().__init__(fp, data_dir, settings, chain, log_name)
5 years ago
v = __version__.split('.')
self._version = struct.pack('>HHH', int(v[0]), int(v[1]), int(v[2]))
self.check_progress_seconds = self.settings.get('check_progress_seconds', 60)
self.check_watched_seconds = self.settings.get('check_watched_seconds', 60)
self.check_expired_seconds = self.settings.get('check_expired_seconds', 60 * 5)
5 years ago
self.check_events_seconds = self.settings.get('check_events_seconds', 10)
4 years ago
self.check_xmr_swaps_seconds = self.settings.get('check_xmr_swaps_seconds', 20)
self._last_checked_progress = 0
self._last_checked_watched = 0
self._last_checked_expired = 0
self._last_checked_events = 0
self._last_checked_xmr_swaps = 0
self._possibly_revoked_offers = collections.deque([], maxlen=48) # TODO: improve
5 years ago
# TODO: Adjust ranges
self.min_delay_event = self.settings.get('min_delay_event', 10)
self.max_delay_event = self.settings.get('max_delay_event', 60)
self.min_delay_retry = self.settings.get('min_delay_retry', 60)
self.max_delay_retry = self.settings.get('max_delay_retry', 5 * 60)
5 years ago
self.min_sequence_lock_seconds = self.settings.get('min_sequence_lock_seconds', 1 * 60 * 60)
self.max_sequence_lock_seconds = self.settings.get('max_sequence_lock_seconds', 96 * 60 * 60)
self._bid_expired_leeway = 5
self.swaps_in_progress = dict()
self.SMSG_SECONDS_IN_HOUR = 60 * 2 if self.chain == 'regtest' else 60 * 60
self.delay_event = threading.Event()
self.threads = []
5 years ago
# Encode key to match network
wif_prefix = chainparams[Coins.PART][self.chain]['key_prefix']
self.network_key = toWIF(wif_prefix, decodeWif(self.settings['network_key']))
self.network_pubkey = self.settings['network_pubkey']
4 years ago
self.network_addr = pubkeyToAddress(chainparams[Coins.PART][self.chain]['pubkey_address'], bytes.fromhex(self.network_pubkey))
5 years ago
self.sqlite_file = os.path.join(self.data_dir, 'db{}.sqlite'.format('' if self.chain == 'mainnet' else ('_' + self.chain)))
5 years ago
db_exists = os.path.exists(self.sqlite_file)
# HACK: create_all hangs when using tox, unless create_engine is called with echo=True
5 years ago
if not db_exists:
if os.getenv('FOR_TOX'):
self.engine = sa.create_engine('sqlite:///' + self.sqlite_file, echo=True)
else:
self.engine = sa.create_engine('sqlite:///' + self.sqlite_file)
close_all_sessions()
5 years ago
Base.metadata.create_all(self.engine)
self.engine.dispose()
self.engine = sa.create_engine('sqlite:///' + self.sqlite_file)
5 years ago
self.session_factory = sessionmaker(bind=self.engine, expire_on_commit=False)
session = scoped_session(self.session_factory)
try:
self.db_version = session.query(DBKVInt).filter_by(key='db_version').first().value
except Exception:
5 years ago
self.log.info('First run')
self.db_version = CURRENT_DB_VERSION
session.add(DBKVInt(
key='db_version',
value=self.db_version
))
session.commit()
try:
self._contract_count = session.query(DBKVInt).filter_by(key='contract_count').first().value
except Exception:
self._contract_count = 0
session.add(DBKVInt(
key='contract_count',
value=self._contract_count
))
session.commit()
session.close()
session.remove()
5 years ago
self.zmqContext = zmq.Context()
self.zmqSubscriber = self.zmqContext.socket(zmq.SUB)
self.zmqSubscriber.connect(self.settings['zmqhost'] + ':' + str(self.settings['zmqport']))
self.zmqSubscriber.setsockopt_string(zmq.SUBSCRIBE, 'smsg')
for c in Coins:
self.setCoinConnectParams(c)
5 years ago
if self.chain == 'mainnet':
self.coin_clients[Coins.PART]['explorers'].append(ExplorerInsight(
self, Coins.PART,
'https://explorer.particl.io/particl-insight-api'))
self.coin_clients[Coins.LTC]['explorers'].append(ExplorerBitAps(
self, Coins.LTC,
'https://api.bitaps.com/ltc/v1/blockchain'))
self.coin_clients[Coins.LTC]['explorers'].append(ExplorerChainz(
self, Coins.LTC,
'http://chainz.cryptoid.info/ltc/api.dws'))
elif self.chain == 'testnet':
self.coin_clients[Coins.PART]['explorers'].append(ExplorerInsight(
self, Coins.PART,
'https://explorer-testnet.particl.io/particl-insight-api'))
self.coin_clients[Coins.LTC]['explorers'].append(ExplorerBitAps(
self, Coins.LTC,
'https://api.bitaps.com/ltc/testnet/v1/blockchain'))
# non-segwit
# https://testnet.litecore.io/insight-api
5 years ago
random.seed(secrets.randbits(128))
def finalise(self):
self.log.info('Finalise')
with self.mxDB:
self.is_running = False
self.delay_event.set()
if self._network:
self._network.stopNetwork()
self._network = None
for t in self.threads:
t.join()
close_all_sessions()
self.engine.dispose()
def setCoinConnectParams(self, coin):
# Set anything that does not require the daemon to be running
5 years ago
chain_client_settings = self.getChainClientSettings(coin)
bindir = os.path.expanduser(chain_client_settings.get('bindir', ''))
datadir = os.path.expanduser(chain_client_settings.get('datadir', os.path.join(cfg.TEST_DATADIRS, chainparams[coin]['name'])))
5 years ago
connection_type = chain_client_settings.get('connection_type', 'none')
rpcauth = None
if connection_type == 'rpc':
if 'rpcauth' in chain_client_settings:
rpcauth = chain_client_settings['rpcauth']
self.log.debug('Read %s rpc credentials from json settings', coin)
5 years ago
elif 'rpcpassword' in chain_client_settings:
rpcauth = chain_client_settings['rpcuser'] + ':' + chain_client_settings['rpcpassword']
self.log.debug('Read %s rpc credentials from json settings', coin)
5 years ago
session = scoped_session(self.session_factory)
try:
last_height_checked = session.query(DBKVInt).filter_by(key='last_height_checked_' + chainparams[coin]['name']).first().value
except Exception:
last_height_checked = 0
session.close()
session.remove()
5 years ago
self.coin_clients[coin] = {
5 years ago
'coin': coin,
'name': chainparams[coin]['name'],
5 years ago
'connection_type': connection_type,
'bindir': bindir,
5 years ago
'datadir': datadir,
'rpchost': chain_client_settings.get('rpchost', '127.0.0.1'),
5 years ago
'rpcport': chain_client_settings.get('rpcport', chainparams[coin][self.chain]['rpcport']),
'rpcauth': rpcauth,
'blocks_confirmed': chain_client_settings.get('blocks_confirmed', 6),
'conf_target': chain_client_settings.get('conf_target', 2),
5 years ago
'watched_outputs': [],
'last_height_checked': last_height_checked,
'use_segwit': chain_client_settings.get('use_segwit', False),
'use_csv': chain_client_settings.get('use_csv', True),
'core_version_group': chain_client_settings.get('core_version_group', 0),
'pid': None,
'core_version': None,
'explorers': [],
'chain_lookups': chain_client_settings.get('chain_lookups', 'local'),
'restore_height': chain_client_settings.get('restore_height', 0),
'fee_priority': chain_client_settings.get('fee_priority', 0),
# Chain state
'chain_height': None,
'chain_best_block': None,
'chain_median_time': None,
5 years ago
}
if self.coin_clients[coin]['connection_type'] == 'rpc':
if coin == Coins.XMR:
self.coin_clients[coin]['walletrpchost'] = chain_client_settings.get('walletrpchost', '127.0.0.1')
self.coin_clients[coin]['walletrpcport'] = chain_client_settings.get('walletrpcport', chainparams[coin][self.chain]['walletrpcport'])
if 'walletrpcpassword' in chain_client_settings:
self.coin_clients[coin]['walletrpcauth'] = (chain_client_settings['walletrpcuser'], chain_client_settings['walletrpcpassword'])
else:
raise ValueError('Missing XMR wallet rpc credentials.')
def ci(self, coin): # Coin interface
4 years ago
return self.coin_clients[coin]['interface']
def createInterface(self, coin):
if coin == Coins.PART:
return PARTInterface(self.coin_clients[coin], self.chain, self)
elif coin == Coins.BTC:
return BTCInterface(self.coin_clients[coin], self.chain, self)
elif coin == Coins.LTC:
return LTCInterface(self.coin_clients[coin], self.chain, self)
elif coin == Coins.NMC:
return NMCInterface(self.coin_clients[coin], self.chain, self)
elif coin == Coins.XMR:
xmr_i = XMRInterface(self.coin_clients[coin], self.chain, self)
chain_client_settings = self.getChainClientSettings(coin)
xmr_i.setWalletFilename(chain_client_settings['walletfile'])
return xmr_i
else:
raise ValueError('Unknown coin type')
def createPassthroughInterface(self, coin):
if coin == Coins.BTC:
return PassthroughBTCInterface(self.coin_clients[coin], self.chain)
else:
raise ValueError('Unknown coin type')
def setCoinRunParams(self, coin):
cc = self.coin_clients[coin]
if coin == Coins.XMR:
return
if cc['connection_type'] == 'rpc' and cc['rpcauth'] is None:
chain_client_settings = self.getChainClientSettings(coin)
authcookiepath = os.path.join(self.getChainDatadirPath(coin), '.cookie')
pidfilename = cc['name']
if cc['name'] == 'bitcoin' or cc['name'] == 'litecoin' or cc['name'] == 'namecoin':
pidfilename += 'd'
pidfilepath = os.path.join(self.getChainDatadirPath(coin), pidfilename + '.pid')
self.log.debug('Reading %s rpc credentials from auth cookie %s', coin, authcookiepath)
# Wait for daemon to start
# Test pids to ensure authcookie is read for the correct process
datadir_pid = -1
for i in range(20):
try:
with open(pidfilepath, 'rb') as fp:
datadir_pid = int(fp.read().decode('utf-8'))
assert(datadir_pid == cc['pid']), 'Mismatched pid'
assert(os.path.exists(authcookiepath))
except Exception:
time.sleep(0.5)
try:
if os.name != 'nt' or cc['core_version_group'] > 17: # Litecoin on windows doesn't write a pid file
assert(datadir_pid == cc['pid']), 'Mismatched pid'
with open(authcookiepath, 'rb') as fp:
cc['rpcauth'] = fp.read().decode('utf-8')
except Exception as e:
self.log.error('Unable to read authcookie for %s, %s, datadir pid %d, daemon pid %s. Error: %s', str(coin), authcookiepath, datadir_pid, cc['pid'], str(e))
raise ValueError('Error, terminating')
def createCoinInterface(self, coin):
if self.coin_clients[coin]['connection_type'] == 'rpc':
self.coin_clients[coin]['interface'] = self.createInterface(coin)
elif self.coin_clients[coin]['connection_type'] == 'passthrough':
self.coin_clients[coin]['interface'] = self.createPassthroughInterface(coin)
5 years ago
def start(self):
self.log.info('Starting BasicSwap %s, database v%d\n\n', __version__, self.db_version)
self.log.info('sqlalchemy version %s', sa.__version__)
5 years ago
self.upgradeDatabase(self.db_version)
for c in Coins:
self.setCoinRunParams(c)
self.createCoinInterface(c)
if self.coin_clients[c]['connection_type'] == 'rpc':
self.waitForDaemonRPC(c)
ci = self.ci(c)
core_version = ci.getDaemonVersion()
self.log.info('%s Core version %d', ci.coin_name(), core_version)
self.coin_clients[c]['core_version'] = core_version
5 years ago
t = threading.Thread(target=threadPollChainState, args=(self, c))
self.threads.append(t)
t.start()
if c == Coins.PART:
self.coin_clients[c]['have_spent_index'] = ci.haveSpentIndex()
# Sanity checks
rv = self.callcoinrpc(c, 'extkey')
if 'result' in rv and 'No keys to list.' in rv['result']:
raise ValueError('No keys loaded.')
if self.callcoinrpc(c, 'getstakinginfo')['enabled'] is not False:
self.log.warning('%s staking is not disabled.', ci.coin_name())
elif c == Coins.XMR:
ci.ensureWalletExists()
self.checkWalletSeed(c)
if 'p2p_host' in self.settings:
network_key = self.getNetworkKey(1)
self._network = bsn.Network(self.settings['p2p_host'], self.settings['p2p_port'], network_key, self)
self._network.startNetwork()
5 years ago
self.initialise()
def stopDaemon(self, coin):
if coin == Coins.XMR:
return
num_tries = 10
authcookiepath = os.path.join(self.getChainDatadirPath(coin), '.cookie')
stopping = False
try:
for i in range(num_tries):
rv = self.callcoincli(coin, 'stop', timeout=10)
self.log.debug('Trying to stop %s', str(coin))
stopping = True
time.sleep(i + 1)
except Exception as ex:
if 'Could not connect' in str(ex):
if stopping:
for i in range(30):
# The lock file doesn't get deleted
# Using .cookie is a temporary workaround, will only work if rpc password is unset.
# TODO: Query lock on .lock properly
if os.path.exists(authcookiepath):
self.log.debug('Waiting on .cookie file %s', str(coin))
time.sleep(i + 1)
time.sleep(4) # Extra time to settle
return
self.log.error('stopDaemon %s', str(ex))
traceback.print_exc()
raise ValueError('Could not stop {}'.format(str(coin)))
def stopDaemons(self):
for c in Coins:
chain_client_settings = self.getChainClientSettings(c)
if self.coin_clients[c]['connection_type'] == 'rpc' and chain_client_settings['manage_daemon'] is True:
self.stopDaemon(c)
5 years ago
def upgradeDatabase(self, db_version):
if db_version >= CURRENT_DB_VERSION:
return
self.log.info('Upgrading database from version %d to %d.', db_version, CURRENT_DB_VERSION)
5 years ago
while True:
if db_version == 6:
session = scoped_session(self.session_factory)
session.execute('ALTER TABLE bids ADD COLUMN security_token BLOB')
session.execute('ALTER TABLE offers ADD COLUMN security_token BLOB')
db_version += 1
self.db_version = db_version
self.setIntKVInSession('db_version', db_version, session)
session.commit()
session.close()
session.remove()
self.log.info('Upgraded database to version {}'.format(self.db_version))
continue
if db_version == 7:
session = scoped_session(self.session_factory)
session.execute('ALTER TABLE transactions ADD COLUMN block_hash BLOB')
session.execute('ALTER TABLE transactions ADD COLUMN block_height INTEGER')
session.execute('ALTER TABLE transactions ADD COLUMN block_time INTEGER')
db_version += 1
self.db_version = db_version
self.setIntKVInSession('db_version', db_version, session)
session.commit()
session.close()
session.remove()
self.log.info('Upgraded database to version {}'.format(self.db_version))
continue
break
if db_version != CURRENT_DB_VERSION:
raise ValueError('Unable to upgrade database.')
def waitForDaemonRPC(self, coin_type):
5 years ago
for i in range(21):
if not self.is_running:
return
try:
self.coin_clients[coin_type]['interface'].testDaemonRPC()
return
5 years ago
except Exception as ex:
5 years ago
self.log.warning('Can\'t connect to %s RPC: %s. Trying again in %d second/s.', coin_type, str(ex), (1 + i))
5 years ago
time.sleep(1 + i)
self.log.error('Can\'t connect to %s RPC, exiting.', coin_type)
self.stopRunning(1) # systemd will try restart if fail_code != 0
5 years ago
def checkSynced(self, coin_from, coin_to):
check_coins = (coin_from, coin_to)
for c in check_coins:
if self.coin_clients[c]['connection_type'] != 'rpc':
continue
if c == Coins.XMR:
continue # TODO
synced = round(self.ci(c).getBlockchainInfo()['verificationprogress'], 3)
if synced < 1.0:
raise ValueError('{} chain is still syncing, currently at {}.'.format(self.coin_clients[c]['name'], synced))
def initialiseWallet(self, coin_type):
if coin_type == Coins.PART:
return
ci = self.ci(coin_type)
self.log.info('Initialising {} wallet.'.format(ci.coin_name()))
if coin_type == Coins.XMR:
key_view = self.getWalletKey(coin_type, 1, for_ed25519=True)
key_spend = self.getWalletKey(coin_type, 2, for_ed25519=True)
ci.initialiseWallet(key_view, key_spend)
root_address = ci.getAddressFromKeys(key_view, key_spend)
key_str = 'main_wallet_addr_' + chainparams[coin_type]['name']
self.setStringKV(key_str, root_address)
return
root_key = self.getWalletKey(coin_type, 1)
root_hash = ci.getAddressHashFromKey(root_key)[::-1]
ci.initialiseWallet(root_key)
key_str = 'main_wallet_seedid_' + chainparams[coin_type]['name']
self.setStringKV(key_str, root_hash.hex())
def setIntKVInSession(self, str_key, int_val, session):
kv = session.query(DBKVInt).filter_by(key=str_key).first()
if not kv:
kv = DBKVInt(key=str_key, value=int_val)
else:
kv.value = int_val
session.add(kv)
def setIntKV(self, str_key, int_val):
self.mxDB.acquire()
try:
session = scoped_session(self.session_factory)
self.setIntKVInSession(str_key, int_val, session)
session.commit()
finally:
session.close()
session.remove()
self.mxDB.release()
def setStringKV(self, str_key, str_val):
with self.mxDB:
try:
session = scoped_session(self.session_factory)
kv = session.query(DBKVString).filter_by(key=str_key).first()
if not kv:
kv = DBKVString(key=str_key, value=str_val)
else:
kv.value = str_val
session.add(kv)
session.commit()
finally:
session.close()
session.remove()
def getStringKV(self, str_key):
self.mxDB.acquire()
try:
session = scoped_session(self.session_factory)
v = session.query(DBKVString).filter_by(key=str_key).first()
if not v:
return None
return v.value
finally:
session.close()
session.remove()
self.mxDB.release()
def activateBid(self, session, bid):
if bid.bid_id in self.swaps_in_progress:
self.log.debug('Bid %s is already in progress', bid.bid_id.hex())
self.log.debug('Loading active bid %s', bid.bid_id.hex())
offer = session.query(Offer).filter_by(offer_id=bid.offer_id).first()
assert(offer), 'Offer not found'
if offer.swap_type == SwapTypes.XMR_SWAP:
xmr_swap = session.query(XmrSwap).filter_by(bid_id=bid.bid_id).first()
self.loadBidTxns(bid, session)
self.watchXmrSwap(bid, offer, xmr_swap)
else:
bid.initiate_tx = session.query(SwapTx).filter(sa.and_(SwapTx.bid_id == bid.bid_id, SwapTx.tx_type == TxTypes.ITX)).first()
bid.participate_tx = session.query(SwapTx).filter(sa.and_(SwapTx.bid_id == bid.bid_id, SwapTx.tx_type == TxTypes.PTX)).first()
self.swaps_in_progress[bid.bid_id] = (bid, offer)
coin_from = Coins(offer.coin_from)
coin_to = Coins(offer.coin_to)
if bid.initiate_tx and bid.initiate_tx.txid:
self.addWatchedOutput(coin_from, bid.bid_id, bid.initiate_tx.txid.hex(), bid.initiate_tx.vout, BidStates.SWAP_INITIATED)
if bid.participate_tx and bid.participate_tx.txid:
self.addWatchedOutput(coin_to, bid.bid_id, bid.participate_tx.txid.hex(), bid.participate_tx.vout, BidStates.SWAP_PARTICIPATING)
if self.coin_clients[coin_from]['last_height_checked'] < 1:
if bid.initiate_tx and bid.initiate_tx.chain_height:
self.coin_clients[coin_from]['last_height_checked'] = bid.initiate_tx.chain_height
if self.coin_clients[coin_to]['last_height_checked'] < 1:
if bid.participate_tx and bid.participate_tx.chain_height:
self.coin_clients[coin_to]['last_height_checked'] = bid.participate_tx.chain_height
# TODO process addresspool if bid has previously been abandoned
def deactivateBid(self, session, offer, bid):
# Remove from in progress
self.log.debug('Removing bid from in-progress: %s', bid.bid_id.hex())
self.swaps_in_progress.pop(bid.bid_id, None)
bid.in_progress = 0
if session is None:
self.saveBid(bid.bid_id, bid)
# Remove any watched outputs
self.removeWatchedOutput(Coins(offer.coin_from), bid.bid_id, None)
self.removeWatchedOutput(Coins(offer.coin_to), bid.bid_id, None)
if bid.state == BidStates.BID_ABANDONED or bid.state == BidStates.SWAP_COMPLETED:
# Return unused addrs to pool
itx_state = bid.getITxState()
ptx_state = bid.getPTxState()
if itx_state is not None and itx_state != TxStates.TX_REDEEMED:
self.returnAddressToPool(bid.bid_id, TxTypes.ITX_REDEEM)
if itx_state is not None and itx_state != TxStates.TX_REFUNDED:
self.returnAddressToPool(bid.bid_id, TxTypes.ITX_REFUND)
if ptx_state is not None and ptx_state != TxStates.TX_REDEEMED:
self.returnAddressToPool(bid.bid_id, TxTypes.PTX_REDEEM)
if ptx_state is not None and ptx_state != TxStates.TX_REFUNDED:
self.returnAddressToPool(bid.bid_id, TxTypes.PTX_REFUND)
use_session = None
try:
if session:
use_session = session
else:
self.mxDB.acquire()
use_session = scoped_session(self.session_factory)
# Remove any delayed events
if self.debug:
use_session.execute('UPDATE eventqueue SET active_ind = 2 WHERE linked_id = x\'{}\' '.format(bid.bid_id.hex()))
else:
use_session.execute('DELETE FROM eventqueue WHERE linked_id = x\'{}\' '.format(bid.bid_id.hex()))
# Unlock locked inputs (TODO)
if offer.swap_type == SwapTypes.XMR_SWAP:
xmr_swap = use_session.query(XmrSwap).filter_by(bid_id=bid.bid_id).first()
if xmr_swap:
try:
self.ci(offer.coin_from).unlockInputs(xmr_swap.a_lock_tx)
except Exception as e:
if self.debug:
self.log.info('unlockInputs failed {}'.format(str(e)))
pass # Invalid parameter, unknown transaction
finally:
if session is None:
use_session.commit()
use_session.close()
use_session.remove()
self.mxDB.release()
5 years ago
def loadFromDB(self):
self.log.info('Loading data from db')
self.mxDB.acquire()
try:
session = scoped_session(self.session_factory)
for bid in session.query(Bid):
if bid.in_progress == 1 or (bid.state and bid.state > BidStates.BID_RECEIVED and bid.state < BidStates.SWAP_COMPLETED):
self.activateBid(session, bid)
5 years ago
finally:
session.close()
session.remove()
self.mxDB.release()
def initialise(self):
self.log.debug('network_key %s\nnetwork_pubkey %s\nnetwork_addr %s',
self.network_key, self.network_pubkey, self.network_addr)
ro = self.callrpc('smsglocalkeys')
found = False
for k in ro['smsg_keys']:
if k['address'] == self.network_addr:
found = True
break
if not found:
self.log.info('Importing network key to SMSG')
self.callrpc('smsgimportprivkey', [self.network_key, 'basicswap offers'])
ro = self.callrpc('smsglocalkeys', ['anon', '-', self.network_addr])
assert(ro['result'] == 'Success.'), 'smsglocalkeys failed'
5 years ago
# TODO: Ensure smsg is enabled for the active wallet.
self.loadFromDB()
# Scan inbox
# TODO: Redundant? small window for zmq messages to go unnoticed during startup?
# options = {'encoding': 'hex'}
options = {'encoding': 'none'}
5 years ago
ro = self.callrpc('smsginbox', ['unread', '', options])
nm = 0
for msg in ro['messages']:
# TODO: Remove workaround for smsginbox bug
get_msg = self.callrpc('smsg', [msg['msgid'], {'encoding': 'hex', 'setread': True}])
self.processMsg(get_msg)
5 years ago
nm += 1
self.log.info('Scanned %d unread messages.', nm)
def validateSwapType(self, coin_from, coin_to, swap_type):
if coin_from == Coins.XMR:
raise ValueError('TODO: xmr coin_from')
if coin_to == Coins.XMR and swap_type != SwapTypes.XMR_SWAP:
raise ValueError('Invalid swap type for XMR')
5 years ago
def validateOfferAmounts(self, coin_from, coin_to, amount, rate, min_bid_amount):
assert(amount >= min_bid_amount), 'amount < min_bid_amount'
assert(amount > chainparams[coin_from][self.chain]['min_amount']), 'From amount below min value for chain'
assert(amount < chainparams[coin_from][self.chain]['max_amount']), 'From amount above max value for chain'
amount_to = int((amount * rate) // self.ci(coin_from).COIN())
5 years ago
assert(amount_to > chainparams[coin_to][self.chain]['min_amount']), 'To amount below min value for chain'
assert(amount_to < chainparams[coin_to][self.chain]['max_amount']), 'To amount above max value for chain'
def validateOfferLockValue(self, coin_from, coin_to, lock_type, lock_value):
if lock_type == OfferMessage.SEQUENCE_LOCK_TIME:
assert(lock_value >= self.min_sequence_lock_seconds and lock_value <= self.max_sequence_lock_seconds), 'Invalid lock_value time'
assert(self.coin_clients[coin_from]['use_csv'] and self.coin_clients[coin_to]['use_csv']), 'Both coins need CSV activated.'
elif lock_type == OfferMessage.SEQUENCE_LOCK_BLOCKS:
assert(lock_value >= 5 and lock_value <= 1000), 'Invalid lock_value blocks'
assert(self.coin_clients[coin_from]['use_csv'] and self.coin_clients[coin_to]['use_csv']), 'Both coins need CSV activated.'
elif lock_type == ABS_LOCK_TIME:
# TODO: range?
5 years ago
assert(not self.coin_clients[coin_from]['use_csv'] or not self.coin_clients[coin_to]['use_csv']), 'Should use CSV.'
assert(lock_value >= 4 * 60 * 60 and lock_value <= 96 * 60 * 60), 'Invalid lock_value time'
elif lock_type == ABS_LOCK_BLOCKS:
# TODO: range?
5 years ago
assert(not self.coin_clients[coin_from]['use_csv'] or not self.coin_clients[coin_to]['use_csv']), 'Should use CSV.'
assert(lock_value >= 10 and lock_value <= 1000), 'Invalid lock_value blocks'
else:
raise ValueError('Unknown locktype')
5 years ago
def postOffer(self, coin_from, coin_to, amount, rate, min_bid_amount, swap_type,
lock_type=SEQUENCE_LOCK_TIME, lock_value=48 * 60 * 60, auto_accept_bids=False, addr_send_from=None, extra_options={}):
5 years ago
# Offer to send offer.amount_from of coin_from in exchange for offer.amount_from * offer.rate of coin_to
assert(coin_from != coin_to), 'coin_from == coin_to'
try:
coin_from_t = Coins(coin_from)
ci_from = self.ci(coin_from_t)
5 years ago
except Exception:
raise ValueError('Unknown coin from type')
try:
coin_to_t = Coins(coin_to)
ci_to = self.ci(coin_to_t)
5 years ago
except Exception:
raise ValueError('Unknown coin to type')
self.validateSwapType(coin_from_t, coin_to_t, swap_type)
5 years ago
self.validateOfferAmounts(coin_from_t, coin_to_t, amount, rate, min_bid_amount)
self.validateOfferLockValue(coin_from_t, coin_to_t, lock_type, lock_value)
5 years ago
self.mxDB.acquire()
session = None
5 years ago
try:
self.checkSynced(coin_from_t, coin_to_t)
offer_addr = self.callrpc('getnewaddress') if addr_send_from is None else addr_send_from
offer_created_at = int(time.time())
4 years ago
msg_buf = OfferMessage()
5 years ago
msg_buf.coin_from = int(coin_from)
msg_buf.coin_to = int(coin_to)
msg_buf.amount_from = int(amount)
5 years ago
msg_buf.rate = int(rate)
msg_buf.min_bid_amount = int(min_bid_amount)
5 years ago
msg_buf.time_valid = 60 * 60
msg_buf.lock_type = lock_type
msg_buf.lock_value = lock_value
msg_buf.swap_type = swap_type
if 'from_fee_override' in extra_options:
msg_buf.fee_rate_from = make_int(extra_options['from_fee_override'], self.ci(coin_from).exp())
else:
# TODO: conf_target = ci_from.settings.get('conf_target', 2)
conf_target = 2
if 'from_fee_conf_target' in extra_options:
conf_target = extra_options['from_fee_conf_target']
fee_rate, fee_src = self.getFeeRateForCoin(coin_from, conf_target)
if 'from_fee_multiplier_percent' in extra_options:
fee_rate *= extra_options['fee_multiplier'] / 100.0
msg_buf.fee_rate_from = make_int(fee_rate, self.ci(coin_from).exp())
if 'to_fee_override' in extra_options:
msg_buf.fee_rate_to = make_int(extra_options['to_fee_override'], self.ci(coin_to).exp())
else:
# TODO: conf_target = ci_to.settings.get('conf_target', 2)
conf_target = 2
if 'to_fee_conf_target' in extra_options:
conf_target = extra_options['to_fee_conf_target']
fee_rate, fee_src = self.getFeeRateForCoin(coin_to, conf_target)
if 'to_fee_multiplier_percent' in extra_options:
fee_rate *= extra_options['fee_multiplier'] / 100.0
msg_buf.fee_rate_to = make_int(fee_rate, self.ci(coin_to).exp())
4 years ago
if swap_type == SwapTypes.XMR_SWAP:
xmr_offer = XmrOffer()
# Delay before the chain a lock refund tx can be mined
xmr_offer.lock_time_1 = ci_from.getExpectedSequence(lock_type, lock_value)
# Delay before the follower can spend from the chain a lock refund tx
xmr_offer.lock_time_2 = ci_from.getExpectedSequence(lock_type, lock_value)
4 years ago
xmr_offer.a_fee_rate = msg_buf.fee_rate_from
xmr_offer.b_fee_rate = msg_buf.fee_rate_to # Unused: TODO - Set priority?
4 years ago
proof_of_funds_hash = getOfferProofOfFundsHash(msg_buf, offer_addr)
proof_addr, proof_sig = self.getProofOfFunds(coin_from_t, int(amount), proof_of_funds_hash)
# TODO: For now proof_of_funds is just a client side checkm, may need to be sent with offers in future however.
5 years ago
offer_bytes = msg_buf.SerializeToString()
payload_hex = str.format('{:02x}', MessageTypes.OFFER) + offer_bytes.hex()
self.callrpc('smsgaddlocaladdress', [offer_addr]) # Enable receiving smsg
options = {'decodehex': True, 'ttl_is_seconds': True}
msg_valid = self.SMSG_SECONDS_IN_HOUR * 1
ro = self.callrpc('smsgsend', [offer_addr, self.network_addr, payload_hex, False, msg_valid, False, options])
5 years ago
msg_id = ro['msgid']
offer_id = bytes.fromhex(msg_id)
security_token = extra_options.get('security_token', None)
if security_token is not None and len(security_token) != 20:
raise ValueError('Security token must be 20 bytes long.')
5 years ago
session = scoped_session(self.session_factory)
offer = Offer(
offer_id=offer_id,
active_ind=1,
5 years ago
coin_from=msg_buf.coin_from,
coin_to=msg_buf.coin_to,
amount_from=msg_buf.amount_from,
rate=msg_buf.rate,
min_bid_amount=msg_buf.min_bid_amount,
time_valid=msg_buf.time_valid,
lock_type=int(msg_buf.lock_type),
lock_value=msg_buf.lock_value,
swap_type=msg_buf.swap_type,
addr_from=offer_addr,
created_at=offer_created_at,
expire_at=offer_created_at + msg_buf.time_valid,
5 years ago
was_sent=True,
auto_accept_bids=auto_accept_bids,
security_token=security_token)
5 years ago
offer.setState(OfferStates.OFFER_SENT)
4 years ago
if swap_type == SwapTypes.XMR_SWAP:
xmr_offer.offer_id = offer_id
session.add(xmr_offer)
session.add(offer)
5 years ago
session.add(SentOffer(offer_id=offer_id))
if addr_send_from is None:
session.add(SmsgAddress(addr=offer_addr, use_type=MessageTypes.OFFER))
5 years ago
session.commit()
5 years ago
finally:
if session:
session.close()
session.remove()
5 years ago
self.mxDB.release()
self.log.info('Sent OFFER %s', offer_id.hex())
return offer_id
def revokeOffer(self, offer_id, security_token=None):
self.log.info('Revoking offer %s', offer_id.hex())
session = None
self.mxDB.acquire()
try:
session = scoped_session(self.session_factory)
offer = session.query(Offer).filter_by(offer_id=offer_id).first()
if offer.security_token is not None and offer.security_token != security_token:
raise ValueError('Mismatched security token')
msg_buf = OfferRevokeMessage()
msg_buf.offer_msg_id = offer_id
signature_enc = self.callcoinrpc(Coins.PART, 'signmessage', [offer.addr_from, offer_id.hex() + '_revoke'])
msg_buf.signature = base64.b64decode(signature_enc)
msg_bytes = msg_buf.SerializeToString()
payload_hex = str.format('{:02x}', MessageTypes.OFFER_REVOKE) + msg_bytes.hex()
options = {'decodehex': True, 'ttl_is_seconds': True}
ro = self.callrpc('smsgsend', [offer.addr_from, self.network_addr, payload_hex, False, offer.time_valid, False, options])
msg_id = ro['msgid']
finally:
if session:
session.close()
session.remove()
self.mxDB.release()
def grindForEd25519Key(self, coin_type, evkey, key_path_base):
ci = self.ci(coin_type)
nonce = 1
while True:
key_path = key_path_base + '/{}'.format(nonce)
extkey = self.callcoinrpc(Coins.PART, 'extkey', ['info', evkey, key_path])['key_info']['result']
privkey = decodeWif(self.callcoinrpc(Coins.PART, 'extkey', ['info', extkey])['key_info']['privkey'])
if ci.isValidKey(privkey):
return privkey
nonce += 1
if nonce > 1000:
raise ValueError('grindForEd25519Key failed')
def getWalletKey(self, coin_type, key_num, for_ed25519=False):
evkey = self.callcoinrpc(Coins.PART, 'extkey', ['account', 'default', 'true'])['evkey']
key_path_base = '44445555h/1h/{}/{}'.format(int(coin_type), key_num)
if not for_ed25519:
extkey = self.callcoinrpc(Coins.PART, 'extkey', ['info', evkey, key_path_base])['key_info']['result']
return decodeWif(self.callcoinrpc(Coins.PART, 'extkey', ['info', extkey])['key_info']['privkey'])
return self.grindForEd25519Key(coin_type, evkey, key_path_base)
def getPathKey(self, coin_from, coin_to, offer_created_at, contract_count, key_no, for_ed25519=False):
4 years ago
evkey = self.callcoinrpc(Coins.PART, 'extkey', ['account', 'default', 'true'])['evkey']
ci = self.ci(coin_to)
days = offer_created_at // 86400
secs = offer_created_at - days * 86400
key_path_base = '44445555h/999999/{}/{}/{}/{}/{}/{}'.format(int(coin_from), int(coin_to), days, secs, contract_count, key_no)
if not for_ed25519:
4 years ago
extkey = self.callcoinrpc(Coins.PART, 'extkey', ['info', evkey, key_path_base])['key_info']['result']
return decodeWif(self.callcoinrpc(Coins.PART, 'extkey', ['info', extkey])['key_info']['privkey'])
return self.grindForEd25519Key(coin_to, evkey, key_path_base)
4 years ago
def getNetworkKey(self, key_num):
evkey = self.callcoinrpc(Coins.PART, 'extkey', ['account', 'default', 'true'])['evkey']
key_path = '44445556h/1h/{}'.format(int(key_num))
extkey = self.callcoinrpc(Coins.PART, 'extkey', ['info', evkey, key_path])['key_info']['result']
return decodeWif(self.callcoinrpc(Coins.PART, 'extkey', ['info', extkey])['key_info']['privkey'])
5 years ago
def getContractPubkey(self, date, contract_count):
account = self.callcoinrpc(Coins.PART, 'extkey', ['account'])
# Derive an address to use for a contract
evkey = self.callcoinrpc(Coins.PART, 'extkey', ['account', 'default', 'true'])['evkey']
# Should the coin path be included?
path = '44445555h'
path += '/' + str(date.year) + '/' + str(date.month) + '/' + str(date.day)
path += '/' + str(contract_count)
extkey = self.callcoinrpc(Coins.PART, 'extkey', ['info', evkey, path])['key_info']['result']
pubkey = self.callcoinrpc(Coins.PART, 'extkey', ['info', extkey])['key_info']['pubkey']
return bytes.fromhex(pubkey)
def getContractPrivkey(self, date, contract_count):
# Derive an address to use for a contract
evkey = self.callcoinrpc(Coins.PART, 'extkey', ['account', 'default', 'true'])['evkey']
path = '44445555h'
path += '/' + str(date.year) + '/' + str(date.month) + '/' + str(date.day)
path += '/' + str(contract_count)
extkey = self.callcoinrpc(Coins.PART, 'extkey', ['info', evkey, path])['key_info']['result']
privkey = self.callcoinrpc(Coins.PART, 'extkey', ['info', extkey])['key_info']['privkey']
raw = decodeAddress(privkey)[1:]
if len(raw) > 32:
raw = raw[:32]
return raw
def getContractSecret(self, date, contract_count):
# Derive a key to use for a contract secret
evkey = self.callcoinrpc(Coins.PART, 'extkey', ['account', 'default', 'true'])['evkey']
path = '44445555h/99999'
path += '/' + str(date.year) + '/' + str(date.month) + '/' + str(date.day)
path += '/' + str(contract_count)
return hashlib.sha256(bytes(self.callcoinrpc(Coins.PART, 'extkey', ['info', evkey, path])['key_info']['result'], 'utf-8')).digest()
def getReceiveAddressFromPool(self, coin_type, bid_id, tx_type):
self.log.debug('Get address from pool bid_id {}, type {}, coin {}'.format(bid_id.hex(), tx_type, coin_type))
self.mxDB.acquire()
try:
session = scoped_session(self.session_factory)
record = session.query(PooledAddress).filter(sa.and_(PooledAddress.coin_type == int(coin_type), PooledAddress.bid_id == None)).first() # noqa: E712,E711
if not record:
address = self.getReceiveAddressForCoin(coin_type)
record = PooledAddress(
addr=address,
coin_type=int(coin_type))
record.bid_id = bid_id
record.tx_type = tx_type
addr = record.addr
session.add(record)
session.commit()
finally:
session.close()
session.remove()
self.mxDB.release()
return addr
def returnAddressToPool(self, bid_id, tx_type):
self.log.debug('Return address to pool bid_id {}, type {}'.format(bid_id.hex(), tx_type))
self.mxDB.acquire()
try:
session = scoped_session(self.session_factory)
try:
record = session.query(PooledAddress).filter(sa.and_(PooledAddress.bid_id == bid_id, PooledAddress.tx_type == tx_type)).one()
self.log.debug('Returning address to pool addr {}'.format(record.addr))
record.bid_id = None
session.commit()
except Exception as ex:
pass
finally:
session.close()
session.remove()
self.mxDB.release()
5 years ago
def getReceiveAddressForCoin(self, coin_type):
new_addr = self.coin_clients[coin_type]['interface'].getNewAddress(self.coin_clients[coin_type]['use_segwit'])
5 years ago
self.log.debug('Generated new receive address %s for %s', new_addr, str(coin_type))
return new_addr
def getRelayFeeRateForCoin(self, coin_type):
return self.callcoinrpc(coin_type, 'getnetworkinfo')['relayfee']
def getFeeRateForCoin(self, coin_type, conf_target=2):
chain_client_settings = self.getChainClientSettings(coin_type)
override_feerate = chain_client_settings.get('override_feerate', None)
if override_feerate:
self.log.debug('Fee rate override used for %s: %f', str(coin_type), override_feerate)
return override_feerate, 'override_feerate'
return self.ci(coin_type).get_fee_rate(conf_target)
5 years ago
def estimateWithdrawFee(self, coin_type, fee_rate):
if coin_type == Coins.XMR:
self.log.error('TODO: estimateWithdrawFee XMR')
return None
tx_vsize = self.getContractSpendTxVSize(coin_type)
est_fee = (fee_rate * tx_vsize) / 1000
return est_fee
def withdrawCoin(self, coin_type, value, addr_to, subfee):
self.log.info('withdrawCoin %s %s to %s %s', value, self.getTicker(coin_type), addr_to, ' subfee' if subfee else '')
ci = self.ci(coin_type)
return ci.withdrawCoin(value, addr_to, subfee)
5 years ago
def cacheNewAddressForCoin(self, coin_type):
self.log.debug('cacheNewAddressForCoin %s', coin_type)
key_str = 'receive_addr_' + chainparams[coin_type]['name']
5 years ago
addr = self.getReceiveAddressForCoin(coin_type)
self.setStringKV(key_str, addr)
5 years ago
return addr
def checkWalletSeed(self, c):
ci = self.ci(c)
if c == Coins.PART:
return True # TODO
if c == Coins.XMR:
expect_address = self.getStringKV('main_wallet_addr_' + chainparams[c]['name'])
if expect_address is None:
self.log.warning('Can\'t find expected main wallet address for coin {}'.format(ci.coin_name()))
return False
if expect_address == ci.getMainWalletAddress():
ci.setWalletSeedWarning(False)
return True
self.log.warning('Wallet for coin {} not derived from swap seed.'.format(ci.coin_name()))
return False
expect_seedid = self.getStringKV('main_wallet_seedid_' + chainparams[c]['name'])
if expect_seedid is None:
self.log.warning('Can\'t find expected wallet seed id for coin {}'.format(ci.coin_name()))
return False
if expect_seedid == ci.getWalletSeedID():
ci.setWalletSeedWarning(False)
return True
self.log.warning('Wallet for coin {} not derived from swap seed.'.format(ci.coin_name()))
return False
def reseedWallet(self, coin_type):
self.log.info('reseedWallet %s', coin_type)
ci = self.ci(coin_type)
if ci.knownWalletSeed():
raise ValueError('{} wallet seed is already derived from the particl mnemonic'.format(ci.coin_name()))
self.initialiseWallet(coin_type)
# TODO: How to scan pruned blocks?
if not self.checkWalletSeed(coin_type):
if coin_type == Coins.XMR:
raise ValueError('TODO: How to reseed XMR wallet?')
else:
raise ValueError('Wallet seed doesn\'t match expected.')
5 years ago
def getCachedAddressForCoin(self, coin_type):
self.log.debug('getCachedAddressForCoin %s', coin_type)
# TODO: auto refresh after used
key_str = 'receive_addr_' + chainparams[coin_type]['name']
self.mxDB.acquire()
try:
session = scoped_session(self.session_factory)
try:
addr = session.query(DBKVString).filter_by(key=key_str).first().value
except Exception:
addr = self.getReceiveAddressForCoin(coin_type)
session.add(DBKVString(
key=key_str,
value=addr
))
session.commit()
finally:
session.close()
session.remove()
self.mxDB.release()
5 years ago
return addr
def getNewContractId(self):
self.mxDB.acquire()
try:
self._contract_count += 1
session = scoped_session(self.session_factory)
session.execute('UPDATE kv_int SET value = {} WHERE KEY="contract_count"'.format(self._contract_count))
session.commit()
finally:
session.close()
session.remove()
self.mxDB.release()
5 years ago
return self._contract_count
def getProofOfFunds(self, coin_type, amount_for, extra_commit_bytes):
ci = self.ci(coin_type)
self.log.debug('getProofOfFunds %s %s', ci.coin_name(), ci.format_amount(amount_for))
5 years ago
if self.coin_clients[coin_type]['connection_type'] != 'rpc':
return (None, None)
# TODO: Lock unspent and use same output/s to fund bid
unspent_addr = dict()
unspent = self.callcoinrpc(coin_type, 'listunspent')
for u in unspent:
unspent_addr[u['address']] = unspent_addr.get(u['address'], 0) + ci.make_int(u['amount'], r=1)
5 years ago
sign_for_addr = None
for addr, value in unspent_addr.items():
if value >= amount_for:
sign_for_addr = addr
break
assert(sign_for_addr is not None), 'Could not find address with enough funds for proof'
self.log.debug('sign_for_addr %s', sign_for_addr)
if self.coin_clients[coin_type]['use_segwit']:
# 'Address does not refer to key' for non p2pkh
addrinfo = self.callcoinrpc(coin_type, 'getaddressinfo', [sign_for_addr])
pkh = addrinfo['scriptPubKey'][4:]
sign_for_addr = encodeAddress(bytes((chainparams[coin_type][self.chain]['pubkey_address'],)) + bytes.fromhex(pkh))
self.log.debug('sign_for_addr converted %s', sign_for_addr)
signature = self.callcoinrpc(coin_type, 'signmessage', [sign_for_addr, sign_for_addr + '_swap_proof_' + extra_commit_bytes.hex()])
5 years ago
return (sign_for_addr, signature)
def saveBidInSession(self, bid_id, bid, session, xmr_swap=None, save_in_progress=None):
session.add(bid)
if bid.initiate_tx:
session.add(bid.initiate_tx)
if bid.participate_tx:
session.add(bid.participate_tx)
if bid.xmr_a_lock_tx:
session.add(bid.xmr_a_lock_tx)
if bid.xmr_a_lock_spend_tx:
session.add(bid.xmr_a_lock_spend_tx)
if bid.xmr_b_lock_tx:
session.add(bid.xmr_b_lock_tx)
for tx_type, tx in bid.txns.items():
session.add(tx)
4 years ago
if xmr_swap is not None:
session.add(xmr_swap)
if save_in_progress is not None:
if not isinstance(save_in_progress, Offer):
raise ValueError('Must specify offer for save_in_progress')
self.swaps_in_progress[bid_id] = (bid, save_in_progress) # (bid, offer)
4 years ago
def saveBid(self, bid_id, bid, xmr_swap=None):
5 years ago
self.mxDB.acquire()
try:
session = scoped_session(self.session_factory)
4 years ago
self.saveBidInSession(bid_id, bid, session, xmr_swap)
session.commit()
finally:
4 years ago
session.close()
session.remove()
self.mxDB.release()
def saveToDB(self, db_record):
self.mxDB.acquire()
try:
session = scoped_session(self.session_factory)
session.add(db_record)
5 years ago
session.commit()
finally:
5 years ago
session.close()
session.remove()
self.mxDB.release()
def createEventInSession(self, delay, event_type, linked_id, session):
5 years ago
self.log.debug('createEvent %d %s', event_type, linked_id.hex())
now = int(time.time())
event = EventQueue(
active_ind=1,
created_at=now,
trigger_at=now + delay,
event_type=event_type,
linked_id=linked_id)
session.add(event)
def createEvent(self, delay, event_type, linked_id):
# self.log.debug('createEvent %d %s', event_type, linked_id.hex())
5 years ago
self.mxDB.acquire()
try:
session = scoped_session(self.session_factory)
self.createEventInSession(delay, event_type, linked_id, session)
5 years ago
session.commit()
finally:
5 years ago
session.close()
session.remove()
self.mxDB.release()
def logBidEvent(self, bid, event_type, event_msg, session):
self.log.debug('logBidEvent %s %s', bid.bid_id.hex(), event_type)
entry = EventLog(
active_ind=1,
created_at=int(time.time()),
linked_type=TableTypes.BID,
linked_id=bid.bid_id,
event_type=int(event_type),
event_msg=event_msg)
if session is not None:
session.add(entry)
return
self.mxDB.acquire()
try:
session = scoped_session(self.session_factory)
session.add(entry)
session.commit()
finally:
session.close()
session.remove()
self.mxDB.release()
def countBidEvents(self, bid, event_type, session):
q = session.execute('SELECT COUNT(*) FROM eventlog WHERE linked_type = {} AND linked_id = x\'{}\' AND event_type = {}'.format(int(TableTypes.BID), bid.bid_id.hex(), int(event_type))).first()
return q[0]
def postBid(self, offer_id, amount, addr_send_from=None, extra_options={}):
5 years ago
# Bid to send bid.amount * offer.rate of coin_to in exchange for bid.amount of coin_from
self.log.debug('postBid %s', offer_id.hex())
offer = self.getOffer(offer_id)
assert(offer), 'Offer not found: {}.'.format(offer_id.hex())
assert(offer.expire_at > int(time.time())), 'Offer has expired'
if offer.swap_type == SwapTypes.XMR_SWAP:
return self.postXmrBid(offer_id, amount, addr_send_from)
self.mxDB.acquire()
try:
msg_buf = BidMessage()
msg_buf.offer_msg_id = offer_id
msg_buf.time_valid = extra_options.get('time_valid', 60 * 10)
msg_buf.amount = int(amount) # amount of coin_from
5 years ago
coin_from = Coins(offer.coin_from)
coin_to = Coins(offer.coin_to)
5 years ago
self.checkSynced(coin_from, coin_to)
contract_count = self.getNewContractId()
5 years ago
amount_to = int((msg_buf.amount * offer.rate) // self.ci(coin_from).COIN())
now = int(time.time())
if offer.swap_type == SwapTypes.SELLER_FIRST:
msg_buf.pkhash_buyer = getKeyID(self.getContractPubkey(dt.datetime.fromtimestamp(now).date(), contract_count))
5 years ago
proof_addr, proof_sig = self.getProofOfFunds(coin_to, amount_to, offer_id)
msg_buf.proof_address = proof_addr
msg_buf.proof_signature = proof_sig
4 years ago
else:
raise ValueError('TODO')
5 years ago
bid_bytes = msg_buf.SerializeToString()
payload_hex = str.format('{:02x}', MessageTypes.BID) + bid_bytes.hex()
5 years ago
if addr_send_from is None:
bid_addr = self.callrpc('getnewaddress')
else:
bid_addr = addr_send_from
self.callrpc('smsgaddlocaladdress', [bid_addr]) # Enable receiving smsg
options = {'decodehex': True, 'ttl_is_seconds': True}
msg_valid = self.SMSG_SECONDS_IN_HOUR * 1
ro = self.callrpc('smsgsend', [bid_addr, offer.addr_from, payload_hex, False, msg_valid, False, options])
msg_id = ro['msgid']
5 years ago
bid_id = bytes.fromhex(msg_id)
bid = Bid(
bid_id=bid_id,
offer_id=offer_id,
amount=msg_buf.amount,
pkhash_buyer=msg_buf.pkhash_buyer,
proof_address=msg_buf.proof_address,
created_at=now,
contract_count=contract_count,
amount_to=amount_to,
expire_at=now + msg_buf.time_valid,
bid_addr=bid_addr,
was_sent=True,
)
bid.setState(BidStates.BID_SENT)
5 years ago
try:
session = scoped_session(self.session_factory)
self.saveBidInSession(bid_id, bid, session)
if addr_send_from is None:
session.add(SmsgAddress(addr=bid_addr, use_type=MessageTypes.BID))
session.commit()
finally:
session.close()
session.remove()
5 years ago
self.log.info('Sent BID %s', bid_id.hex())
return bid_id
finally:
self.mxDB.release()
5 years ago
def getOffer(self, offer_id, sent=False):
self.mxDB.acquire()
try:
session = scoped_session(self.session_factory)
return session.query(Offer).filter_by(offer_id=offer_id).first()
finally:
session.close()
session.remove()
self.mxDB.release()
def loadBidTxns(self, bid, session):
bid.txns = {}
for stx in session.query(SwapTx).filter(sa.and_(SwapTx.bid_id == bid.bid_id)):
if stx.tx_type == TxTypes.ITX:
bid.initiate_tx = stx
elif stx.tx_type == TxTypes.PTX:
bid.participate_tx = stx
elif stx.tx_type == TxTypes.XMR_SWAP_A_LOCK:
bid.xmr_a_lock_tx = stx
elif stx.tx_type == TxTypes.XMR_SWAP_A_LOCK_SPEND:
bid.xmr_a_lock_spend_tx = stx
elif stx.tx_type == TxTypes.XMR_SWAP_B_LOCK:
bid.xmr_b_lock_tx = stx
else:
bid.txns[stx.tx_type] = stx
def getXmrBidFromSession(self, session, bid_id, sent=False):
bid = session.query(Bid).filter_by(bid_id=bid_id).first()
xmr_swap = None
if bid:
xmr_swap = session.query(XmrSwap).filter_by(bid_id=bid_id).first()
self.loadBidTxns(bid, session)
return bid, xmr_swap
4 years ago
def getXmrBid(self, bid_id, sent=False):
self.mxDB.acquire()
try:
session = scoped_session(self.session_factory)
return self.getXmrBidFromSession(session, bid_id, sent)
4 years ago
finally:
session.close()
session.remove()
self.mxDB.release()
def getXmrOfferFromSession(self, session, offer_id, sent=False):
offer = session.query(Offer).filter_by(offer_id=offer_id).first()
xmr_offer = None
if offer:
xmr_offer = session.query(XmrOffer).filter_by(offer_id=offer_id).first()
return offer, xmr_offer
4 years ago
def getXmrOffer(self, offer_id, sent=False):
self.mxDB.acquire()
try:
session = scoped_session(self.session_factory)
return self.getXmrOfferFromSession(session, offer_id, sent)
4 years ago
finally:
session.close()
session.remove()
self.mxDB.release()
5 years ago
def getBid(self, bid_id):
self.mxDB.acquire()
try:
session = scoped_session(self.session_factory)
bid = session.query(Bid).filter_by(bid_id=bid_id).first()
if bid:
self.loadBidTxns(bid, session)
return bid
5 years ago
finally:
session.close()
session.remove()
self.mxDB.release()
def getBidAndOffer(self, bid_id):
self.mxDB.acquire()
try:
session = scoped_session(self.session_factory)
bid = session.query(Bid).filter_by(bid_id=bid_id).first()
offer = None
if bid:
offer = session.query(Offer).filter_by(offer_id=bid.offer_id).first()
if offer and offer.swap_type == SwapTypes.XMR_SWAP:
self.loadBidTxns(bid, session)
else:
bid.initiate_tx = session.query(SwapTx).filter(sa.and_(SwapTx.bid_id == bid_id, SwapTx.tx_type == TxTypes.ITX)).first()
bid.participate_tx = session.query(SwapTx).filter(sa.and_(SwapTx.bid_id == bid_id, SwapTx.tx_type == TxTypes.PTX)).first()
return bid, offer
5 years ago
finally:
session.close()
session.remove()
self.mxDB.release()
def getXmrBidAndOffer(self, bid_id, list_events=True):
self.mxDB.acquire()
try:
session = scoped_session(self.session_factory)
xmr_swap = None
offer = None
xmr_offer = None
events = []
bid = session.query(Bid).filter_by(bid_id=bid_id).first()
if bid:
offer = session.query(Offer).filter_by(offer_id=bid.offer_id).first()
if offer and offer.swap_type == SwapTypes.XMR_SWAP:
xmr_swap = session.query(XmrSwap).filter_by(bid_id=bid.bid_id).first()
xmr_offer = session.query(XmrOffer).filter_by(offer_id=bid.offer_id).first()
self.loadBidTxns(bid, session)
else:
bid.initiate_tx = session.query(SwapTx).filter(sa.and_(SwapTx.bid_id == bid_id, SwapTx.tx_type == TxTypes.ITX)).first()
bid.participate_tx = session.query(SwapTx).filter(sa.and_(SwapTx.bid_id == bid_id, SwapTx.tx_type == TxTypes.PTX)).first()
if list_events:
events = self.list_bid_events(bid.bid_id, session)
return bid, xmr_swap, offer, xmr_offer, events
finally:
session.close()
session.remove()
self.mxDB.release()
def list_bid_events(self, bid_id, session):
query_str = 'SELECT created_at, event_type, event_msg FROM eventlog ' + \
'WHERE active_ind = 1 AND linked_type = {} AND linked_id = x\'{}\' '.format(TableTypes.BID, bid_id.hex())
q = session.execute(query_str)
events = []
for row in q:
events.append({'at': row[0], 'desc': describeEventEntry(row[1], row[2])})
query_str = 'SELECT created_at, trigger_at FROM eventqueue ' + \
'WHERE active_ind = 1 AND linked_id = x\'{}\' '.format(bid_id.hex())
q = session.execute(query_str)
for row in q:
events.append({'at': row[0], 'desc': 'Delaying until: {}'.format(format_timestamp(row[1], with_seconds=True))})
return events
5 years ago
def acceptBid(self, bid_id):
self.log.info('Accepting bid %s', bid_id.hex())
bid, offer = self.getBidAndOffer(bid_id)
assert(bid), 'Bid not found'
assert(offer), 'Offer not found'
# Ensure bid is still valid
now = int(time.time())
assert(bid.expire_at > now), 'Bid expired'
assert(bid.state == BidStates.BID_RECEIVED), 'Wrong bid state: {}'.format(str(BidStates(bid.state)))
if offer.swap_type == SwapTypes.XMR_SWAP:
return self.acceptXmrBid(bid_id)
5 years ago
if bid.contract_count is None:
bid.contract_count = self.getNewContractId()
coin_from = Coins(offer.coin_from)
ci_from = self.ci(coin_from)
5 years ago
bid_date = dt.datetime.fromtimestamp(bid.created_at).date()
secret = self.getContractSecret(bid_date, bid.contract_count)
secret_hash = hashlib.sha256(secret).digest()
pubkey_refund = self.getContractPubkey(bid_date, bid.contract_count)
pkhash_refund = getKeyID(pubkey_refund)
if bid.initiate_tx is not None:
5 years ago
self.log.warning('Initiate txn %s already exists for bid %s', bid.initiate_tx.txid, bid_id.hex())
txid = bid.initiate_tx.txid
script = bid.initiate_tx.script
else:
if offer.lock_type < ABS_LOCK_BLOCKS:
sequence = ci_from.getExpectedSequence(offer.lock_type, offer.lock_value)
script = atomic_swap_1.buildContractScript(sequence, secret_hash, bid.pkhash_buyer, pkhash_refund)
else:
if offer.lock_type == ABS_LOCK_BLOCKS:
lock_value = self.callcoinrpc(coin_from, 'getblockchaininfo')['blocks'] + offer.lock_value
else:
lock_value = int(time.time()) + offer.lock_value
5 years ago
self.log.debug('Initiate %s lock_value %d %d', coin_from, offer.lock_value, lock_value)
script = atomic_swap_1.buildContractScript(lock_value, secret_hash, bid.pkhash_buyer, pkhash_refund, OpCodes.OP_CHECKLOCKTIMEVERIFY)
5 years ago
p2sh = self.callcoinrpc(Coins.PART, 'decodescript', [script.hex()])['p2sh']
5 years ago
bid.pkhash_seller = pkhash_refund
5 years ago
txn = self.createInitiateTxn(coin_from, bid_id, bid, script)
5 years ago
# Store the signed refund txn in case wallet is locked when refund is possible
refund_txn = self.createRefundTxn(coin_from, txn, offer, bid, script)
bid.initiate_txn_refund = bytes.fromhex(refund_txn)
5 years ago
txid = self.submitTxn(coin_from, txn)
self.log.debug('Submitted initiate txn %s to %s chain for bid %s', txid, chainparams[coin_from]['name'], bid_id.hex())
bid.initiate_tx = SwapTx(
bid_id=bid_id,
tx_type=TxTypes.ITX,
txid=bytes.fromhex(txid),
script=script,
)
bid.setITxState(TxStates.TX_SENT)
5 years ago
# Check non-bip68 final
try:
txid = self.submitTxn(coin_from, bid.initiate_txn_refund.hex())
self.log.error('Submit refund_txn unexpectedly worked: ' + txid)
except Exception as ex:
if 'non-BIP68-final' not in str(ex) and 'non-final' not in str(ex):
self.log.error('Submit refund_txn unexpected error' + str(ex))
5 years ago
if txid is not None:
msg_buf = BidAcceptMessage()
msg_buf.bid_msg_id = bid_id
msg_buf.initiate_txid = bytes.fromhex(txid)
msg_buf.contract_script = bytes(script)
bid_bytes = msg_buf.SerializeToString()
payload_hex = str.format('{:02x}', MessageTypes.BID_ACCEPT) + bid_bytes.hex()
options = {'decodehex': True, 'ttl_is_seconds': True}
# TODO: set msg_valid based on bid / offer parameters
msg_valid = self.SMSG_SECONDS_IN_HOUR * 48
ro = self.callrpc('smsgsend', [offer.addr_from, bid.bid_addr, payload_hex, False, msg_valid, False, options])
5 years ago
msg_id = ro['msgid']
accept_msg_id = bytes.fromhex(msg_id)
bid.accept_msg_id = accept_msg_id
bid.setState(BidStates.BID_ACCEPTED)
self.log.info('Sent BID_ACCEPT %s', accept_msg_id.hex())
self.saveBid(bid_id, bid)
self.swaps_in_progress[bid_id] = (bid, offer)
4 years ago
def postXmrBid(self, offer_id, amount, addr_send_from=None):
# Bid to send bid.amount * offer.rate of coin_to in exchange for bid.amount of coin_from
# Send MSG1L F -> L
self.log.debug('postXmrBid %s', offer_id.hex())
4 years ago
self.mxDB.acquire()
try:
offer, xmr_offer = self.getXmrOffer(offer_id)
assert(offer), 'Offer not found: {}.'.format(offer_id.hex())
assert(xmr_offer), 'XMR offer not found: {}.'.format(offer_id.hex())
assert(offer.expire_at > int(time.time())), 'Offer has expired'
coin_from = Coins(offer.coin_from)
coin_to = Coins(offer.coin_to)
ci_from = self.ci(coin_from)
ci_to = self.ci(coin_to)
self.checkSynced(coin_from, coin_to)
msg_buf = XmrBidMessage()
msg_buf.offer_msg_id = offer_id
msg_buf.time_valid = 60 * 10
msg_buf.amount = int(amount) # Amount of coin_from
address_out = self.getReceiveAddressFromPool(coin_from, offer_id, TxTypes.XMR_SWAP_A_LOCK)
msg_buf.dest_af = ci_from.decodeAddress(address_out)
4 years ago
bid_created_at = int(time.time())
if offer.swap_type != SwapTypes.XMR_SWAP:
raise ValueError('TODO')
# Follower to leader
xmr_swap = XmrSwap()
xmr_swap.contract_count = self.getNewContractId()
xmr_swap.dest_af = msg_buf.dest_af
xmr_swap.start_chain_a_height = ci_from.getChainHeight()
xmr_swap.b_restore_height = ci_to.getChainHeight()
if xmr_swap.b_restore_height < ci_to._restore_height:
xmr_swap.b_restore_height = ci_to._restore_height
self.log.warning('XMR swap restore height clamped to {}'.format(ci_to._restore_height))
kbvf = self.getPathKey(coin_from, coin_to, bid_created_at, xmr_swap.contract_count, 1, for_ed25519=True)
kbsf = self.getPathKey(coin_from, coin_to, bid_created_at, xmr_swap.contract_count, 2, for_ed25519=True)
4 years ago
kaf = self.getPathKey(coin_from, coin_to, bid_created_at, xmr_swap.contract_count, 3)
xmr_swap.vkbvf = kbvf
xmr_swap.pkbvf = ci_to.getPubkey(kbvf)
xmr_swap.pkbsf = ci_to.getPubkey(kbsf)
4 years ago
xmr_swap.pkaf = ci_from.getPubkey(kaf)
xmr_swap.kbsf_dleag = ci_to.proveDLEAG(kbsf)
xmr_swap.pkasf = xmr_swap.kbsf_dleag[0: 33]
assert(xmr_swap.pkasf == ci_from.getPubkey(kbsf))
4 years ago
msg_buf.pkaf = xmr_swap.pkaf
msg_buf.kbvf = kbvf
msg_buf.kbsf_dleag = xmr_swap.kbsf_dleag[:16000]
bid_bytes = msg_buf.SerializeToString()
payload_hex = str.format('{:02x}', MessageTypes.XMR_BID_FL) + bid_bytes.hex()
4 years ago
if addr_send_from is None:
bid_addr = self.callrpc('getnewaddress')
else:
bid_addr = addr_send_from
self.callrpc('smsgaddlocaladdress', [bid_addr]) # Enable receiving smsg
options = {'decodehex': True, 'ttl_is_seconds': True}
msg_valid = self.SMSG_SECONDS_IN_HOUR * 1
ro = self.callrpc('smsgsend', [bid_addr, offer.addr_from, payload_hex, False, msg_valid, False, options])
xmr_swap.bid_id = bytes.fromhex(ro['msgid'])
msg_buf2 = XmrSplitMessage(
msg_id=xmr_swap.bid_id,
msg_type=XmrSplitMsgTypes.BID,
sequence=2,
dleag=xmr_swap.kbsf_dleag[16000:32000]
)
msg_bytes = msg_buf2.SerializeToString()
payload_hex = str.format('{:02x}', MessageTypes.XMR_BID_SPLIT) + msg_bytes.hex()
ro = self.callrpc('smsgsend', [bid_addr, offer.addr_from, payload_hex, False, msg_valid, False, options])
xmr_swap.bid_msg_id2 = bytes.fromhex(ro['msgid'])
msg_buf3 = XmrSplitMessage(
msg_id=xmr_swap.bid_id,
msg_type=XmrSplitMsgTypes.BID,
sequence=3,
dleag=xmr_swap.kbsf_dleag[32000:]
)
msg_bytes = msg_buf3.SerializeToString()
payload_hex = str.format('{:02x}', MessageTypes.XMR_BID_SPLIT) + msg_bytes.hex()
ro = self.callrpc('smsgsend', [bid_addr, offer.addr_from, payload_hex, False, msg_valid, False, options])
xmr_swap.bid_msg_id3 = bytes.fromhex(ro['msgid'])
bid = Bid(
bid_id=xmr_swap.bid_id,
offer_id=offer_id,
amount=msg_buf.amount,
created_at=bid_created_at,
contract_count=xmr_swap.contract_count,
amount_to=(msg_buf.amount * offer.rate) // ci_from.COIN(),
4 years ago
expire_at=bid_created_at + msg_buf.time_valid,
bid_addr=bid_addr,
was_sent=True,
)
bid.setState(BidStates.BID_SENT)
try:
session = scoped_session(self.session_factory)
self.saveBidInSession(xmr_swap.bid_id, bid, session, xmr_swap)
if addr_send_from is None:
session.add(SmsgAddress(addr=bid_addr, use_type=MessageTypes.BID))
session.commit()
finally:
session.close()
session.remove()
4 years ago
self.log.info('Sent XMR_BID_FL %s', xmr_swap.bid_id.hex())
4 years ago
return xmr_swap.bid_id
finally:
self.mxDB.release()
def acceptXmrBid(self, bid_id):
# MSG1F and MSG2F L -> F
self.log.info('Accepting xmr bid %s', bid_id.hex())
now = int(time.time())
self.mxDB.acquire()
try:
bid, xmr_swap = self.getXmrBid(bid_id)
assert(bid), 'Bid not found: {}.'.format(bid_id.hex())
assert(xmr_swap), 'XMR swap not found: {}.'.format(bid_id.hex())
assert(bid.expire_at > now), 'Bid expired'
assert(bid.state == BidStates.BID_RECEIVED), 'Wrong bid state: {}'.format(str(BidStates(bid.state)))
4 years ago
offer, xmr_offer = self.getXmrOffer(bid.offer_id)
assert(offer), 'Offer not found: {}.'.format(bid.offer_id.hex())
assert(xmr_offer), 'XMR offer not found: {}.'.format(bid.offer_id.hex())
assert(offer.expire_at > now), 'Offer has expired'
coin_from = Coins(offer.coin_from)
coin_to = Coins(offer.coin_to)
ci_from = self.ci(coin_from)
ci_to = self.ci(coin_to)
if xmr_swap.contract_count is None:
xmr_swap.contract_count = self.getNewContractId()
kbvl = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, 1, for_ed25519=True)
kbsl = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, 2, for_ed25519=True)
4 years ago
kal = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, 3)
4 years ago
xmr_swap.vkbvl = kbvl
xmr_swap.pkbvl = ci_to.getPubkey(kbvl)
xmr_swap.pkbsl = ci_to.getPubkey(kbsl)
xmr_swap.vkbv = ci_to.sumKeys(kbvl, xmr_swap.vkbvf)
xmr_swap.pkbv = ci_to.sumPubkeys(xmr_swap.pkbvl, xmr_swap.pkbvf)
xmr_swap.pkbs = ci_to.sumPubkeys(xmr_swap.pkbsl, xmr_swap.pkbsf)
4 years ago
xmr_swap.pkal = ci_from.getPubkey(kal)
xmr_swap.kbsl_dleag = ci_to.proveDLEAG(kbsl)
# MSG2F
xmr_swap.a_lock_tx, xmr_swap.a_lock_tx_script = ci_from.createScriptLockTx(
bid.amount,
xmr_swap.pkal, xmr_swap.pkaf,
)
xmr_swap.a_lock_tx = ci_from.fundTx(xmr_swap.a_lock_tx, xmr_offer.a_fee_rate)
xmr_swap.a_lock_tx_id = ci_from.getTxHash(xmr_swap.a_lock_tx)
a_lock_tx_dest = ci_from.getScriptDest(xmr_swap.a_lock_tx_script)
4 years ago
xmr_swap.a_lock_refund_tx, xmr_swap.a_lock_refund_tx_script, xmr_swap.a_swap_refund_value = ci_from.createScriptLockRefundTx(
xmr_swap.a_lock_tx, xmr_swap.a_lock_tx_script,
xmr_swap.pkal, xmr_swap.pkaf,
xmr_offer.lock_time_1, xmr_offer.lock_time_2,
4 years ago
xmr_offer.a_fee_rate
)
xmr_swap.a_lock_refund_tx_id = ci_from.getTxHash(xmr_swap.a_lock_refund_tx)
4 years ago
xmr_swap.al_lock_refund_tx_sig = ci_from.signTx(kal, xmr_swap.a_lock_refund_tx, 0, xmr_swap.a_lock_tx_script, bid.amount)
v = ci_from.verifyTxSig(xmr_swap.a_lock_refund_tx, xmr_swap.al_lock_refund_tx_sig, xmr_swap.pkal, 0, xmr_swap.a_lock_tx_script, bid.amount)
4 years ago
assert(v)
pkh_refund_to = ci_from.decodeAddress(self.getReceiveAddressForCoin(coin_from))
4 years ago
xmr_swap.a_lock_refund_spend_tx = ci_from.createScriptLockRefundSpendTx(
xmr_swap.a_lock_refund_tx, xmr_swap.a_lock_refund_tx_script,
pkh_refund_to,
4 years ago
xmr_offer.a_fee_rate
)
xmr_swap.a_lock_refund_spend_tx_id = ci_from.getTxHash(xmr_swap.a_lock_refund_spend_tx)
4 years ago
msg_buf = XmrBidAcceptMessage()
msg_buf.bid_msg_id = bid_id
msg_buf.pkal = xmr_swap.pkal
msg_buf.kbvl = kbvl
msg_buf.kbsl_dleag = xmr_swap.kbsl_dleag[:16000]
# MSG2F
msg_buf.a_lock_tx = xmr_swap.a_lock_tx
msg_buf.a_lock_tx_script = xmr_swap.a_lock_tx_script
msg_buf.a_lock_refund_tx = xmr_swap.a_lock_refund_tx
msg_buf.a_lock_refund_tx_script = xmr_swap.a_lock_refund_tx_script
msg_buf.a_lock_refund_spend_tx = xmr_swap.a_lock_refund_spend_tx
msg_buf.al_lock_refund_tx_sig = xmr_swap.al_lock_refund_tx_sig
msg_bytes = msg_buf.SerializeToString()
payload_hex = str.format('{:02x}', MessageTypes.XMR_BID_ACCEPT_LF) + msg_bytes.hex()
4 years ago
options = {'decodehex': True, 'ttl_is_seconds': True}
msg_valid = self.SMSG_SECONDS_IN_HOUR * 48
ro = self.callrpc('smsgsend', [offer.addr_from, bid.bid_addr, payload_hex, False, msg_valid, False, options])
msg_id = ro['msgid']
bid.accept_msg_id = bytes.fromhex(msg_id)
xmr_swap.bid_accept_msg_id = bid.accept_msg_id
msg_buf2 = XmrSplitMessage(
msg_id=bid_id,
msg_type=XmrSplitMsgTypes.BID_ACCEPT,
sequence=2,
dleag=xmr_swap.kbsl_dleag[16000:32000]
)
msg_bytes = msg_buf2.SerializeToString()
payload_hex = str.format('{:02x}', MessageTypes.XMR_BID_SPLIT) + msg_bytes.hex()
ro = self.callrpc('smsgsend', [offer.addr_from, bid.bid_addr, payload_hex, False, msg_valid, False, options])
xmr_swap.bid_accept_msg_id2 = bytes.fromhex(ro['msgid'])
msg_buf3 = XmrSplitMessage(
msg_id=bid_id,
msg_type=XmrSplitMsgTypes.BID_ACCEPT,
sequence=3,
dleag=xmr_swap.kbsl_dleag[32000:]
)
msg_bytes = msg_buf3.SerializeToString()
payload_hex = str.format('{:02x}', MessageTypes.XMR_BID_SPLIT) + msg_bytes.hex()
ro = self.callrpc('smsgsend', [offer.addr_from, bid.bid_addr, payload_hex, False, msg_valid, False, options])
xmr_swap.bid_accept_msg_id3 = bytes.fromhex(ro['msgid'])
bid.setState(BidStates.BID_ACCEPTED)
self.saveBid(bid_id, bid, xmr_swap=xmr_swap)
4 years ago
# Add to swaps_in_progress only when waiting on txns
self.log.info('Sent XMR_BID_ACCEPT_LF %s', bid_id.hex())
4 years ago
return bid_id
finally:
self.mxDB.release()
5 years ago
def abandonBid(self, bid_id):
self.log.info('Abandoning Bid %s', bid_id.hex())
self.mxDB.acquire()
try:
session = scoped_session(self.session_factory)
bid = session.query(Bid).filter_by(bid_id=bid_id).first()
assert(bid), 'Bid not found'
offer = session.query(Offer).filter_by(offer_id=bid.offer_id).first()
assert(offer), 'Offer not found'
# Mark bid as abandoned, no further processing will be done
bid.setState(BidStates.BID_ABANDONED)
self.deactivateBid(session, offer, bid)
session.add(bid)
5 years ago
session.commit()
finally:
session.close()
session.remove()
self.mxDB.release()
def setBidError(self, bid_id, bid, error_str, save_bid=True):
self.log.error('Bid %s - Error: %s', bid_id.hex(), error_str)
bid.setState(BidStates.BID_ERROR)
bid.state_note = 'error msg: ' + error_str
if save_bid:
self.saveBid(bid_id, bid)
def createInitiateTxn(self, coin_type, bid_id, bid, initiate_script):
5 years ago
if self.coin_clients[coin_type]['connection_type'] != 'rpc':
return None
ci = self.ci(coin_type)
5 years ago
if self.coin_clients[coin_type]['use_segwit']:
addr_to = self.encodeSegwitP2WSH(coin_type, getP2WSH(initiate_script))
5 years ago
else:
addr_to = self.getScriptAddress(coin_type, initiate_script)
5 years ago
self.log.debug('Create initiate txn for coin %s to %s for bid %s', str(coin_type), addr_to, bid_id.hex())
txn = self.callcoinrpc(coin_type, 'createrawtransaction', [[], {addr_to: ci.format_amount(bid.amount)}])
options = {
'lockUnspents': True,
'conf_target': self.coin_clients[coin_type]['conf_target'],
}
txn_funded = self.callcoinrpc(coin_type, 'fundrawtransaction', [txn, options])['hex']
5 years ago
txn_signed = self.callcoinrpc(coin_type, 'signrawtransactionwithwallet', [txn_funded])['hex']
return txn_signed
def deriveParticipateScript(self, bid_id, bid, offer):
self.log.debug('deriveParticipateScript for bid %s', bid_id.hex())
coin_to = Coins(offer.coin_to)
ci_to = self.ci(coin_to)
5 years ago
bid_date = dt.datetime.fromtimestamp(bid.created_at).date()
secret_hash = atomic_swap_1.extractScriptSecretHash(bid.initiate_tx.script)
5 years ago
pkhash_seller = bid.pkhash_seller
pkhash_buyer_refund = bid.pkhash_buyer
# Participate txn is locked for half the time of the initiate txn
lock_value = offer.lock_value // 2
if offer.lock_type < ABS_LOCK_BLOCKS:
sequence = ci_to.getExpectedSequence(offer.lock_type, lock_value)
participate_script = atomic_swap_1.buildContractScript(sequence, secret_hash, pkhash_seller, pkhash_buyer_refund)
else:
5 years ago
# Lock from the height or time of the block containing the initiate txn
coin_from = Coins(offer.coin_from)
initiate_tx_block_hash = self.callcoinrpc(coin_from, 'getblockhash', [bid.initiate_tx.chain_height, ])
initiate_tx_block_time = int(self.callcoinrpc(coin_from, 'getblock', [initiate_tx_block_hash, ])['time'])
if offer.lock_type == ABS_LOCK_BLOCKS:
5 years ago
# Walk the coin_to chain back until block time matches
blockchaininfo = self.callcoinrpc(coin_to, 'getblockchaininfo')
cblock_hash = blockchaininfo['bestblockhash']
cblock_height = blockchaininfo['blocks']
max_tries = 1000
for i in range(max_tries):
prev_block = self.callcoinrpc(coin_to, 'getblock', [cblock_hash, ])
5 years ago
self.log.debug('prev_block %s', str(prev_block))
if prev_block['time'] <= initiate_tx_block_time:
break
# cblock_hash and height are out of step unless loop breaks
cblock_hash = prev_block['previousblockhash']
cblock_height = prev_block['height']
assert(prev_block['time'] <= initiate_tx_block_time), 'Block not found for lock height'
self.log.debug('Setting lock value from height of block %s %s', coin_to, cblock_hash)
contract_lock_value = cblock_height + lock_value
else:
5 years ago
self.log.debug('Setting lock value from time of block %s %s', coin_from, initiate_tx_block_hash)
contract_lock_value = initiate_tx_block_time + lock_value
self.log.debug('participate %s lock_value %d %d', coin_to, lock_value, contract_lock_value)
participate_script = atomic_swap_1.buildContractScript(contract_lock_value, secret_hash, pkhash_seller, pkhash_buyer_refund, OpCodes.OP_CHECKLOCKTIMEVERIFY)
return participate_script
5 years ago
def createParticipateTxn(self, bid_id, bid, offer, participate_script):
5 years ago
self.log.debug('createParticipateTxn')
offer_id = bid.offer_id
coin_to = Coins(offer.coin_to)
if self.coin_clients[coin_to]['connection_type'] != 'rpc':
return None
ci = self.ci(coin_to)
5 years ago
amount_to = bid.amount_to
# Check required?
assert(amount_to == (bid.amount * offer.rate) // self.ci(offer.coin_from).COIN())
5 years ago
if bid.debug_ind == DebugTypes.MAKE_INVALID_PTX:
amount_to -= 1
self.log.debug('bid %s: Make invalid PTx for testing: %d.', bid_id.hex(), bid.debug_ind)
self.logBidEvent(bid, EventLogTypes.DEBUG_TWEAK_APPLIED, 'ind {}'.format(bid.debug_ind), None)
5 years ago
if self.coin_clients[coin_to]['use_segwit']:
p2wsh = getP2WSH(participate_script)
5 years ago
addr_to = self.encodeSegwitP2WSH(coin_to, p2wsh)
else:
addr_to = self.getScriptAddress(coin_to, participate_script)
5 years ago
txn = self.callcoinrpc(coin_to, 'createrawtransaction', [[], {addr_to: ci.format_amount(amount_to)}])
options = {
'lockUnspents': True,
'conf_target': self.coin_clients[coin_to]['conf_target'],
}
txn_funded = self.callcoinrpc(coin_to, 'fundrawtransaction', [txn, options])['hex']
5 years ago
txn_signed = self.callcoinrpc(coin_to, 'signrawtransactionwithwallet', [txn_funded])['hex']
refund_txn = self.createRefundTxn(coin_to, txn_signed, offer, bid, participate_script, tx_type=TxTypes.PTX_REFUND)
5 years ago
bid.participate_txn_refund = bytes.fromhex(refund_txn)
chain_height = self.callcoinrpc(coin_to, 'getblockchaininfo')['blocks']
txjs = self.callcoinrpc(coin_to, 'decoderawtransaction', [txn_signed])
txid = txjs['txid']
if self.coin_clients[coin_to]['use_segwit']:
vout = getVoutByP2WSH(txjs, p2wsh.hex())
else:
vout = getVoutByAddress(txjs, addr_to)
self.addParticipateTxn(bid_id, bid, coin_to, txid, vout, chain_height)
bid.participate_tx.script = participate_script
5 years ago
return txn_signed
def getContractSpendTxVSize(self, coin_type, redeem=True):
tx_vsize = 5 # Add a few bytes, sequence in script takes variable amount of bytes
if coin_type == Coins.PART:
tx_vsize += 204 if redeem else 187
if self.coin_clients[coin_type]['use_segwit']:
tx_vsize += 143 if redeem else 134
else:
tx_vsize += 323 if redeem else 287
return tx_vsize
def createRedeemTxn(self, coin_type, bid, for_txn_type='participate', addr_redeem_out=None, fee_rate=None):
self.log.debug('createRedeemTxn for coin %s', str(coin_type))
ci = self.ci(coin_type)
5 years ago
if for_txn_type == 'participate':
prev_txnid = bid.participate_tx.txid.hex()
prev_n = bid.participate_tx.vout
txn_script = bid.participate_tx.script
5 years ago
prev_amount = bid.amount_to
else:
prev_txnid = bid.initiate_tx.txid.hex()
prev_n = bid.initiate_tx.vout
txn_script = bid.initiate_tx.script
5 years ago
prev_amount = bid.amount
if self.coin_clients[coin_type]['use_segwit']:
prev_p2wsh = getP2WSH(txn_script)
script_pub_key = prev_p2wsh.hex()
else:
script_pub_key = getP2SHScriptForHash(getKeyID(txn_script)).hex()
prevout = {
'txid': prev_txnid,
'vout': prev_n,
'scriptPubKey': script_pub_key,
'redeemScript': txn_script.hex(),
'amount': ci.format_amount(prev_amount)}
5 years ago
bid_date = dt.datetime.fromtimestamp(bid.created_at).date()
wif_prefix = chainparams[Coins.PART][self.chain]['key_prefix']
pubkey = self.getContractPubkey(bid_date, bid.contract_count)
privkey = toWIF(wif_prefix, self.getContractPrivkey(bid_date, bid.contract_count))
secret = bid.recovered_secret
if secret is None:
secret = self.getContractSecret(bid_date, bid.contract_count)
assert(len(secret) == 32), 'Bad secret length'
if self.coin_clients[coin_type]['connection_type'] != 'rpc':
return None
prevout_s = ' in={}:{}'.format(prev_txnid, prev_n)
if fee_rate is None:
fee_rate, fee_src = self.getFeeRateForCoin(coin_type)
5 years ago
tx_vsize = self.getContractSpendTxVSize(coin_type)
tx_fee = (fee_rate * tx_vsize) / 1000
self.log.debug('Redeem tx fee %s, rate %s', ci.format_amount(tx_fee, conv_int=True, r=1), str(fee_rate))
5 years ago
amount_out = prev_amount - ci.make_int(tx_fee, r=1)
assert(amount_out > 0), 'Amount out <= 0'
5 years ago
if addr_redeem_out is None:
addr_redeem_out = self.getReceiveAddressFromPool(coin_type, bid.bid_id, TxTypes.PTX_REDEEM if for_txn_type == 'participate' else TxTypes.ITX_REDEEM)
5 years ago
assert(addr_redeem_out is not None)
if self.coin_clients[coin_type]['use_segwit']:
# Change to btc hrp
addr_redeem_out = self.encodeSegwit(Coins.PART, self.decodeSegwit(coin_type, addr_redeem_out))
else:
addr_redeem_out = replaceAddrPrefix(addr_redeem_out, Coins.PART, self.chain)
self.log.debug('addr_redeem_out %s', addr_redeem_out)
output_to = ' outaddr={}:{}'.format(ci.format_amount(amount_out), addr_redeem_out)
5 years ago
if coin_type == Coins.PART:
redeem_txn = self.calltx('-create' + prevout_s + output_to)
else:
redeem_txn = self.calltx('-btcmode -create nversion=2' + prevout_s + output_to)
options = {}
if self.coin_clients[coin_type]['use_segwit']:
options['force_segwit'] = True
redeem_sig = self.callcoinrpc(Coins.PART, 'createsignaturewithkey', [redeem_txn, prevout, privkey, 'ALL', options])
if coin_type == Coins.PART or self.coin_clients[coin_type]['use_segwit']:
witness_stack = [
redeem_sig,
pubkey.hex(),
secret.hex(),
'01',
txn_script.hex()]
redeem_txn = self.calltx(redeem_txn + ' witness=0:' + ':'.join(witness_stack))
else:
script = format(len(redeem_sig) // 2, '02x') + redeem_sig
script += format(33, '02x') + pubkey.hex()
script += format(32, '02x') + secret.hex()
script += format(OpCodes.OP_1, '02x')
script += format(OpCodes.OP_PUSHDATA1, '02x') + format(len(txn_script), '02x') + txn_script.hex()
redeem_txn = self.calltx(redeem_txn + ' scriptsig=0:' + script)
ro = self.callcoinrpc(Coins.PART, 'verifyrawtransaction', [redeem_txn, [prevout]])
assert(ro['inputs_valid'] is True), 'inputs_valid is false'
assert(ro['complete'] is True), 'complete is false'
assert(ro['validscripts'] == 1), 'validscripts != 1'
if self.debug:
# Check fee
if self.coin_clients[coin_type]['connection_type'] == 'rpc':
redeem_txjs = self.callcoinrpc(coin_type, 'decoderawtransaction', [redeem_txn])
self.log.debug('vsize paid, actual vsize %d %d', tx_vsize, redeem_txjs['vsize'])
assert(tx_vsize >= redeem_txjs['vsize']), 'Underpaid fee'
5 years ago
redeem_txjs = self.callcoinrpc(Coins.PART, 'decoderawtransaction', [redeem_txn])
self.log.debug('Have valid redeem txn %s for contract %s tx %s', redeem_txjs['txid'], for_txn_type, prev_txnid)
return redeem_txn
def createRefundTxn(self, coin_type, txn, offer, bid, txn_script, addr_refund_out=None, tx_type=TxTypes.ITX_REFUND):
5 years ago
self.log.debug('createRefundTxn')
if self.coin_clients[coin_type]['connection_type'] != 'rpc':
return None
txjs = self.callcoinrpc(Coins.PART, 'decoderawtransaction', [txn])
if self.coin_clients[coin_type]['use_segwit']:
p2wsh = getP2WSH(txn_script)
vout = getVoutByP2WSH(txjs, p2wsh.hex())
else:
addr_to = self.getScriptAddress(Coins.PART, txn_script)
vout = getVoutByAddress(txjs, addr_to)
bid_date = dt.datetime.fromtimestamp(bid.created_at).date()
wif_prefix = chainparams[Coins.PART][self.chain]['key_prefix']
pubkey = self.getContractPubkey(bid_date, bid.contract_count)
privkey = toWIF(wif_prefix, self.getContractPrivkey(bid_date, bid.contract_count))
prev_amount = txjs['vout'][vout]['value']
prevout = {
'txid': txjs['txid'],
'vout': vout,
'scriptPubKey': txjs['vout'][vout]['scriptPubKey']['hex'],
'redeemScript': txn_script.hex(),
'amount': prev_amount}
lock_value = DeserialiseNum(txn_script, 64)
if offer.lock_type < ABS_LOCK_BLOCKS:
sequence = lock_value
else:
sequence = 1
5 years ago
prevout_s = ' in={}:{}:{}'.format(txjs['txid'], vout, sequence)
fee_rate, fee_src = self.getFeeRateForCoin(coin_type)
5 years ago
tx_vsize = self.getContractSpendTxVSize(coin_type, False)
tx_fee = (fee_rate * tx_vsize) / 1000
ci = self.ci(coin_type)
self.log.debug('Refund tx fee %s, rate %s', ci.format_amount(tx_fee, conv_int=True, r=1), str(fee_rate))
5 years ago
amount_out = ci.make_int(prev_amount, r=1) - ci.make_int(tx_fee, r=1)
5 years ago
if amount_out <= 0:
raise ValueError('Refund amount out <= 0')
5 years ago
if addr_refund_out is None:
addr_refund_out = self.getReceiveAddressFromPool(coin_type, bid.bid_id, tx_type)
assert(addr_refund_out is not None), 'addr_refund_out is null'
5 years ago
if self.coin_clients[coin_type]['use_segwit']:
# Change to btc hrp
addr_refund_out = self.encodeSegwit(Coins.PART, self.decodeSegwit(coin_type, addr_refund_out))
else:
addr_refund_out = replaceAddrPrefix(addr_refund_out, Coins.PART, self.chain)
self.log.debug('addr_refund_out %s', addr_refund_out)
output_to = ' outaddr={}:{}'.format(ci.format_amount(amount_out), addr_refund_out)
5 years ago
if coin_type == Coins.PART:
refund_txn = self.calltx('-create' + prevout_s + output_to)
else:
refund_txn = self.calltx('-btcmode -create nversion=2' + prevout_s + output_to)
if offer.lock_type == ABS_LOCK_BLOCKS or offer.lock_type == ABS_LOCK_TIME:
refund_txn = self.calltx('{} locktime={}'.format(refund_txn, lock_value))
5 years ago
options = {}
if self.coin_clients[coin_type]['use_segwit']:
options['force_segwit'] = True
refund_sig = self.callcoinrpc(Coins.PART, 'createsignaturewithkey', [refund_txn, prevout, privkey, 'ALL', options])
if coin_type == Coins.PART or self.coin_clients[coin_type]['use_segwit']:
witness_stack = [
refund_sig,
pubkey.hex(),
'', # SCRIPT_VERIFY_MINIMALIF
txn_script.hex()]
refund_txn = self.calltx(refund_txn + ' witness=0:' + ':'.join(witness_stack))
else:
script = format(len(refund_sig) // 2, '02x') + refund_sig
script += format(33, '02x') + pubkey.hex()
script += format(OpCodes.OP_0, '02x')
script += format(OpCodes.OP_PUSHDATA1, '02x') + format(len(txn_script), '02x') + txn_script.hex()
refund_txn = self.calltx(refund_txn + ' scriptsig=0:' + script)
ro = self.callcoinrpc(Coins.PART, 'verifyrawtransaction', [refund_txn, [prevout]])
assert(ro['inputs_valid'] is True), 'inputs_valid is false'
assert(ro['complete'] is True), 'complete is false'
assert(ro['validscripts'] == 1), 'validscripts != 1'
if self.debug:
# Check fee
if self.coin_clients[coin_type]['connection_type'] == 'rpc':
refund_txjs = self.callcoinrpc(coin_type, 'decoderawtransaction', [refund_txn])
self.log.debug('vsize paid, actual vsize %d %d', tx_vsize, refund_txjs['vsize'])
assert(tx_vsize >= refund_txjs['vsize']), 'underpaid fee'
5 years ago
refund_txjs = self.callcoinrpc(Coins.PART, 'decoderawtransaction', [refund_txn])
self.log.debug('Have valid refund txn %s for contract tx %s', refund_txjs['txid'], txjs['txid'])
return refund_txn
def submitTxn(self, coin_type, txn):
# self.log.debug('submitTxn %s', str(coin_type))
if txn is None:
return None
if self.coin_clients[coin_type]['connection_type'] != 'rpc':
return None
return self.callcoinrpc(coin_type, 'sendrawtransaction', [txn])
def initiateTxnConfirmed(self, bid_id, bid, offer):
self.log.debug('initiateTxnConfirmed for bid %s', bid_id.hex())
bid.setState(BidStates.SWAP_INITIATED)
bid.setITxState(TxStates.TX_CONFIRMED)
5 years ago
if bid.debug_ind == DebugTypes.BUYER_STOP_AFTER_ITX:
self.log.debug('bid %s: Abandoning bid for testing: %d.', bid_id.hex(), bid.debug_ind)
bid.setState(BidStates.BID_ABANDONED)
self.logBidEvent(bid, EventLogTypes.DEBUG_TWEAK_APPLIED, 'ind {}'.format(bid.debug_ind), None)
return # Bid saved in checkBidState
5 years ago
# Seller first mode, buyer participates
participate_script = self.deriveParticipateScript(bid_id, bid, offer)
5 years ago
if bid.was_sent:
if bid.participate_tx is not None:
self.log.warning('Participate txn %s already exists for bid %s', bid.participate_tx.txid, bid_id.hex())
else:
self.log.debug('Preparing participate txn for bid %s', bid_id.hex())
5 years ago
coin_to = Coins(offer.coin_to)
txn = self.createParticipateTxn(bid_id, bid, offer, participate_script)
txid = self.submitTxn(coin_to, txn)
self.log.debug('Submitted participate txn %s to %s chain for bid %s', txid, chainparams[coin_to]['name'], bid_id.hex())
bid.setPTxState(TxStates.TX_SENT)
else:
bid.participate_tx = SwapTx(
bid_id=bid_id,
tx_type=TxTypes.PTX,
script=participate_script,
)
5 years ago
# Bid saved in checkBidState
5 years ago
def setLastHeightChecked(self, coin_type, tx_height):
5 years ago
chain_name = chainparams[coin_type]['name']
if tx_height < 1:
tx_height = self.lookupChainHeight(coin_type)
5 years ago
if len(self.coin_clients[coin_type]['watched_outputs']) == 0:
self.coin_clients[coin_type]['last_height_checked'] = tx_height
self.log.debug('Start checking %s chain at height %d', chain_name, tx_height)
if self.coin_clients[coin_type]['last_height_checked'] > tx_height:
self.coin_clients[coin_type]['last_height_checked'] = tx_height
self.log.debug('Rewind checking of %s chain to height %d', chain_name, tx_height)
return tx_height
def addParticipateTxn(self, bid_id, bid, coin_type, txid_hex, vout, tx_height):
# TODO: Check connection type
participate_txn_height = self.setLastHeightChecked(coin_type, tx_height)
if bid.participate_tx is None:
bid.participate_tx = SwapTx(
bid_id=bid_id,
tx_type=TxTypes.PTX,
)
bid.participate_tx.txid = bytes.fromhex(txid_hex)
bid.participate_tx.vout = vout
bid.participate_tx.chain_height = participate_txn_height
# Start checking for spends of participate_txn before fully confirmed
self.log.debug('Watching %s chain for spend of output %s %d', chainparams[coin_type]['name'], txid_hex, vout)
self.addWatchedOutput(coin_type, bid_id, txid_hex, vout, BidStates.SWAP_PARTICIPATING)
5 years ago
def participateTxnConfirmed(self, bid_id, bid, offer):
self.log.debug('participateTxnConfirmed for bid %s', bid_id.hex())
bid.setState(BidStates.SWAP_PARTICIPATING)
bid.setPTxState(TxStates.TX_CONFIRMED)
5 years ago
# Seller redeems from participate txn
if bid.was_received:
coin_to = Coins(offer.coin_to)
txn = self.createRedeemTxn(coin_to, bid)
txid = self.submitTxn(coin_to, txn)
self.log.debug('Submitted participate redeem txn %s to %s chain for bid %s', txid, chainparams[coin_to]['name'], bid_id.hex())
# TX_REDEEMED will be set when spend is detected
# TODO: Wait for depth?
# bid saved in checkBidState
5 years ago
def getAddressBalance(self, coin_type, address):
if self.coin_clients[coin_type]['chain_lookups'] == 'explorer':
explorers = self.coin_clients[coin_type]['explorers']
# TODO: random offset into explorers, try blocks
for exp in explorers:
return exp.getBalance(address)
return self.lookupUnspentByAddress(coin_type, address, sum_output=True)
5 years ago
def lookupChainHeight(self, coin_type):
return self.callcoinrpc(coin_type, 'getblockchaininfo')['blocks']
def lookupUnspentByAddress(self, coin_type, address, sum_output=False, assert_amount=None, assert_txid=None):
ci = self.ci(coin_type)
if self.coin_clients[coin_type]['chain_lookups'] == 'explorer':
explorers = self.coin_clients[coin_type]['explorers']
# TODO: random offset into explorers, try blocks
for exp in explorers:
# TODO: ExplorerBitAps use only gettransaction if assert_txid is set
rv = exp.lookupUnspentByAddress(address)
if assert_amount is not None:
assert(rv['value'] == int(assert_amount)), 'Incorrect output amount in txn {}: {} != {}.'.format(assert_txid, rv['value'], int(assert_amount))
if assert_txid is not None:
assert(rv['txid)'] == assert_txid), 'Incorrect txid'
return rv
raise ValueError('No explorer for lookupUnspentByAddress {}'.format(str(coin_type)))
if self.coin_clients[coin_type]['connection_type'] != 'rpc':
raise ValueError('No RPC connection for lookupUnspentByAddress {}'.format(str(coin_type)))
if assert_txid is not None:
try:
ro = self.callcoinrpc(coin_type, 'getmempoolentry', [assert_txid])
self.log.debug('Tx %s found in mempool, fee %s', assert_txid, ro['fee'])
# TODO: Save info
return None
except Exception:
pass
5 years ago
num_blocks = self.callcoinrpc(coin_type, 'getblockchaininfo')['blocks']
sum_unspent = 0
self.log.debug('[rm] scantxoutset start') # scantxoutset is slow
ro = self.callcoinrpc(coin_type, 'scantxoutset', ['start', ['addr({})'.format(address)]])
self.log.debug('[rm] scantxoutset end')
for o in ro['unspents']:
if assert_txid and o['txid'] != assert_txid:
continue
# Verify amount
if assert_amount:
assert(make_int(o['amount']) == int(assert_amount)), 'Incorrect output amount in txn {}: {} != {}.'.format(assert_txid, make_int(o['amount']), int(assert_amount))
5 years ago
if not sum_output:
if o['height'] > 0:
n_conf = num_blocks - o['height']
else:
n_conf = -1
return {
'txid': o['txid'],
'index': o['vout'],
'height': o['height'],
'n_conf': n_conf,
'value': ci.make_int(o['amount']),
5 years ago
}
else:
sum_unspent += ci.make_int(o['amount'])
5 years ago
if sum_output:
return sum_unspent
return None
4 years ago
def checkXmrBidState(self, bid_id, bid, offer):
rv = False
ci_from = self.ci(Coins(offer.coin_from))
ci_to = self.ci(Coins(offer.coin_to))
session = None
try:
self.mxDB.acquire()
session = scoped_session(self.session_factory)
xmr_offer = session.query(XmrOffer).filter_by(offer_id=offer.offer_id).first()
assert(xmr_offer), 'XMR offer not found: {}.'.format(offer.offer_id.hex())
xmr_swap = session.query(XmrSwap).filter_by(bid_id=bid.bid_id).first()
assert(xmr_swap), 'XMR swap not found: {}.'.format(bid.bid_id.hex())
if TxTypes.XMR_SWAP_A_LOCK_REFUND in bid.txns:
refund_tx = bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND]
if bid.was_received:
if bid.debug_ind == DebugTypes.BID_DONT_SPEND_COIN_A_LOCK_REFUND:
self.log.debug('XMR bid %s: Stalling bid for testing: %d.', bid_id.hex(), bid.debug_ind)
bid.setState(BidStates.BID_STALLED_FOR_TEST)
rv = True
self.saveBidInSession(bid_id, bid, session, xmr_swap)
self.logBidEvent(bid, EventLogTypes.DEBUG_TWEAK_APPLIED, 'ind {}'.format(bid.debug_ind), session)
session.commit()
return rv
if TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND not in bid.txns:
try:
txid = ci_from.publishTx(xmr_swap.a_lock_refund_spend_tx)
self.logBidEvent(bid, EventLogTypes.LOCK_TX_A_REFUND_SPEND_TX_PUBLISHED, '', session)
self.log.info('Submitted coin a lock refund spend tx for bid {}'.format(bid_id.hex()))
bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND] = SwapTx(
bid_id=bid_id,
tx_type=TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND,
txid=bytes.fromhex(txid),
)
self.saveBidInSession(bid_id, bid, session, xmr_swap)
session.commit()
except Exception as ex:
self.log.debug('Trying to publish coin a lock refund spend tx: %s', str(ex))
if bid.was_sent:
if xmr_swap.a_lock_refund_swipe_tx is None:
self.createCoinALockRefundSwipeTx(ci_from, bid, offer, xmr_swap, xmr_offer)
self.saveBidInSession(bid_id, bid, session, xmr_swap)
session.commit()
if TxTypes.XMR_SWAP_A_LOCK_REFUND_SWIPE not in bid.txns:
try:
txid = ci_from.publishTx(xmr_swap.a_lock_refund_swipe_tx)
self.logBidEvent(bid, EventLogTypes.LOCK_TX_A_REFUND_SWIPE_TX_PUBLISHED, '', session)
self.log.info('Submitted coin a lock refund swipe tx for bid {}'.format(bid_id.hex()))
bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND_SWIPE] = SwapTx(
bid_id=bid_id,
tx_type=TxTypes.XMR_SWAP_A_LOCK_REFUND_SWIPE,
txid=bytes.fromhex(txid),
)
self.saveBidInSession(bid_id, bid, session, xmr_swap)
session.commit()
except Exception as ex:
self.log.debug('Trying to publish coin a lock refund swipe tx: %s', str(ex))
if BidStates(bid.state) == BidStates.XMR_SWAP_NOSCRIPT_TX_RECOVERED:
txid_hex = bid.xmr_b_lock_tx.spend_txid.hex()
found_tx = ci_to.findTxnByHash(txid_hex)
if found_tx is not None:
self.log.info('Found coin b lock recover tx bid %s', bid_id.hex())
rv = True # Remove from swaps_in_progress
bid.setState(BidStates.XMR_SWAP_FAILED_REFUNDED)
self.saveBidInSession(bid_id, bid, session, xmr_swap)
session.commit()
return rv
else: # not XMR_SWAP_A_LOCK_REFUND in bid.txns
if len(xmr_swap.al_lock_refund_tx_sig) > 0 and len(xmr_swap.af_lock_refund_tx_sig) > 0:
try:
txid = ci_from.publishTx(xmr_swap.a_lock_refund_tx)
self.log.info('Submitted coin a lock refund tx for bid {}'.format(bid_id.hex()))
self.logBidEvent(bid, EventLogTypes.LOCK_TX_A_REFUND_TX_PUBLISHED, '', session)
bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND] = SwapTx(
bid_id=bid_id,
tx_type=TxTypes.XMR_SWAP_A_LOCK_REFUND,
txid=bytes.fromhex(txid),
)
self.saveBidInSession(bid_id, bid, session, xmr_swap)
session.commit()
return rv
except Exception as ex:
if 'Transaction already in block chain' in str(ex):
self.log.info('Found coin a lock refund tx for bid {}'.format(bid_id.hex()))
txid = ci_from.getTxHash(xmr_swap.a_lock_refund_tx)
bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND] = SwapTx(
bid_id=bid_id,
tx_type=TxTypes.XMR_SWAP_A_LOCK_REFUND,
txid=txid,
)
self.saveBidInSession(bid_id, bid, session, xmr_swap)
session.commit()
return rv
state = BidStates(bid.state)
if state == BidStates.SWAP_COMPLETED:
rv = True # Remove from swaps_in_progress
elif state == BidStates.XMR_SWAP_FAILED_REFUNDED:
rv = True # Remove from swaps_in_progress
elif state == BidStates.XMR_SWAP_FAILED_SWIPED:
rv = True # Remove from swaps_in_progress
elif state == BidStates.XMR_SWAP_HAVE_SCRIPT_COIN_SPEND_TX:
if bid.xmr_a_lock_tx is None:
return rv
# TODO: Timeout waiting for transactions
bid_changed = False
a_lock_tx_dest = ci_from.getScriptDest(xmr_swap.a_lock_tx_script)
utxos, chain_height = ci_from.getOutput(bid.xmr_a_lock_tx.txid, a_lock_tx_dest, bid.amount)
if len(utxos) < 1:
return rv
if len(utxos) > 1:
raise ValueError('Too many outputs for chain A lock tx')
utxo = utxos[0]
if not bid.xmr_a_lock_tx.chain_height and utxo['height'] != 0:
self.logBidEvent(bid, EventLogTypes.LOCK_TX_A_SEEN, '', session)
block_header = ci_from.getBlockHeaderFromHeight(utxo['height'])
bid.xmr_a_lock_tx.block_hash = bytes.fromhex(block_header['hash'])
bid.xmr_a_lock_tx.block_height = block_header['height']
bid.xmr_a_lock_tx.block_time = block_header['time'] # Or median_time?
bid_changed = True
if bid.xmr_a_lock_tx.chain_height != utxo['height'] and utxo['height'] != 0:
bid.xmr_a_lock_tx.chain_height = utxo['height']
bid_changed = True
if utxo['depth'] >= ci_from.blocks_confirmed:
self.logBidEvent(bid, EventLogTypes.LOCK_TX_A_CONFIRMED, '', session)
bid.xmr_a_lock_tx.setState(TxStates.TX_CONFIRMED)
bid.setState(BidStates.XMR_SWAP_SCRIPT_COIN_LOCKED)
bid_changed = True
if bid.was_sent:
delay = random.randrange(self.min_delay_event, self.max_delay_event)
self.log.info('Sending xmr swap chain B lock tx for bid %s in %d seconds', bid_id.hex(), delay)
self.createEventInSession(delay, EventTypes.SEND_XMR_SWAP_LOCK_TX_B, bid_id, session)
# bid.setState(BidStates.SWAP_DELAYING)
if bid_changed:
self.saveBidInSession(bid_id, bid, session, xmr_swap)
session.commit()
elif state == BidStates.XMR_SWAP_SCRIPT_COIN_LOCKED:
if bid.was_sent and bid.xmr_b_lock_tx is None:
return rv
bid_changed = False
# Have to use findTxB instead of relying on the first seen height to detect chain reorgs
found_tx = ci_to.findTxB(xmr_swap.vkbv, xmr_swap.pkbs, bid.amount_to, ci_to.blocks_confirmed, xmr_swap.b_restore_height)
if isinstance(found_tx, int) and found_tx == -1:
if self.countBidEvents(bid, EventLogTypes.LOCK_TX_B_INVALID, session) < 1:
self.logBidEvent(bid, EventLogTypes.LOCK_TX_B_INVALID, 'Detected invalid lock tx B', session)
bid_changed = True
elif found_tx is not None:
if bid.xmr_b_lock_tx is None or not bid.xmr_b_lock_tx.chain_height:
self.logBidEvent(bid, EventLogTypes.LOCK_TX_B_SEEN, '', session)
if bid.xmr_b_lock_tx is None:
self.log.debug('Found {} lock tx in chain'.format(ci_to.coin_name()))
b_lock_tx_id = bytes.fromhex(found_tx['txid'])
bid.xmr_b_lock_tx = SwapTx(
bid_id=bid_id,
tx_type=TxTypes.XMR_SWAP_B_LOCK,
txid=b_lock_tx_id,
chain_height=found_tx['height'],
)
bid_changed = True
else:
bid.xmr_b_lock_tx.chain_height = found_tx['height']
bid_changed = True
if bid.xmr_b_lock_tx and bid.xmr_b_lock_tx.chain_height is not None and bid.xmr_b_lock_tx.chain_height > 0:
chain_height = ci_to.getChainHeight()
if chain_height - bid.xmr_b_lock_tx.chain_height >= ci_to.blocks_confirmed:
self.logBidEvent(bid, EventLogTypes.LOCK_TX_B_CONFIRMED, '', session)
bid.xmr_b_lock_tx.setState(TxStates.TX_CONFIRMED)
bid.setState(BidStates.XMR_SWAP_NOSCRIPT_COIN_LOCKED)
if bid.was_received:
delay = random.randrange(self.min_delay_event, self.max_delay_event)
self.log.info('Releasing xmr script coin lock tx for bid %s in %d seconds', bid_id.hex(), delay)
self.createEventInSession(delay, EventTypes.SEND_XMR_LOCK_RELEASE, bid_id, session)
if bid_changed:
self.saveBidInSession(bid_id, bid, session, xmr_swap)
session.commit()
elif state == BidStates.XMR_SWAP_LOCK_RELEASED:
# Wait for script spend tx to confirm
# TODO: Use explorer to get tx / block hash for getrawtransaction
if bid.was_received:
try:
txn_hex = ci_from.getMempoolTx(xmr_swap.a_lock_spend_tx_id)
self.log.info('Found lock spend txn in %s mempool, %s', ci_from.coin_name(), xmr_swap.a_lock_spend_tx_id.hex())
self.process_XMR_SWAP_A_LOCK_tx_spend(bid_id, xmr_swap.a_lock_spend_tx_id.hex(), txn_hex)
except Exception as e:
self.log.debug('getrawtransaction lock spend tx failed: %s', str(e))
elif state == BidStates.XMR_SWAP_NOSCRIPT_TX_REDEEMED:
txid_hex = bid.xmr_b_lock_tx.spend_txid.hex()
found_tx = ci_to.findTxnByHash(txid_hex)
if found_tx is not None:
self.log.info('Found coin b lock spend tx bid %s', bid_id.hex())
rv = True # Remove from swaps_in_progress
bid.setState(BidStates.SWAP_COMPLETED)
self.saveBidInSession(bid_id, bid, session, xmr_swap)
session.commit()
except Exception as ex:
raise ex
finally:
if session:
session.close()
session.remove()
self.mxDB.release()
return rv
5 years ago
def checkBidState(self, bid_id, bid, offer):
# assert(self.mxDB.locked())
# Return True to remove bid from in-progress list
state = BidStates(bid.state)
self.log.debug('checkBidState %s %s', bid_id.hex(), str(state))
4 years ago
if offer.swap_type == SwapTypes.XMR_SWAP:
return self.checkXmrBidState(bid_id, bid, offer)
save_bid = False
5 years ago
coin_from = Coins(offer.coin_from)
coin_to = Coins(offer.coin_to)
# TODO: Batch calls to scantxoutset
# TODO: timeouts
if state == BidStates.BID_ABANDONED:
self.log.info('Deactivating abandoned bid: %s', bid_id.hex())
return True # Mark bid for archiving
5 years ago
if state == BidStates.BID_ACCEPTED:
# Waiting for initiate txn to be confirmed in 'from' chain
initiate_txnid_hex = bid.initiate_tx.txid.hex()
p2sh = self.getScriptAddress(coin_from, bid.initiate_tx.script)
5 years ago
index = None
tx_height = None
last_initiate_txn_conf = bid.initiate_tx.conf
5 years ago
if coin_from == Coins.PART: # Has txindex
try:
initiate_txn = self.callcoinrpc(coin_from, 'getrawtransaction', [initiate_txnid_hex, True])
# Verify amount
vout = getVoutByAddress(initiate_txn, p2sh)
out_value = make_int(initiate_txn['vout'][vout]['value'])
assert(out_value == int(bid.amount)), 'Incorrect output amount in initiate txn {}: {} != {}.'.format(initiate_txnid_hex, out_value, int(bid.amount))
5 years ago
bid.initiate_tx.conf = initiate_txn['confirmations']
try:
tx_height = initiate_txn['height']
except Exception:
tx_height = -1
5 years ago
index = vout
except Exception:
pass
else:
if self.coin_clients[coin_from]['use_segwit']:
addr = self.encodeSegwitP2WSH(coin_from, getP2WSH(bid.initiate_tx.script))
5 years ago
else:
addr = p2sh
found = self.lookupUnspentByAddress(coin_from, addr, assert_amount=bid.amount, assert_txid=initiate_txnid_hex)
if found:
bid.initiate_tx.conf = found['n_conf']
5 years ago
index = found['index']
tx_height = found['height']
5 years ago
if bid.initiate_tx.conf != last_initiate_txn_conf:
save_bid = True
if bid.initiate_tx.conf is not None:
self.log.debug('initiate_txnid %s confirms %d', initiate_txnid_hex, bid.initiate_tx.conf)
5 years ago
if bid.initiate_tx.vout is None:
bid.initiate_tx.vout = index
5 years ago
# Start checking for spends of initiate_txn before fully confirmed
bid.initiate_tx.chain_height = self.setLastHeightChecked(coin_from, tx_height)
self.addWatchedOutput(coin_from, bid_id, initiate_txnid_hex, bid.initiate_tx.vout, BidStates.SWAP_INITIATED)
if bid.getITxState() is None or bid.getITxState() < TxStates.TX_SENT:
bid.setITxState(TxStates.TX_SENT)
save_bid = True
5 years ago
if bid.initiate_tx.conf >= self.coin_clients[coin_from]['blocks_confirmed']:
5 years ago
self.initiateTxnConfirmed(bid_id, bid, offer)
save_bid = True
5 years ago
# Bid times out if buyer doesn't see tx in chain within INITIATE_TX_TIMEOUT seconds
if bid.initiate_tx is None and \
bid.state_time + INITIATE_TX_TIMEOUT < int(time.time()):
5 years ago
self.log.info('Swap timed out waiting for initiate tx for bid %s', bid_id.hex())
bid.setState(BidStates.SWAP_TIMEDOUT, 'Timed out waiting for initiate tx')
5 years ago
self.saveBid(bid_id, bid)
return True # Mark bid for archiving
elif state == BidStates.SWAP_INITIATED:
# Waiting for participate txn to be confirmed in 'to' chain
if self.coin_clients[coin_to]['use_segwit']:
addr = self.encodeSegwitP2WSH(coin_to, getP2WSH(bid.participate_tx.script))
5 years ago
else:
addr = self.getScriptAddress(coin_to, bid.participate_tx.script)
5 years ago
found = self.lookupUnspentByAddress(coin_to, addr, assert_amount=bid.amount_to)
if found:
if bid.participate_tx.conf != found['n_conf']:
save_bid = True
bid.participate_tx.conf = found['n_conf']
5 years ago
index = found['index']
if bid.participate_tx is None or bid.participate_tx.txid is None:
5 years ago
self.log.debug('Found bid %s participate txn %s in chain %s', bid_id.hex(), found['txid'], coin_to)
self.addParticipateTxn(bid_id, bid, coin_to, found['txid'], found['index'], found['height'])
bid.setPTxState(TxStates.TX_SENT)
save_bid = True
5 years ago
if bid.participate_tx.conf is not None:
self.log.debug('participate txid %s confirms %d', bid.participate_tx.txid.hex(), bid.participate_tx.conf)
if bid.participate_tx.conf >= self.coin_clients[coin_to]['blocks_confirmed']:
5 years ago
self.participateTxnConfirmed(bid_id, bid, offer)
save_bid = True
5 years ago
elif state == BidStates.SWAP_PARTICIPATING:
# Waiting for initiate txn spend
pass
5 years ago
elif state == BidStates.BID_ERROR:
# Wait for user input
pass
5 years ago
else:
self.log.warning('checkBidState unknown state %s', state)
if state > BidStates.BID_ACCEPTED:
# Wait for spend of all known swap txns
if (bid.getITxState() is None or bid.getITxState() >= TxStates.TX_REDEEMED) \
and (bid.getPTxState() is None or bid.getPTxState() >= TxStates.TX_REDEEMED):
5 years ago
self.log.info('Swap completed for bid %s', bid_id.hex())
if bid.getITxState() == TxStates.TX_REDEEMED:
self.returnAddressToPool(bid_id, TxTypes.ITX_REFUND)
else:
self.returnAddressToPool(bid_id, TxTypes.ITX_REDEEM)
if bid.getPTxState() == TxStates.TX_REDEEMED:
self.returnAddressToPool(bid_id, TxTypes.PTX_REFUND)
else:
self.returnAddressToPool(bid_id, TxTypes.PTX_REDEEM)
5 years ago
bid.setState(BidStates.SWAP_COMPLETED)
self.saveBid(bid_id, bid)
return True # Mark bid for archiving
if save_bid:
self.saveBid(bid_id, bid)
5 years ago
# Try refund, keep trying until sent tx is spent
if (bid.getITxState() == TxStates.TX_SENT or bid.getITxState() == TxStates.TX_CONFIRMED) \
5 years ago
and bid.initiate_txn_refund is not None:
try:
txid = self.submitTxn(coin_from, bid.initiate_txn_refund.hex())
self.log.debug('Submitted initiate refund txn %s to %s chain for bid %s', txid, chainparams[coin_from]['name'], bid_id.hex())
# State will update when spend is detected
except Exception as ex:
if 'non-BIP68-final (code 64)' not in str(ex) and 'non-final' not in str(ex):
self.log.warning('Error trying to submit initiate refund txn: %s', str(ex))
if (bid.getPTxState() == TxStates.TX_SENT or bid.getPTxState() == TxStates.TX_CONFIRMED) \
5 years ago
and bid.participate_txn_refund is not None:
try:
txid = self.submitTxn(coin_to, bid.participate_txn_refund.hex())
self.log.debug('Submitted participate refund txn %s to %s chain for bid %s', txid, chainparams[coin_to]['name'], bid_id.hex())
# State will update when spend is detected
except Exception as ex:
if 'non-BIP68-final (code 64)' not in str(ex) and 'non-final' not in str(ex):
self.log.warning('Error trying to submit participate refund txn: %s', str(ex))
5 years ago
return False # Bid is still active
def extractSecret(self, coin_type, bid, spend_in):
try:
if coin_type == Coins.PART or self.coin_clients[coin_type]['use_segwit']:
assert(len(spend_in['txinwitness']) == 5), 'Bad witness size'
5 years ago
return bytes.fromhex(spend_in['txinwitness'][2])
else:
script_sig = spend_in['scriptSig']['asm'].split(' ')
assert(len(script_sig) == 5), 'Bad witness size'
5 years ago
return bytes.fromhex(script_sig[2])
except Exception:
return None
def addWatchedOutput(self, coin_type, bid_id, txid_hex, vout, tx_type, swap_type=None):
self.log.debug('Adding watched output %s bid %s tx %s type %s', coin_type, bid_id.hex(), txid_hex, tx_type)
watched = self.coin_clients[coin_type]['watched_outputs']
for wo in watched:
if wo.bid_id == bid_id and wo.txid_hex == txid_hex and wo.vout == vout:
self.log.debug('Output already being watched.')
return
watched.append(WatchedOutput(bid_id, txid_hex, vout, tx_type, swap_type))
5 years ago
def removeWatchedOutput(self, coin_type, bid_id, txid_hex):
# Remove all for bid if txid is None
self.log.debug('removeWatchedOutput %s %s %s', str(coin_type), bid_id.hex(), txid_hex)
old_len = len(self.coin_clients[coin_type]['watched_outputs'])
for i in range(old_len - 1, -1, -1):
wo = self.coin_clients[coin_type]['watched_outputs'][i]
if wo.bid_id == bid_id and (txid_hex is None or wo.txid_hex == txid_hex):
5 years ago
del self.coin_clients[coin_type]['watched_outputs'][i]
self.log.debug('Removed watched output %s %s %s', str(coin_type), bid_id.hex(), wo.txid_hex)
5 years ago
def initiateTxnSpent(self, bid_id, spend_txid, spend_n, spend_txn):
self.log.debug('Bid %s initiate txn spent by %s %d', bid_id.hex(), spend_txid, spend_n)
if bid_id in self.swaps_in_progress:
bid = self.swaps_in_progress[bid_id][0]
offer = self.swaps_in_progress[bid_id][1]
bid.initiate_tx.spend_txid = bytes.fromhex(spend_txid)
bid.initiate_tx.spend_n = spend_n
5 years ago
spend_in = spend_txn['vin'][spend_n]
coin_from = Coins(offer.coin_from)
coin_to = Coins(offer.coin_to)
secret = self.extractSecret(coin_from, bid, spend_in)
if secret is None:
self.log.info('Bid %s initiate txn refunded by %s %d', bid_id.hex(), spend_txid, spend_n)
# TODO: Wait for depth?
bid.setITxState(TxStates.TX_REFUNDED)
5 years ago
else:
self.log.info('Bid %s initiate txn redeemed by %s %d', bid_id.hex(), spend_txid, spend_n)
# TODO: Wait for depth?
bid.setITxState(TxStates.TX_REDEEMED)
5 years ago
self.removeWatchedOutput(coin_from, bid_id, bid.initiate_tx.txid.hex())
5 years ago
self.saveBid(bid_id, bid)
def participateTxnSpent(self, bid_id, spend_txid, spend_n, spend_txn):
self.log.debug('Bid %s participate txn spent by %s %d', bid_id.hex(), spend_txid, spend_n)
# TODO: More SwapTypes
if bid_id in self.swaps_in_progress:
bid = self.swaps_in_progress[bid_id][0]
offer = self.swaps_in_progress[bid_id][1]
bid.participate_tx.spend_txid = bytes.fromhex(spend_txid)
bid.participate_tx.spend_n = spend_n
5 years ago
spend_in = spend_txn['vin'][spend_n]
coin_from = Coins(offer.coin_from)
coin_to = Coins(offer.coin_to)
secret = self.extractSecret(coin_to, bid, spend_in)
if secret is None:
self.log.info('Bid %s participate txn refunded by %s %d', bid_id.hex(), spend_txid, spend_n)
# TODO: Wait for depth?
bid.setPTxState(TxStates.TX_REFUNDED)
5 years ago
else:
self.log.debug('Secret %s extracted from participate spend %s %d', secret.hex(), spend_txid, spend_n)
bid.recovered_secret = secret
# TODO: Wait for depth?
bid.setPTxState(TxStates.TX_REDEEMED)
5 years ago
if bid.was_sent:
txn = self.createRedeemTxn(coin_from, bid, for_txn_type='initiate')
txid = self.submitTxn(coin_from, txn)
5 years ago
bid.initiate_tx.spend_txid = bytes.fromhex(txid)
# bid.initiate_txn_redeem = bytes.fromhex(txn) # Worth keeping?
self.log.debug('Submitted initiate redeem txn %s to %s chain for bid %s', txid, chainparams[coin_from]['name'], bid_id.hex())
5 years ago
# TODO: Wait for depth? new state SWAP_TXI_REDEEM_SENT?
self.removeWatchedOutput(coin_to, bid_id, bid.participate_tx.txid.hex())
5 years ago
self.saveBid(bid_id, bid)
def process_XMR_SWAP_A_LOCK_tx_spend(self, bid_id, spend_txid_hex, spend_txn_hex):
self.log.debug('Detected spend of XMR swap coin a lock tx for bid %s', bid_id.hex())
self.mxDB.acquire()
try:
session = scoped_session(self.session_factory)
bid, xmr_swap = self.getXmrBidFromSession(session, bid_id)
assert(bid), 'Bid not found: {}.'.format(bid_id.hex())
assert(xmr_swap), 'XMR swap not found: {}.'.format(bid_id.hex())
offer, xmr_offer = self.getXmrOfferFromSession(session, bid.offer_id, sent=False)
assert(offer), 'Offer not found: {}.'.format(bid.offer_id.hex())
assert(xmr_offer), 'XMR offer not found: {}.'.format(bid.offer_id.hex())
coin_from = Coins(offer.coin_from)
coin_to = Coins(offer.coin_to)
state = BidStates(bid.state)
spending_txid = bytes.fromhex(spend_txid_hex)
if spending_txid == xmr_swap.a_lock_spend_tx_id:
if state == BidStates.XMR_SWAP_LOCK_RELEASED:
xmr_swap.a_lock_spend_tx = bytes.fromhex(spend_txn_hex)
bid.setState(BidStates.XMR_SWAP_SCRIPT_TX_REDEEMED) # TODO: Wait for confirmation?
if not bid.was_received:
bid.setState(BidStates.SWAP_COMPLETED)
if bid.was_received:
bid.setState(BidStates.SWAP_DELAYING)
delay = random.randrange(self.min_delay_event, self.max_delay_event)
self.log.info('Redeeming coin b lock tx for bid %s in %d seconds', bid_id.hex(), delay)
self.createEventInSession(delay, EventTypes.REDEEM_XMR_SWAP_LOCK_TX_B, bid_id, session)
else:
# Could already be processed if spend was detected in the mempool
self.log.warning('Coin a lock tx spend ignored due to bid state for bid {}'.format(bid_id.hex()))
elif spending_txid == xmr_swap.a_lock_refund_tx_id:
self.log.debug('Coin a lock tx spent by lock refund tx.')
pass
else:
self.setBidError(bid.bid_id, bid, 'Unexpected txn spent coin a lock tx: {}'.format(spend_txid_hex), save_bid=False)
self.saveBidInSession(bid_id, bid, session, xmr_swap, save_in_progress=offer)
session.commit()
except Exception as ex:
self.log.error('process_XMR_SWAP_A_LOCK_tx_spend %s', str(ex))
if self.debug:
traceback.print_exc()
finally:
session.close()
session.remove()
self.mxDB.release()
def process_XMR_SWAP_A_LOCK_REFUND_tx_spend(self, bid_id, spend_txid_hex, spend_txn):
self.log.debug('Detected spend of XMR swap coin a lock refund tx for bid %s', bid_id.hex())
self.mxDB.acquire()
try:
session = scoped_session(self.session_factory)
bid, xmr_swap = self.getXmrBidFromSession(session, bid_id)
assert(bid), 'Bid not found: {}.'.format(bid_id.hex())
assert(xmr_swap), 'XMR swap not found: {}.'.format(bid_id.hex())
offer, xmr_offer = self.getXmrOfferFromSession(session, bid.offer_id, sent=False)
assert(offer), 'Offer not found: {}.'.format(bid.offer_id.hex())
assert(xmr_offer), 'XMR offer not found: {}.'.format(bid.offer_id.hex())
coin_from = Coins(offer.coin_from)
coin_to = Coins(offer.coin_to)
state = BidStates(bid.state)
spending_txid = bytes.fromhex(spend_txid_hex)
if spending_txid == xmr_swap.a_lock_refund_spend_tx_id:
self.log.info('Found coin a lock refund spend tx, bid {}'.format(bid_id.hex()))
if bid.was_sent:
xmr_swap.a_lock_refund_spend_tx = bytes.fromhex(spend_txn['hex']) # Replace with fully signed tx
if TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND not in bid.txns:
bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND] = SwapTx(
bid_id=bid_id,
tx_type=TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND,
txid=xmr_swap.a_lock_refund_spend_tx_id,
)
if bid.xmr_b_lock_tx is not None:
delay = random.randrange(self.min_delay_event, self.max_delay_event)
self.log.info('Recovering xmr swap chain B lock tx for bid %s in %d seconds', bid_id.hex(), delay)
self.createEventInSession(delay, EventTypes.RECOVER_XMR_SWAP_LOCK_TX_B, bid_id, session)
else:
bid.setState(BidStates.XMR_SWAP_FAILED_REFUNDED)
if bid.was_received:
if not bid.was_sent:
bid.setState(BidStates.XMR_SWAP_FAILED_REFUNDED)
else:
self.log.info('Coin a lock refund spent by unknown tx, bid {}'.format(bid_id.hex()))
bid.setState(BidStates.XMR_SWAP_FAILED_SWIPED)
self.saveBidInSession(bid_id, bid, session, xmr_swap, save_in_progress=offer)
session.commit()
except Exception as ex:
self.log.error('process_XMR_SWAP_A_LOCK_REFUND_tx_spend %s', str(ex))
if self.debug:
traceback.print_exc()
finally:
session.close()
session.remove()
self.mxDB.release()
def processSpentOutput(self, coin_type, watched_output, spend_txid_hex, spend_n, spend_txn):
if watched_output.swap_type == SwapTypes.XMR_SWAP:
if watched_output.tx_type == TxTypes.XMR_SWAP_A_LOCK:
self.process_XMR_SWAP_A_LOCK_tx_spend(watched_output.bid_id, spend_txid_hex, spend_txn['hex'])
elif watched_output.tx_type == TxTypes.XMR_SWAP_A_LOCK_REFUND:
self.process_XMR_SWAP_A_LOCK_REFUND_tx_spend(watched_output.bid_id, spend_txid_hex, spend_txn)
self.removeWatchedOutput(coin_type, watched_output.bid_id, watched_output.txid_hex)
return
if watched_output.tx_type == BidStates.SWAP_PARTICIPATING:
self.participateTxnSpent(watched_output.bid_id, spend_txid_hex, spend_n, spend_txn)
else:
self.initiateTxnSpent(watched_output.bid_id, spend_txid_hex, spend_n, spend_txn)
5 years ago
def checkForSpends(self, coin_type, c):
# assert(self.mxDB.locked())
self.log.debug('checkForSpends %s', coin_type)
5 years ago
if coin_type == Coins.PART and self.coin_clients[coin_type]['have_spent_index']:
5 years ago
# TODO: batch getspentinfo
for o in c['watched_outputs']:
found_spend = None
try:
found_spend = self.callcoinrpc(Coins.PART, 'getspentinfo', [{'txid': o.txid_hex, 'index': o.vout}])
except Exception as ex:
if 'Unable to get spent info' not in str(ex):
self.log.warning('getspentinfo %s', str(ex))
5 years ago
if found_spend is not None:
self.log.debug('Found spend in spentindex %s %d in %s %d', o.txid_hex, o.vout, found_spend['txid'], found_spend['index'])
5 years ago
spend_txid = found_spend['txid']
spend_n = found_spend['index']
spend_txn = self.callcoinrpc(Coins.PART, 'getrawtransaction', [spend_txid, True])
self.processSpentOutput(coin_type, o, spend_txid, spend_n, spend_txn)
5 years ago
else:
chain_blocks = self.callcoinrpc(coin_type, 'getblockchaininfo')['blocks']
last_height_checked = c['last_height_checked']
self.log.debug('chain_blocks, last_height_checked %s %s', chain_blocks, last_height_checked)
while last_height_checked < chain_blocks:
block_hash = self.callcoinrpc(coin_type, 'getblockhash', [last_height_checked + 1])
try:
block = self.callcoinrpc(coin_type, 'getblock', [block_hash, 2])
except Exception as e:
if 'Block not available (pruned data)' in str(e):
# TODO: Better solution?
bci = self.callcoinrpc(coin_type, 'getblockchaininfo')
self.log.error('Coin %s last_height_checked %d set to pruneheight %d', self.ci(coin_type).coin_name(), last_height_checked, bci['pruneheight'])
last_height_checked = bci['pruneheight']
continue
5 years ago
for tx in block['tx']:
for i, inp in enumerate(tx['vin']):
for o in c['watched_outputs']:
inp_txid = inp.get('txid', None)
if inp_txid is None: # Coinbase
continue
if inp_txid == o.txid_hex and inp['vout'] == o.vout:
self.log.debug('Found spend from search %s %d in %s %d', o.txid_hex, o.vout, tx['txid'], i)
self.processSpentOutput(coin_type, o, tx['txid'], i, tx)
5 years ago
last_height_checked += 1
if c['last_height_checked'] != last_height_checked:
c['last_height_checked'] = last_height_checked
self.setIntKV('last_height_checked_' + chainparams[coin_type]['name'], last_height_checked)
5 years ago
def expireMessages(self):
self.mxDB.acquire()
try:
now = int(time.time())
options = {'encoding': 'none'}
ro = self.callrpc('smsginbox', ['all', '', options])
num_messages = 0
num_removed = 0
5 years ago
for msg in ro['messages']:
num_messages += 1
expire_at = msg['sent'] + msg['ttl']
5 years ago
if expire_at < now:
options = {'encoding': 'none', 'delete': True}
del_msg = self.callrpc('smsg', [msg['msgid'], options])
num_removed += 1
if num_messages + num_removed > 0:
self.log.info('Expired {} / {} messages.'.format(num_removed, num_messages))
5 years ago
self.log.debug('TODO: Expire records from db')
5 years ago
5 years ago
finally:
self.mxDB.release()
def checkEvents(self):
self.mxDB.acquire()
4 years ago
now = int(time.time())
session = None
5 years ago
try:
session = scoped_session(self.session_factory)
q = session.query(EventQueue).filter(sa.and_(EventQueue.active_ind == 1, EventQueue.trigger_at <= now))
5 years ago
for row in q:
try:
if row.event_type == EventTypes.ACCEPT_BID:
self.acceptBid(row.linked_id)
elif row.event_type == EventTypes.ACCEPT_XMR_BID:
self.acceptXmrBid(row.linked_id)
elif row.event_type == EventTypes.SIGN_XMR_SWAP_LOCK_TX_A:
self.sendXmrBidTxnSigsFtoL(row.linked_id, session)
elif row.event_type == EventTypes.SEND_XMR_SWAP_LOCK_TX_A:
self.sendXmrBidCoinALockTx(row.linked_id, session)
elif row.event_type == EventTypes.SEND_XMR_SWAP_LOCK_TX_B:
self.sendXmrBidCoinBLockTx(row.linked_id, session)
elif row.event_type == EventTypes.SEND_XMR_LOCK_RELEASE:
self.sendXmrBidLockRelease(row.linked_id, session)
elif row.event_type == EventTypes.REDEEM_XMR_SWAP_LOCK_TX_A:
self.redeemXmrBidCoinALockTx(row.linked_id, session)
elif row.event_type == EventTypes.REDEEM_XMR_SWAP_LOCK_TX_B:
self.redeemXmrBidCoinBLockTx(row.linked_id, session)
elif row.event_type == EventTypes.RECOVER_XMR_SWAP_LOCK_TX_B:
self.recoverXmrBidCoinBLockTx(row.linked_id, session)
else:
self.log.warning('Unknown event type: %d', row.event_type)
except Exception as ex:
if self.debug:
traceback.print_exc()
self.log.error('checkEvents failed: {}'.format(str(ex)))
if self.debug:
session.execute('UPDATE eventqueue SET active_ind = 2 WHERE trigger_at <= {}'.format(now))
else:
session.execute('DELETE FROM eventqueue WHERE trigger_at <= {}'.format(now))
5 years ago
session.commit()
5 years ago
finally:
if session:
session.close()
session.remove()
5 years ago
self.mxDB.release()
4 years ago
def checkXmrSwaps(self):
self.mxDB.acquire()
now = int(time.time())
ttl_xmr_split_messages = 60 * 60
session = None
4 years ago
try:
session = scoped_session(self.session_factory)
q = session.query(Bid).filter(Bid.state == BidStates.BID_RECEIVING)
for bid in q:
q = session.execute('SELECT COUNT(*) FROM xmr_split_data WHERE bid_id = x\'{}\' AND msg_type = {}'.format(bid.bid_id.hex(), XmrSplitMsgTypes.BID)).first()
4 years ago
num_segments = q[0]
if num_segments > 1:
try:
self.receiveXmrBid(bid, session)
except Exception as ex:
self.log.info('Verify xmr bid {} failed: {}'.format(bid.bid_id.hex(), str(ex)))
bid.setState(BidStates.BID_ERROR, 'Failed validation: ' + str(ex))
4 years ago
session.add(bid)
self.updateBidInProgress(bid)
4 years ago
continue
if bid.created_at + ttl_xmr_split_messages < now:
self.log.debug('Expiring partially received bid: {}'.format(bid.bid_id.hex()))
bid.setState(BidStates.BID_ERROR, 'Timed out')
session.add(bid)
q = session.query(Bid).filter(Bid.state == BidStates.BID_RECEIVING_ACC)
for bid in q:
q = session.execute('SELECT COUNT(*) FROM xmr_split_data WHERE bid_id = x\'{}\' AND msg_type = {}'.format(bid.bid_id.hex(), XmrSplitMsgTypes.BID_ACCEPT)).first()
4 years ago
num_segments = q[0]
if num_segments > 1:
try:
self.receiveXmrBidAccept(bid, session)
except Exception as ex:
self.log.info('Verify xmr bid accept {} failed: {}'.format(bid.bid_id.hex(), str(ex)))
bid.setState(BidStates.BID_ERROR, 'Failed accept validation: ' + str(ex))
4 years ago
session.add(bid)
self.updateBidInProgress(bid)
4 years ago
continue
if bid.created_at + ttl_xmr_split_messages < now:
self.log.debug('Expiring partially received bid accept: {}'.format(bid.bid_id.hex()))
bid.setState(BidStates.BID_ERROR, 'Timed out')
session.add(bid)
# Expire old records
q = session.query(XmrSplitData).filter(XmrSplitData.created_at + ttl_xmr_split_messages < now)
q.delete(synchronize_session=False)
4 years ago
session.commit()
finally:
if session:
session.close()
session.remove()
4 years ago
self.mxDB.release()
5 years ago
def processOffer(self, msg):
assert(msg['to'] == self.network_addr), 'Offer received on wrong address'
offer_bytes = bytes.fromhex(msg['hex'][2:-2])
offer_data = OfferMessage()
offer_data.ParseFromString(offer_bytes)
# Validate data
now = int(time.time())
coin_from = Coins(offer_data.coin_from)
ci_from = self.ci(coin_from)
5 years ago
coin_to = Coins(offer_data.coin_to)
ci_to = self.ci(coin_to)
5 years ago
chain_from = chainparams[coin_from][self.chain]
assert(offer_data.coin_from != offer_data.coin_to), 'coin_from == coin_to'
self.validateSwapType(coin_from, coin_to, offer_data.swap_type)
5 years ago
self.validateOfferAmounts(coin_from, coin_to, offer_data.amount_from, offer_data.rate, offer_data.min_bid_amount)
self.validateOfferLockValue(coin_from, coin_to, offer_data.lock_type, offer_data.lock_value)
5 years ago
assert(offer_data.time_valid >= MIN_OFFER_VALID_TIME and offer_data.time_valid <= MAX_OFFER_VALID_TIME), 'Invalid time_valid'
assert(msg['sent'] + offer_data.time_valid >= now), 'Offer expired'
if offer_data.swap_type == SwapTypes.SELLER_FIRST:
assert(len(offer_data.proof_address) == 0), 'Unexpected data'
assert(len(offer_data.proof_signature) == 0), 'Unexpected data'
assert(len(offer_data.pkhash_seller) == 0), 'Unexpected data'
assert(len(offer_data.secret_hash) == 0), 'Unexpected data'
5 years ago
elif offer_data.swap_type == SwapTypes.BUYER_FIRST:
raise ValueError('TODO')
elif offer_data.swap_type == SwapTypes.XMR_SWAP:
assert(coin_from != Coins.XMR)
assert(coin_to == Coins.XMR)
self.log.debug('TODO - More restrictions')
5 years ago
else:
raise ValueError('Unknown swap type {}.'.format(offer_data.swap_type))
offer_id = bytes.fromhex(msg['msgid'])
if self.isOfferRevoked(offer_id, msg['from']):
raise ValueError('Offer has been revoked {}.'.format(offer_id.hex()))
5 years ago
session = scoped_session(self.session_factory)
try:
# Check for sent
existing_offer = self.getOffer(offer_id)
if existing_offer is None:
offer = Offer(
offer_id=offer_id,
active_ind=1,
coin_from=offer_data.coin_from,
coin_to=offer_data.coin_to,
amount_from=offer_data.amount_from,
rate=offer_data.rate,
min_bid_amount=offer_data.min_bid_amount,
time_valid=offer_data.time_valid,
lock_type=int(offer_data.lock_type),
lock_value=offer_data.lock_value,
swap_type=offer_data.swap_type,
addr_from=msg['from'],
created_at=msg['sent'],
expire_at=msg['sent'] + offer_data.time_valid,
was_sent=False)
offer.setState(OfferStates.OFFER_RECEIVED)
session.add(offer)
if offer.swap_type == SwapTypes.XMR_SWAP:
xmr_offer = XmrOffer()
xmr_offer.offer_id = offer_id
xmr_offer.lock_time_1 = ci_from.getExpectedSequence(offer_data.lock_type, offer_data.lock_value)
xmr_offer.lock_time_2 = ci_from.getExpectedSequence(offer_data.lock_type, offer_data.lock_value)
xmr_offer.a_fee_rate = offer_data.fee_rate_from
xmr_offer.b_fee_rate = offer_data.fee_rate_to
session.add(xmr_offer)
self.log.debug('Received new offer %s', offer_id.hex())
else:
existing_offer.setState(OfferStates.OFFER_RECEIVED)
session.add(existing_offer)
session.commit()
finally:
session.close()
session.remove()
5 years ago
def processOfferRevoke(self, msg):
assert(msg['to'] == self.network_addr), 'Message received on wrong address'
msg_bytes = bytes.fromhex(msg['hex'][2:-2])
msg_data = OfferRevokeMessage()
msg_data.ParseFromString(msg_bytes)
now = int(time.time())
self.mxDB.acquire()
session = None
try:
session = scoped_session(self.session_factory)
if len(msg_data.offer_msg_id) != 28:
raise ValueError('Invalid msg_id length')
if len(msg_data.signature) != 65:
raise ValueError('Invalid signature length')
offer = session.query(Offer).filter_by(offer_id=msg_data.offer_msg_id).first()
if offer is None:
self.storeOfferRevoke(msg_data.offer_msg_id, msg_data.signature)
raise ValueError('Offer not found: {}'.format(msg_data.offer_msg_id.hex()))
if offer.expire_at <= now:
raise ValueError('Offer already expired: {}'.format(msg_data.offer_msg_id.hex()))
signature_enc = base64.b64encode(msg_data.signature).decode('utf-8')
passed = self.callcoinrpc(Coins.PART, 'verifymessage', [offer.addr_from, signature_enc, msg_data.offer_msg_id.hex() + '_revoke'])
assert(passed is True), 'Signature invalid'
offer.active_ind = 2
# TODO: Remove message, or wait for expire
session.add(offer)
session.commit()
finally:
if session:
session.close()
session.remove()
self.mxDB.release()
5 years ago
def processBid(self, msg):
self.log.debug('Processing bid msg %s', msg['msgid'])
now = int(time.time())
bid_bytes = bytes.fromhex(msg['hex'][2:-2])
bid_data = BidMessage()
bid_data.ParseFromString(bid_bytes)
# Validate data
assert(len(bid_data.offer_msg_id) == 28), 'Bad offer_id length'
5 years ago
assert(bid_data.time_valid >= MIN_BID_VALID_TIME and bid_data.time_valid <= MAX_BID_VALID_TIME), 'Invalid time_valid'
offer_id = bid_data.offer_msg_id
offer = self.getOffer(offer_id, sent=True)
assert(offer and offer.was_sent), 'Unknown offerid'
assert(offer.state == OfferStates.OFFER_RECEIVED), 'Bad offer state'
assert(msg['to'] == offer.addr_from), 'Received on incorrect address'
assert(now <= offer.expire_at), 'Offer expired'
assert(bid_data.amount >= offer.min_bid_amount), 'Bid amount below minimum'
assert(bid_data.amount <= offer.amount_from), 'Bid amount above offer amount'
5 years ago
assert(now <= msg['sent'] + bid_data.time_valid), 'Bid expired'
# TODO: Allow higher bids
5 years ago
# assert(bid_data.rate != offer['data'].rate), 'Bid rate mismatch'
coin_to = Coins(offer.coin_to)
amount_to = int((bid_data.amount * offer.rate) // self.ci(offer.coin_from).COIN())
5 years ago
swap_type = offer.swap_type
if swap_type == SwapTypes.SELLER_FIRST:
assert(len(bid_data.pkhash_buyer) == 20), 'Bad pkhash_buyer length'
# Verify proof of funds
bid_proof_address = replaceAddrPrefix(bid_data.proof_address, Coins.PART, self.chain)
mm = chainparams[coin_to]['message_magic']
passed = self.ci(Coins.PART).verifyMessage(bid_proof_address, bid_data.proof_address + '_swap_proof_' + offer_id.hex(), bid_data.proof_signature, mm)
5 years ago
assert(passed is True), 'Proof of funds signature invalid'
if self.coin_clients[coin_to]['use_segwit']:
addr_search = self.encodeSegwit(coin_to, decodeAddress(bid_data.proof_address)[1:])
else:
addr_search = bid_data.proof_address
sum_unspent = self.getAddressBalance(coin_to, addr_search)
self.log.debug('Proof of funds %s %s', bid_data.proof_address, self.ci(coin_to).format_amount(sum_unspent))
assert(sum_unspent >= amount_to), 'Proof of funds failed'
5 years ago
elif swap_type == SwapTypes.BUYER_FIRST:
raise ValueError('TODO')
else:
raise ValueError('Unknown swap type {}.'.format(swap_type))
bid_id = bytes.fromhex(msg['msgid'])
bid = self.getBid(bid_id)
if bid is None:
bid = Bid(
bid_id=bid_id,
offer_id=offer_id,
amount=bid_data.amount,
pkhash_buyer=bid_data.pkhash_buyer,
created_at=msg['sent'],
amount_to=amount_to,
5 years ago
expire_at=msg['sent'] + bid_data.time_valid,
bid_addr=msg['from'],
was_received=True,
)
else:
assert(bid.state == BidStates.BID_SENT), 'Wrong bid state: {}'.format(str(BidStates(bid.state)))
5 years ago
bid.created_at = msg['sent']
bid.expire_at = msg['sent'] + bid_data.time_valid
bid.was_received = True
if len(bid_data.proof_address) > 0:
bid.proof_address = bid_data.proof_address
bid.setState(BidStates.BID_RECEIVED)
self.log.info('Received valid bid %s for offer %s', bid_id.hex(), bid_data.offer_msg_id.hex())
self.saveBid(bid_id, bid)
# Auto accept bid if set and no other non-abandoned bid for this order exists
if offer.auto_accept_bids:
if self.countAcceptedBids(offer_id) > 0:
self.log.info('Not auto accepting bid %s, already have', bid_id.hex())
elif bid_data.amount != offer.amount_from:
self.log.info('Not auto accepting bid %s, want exact amount match', bid_id.hex())
5 years ago
else:
delay = random.randrange(self.min_delay_event, self.max_delay_event)
5 years ago
self.log.info('Auto accepting bid %s in %d seconds', bid_id.hex(), delay)
self.createEvent(delay, EventTypes.ACCEPT_BID, bid_id)
5 years ago
def processBidAccept(self, msg):
self.log.debug('Processing bid accepted msg %s', msg['msgid'])
now = int(time.time())
bid_accept_bytes = bytes.fromhex(msg['hex'][2:-2])
bid_accept_data = BidAcceptMessage()
bid_accept_data.ParseFromString(bid_accept_bytes)
assert(len(bid_accept_data.bid_msg_id) == 28), 'Bad bid_msg_id length'
assert(len(bid_accept_data.initiate_txid) == 32), 'Bad initiate_txid length'
assert(len(bid_accept_data.contract_script) < 100), 'Bad contract_script length'
self.log.debug('for bid %s', bid_accept_data.bid_msg_id.hex())
bid_id = bid_accept_data.bid_msg_id
bid, offer = self.getBidAndOffer(bid_id)
assert(bid is not None and bid.was_sent is True), 'Unknown bidid'
assert(offer), 'Offer not found ' + bid.offer_id.hex()
coin_from = Coins(offer.coin_from)
ci_from = self.ci(coin_from)
5 years ago
assert(bid.expire_at > now + self._bid_expired_leeway), 'Bid expired'
5 years ago
if bid.state >= BidStates.BID_ACCEPTED:
if bid.was_received: # Sent to self
self.log.info('Received valid bid accept %s for bid %s sent to self', bid.accept_msg_id.hex(), bid_id.hex())
return
raise ValueError('Wrong bid state: {}'.format(str(BidStates(bid.state))))
5 years ago
use_csv = True if offer.lock_type < ABS_LOCK_BLOCKS else False
# TODO: Verify script without decoding?
decoded_script = self.callcoinrpc(Coins.PART, 'decodescript', [bid_accept_data.contract_script.hex()])
lock_check_op = 'OP_CHECKSEQUENCEVERIFY' if use_csv else 'OP_CHECKLOCKTIMEVERIFY'
prog = re.compile(r'OP_IF OP_SIZE 32 OP_EQUALVERIFY OP_SHA256 (\w+) OP_EQUALVERIFY OP_DUP OP_HASH160 (\w+) OP_ELSE (\d+) {} OP_DROP OP_DUP OP_HASH160 (\w+) OP_ENDIF OP_EQUALVERIFY OP_CHECKSIG'.format(lock_check_op))
rr = prog.match(decoded_script['asm'])
if not rr:
raise ValueError('Bad script')
scriptvalues = rr.groups()
5 years ago
assert(len(scriptvalues[0]) == 64), 'Bad secret_hash length'
assert(bytes.fromhex(scriptvalues[1]) == bid.pkhash_buyer), 'pkhash_buyer mismatch'
5 years ago
script_lock_value = int(scriptvalues[2])
if use_csv:
expect_sequence = ci_from.getExpectedSequence(offer.lock_type, offer.lock_value)
5 years ago
assert(script_lock_value == expect_sequence), 'sequence mismatch'
else:
5 years ago
if offer.lock_type == ABS_LOCK_BLOCKS:
self.log.warning('TODO: validate absolute lock values')
else:
assert(script_lock_value <= bid.created_at + offer.lock_value + INITIATE_TX_TIMEOUT), 'script lock time too high'
assert(script_lock_value >= bid.created_at + offer.lock_value), 'script lock time too low'
5 years ago
assert(len(scriptvalues[3]) == 40), 'pkhash_refund bad length'
assert(bid.accept_msg_id is None), 'Bid already accepted'
bid.accept_msg_id = bytes.fromhex(msg['msgid'])
bid.initiate_tx = SwapTx(
bid_id=bid_id,
tx_type=TxTypes.ITX,
txid=bid_accept_data.initiate_txid,
script=bid_accept_data.contract_script,
)
5 years ago
bid.pkhash_seller = bytes.fromhex(scriptvalues[3])
bid.setState(BidStates.BID_ACCEPTED)
bid.setITxState(TxStates.TX_NONE)
5 years ago
self.log.info('Received valid bid accept %s for bid %s', bid.accept_msg_id.hex(), bid_id.hex())
self.saveBid(bid_id, bid)
self.swaps_in_progress[bid_id] = (bid, offer)
4 years ago
def receiveXmrBid(self, bid, session):
self.log.debug('Receiving xmr bid %s', bid.bid_id.hex())
now = int(time.time())
offer, xmr_offer = self.getXmrOfferFromSession(session, bid.offer_id, sent=True)
assert(offer and offer.was_sent), 'Offer not found: {}.'.format(bid.offer_id.hex())
assert(xmr_offer), 'XMR offer not found: {}.'.format(bid.offer_id.hex())
4 years ago
xmr_swap = session.query(XmrSwap).filter_by(bid_id=bid.bid_id).first()
assert(xmr_swap), 'XMR swap not found: {}.'.format(bid.bid_id.hex())
ci_from = self.ci(Coins(offer.coin_from))
ci_to = self.ci(Coins(offer.coin_to))
4 years ago
if len(xmr_swap.kbsf_dleag) < ci_to.lengthDLEAG():
q = session.query(XmrSplitData).filter(sa.and_(XmrSplitData.bid_id == bid.bid_id, XmrSplitData.msg_type == XmrSplitMsgTypes.BID)).order_by(XmrSplitData.msg_sequence.asc())
for row in q:
xmr_swap.kbsf_dleag += row.dleag
if not ci_to.verifyKey(xmr_swap.vkbvf):
raise ValueError('Invalid key.')
if not ci_to.verifyDLEAG(xmr_swap.kbsf_dleag):
raise ValueError('Invalid DLEAG proof.')
# Extract pubkeys from MSG1L DLEAG
xmr_swap.pkasf = xmr_swap.kbsf_dleag[0: 33]
if not ci_from.verifyPubkey(xmr_swap.pkasf):
raise ValueError('Invalid coin a pubkey.')
xmr_swap.pkbsf = xmr_swap.kbsf_dleag[33: 33 + 32]
if not ci_to.verifyPubkey(xmr_swap.pkbsf):
raise ValueError('Invalid coin b pubkey.')
4 years ago
if not ci_from.verifyPubkey(xmr_swap.pkaf):
raise ValueError('Invalid pubkey.')
self.log.info('Received valid bid %s for xmr offer %s', bid.bid_id.hex(), bid.offer_id.hex())
4 years ago
bid.setState(BidStates.BID_RECEIVED)
self.saveBidInSession(bid.bid_id, bid, session, xmr_swap)
# Auto accept bid if set and no other non-abandoned bid for this order exists
if offer.auto_accept_bids:
if self.countAcceptedBids(bid.offer_id) > 0:
self.log.info('Not auto accepting bid %s, already have', bid.bid_id.hex())
elif bid.amount != offer.amount_from:
self.log.info('Not auto accepting bid %s, want exact amount match', bid.bid_id.hex())
else:
delay = random.randrange(self.min_delay_event, self.max_delay_event)
self.log.info('Auto accepting xmr bid %s in %d seconds', bid.bid_id.hex(), delay)
self.createEventInSession(delay, EventTypes.ACCEPT_XMR_BID, bid.bid_id, session)
4 years ago
def receiveXmrBidAccept(self, bid, session):
# Follower receiving MSG1F and MSG2F
4 years ago
self.log.debug('Receiving xmr bid accept %s', bid.bid_id.hex())
now = int(time.time())
offer, xmr_offer = self.getXmrOffer(bid.offer_id, sent=True)
assert(offer), 'Offer not found: {}.'.format(bid.offer_id.hex())
assert(xmr_offer), 'XMR offer not found: {}.'.format(bid.offer_id.hex())
xmr_swap = session.query(XmrSwap).filter_by(bid_id=bid.bid_id).first()
assert(xmr_swap), 'XMR swap not found: {}.'.format(bid.bid_id.hex())
ci_from = self.ci(Coins(offer.coin_from))
ci_to = self.ci(Coins(offer.coin_to))
4 years ago
if len(xmr_swap.kbsl_dleag) < ci_to.lengthDLEAG():
q = session.query(XmrSplitData).filter(sa.and_(XmrSplitData.bid_id == bid.bid_id, XmrSplitData.msg_type == XmrSplitMsgTypes.BID_ACCEPT)).order_by(XmrSplitData.msg_sequence.asc())
for row in q:
xmr_swap.kbsl_dleag += row.dleag
if not ci_to.verifyKey(xmr_swap.vkbvl):
raise ValueError('Invalid key.')
if not ci_to.verifyDLEAG(xmr_swap.kbsl_dleag):
raise ValueError('Invalid DLEAG proof.')
# Extract pubkeys from MSG1F DLEAG
xmr_swap.pkasl = xmr_swap.kbsl_dleag[0: 33]
if not ci_from.verifyPubkey(xmr_swap.pkasl):
raise ValueError('Invalid coin a pubkey.')
xmr_swap.pkbsl = xmr_swap.kbsl_dleag[33: 33 + 32]
if not ci_to.verifyPubkey(xmr_swap.pkbsl):
raise ValueError('Invalid coin b pubkey.')
xmr_swap.vkbv = ci_to.sumKeys(xmr_swap.vkbvl, xmr_swap.vkbvf)
xmr_swap.pkbv = ci_to.sumPubkeys(xmr_swap.pkbvl, xmr_swap.pkbvf)
xmr_swap.pkbs = ci_to.sumPubkeys(xmr_swap.pkbsl, xmr_swap.pkbsf)
4 years ago
if not ci_from.verifyPubkey(xmr_swap.pkal):
raise ValueError('Invalid pubkey.')
if xmr_swap.pkbvl == xmr_swap.pkbvf:
raise ValueError('Duplicate scriptless view pubkey.')
if xmr_swap.pkbsl == xmr_swap.pkbsf:
raise ValueError('Duplicate scriptless spend pubkey.')
if xmr_swap.pkal == xmr_swap.pkaf:
raise ValueError('Duplicate script spend pubkey.')
bid.setState(BidStates.SWAP_DELAYING)
4 years ago
self.saveBidInSession(bid.bid_id, bid, session, xmr_swap)
delay = random.randrange(self.min_delay_event, self.max_delay_event)
self.log.info('Responding to xmr bid accept %s in %d seconds', bid.bid_id.hex(), delay)
self.createEventInSession(delay, EventTypes.SIGN_XMR_SWAP_LOCK_TX_A, bid.bid_id, session)
4 years ago
def processXmrBid(self, msg):
# MSG1L
4 years ago
self.log.debug('Processing xmr bid msg %s', msg['msgid'])
now = int(time.time())
bid_bytes = bytes.fromhex(msg['hex'][2:-2])
bid_data = XmrBidMessage()
bid_data.ParseFromString(bid_bytes)
# Validate data
assert(len(bid_data.offer_msg_id) == 28), 'Bad offer_id length'
assert(bid_data.time_valid >= MIN_BID_VALID_TIME and bid_data.time_valid <= MAX_BID_VALID_TIME), 'Invalid time_valid'
offer_id = bid_data.offer_msg_id
offer, xmr_offer = self.getXmrOffer(offer_id, sent=True)
assert(offer and offer.was_sent), 'Offer not found: {}.'.format(offer_id.hex())
assert(xmr_offer), 'XMR offer not found: {}.'.format(offer_id.hex())
ci_from = self.ci(Coins(offer.coin_from))
ci_to = self.ci(Coins(offer.coin_to))
4 years ago
assert(offer.state == OfferStates.OFFER_RECEIVED), 'Bad offer state'
assert(msg['to'] == offer.addr_from), 'Received on incorrect address'
assert(now <= offer.expire_at), 'Offer expired'
assert(bid_data.amount >= offer.min_bid_amount), 'Bid amount below minimum'
assert(bid_data.amount <= offer.amount_from), 'Bid amount above offer amount'
assert(now <= msg['sent'] + bid_data.time_valid), 'Bid expired'
self.log.debug('TODO: xmr bid validation')
assert(ci_to.verifyKey(bid_data.kbvf))
assert(ci_from.verifyPubkey(bid_data.pkaf))
4 years ago
bid_id = bytes.fromhex(msg['msgid'])
bid, xmr_swap = self.getXmrBid(bid_id)
if bid is None:
bid = Bid(
bid_id=bid_id,
offer_id=offer_id,
amount=bid_data.amount,
created_at=msg['sent'],
amount_to=(bid_data.amount * offer.rate) // ci_from.COIN(),
4 years ago
expire_at=msg['sent'] + bid_data.time_valid,
bid_addr=msg['from'],
was_received=True,
)
xmr_swap = XmrSwap(
bid_id=bid_id,
dest_af=bid_data.dest_af,
4 years ago
pkaf=bid_data.pkaf,
vkbvf=bid_data.kbvf,
pkbvf=ci_to.getPubkey(bid_data.kbvf),
4 years ago
kbsf_dleag=bid_data.kbsf_dleag,
b_restore_height=ci_to.getChainHeight(),
start_chain_a_height=ci_from.getChainHeight(),
4 years ago
)
if xmr_swap.b_restore_height < ci_to._restore_height:
xmr_swap.b_restore_height = ci_to._restore_height
self.log.warning('XMR swap restore height clamped to {}'.format(ci_to._restore_height))
4 years ago
else:
assert(bid.state == BidStates.BID_SENT), 'Wrong bid state: {}'.format(str(BidStates(bid.state)))
4 years ago
bid.created_at = msg['sent']
bid.expire_at = msg['sent'] + bid_data.time_valid
bid.was_received = True
bid.setState(BidStates.BID_RECEIVING)
self.log.info('Receiving xmr bid %s for offer %s', bid_id.hex(), bid_data.offer_msg_id.hex())
self.saveBid(bid_id, bid, xmr_swap=xmr_swap)
def processXmrBidAccept(self, msg):
# F receiving MSG1F and MSG2F
4 years ago
self.log.debug('Processing xmr bid accept msg %s', msg['msgid'])
now = int(time.time())
msg_bytes = bytes.fromhex(msg['hex'][2:-2])
msg_data = XmrBidAcceptMessage()
msg_data.ParseFromString(msg_bytes)
assert(len(msg_data.bid_msg_id) == 28), 'Bad bid_msg_id length'
self.log.debug('for bid %s', msg_data.bid_msg_id.hex())
bid, xmr_swap = self.getXmrBid(msg_data.bid_msg_id)
assert(bid), 'Bid not found: {}.'.format(msg_data.bid_id.hex())
assert(xmr_swap), 'XMR swap not found: {}.'.format(msg_data.bid_id.hex())
4 years ago
offer, xmr_offer = self.getXmrOffer(bid.offer_id, sent=True)
assert(offer), 'Offer not found: {}.'.format(bid.offer_id.hex())
assert(xmr_offer), 'XMR offer not found: {}.'.format(bid.offer_id.hex())
ci_from = self.ci(Coins(offer.coin_from))
ci_to = self.ci(Coins(offer.coin_to))
4 years ago
try:
xmr_swap.pkal = msg_data.pkal
xmr_swap.vkbvl = msg_data.kbvl
xmr_swap.pkbvl = ci_to.getPubkey(msg_data.kbvl)
xmr_swap.kbsl_dleag = msg_data.kbsl_dleag
xmr_swap.a_lock_tx = msg_data.a_lock_tx
xmr_swap.a_lock_tx_script = msg_data.a_lock_tx_script
xmr_swap.a_lock_refund_tx = msg_data.a_lock_refund_tx
xmr_swap.a_lock_refund_tx_script = msg_data.a_lock_refund_tx_script
xmr_swap.a_lock_refund_spend_tx = msg_data.a_lock_refund_spend_tx
xmr_swap.a_lock_refund_spend_tx_id = ci_from.getTxHash(xmr_swap.a_lock_refund_spend_tx)
xmr_swap.al_lock_refund_tx_sig = msg_data.al_lock_refund_tx_sig
# TODO: check_a_lock_tx_inputs without txindex
check_a_lock_tx_inputs = False
xmr_swap.a_lock_tx_id, xmr_swap.a_lock_tx_vout = ci_from.verifyLockTx(
xmr_swap.a_lock_tx, xmr_swap.a_lock_tx_script,
bid.amount,
xmr_swap.pkal, xmr_swap.pkaf,
xmr_offer.a_fee_rate,
check_a_lock_tx_inputs
)
a_lock_tx_dest = ci_from.getScriptDest(xmr_swap.a_lock_tx_script)
xmr_swap.a_lock_refund_tx_id, xmr_swap.a_swap_refund_value = ci_from.verifyLockRefundTx(
xmr_swap.a_lock_refund_tx, xmr_swap.a_lock_refund_tx_script,
xmr_swap.a_lock_tx_id, xmr_swap.a_lock_tx_vout, xmr_offer.lock_time_1, xmr_swap.a_lock_tx_script,
xmr_swap.pkal, xmr_swap.pkaf,
xmr_offer.lock_time_2,
bid.amount, xmr_offer.a_fee_rate
)
ci_from.verifyLockRefundSpendTx(
xmr_swap.a_lock_refund_spend_tx,
xmr_swap.a_lock_refund_tx_id, xmr_swap.a_lock_refund_tx_script,
xmr_swap.pkal,
xmr_swap.a_swap_refund_value, xmr_offer.a_fee_rate
)
4 years ago
self.log.info('Checking leader\'s lock refund tx signature')
v = ci_from.verifyTxSig(xmr_swap.a_lock_refund_tx, xmr_swap.al_lock_refund_tx_sig, xmr_swap.pkal, 0, xmr_swap.a_lock_tx_script, bid.amount)
bid.setState(BidStates.BID_RECEIVING_ACC)
self.saveBid(bid.bid_id, bid, xmr_swap=xmr_swap)
except Exception as ex:
if self.debug:
traceback.print_exc()
self.setBidError(bid.bid_id, bid, str(ex))
def watchXmrSwap(self, bid, offer, xmr_swap):
self.log.debug('XMR swap in progress, bid %s', bid.bid_id.hex())
self.swaps_in_progress[bid.bid_id] = (bid, offer)
coin_from = Coins(offer.coin_from)
self.setLastHeightChecked(coin_from, xmr_swap.start_chain_a_height)
self.addWatchedOutput(coin_from, bid.bid_id, bid.xmr_a_lock_tx.txid.hex(), bid.xmr_a_lock_tx.vout, TxTypes.XMR_SWAP_A_LOCK, SwapTypes.XMR_SWAP)
self.addWatchedOutput(coin_from, bid.bid_id, xmr_swap.a_lock_refund_tx_id.hex(), 0, TxTypes.XMR_SWAP_A_LOCK_REFUND, SwapTypes.XMR_SWAP)
bid.in_progress = 1
def sendXmrBidTxnSigsFtoL(self, bid_id, session):
# F -> L: Sending MSG3L
self.log.debug('Signing xmr bid lock txns %s', bid_id.hex())
bid, xmr_swap = self.getXmrBidFromSession(session, bid_id)
assert(bid), 'Bid not found: {}.'.format(bid_id.hex())
assert(xmr_swap), 'XMR swap not found: {}.'.format(bid_id.hex())
offer, xmr_offer = self.getXmrOfferFromSession(session, bid.offer_id, sent=False)
assert(offer), 'Offer not found: {}.'.format(bid.offer_id.hex())
assert(xmr_offer), 'XMR offer not found: {}.'.format(bid.offer_id.hex())
coin_from = Coins(offer.coin_from)
coin_to = Coins(offer.coin_to)
ci_from = self.ci(coin_from)
ci_to = self.ci(coin_to)
4 years ago
try:
kaf = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, 3)
xmr_swap.af_lock_refund_spend_tx_esig = ci_from.signTxOtVES(kaf, xmr_swap.pkasl, xmr_swap.a_lock_refund_spend_tx, 0, xmr_swap.a_lock_refund_tx_script, xmr_swap.a_swap_refund_value)
xmr_swap.af_lock_refund_tx_sig = ci_from.signTx(kaf, xmr_swap.a_lock_refund_tx, 0, xmr_swap.a_lock_tx_script, bid.amount)
self.addLockRefundSigs(xmr_swap, ci_from)
msg_buf = XmrBidLockTxSigsMessage(
bid_msg_id=bid_id,
af_lock_refund_spend_tx_esig=xmr_swap.af_lock_refund_spend_tx_esig,
af_lock_refund_tx_sig=xmr_swap.af_lock_refund_tx_sig
)
msg_bytes = msg_buf.SerializeToString()
payload_hex = str.format('{:02x}', MessageTypes.XMR_BID_TXN_SIGS_FL) + msg_bytes.hex()
options = {'decodehex': True, 'ttl_is_seconds': True}
# TODO: set msg_valid based on bid / offer parameters
msg_valid = self.SMSG_SECONDS_IN_HOUR * 48
ro = self.callrpc('smsgsend', [bid.bid_addr, offer.addr_from, payload_hex, False, msg_valid, False, options])
xmr_swap.coin_a_lock_tx_sigs_l_msg_id = bytes.fromhex(ro['msgid'])
self.log.info('Sent XMR_BID_TXN_SIGS_FL %s', xmr_swap.coin_a_lock_tx_sigs_l_msg_id.hex())
a_lock_tx_id = ci_from.getTxHash(xmr_swap.a_lock_tx)
a_lock_tx_vout = ci_from.getTxOutputPos(xmr_swap.a_lock_tx, xmr_swap.a_lock_tx_script)
self.log.debug('Waiting for lock txn %s to %s chain for bid %s', a_lock_tx_id.hex(), chainparams[coin_from]['name'], bid_id.hex())
bid.xmr_a_lock_tx = SwapTx(
bid_id=bid_id,
tx_type=TxTypes.XMR_SWAP_A_LOCK,
txid=a_lock_tx_id,
vout=a_lock_tx_vout,
)
bid.xmr_a_lock_tx.setState(TxStates.TX_NONE)
bid.setState(BidStates.BID_ACCEPTED) # XMR
self.watchXmrSwap(bid, offer, xmr_swap)
self.saveBidInSession(bid_id, bid, session, xmr_swap)
except Exception as ex:
if self.debug:
traceback.print_exc()
def sendXmrBidCoinALockTx(self, bid_id, session):
# Send coin A lock tx and MSG4F L -> F
self.log.debug('Sending coin A lock tx for xmr bid %s', bid_id.hex())
bid, xmr_swap = self.getXmrBidFromSession(session, bid_id)
assert(bid), 'Bid not found: {}.'.format(bid_id.hex())
assert(xmr_swap), 'XMR swap not found: {}.'.format(bid_id.hex())
offer, xmr_offer = self.getXmrOfferFromSession(session, bid.offer_id, sent=False)
assert(offer), 'Offer not found: {}.'.format(bid.offer_id.hex())
assert(xmr_offer), 'XMR offer not found: {}.'.format(bid.offer_id.hex())
coin_from = Coins(offer.coin_from)
coin_to = Coins(offer.coin_to)
ci_from = self.ci(coin_from)
ci_to = self.ci(coin_to)
kal = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, 3)
xmr_swap.a_lock_spend_tx = ci_from.createScriptLockSpendTx(
xmr_swap.a_lock_tx, xmr_swap.a_lock_tx_script,
xmr_swap.dest_af,
xmr_offer.a_fee_rate)
xmr_swap.a_lock_spend_tx_id = ci_from.getTxHash(xmr_swap.a_lock_spend_tx)
xmr_swap.al_lock_spend_tx_esig = ci_from.signTxOtVES(kal, xmr_swap.pkasf, xmr_swap.a_lock_spend_tx, 0, xmr_swap.a_lock_tx_script, bid.amount) # self.a_swap_value
# Proof leader can sign for kal
xmr_swap.kal_sig = ci_from.signCompact(kal, 'proof key owned for swap')
msg_buf = XmrBidLockSpendTxMessage(
bid_msg_id=bid_id,
a_lock_spend_tx=xmr_swap.a_lock_spend_tx,
kal_sig=xmr_swap.kal_sig)
msg_bytes = msg_buf.SerializeToString()
payload_hex = str.format('{:02x}', MessageTypes.XMR_BID_LOCK_SPEND_TX_LF) + msg_bytes.hex()
options = {'decodehex': True, 'ttl_is_seconds': True}
# TODO: set msg_valid based on bid / offer parameters
msg_valid = self.SMSG_SECONDS_IN_HOUR * 48
ro = self.callrpc('smsgsend', [offer.addr_from, bid.bid_addr, payload_hex, False, msg_valid, False, options])
xmr_swap.coin_a_lock_refund_spend_tx_msg_id = bytes.fromhex(ro['msgid'])
# TODO: Separate MSG4F and txn sending
# publishalocktx
lock_tx_signed = ci_from.signTxWithWallet(xmr_swap.a_lock_tx)
txid_hex = ci_from.publishTx(lock_tx_signed)
vout_pos = ci_from.getTxOutputPos(xmr_swap.a_lock_tx, xmr_swap.a_lock_tx_script)
self.log.debug('Submitted lock txn %s to %s chain for bid %s', txid_hex, chainparams[coin_from]['name'], bid_id.hex())
bid.xmr_a_lock_tx = SwapTx(
bid_id=bid_id,
tx_type=TxTypes.XMR_SWAP_A_LOCK,
txid=bytes.fromhex(txid_hex),
vout=vout_pos,
)
bid.xmr_a_lock_tx.setState(TxStates.TX_SENT)
bid.setState(BidStates.XMR_SWAP_HAVE_SCRIPT_COIN_SPEND_TX)
self.watchXmrSwap(bid, offer, xmr_swap)
self.logBidEvent(bid, EventLogTypes.LOCK_TX_A_PUBLISHED, '', session)
self.saveBidInSession(bid_id, bid, session, xmr_swap)
def sendXmrBidCoinBLockTx(self, bid_id, session):
# Follower sending coin B lock tx
self.log.debug('Sending coin B lock tx for xmr bid %s', bid_id.hex())
bid, xmr_swap = self.getXmrBidFromSession(session, bid_id)
assert(bid), 'Bid not found: {}.'.format(bid_id.hex())
assert(xmr_swap), 'XMR swap not found: {}.'.format(bid_id.hex())
offer, xmr_offer = self.getXmrOfferFromSession(session, bid.offer_id, sent=False)
assert(offer), 'Offer not found: {}.'.format(bid.offer_id.hex())
assert(xmr_offer), 'XMR offer not found: {}.'.format(bid.offer_id.hex())
coin_from = Coins(offer.coin_from)
coin_to = Coins(offer.coin_to)
ci_from = self.ci(coin_from)
ci_to = self.ci(coin_to)
if bid.debug_ind == DebugTypes.BID_STOP_AFTER_COIN_A_LOCK:
self.log.debug('XMR bid %s: Stalling bid for testing: %d.', bid_id.hex(), bid.debug_ind)
bid.setState(BidStates.BID_STALLED_FOR_TEST)
self.saveBidInSession(bid_id, bid, session, xmr_swap, save_in_progress=offer)
self.logBidEvent(bid, EventLogTypes.DEBUG_TWEAK_APPLIED, 'ind {}'.format(bid.debug_ind), session)
return
if bid.debug_ind == DebugTypes.CREATE_INVALID_COIN_B_LOCK:
bid.amount_to -= int(bid.amount_to * 0.1)
self.log.debug('XMR bid %s: Debug %d - Reducing lock b txn amount by 10%% to %s.', bid_id.hex(), bid.debug_ind, ci_to.format_amount(bid.amount_to))
self.logBidEvent(bid, EventLogTypes.DEBUG_TWEAK_APPLIED, 'ind {}'.format(bid.debug_ind), session)
try:
b_lock_tx_id = ci_to.publishBLockTx(xmr_swap.pkbv, xmr_swap.pkbs, bid.amount_to, xmr_offer.b_fee_rate)
except Exception as ex:
error_msg = 'publishBLockTx failed for bid {} with error {}'.format(bid_id.hex(), str(ex))
num_retries = self.countBidEvents(bid, EventLogTypes.FAILED_TX_B_LOCK_PUBLISH, session)
if num_retries > 0:
error_msg += ', retry no. {}'.format(num_retries)
self.log.error(error_msg)
str_error = str(ex)
if num_retries < 5 and ('not enough unlocked money' in str_error or 'transaction was rejected by daemon' in str_error):
delay = random.randrange(self.min_delay_retry, self.max_delay_retry)
self.log.info('Retrying sending xmr swap chain B lock tx for bid %s in %d seconds', bid_id.hex(), delay)
self.createEventInSession(delay, EventTypes.SEND_XMR_SWAP_LOCK_TX_B, bid_id, session)
else:
self.setBidError(bid_id, bid, 'publishBLockTx failed: ' + str(ex), save_bid=False)
self.saveBidInSession(bid_id, bid, session, xmr_swap, save_in_progress=offer)
self.logBidEvent(bid, EventLogTypes.FAILED_TX_B_LOCK_PUBLISH, str_error, session)
return
self.log.debug('Submitted lock txn %s to %s chain for bid %s', b_lock_tx_id.hex(), chainparams[coin_to]['name'], bid_id.hex())
bid.xmr_b_lock_tx = SwapTx(
bid_id=bid_id,
tx_type=TxTypes.XMR_SWAP_B_LOCK,
txid=b_lock_tx_id,
)
bid.xmr_b_lock_tx.setState(TxStates.TX_NONE)
self.logBidEvent(bid, EventLogTypes.LOCK_TX_B_PUBLISHED, '', session)
self.saveBidInSession(bid_id, bid, session, xmr_swap, save_in_progress=offer)
def sendXmrBidLockRelease(self, bid_id, session):
# Leader sending lock tx a release secret (MSG5F)
self.log.debug('Sending bid secret for xmr bid %s', bid_id.hex())
bid, xmr_swap = self.getXmrBidFromSession(session, bid_id)
assert(bid), 'Bid not found: {}.'.format(bid_id.hex())
assert(xmr_swap), 'XMR swap not found: {}.'.format(bid_id.hex())
offer, xmr_offer = self.getXmrOfferFromSession(session, bid.offer_id, sent=False)
assert(offer), 'Offer not found: {}.'.format(bid.offer_id.hex())
assert(xmr_offer), 'XMR offer not found: {}.'.format(bid.offer_id.hex())
coin_from = Coins(offer.coin_from)
coin_to = Coins(offer.coin_to)
msg_buf = XmrBidLockReleaseMessage(
bid_msg_id=bid_id,
al_lock_spend_tx_esig=xmr_swap.al_lock_spend_tx_esig)
msg_bytes = msg_buf.SerializeToString()
payload_hex = str.format('{:02x}', MessageTypes.XMR_BID_LOCK_RELEASE_LF) + msg_bytes.hex()
options = {'decodehex': True, 'ttl_is_seconds': True}
# TODO: set msg_valid based on bid / offer parameters
msg_valid = self.SMSG_SECONDS_IN_HOUR * 48
ro = self.callrpc('smsgsend', [offer.addr_from, bid.bid_addr, payload_hex, False, msg_valid, False, options])
xmr_swap.coin_a_lock_refund_spend_tx_msg_id = bytes.fromhex(ro['msgid'])
bid.setState(BidStates.XMR_SWAP_LOCK_RELEASED)
self.saveBidInSession(bid_id, bid, session, xmr_swap, save_in_progress=offer)
def redeemXmrBidCoinALockTx(self, bid_id, session):
# Follower redeeming A lock tx
self.log.debug('Redeeming coin A lock tx for xmr bid %s', bid_id.hex())
bid, xmr_swap = self.getXmrBidFromSession(session, bid_id)
assert(bid), 'Bid not found: {}.'.format(bid_id.hex())
assert(xmr_swap), 'XMR swap not found: {}.'.format(bid_id.hex())
offer, xmr_offer = self.getXmrOfferFromSession(session, bid.offer_id, sent=False)
assert(offer), 'Offer not found: {}.'.format(bid.offer_id.hex())
assert(xmr_offer), 'XMR offer not found: {}.'.format(bid.offer_id.hex())
coin_from = Coins(offer.coin_from)
coin_to = Coins(offer.coin_to)
ci_from = self.ci(coin_from)
ci_to = self.ci(coin_to)
kbsf = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, 2, for_ed25519=True)
kaf = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, 3)
al_lock_spend_sig = ci_from.decryptOtVES(kbsf, xmr_swap.al_lock_spend_tx_esig)
v = ci_from.verifyTxSig(xmr_swap.a_lock_spend_tx, al_lock_spend_sig, xmr_swap.pkal, 0, xmr_swap.a_lock_tx_script, bid.amount)
assert(v)
af_lock_spend_sig = ci_from.signTx(kaf, xmr_swap.a_lock_spend_tx, 0, xmr_swap.a_lock_tx_script, bid.amount)
v = ci_from.verifyTxSig(xmr_swap.a_lock_spend_tx, af_lock_spend_sig, xmr_swap.pkaf, 0, xmr_swap.a_lock_tx_script, bid.amount)
assert(v)
witness_stack = [
b'',
al_lock_spend_sig,
af_lock_spend_sig,
xmr_swap.a_lock_tx_script,
]
xmr_swap.a_lock_spend_tx = ci_from.setTxSignature(xmr_swap.a_lock_spend_tx, witness_stack)
txid = bytes.fromhex(ci_from.publishTx(xmr_swap.a_lock_spend_tx))
self.log.debug('Submitted lock spend txn %s to %s chain for bid %s', txid.hex(), chainparams[coin_from]['name'], bid_id.hex())
bid.xmr_a_lock_spend_tx = SwapTx(
bid_id=bid_id,
tx_type=TxTypes.XMR_SWAP_A_LOCK_SPEND,
txid=txid,
)
bid.xmr_a_lock_spend_tx.setState(TxStates.TX_NONE)
self.saveBidInSession(bid_id, bid, session, xmr_swap, save_in_progress=offer)
def redeemXmrBidCoinBLockTx(self, bid_id, session):
# Leader redeeming B lock tx
self.log.debug('Redeeming coin B lock tx for xmr bid %s', bid_id.hex())
bid, xmr_swap = self.getXmrBidFromSession(session, bid_id)
assert(bid), 'Bid not found: {}.'.format(bid_id.hex())
assert(xmr_swap), 'XMR swap not found: {}.'.format(bid_id.hex())
offer, xmr_offer = self.getXmrOfferFromSession(session, bid.offer_id, sent=False)
assert(offer), 'Offer not found: {}.'.format(bid.offer_id.hex())
assert(xmr_offer), 'XMR offer not found: {}.'.format(bid.offer_id.hex())
coin_from = Coins(offer.coin_from)
coin_to = Coins(offer.coin_to)
ci_from = self.ci(coin_from)
ci_to = self.ci(coin_to)
# Extract the leader's decrypted signature and use it to recover the follower's privatekey
xmr_swap.al_lock_spend_tx_sig = ci_from.extractLeaderSig(xmr_swap.a_lock_spend_tx)
kbsf = ci_from.recoverEncKey(xmr_swap.al_lock_spend_tx_esig, xmr_swap.al_lock_spend_tx_sig, xmr_swap.pkasf)
assert(kbsf is not None)
kbsl = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, 2, for_ed25519=True)
vkbs = ci_to.sumKeys(kbsl, kbsf)
address_to = ci_to.getMainWalletAddress()
try:
txid = ci_to.spendBLockTx(address_to, xmr_swap.vkbv, vkbs, bid.amount_to, xmr_offer.b_fee_rate, xmr_swap.b_restore_height)
self.log.debug('Submitted lock B spend txn %s to %s chain for bid %s', txid.hex(), ci_to.coin_name(), bid_id.hex())
except Exception as ex:
# TODO: Make min-conf 10?
error_msg = 'spendBLockTx failed for bid {} with error {}'.format(bid_id.hex(), str(ex))
num_retries = self.countBidEvents(bid, EventLogTypes.FAILED_TX_B_SPEND, session)
if num_retries > 0:
error_msg += ', retry no. {}'.format(num_retries)
self.log.error(error_msg)
str_error = str(ex)
if num_retries < 100 and 'Invalid unlocked_balance' in str_error:
delay = random.randrange(self.min_delay_retry, self.max_delay_retry)
self.log.info('Retrying sending xmr swap chain B spend tx for bid %s in %d seconds', bid_id.hex(), delay)
self.createEventInSession(delay, EventTypes.REDEEM_XMR_SWAP_LOCK_TX_B, bid_id, session)
else:
self.setBidError(bid_id, bid, 'spendBLockTx failed: ' + str(ex), save_bid=False)
self.saveBidInSession(bid_id, bid, session, xmr_swap, save_in_progress=offer)
self.logBidEvent(bid, EventLogTypes.FAILED_TX_B_SPEND, str_error, session)
return
bid.xmr_b_lock_tx.spend_txid = txid
bid.setState(BidStates.XMR_SWAP_NOSCRIPT_TX_REDEEMED)
# TODO: Why does using bid.txns error here?
self.saveBidInSession(bid_id, bid, session, xmr_swap, save_in_progress=offer)
def recoverXmrBidCoinBLockTx(self, bid_id, session):
# Follower recovering B lock tx
self.log.debug('Recovering coin B lock tx for xmr bid %s', bid_id.hex())
bid, xmr_swap = self.getXmrBidFromSession(session, bid_id)
assert(bid), 'Bid not found: {}.'.format(bid_id.hex())
assert(xmr_swap), 'XMR swap not found: {}.'.format(bid_id.hex())
offer, xmr_offer = self.getXmrOfferFromSession(session, bid.offer_id, sent=False)
assert(offer), 'Offer not found: {}.'.format(bid.offer_id.hex())
assert(xmr_offer), 'XMR offer not found: {}.'.format(bid.offer_id.hex())
coin_from = Coins(offer.coin_from)
coin_to = Coins(offer.coin_to)
ci_from = self.ci(coin_from)
ci_to = self.ci(coin_to)
# Extract the follower's decrypted signature and use it to recover the leader's privatekey
af_lock_refund_spend_tx_sig = ci_from.extractFollowerSig(xmr_swap.a_lock_refund_spend_tx)
kbsl = ci_from.recoverEncKey(xmr_swap.af_lock_refund_spend_tx_esig, af_lock_refund_spend_tx_sig, xmr_swap.pkasl)
assert(kbsl is not None)
kbsf = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, 2, for_ed25519=True)
vkbs = ci_to.sumKeys(kbsl, kbsf)
address_to = ci_to.getMainWalletAddress()
try:
txid = ci_to.spendBLockTx(address_to, xmr_swap.vkbv, vkbs, bid.amount_to, xmr_offer.b_fee_rate, xmr_swap.b_restore_height)
self.log.debug('Submitted lock B refund txn %s to %s chain for bid %s', txid.hex(), ci_to.coin_name(), bid_id.hex())
self.logBidEvent(bid, EventLogTypes.LOCK_TX_B_REFUND_TX_PUBLISHED, '', session)
except Exception as ex:
# TODO: Make min-conf 10?
error_msg = 'spendBLockTx refund failed for bid {} with error {}'.format(bid_id.hex(), str(ex))
num_retries = self.countBidEvents(bid, EventLogTypes.FAILED_TX_B_REFUND, session)
if num_retries > 0:
error_msg += ', retry no. {}'.format(num_retries)
self.log.error(error_msg)
str_error = str(ex)
if num_retries < 100 and 'Invalid unlocked_balance' in str_error:
delay = random.randrange(self.min_delay_retry, self.max_delay_retry)
self.log.info('Retrying sending xmr swap chain B refund tx for bid %s in %d seconds', bid_id.hex(), delay)
self.createEventInSession(delay, EventTypes.RECOVER_XMR_SWAP_LOCK_TX_B, bid_id, session)
else:
self.setBidError(bid_id, bid, 'spendBLockTx for refund failed: ' + str(ex), save_bid=False)
self.saveBidInSession(bid_id, bid, session, xmr_swap, save_in_progress=offer)
self.logBidEvent(bid, EventLogTypes.FAILED_TX_B_REFUND, str_error, session)
return
bid.xmr_b_lock_tx.spend_txid = txid
bid.setState(BidStates.XMR_SWAP_NOSCRIPT_TX_RECOVERED)
self.saveBidInSession(bid_id, bid, session, xmr_swap, save_in_progress=offer)
def processXmrBidCoinALockSigs(self, msg):
# Leader processing MSG3L
self.log.debug('Processing xmr coin a follower lock sigs msg %s', msg['msgid'])
now = int(time.time())
msg_bytes = bytes.fromhex(msg['hex'][2:-2])
msg_data = XmrBidLockTxSigsMessage()
msg_data.ParseFromString(msg_bytes)
assert(len(msg_data.bid_msg_id) == 28), 'Bad bid_msg_id length'
bid_id = msg_data.bid_msg_id
bid, xmr_swap = self.getXmrBid(bid_id)
assert(bid), 'Bid not found: {}.'.format(bid_id.hex())
assert(xmr_swap), 'XMR swap not found: {}.'.format(bid_id.hex())
offer, xmr_offer = self.getXmrOffer(bid.offer_id, sent=False)
assert(offer), 'Offer not found: {}.'.format(bid.offer_id.hex())
assert(xmr_offer), 'XMR offer not found: {}.'.format(bid.offer_id.hex())
coin_from = Coins(offer.coin_from)
coin_to = Coins(offer.coin_to)
ci_from = self.ci(coin_from)
ci_to = self.ci(coin_to)
try:
xmr_swap.af_lock_refund_spend_tx_esig = msg_data.af_lock_refund_spend_tx_esig
xmr_swap.af_lock_refund_tx_sig = msg_data.af_lock_refund_tx_sig
kbsl = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, 2, for_ed25519=True)
kal = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, 3)
xmr_swap.af_lock_refund_spend_tx_sig = ci_from.decryptOtVES(kbsl, xmr_swap.af_lock_refund_spend_tx_esig)
al_lock_refund_spend_tx_sig = ci_from.signTx(kal, xmr_swap.a_lock_refund_spend_tx, 0, xmr_swap.a_lock_refund_tx_script, xmr_swap.a_swap_refund_value)
self.log.debug('Setting lock refund spend tx sigs')
witness_stack = [
b'',
al_lock_refund_spend_tx_sig,
xmr_swap.af_lock_refund_spend_tx_sig,
bytes((1,)),
xmr_swap.a_lock_refund_tx_script,
]
signed_tx = ci_from.setTxSignature(xmr_swap.a_lock_refund_spend_tx, witness_stack)
assert(signed_tx), 'setTxSignature failed'
xmr_swap.a_lock_refund_spend_tx = signed_tx
v = ci_from.verifyTxSig(xmr_swap.a_lock_refund_spend_tx, xmr_swap.af_lock_refund_spend_tx_sig, xmr_swap.pkaf, 0, xmr_swap.a_lock_refund_tx_script, xmr_swap.a_swap_refund_value)
assert(v), 'Invalid signature for lock refund spend txn'
self.addLockRefundSigs(xmr_swap, ci_from)
delay = random.randrange(self.min_delay_event, self.max_delay_event)
self.log.info('Sending coin A lock tx for xmr bid %s in %d seconds', bid_id.hex(), delay)
self.createEvent(delay, EventTypes.SEND_XMR_SWAP_LOCK_TX_A, bid_id)
bid.setState(BidStates.SWAP_DELAYING)
self.saveBid(bid_id, bid, xmr_swap=xmr_swap)
except Exception as ex:
if self.debug:
traceback.print_exc()
self.setBidError(bid_id, bid, str(ex))
4 years ago
def processXmrBidLockSpendTx(self, msg):
# Follower receiving MSG4F
self.log.debug('Processing xmr bid lock spend tx msg %s', msg['msgid'])
now = int(time.time())
msg_bytes = bytes.fromhex(msg['hex'][2:-2])
msg_data = XmrBidLockSpendTxMessage()
msg_data.ParseFromString(msg_bytes)
assert(len(msg_data.bid_msg_id) == 28), 'Bad bid_msg_id length'
bid_id = msg_data.bid_msg_id
bid, xmr_swap = self.getXmrBid(bid_id)
assert(bid), 'Bid not found: {}.'.format(bid_id.hex())
assert(xmr_swap), 'XMR swap not found: {}.'.format(bid_id.hex())
offer, xmr_offer = self.getXmrOffer(bid.offer_id, sent=False)
assert(offer), 'Offer not found: {}.'.format(bid.offer_id.hex())
assert(xmr_offer), 'XMR offer not found: {}.'.format(bid.offer_id.hex())
ci_from = self.ci(Coins(offer.coin_from))
ci_to = self.ci(Coins(offer.coin_to))
try:
xmr_swap.a_lock_spend_tx = msg_data.a_lock_spend_tx
xmr_swap.a_lock_spend_tx_id = ci_from.getTxHash(xmr_swap.a_lock_spend_tx)
xmr_swap.kal_sig = msg_data.kal_sig
ci_from.verifyLockSpendTx(
xmr_swap.a_lock_spend_tx,
xmr_swap.a_lock_tx, xmr_swap.a_lock_tx_script,
xmr_swap.dest_af, xmr_offer.a_fee_rate)
ci_from.verifyCompact(xmr_swap.pkal, 'proof key owned for swap', xmr_swap.kal_sig)
bid.setState(BidStates.XMR_SWAP_HAVE_SCRIPT_COIN_SPEND_TX)
self.saveBid(bid_id, bid, xmr_swap=xmr_swap)
except Exception as ex:
if self.debug:
traceback.print_exc()
self.setBidError(bid_id, bid, str(ex))
# Update copy of bid in swaps_in_progress
self.swaps_in_progress[bid_id] = (bid, offer)
4 years ago
def processXmrSplitMessage(self, msg):
self.log.debug('Processing xmr split msg %s', msg['msgid'])
now = int(time.time())
msg_bytes = bytes.fromhex(msg['hex'][2:-2])
msg_data = XmrSplitMessage()
msg_data.ParseFromString(msg_bytes)
# Validate data
assert(len(msg_data.msg_id) == 28), 'Bad msg_id length'
if msg_data.msg_type == XmrSplitMsgTypes.BID or msg_data.msg_type == XmrSplitMsgTypes.BID_ACCEPT:
try:
session = scoped_session(self.session_factory)
q = session.execute('SELECT COUNT(*) FROM xmr_split_data WHERE bid_id = x\'{}\' AND msg_type = {} AND msg_sequence = {}'.format(msg_data.msg_id.hex(), msg_data.msg_type, msg_data.sequence)).first()
num_exists = q[0]
if num_exists > 0:
self.log.warning('Ignoring duplicate xmr_split_data entry: ({}, {}, {})'.format(msg_data.msg_id.hex(), msg_data.msg_type, msg_data.sequence))
return
dbr = XmrSplitData()
dbr.bid_id = msg_data.msg_id
dbr.msg_type = msg_data.msg_type
dbr.msg_sequence = msg_data.sequence
dbr.dleag = msg_data.dleag
dbr.created_at = now
session.add(dbr)
session.commit()
finally:
session.close()
session.remove()
4 years ago
def processXmrLockReleaseMessage(self, msg):
self.log.debug('Processing xmr secret msg %s', msg['msgid'])
now = int(time.time())
msg_bytes = bytes.fromhex(msg['hex'][2:-2])
msg_data = XmrBidLockReleaseMessage()
msg_data.ParseFromString(msg_bytes)
# Validate data
assert(len(msg_data.bid_msg_id) == 28), 'Bad msg_id length'
bid_id = msg_data.bid_msg_id
bid, xmr_swap = self.getXmrBid(bid_id)
assert(bid), 'Bid not found: {}.'.format(bid_id.hex())
assert(xmr_swap), 'XMR swap not found: {}.'.format(bid_id.hex())
offer, xmr_offer = self.getXmrOffer(bid.offer_id, sent=False)
assert(offer), 'Offer not found: {}.'.format(bid.offer_id.hex())
assert(xmr_offer), 'XMR offer not found: {}.'.format(bid.offer_id.hex())
ci_from = self.ci(Coins(offer.coin_from))
xmr_swap.al_lock_spend_tx_esig = msg_data.al_lock_spend_tx_esig
try:
v = ci_from.verifyTxOtVES(
xmr_swap.a_lock_spend_tx, xmr_swap.al_lock_spend_tx_esig,
xmr_swap.pkal, xmr_swap.pkasf, 0, xmr_swap.a_lock_tx_script, bid.amount)
assert(v), 'verifyTxOtVES failed'
except Exception as ex:
if self.debug:
traceback.print_exc()
self.setBidError(bid_id, bid, str(ex))
self.swaps_in_progress[bid_id] = (bid, offer)
return
delay = random.randrange(self.min_delay_event, self.max_delay_event)
self.log.info('Redeeming coin A lock tx for xmr bid %s in %d seconds', bid_id.hex(), delay)
self.createEvent(delay, EventTypes.REDEEM_XMR_SWAP_LOCK_TX_A, bid_id)
bid.setState(BidStates.XMR_SWAP_LOCK_RELEASED)
self.saveBid(bid_id, bid, xmr_swap=xmr_swap)
self.swaps_in_progress[bid_id] = (bid, offer)
5 years ago
def processMsg(self, msg):
self.mxDB.acquire()
try:
msg_type = int(msg['hex'][:2], 16)
rv = None
if msg_type == MessageTypes.OFFER:
self.processOffer(msg)
elif msg_type == MessageTypes.BID:
self.processBid(msg)
elif msg_type == MessageTypes.BID_ACCEPT:
self.processBidAccept(msg)
elif msg_type == MessageTypes.XMR_BID_FL:
4 years ago
self.processXmrBid(msg)
elif msg_type == MessageTypes.XMR_BID_ACCEPT_LF:
4 years ago
self.processXmrBidAccept(msg)
elif msg_type == MessageTypes.XMR_BID_TXN_SIGS_FL:
self.processXmrBidCoinALockSigs(msg)
elif msg_type == MessageTypes.XMR_BID_LOCK_SPEND_TX_LF:
self.processXmrBidLockSpendTx(msg)
4 years ago
elif msg_type == MessageTypes.XMR_BID_SPLIT:
self.processXmrSplitMessage(msg)
elif msg_type == MessageTypes.XMR_BID_LOCK_RELEASE_LF:
self.processXmrLockReleaseMessage(msg)
if msg_type == MessageTypes.OFFER_REVOKE:
self.processOfferRevoke(msg)
5 years ago
except Exception as ex:
self.log.error('processMsg %s', str(ex))
if self.debug:
traceback.print_exc()
5 years ago
finally:
self.mxDB.release()
def processZmqSmsg(self):
message = self.zmqSubscriber.recv()
clear = self.zmqSubscriber.recv()
if message[0] == 3: # Paid smsg
5 years ago
return # TODO: Switch to paid?
5 years ago
msg_id = message[2:]
options = {'encoding': 'hex', 'setread': True}
num_tries = 5
for i in range(num_tries + 1):
try:
msg = self.callrpc('smsg', [msg_id.hex(), options])
break
except Exception as e:
if 'Unknown message id' in str(e) and i < num_tries:
time.sleep(1)
else:
raise e
5 years ago
self.processMsg(msg)
def update(self):
try:
# while True:
message = self.zmqSubscriber.recv(flags=zmq.NOBLOCK)
if message == b'smsg':
self.processZmqSmsg()
except zmq.Again as ex:
5 years ago
pass
except Exception as ex:
self.log.error('smsg zmq %s', str(ex))
if self.debug:
traceback.print_exc()
5 years ago
self.mxDB.acquire()
try:
# TODO: Wait for blocks / txns, would need to check multiple coins
now = int(time.time())
4 years ago
if now - self._last_checked_progress >= self.check_progress_seconds:
5 years ago
to_remove = []
for bid_id, v in self.swaps_in_progress.items():
try:
if self.checkBidState(bid_id, v[0], v[1]) is True:
to_remove.append((bid_id, v[0], v[1]))
except Exception as ex:
self.log.error('checkBidState %s %s', bid_id.hex(), str(ex))
if self.debug:
traceback.print_exc()
self.setBidError(bid_id, v[0], str(ex))
for bid_id, bid, offer in to_remove:
self.deactivateBid(None, offer, bid)
4 years ago
self._last_checked_progress = now
5 years ago
4 years ago
if now - self._last_checked_watched >= self.check_watched_seconds:
5 years ago
for k, c in self.coin_clients.items():
if len(c['watched_outputs']) > 0:
self.checkForSpends(k, c)
4 years ago
self._last_checked_watched = now
5 years ago
4 years ago
if now - self._last_checked_expired >= self.check_expired_seconds:
5 years ago
self.expireMessages()
4 years ago
self._last_checked_expired = now
5 years ago
4 years ago
if now - self._last_checked_events >= self.check_events_seconds:
5 years ago
self.checkEvents()
4 years ago
self._last_checked_events = now
if now - self._last_checked_xmr_swaps >= self.check_xmr_swaps_seconds:
self.checkXmrSwaps()
self._last_checked_xmr_swaps = now
5 years ago
except Exception as ex:
self.log.error('update %s', str(ex))
if self.debug:
traceback.print_exc()
5 years ago
finally:
self.mxDB.release()
def manualBidUpdate(self, bid_id, data):
self.log.info('Manually updating bid %s', bid_id.hex())
self.mxDB.acquire()
try:
bid, offer = self.getBidAndOffer(bid_id)
assert(bid), 'Bid not found {}'.format(bid_id.hex())
assert(offer), 'Offer not found {}'.format(bid.offer_id.hex())
has_changed = False
if bid.state != data['bid_state']:
bid.setState(data['bid_state'])
self.log.debug('Set state to %s', strBidState(bid.state))
has_changed = True
if has_changed:
session = scoped_session(self.session_factory)
try:
activate_bid = False
if offer.swap_type == SwapTypes.SELLER_FIRST:
if bid.state and bid.state > BidStates.BID_RECEIVED and bid.state < BidStates.SWAP_COMPLETED:
activate_bid = True
else:
self.log.debug('TODO - determine in-progress for manualBidUpdate')
if offer.swap_type == SwapTypes.XMR_SWAP:
if bid.state and bid.state in (BidStates.XMR_SWAP_HAVE_SCRIPT_COIN_SPEND_TX, BidStates.XMR_SWAP_SCRIPT_COIN_LOCKED, BidStates.XMR_SWAP_NOSCRIPT_COIN_LOCKED, BidStates.XMR_SWAP_LOCK_RELEASED, BidStates.XMR_SWAP_NOSCRIPT_TX_REDEEMED):
activate_bid = True
if activate_bid:
self.activateBid(session, bid)
else:
self.deactivateBid(session, offer, bid)
self.saveBidInSession(bid_id, bid, session)
session.commit()
finally:
session.close()
session.remove()
else:
raise ValueError('No changes')
finally:
self.mxDB.release()
def editSettings(self, coin_name, data):
self.log.info('Updating settings %s', coin_name)
with self.mxDB:
settings_cc = self.settings['chainclients'][coin_name]
settings_changed = False
if 'lookups' in data:
if settings_cc.get('chain_lookups', 'local') != data['lookups']:
settings_changed = True
settings_cc['chain_lookups'] = data['lookups']
for coin, cc in self.coin_clients.items():
if cc['name'] == coin_name:
cc['chain_lookups'] = data['lookups']
break
if 'fee_priority' in data:
new_fee_priority = data['fee_priority']
assert(new_fee_priority >= 0 and new_fee_priority < 4), 'Invalid priority'
if settings_cc.get('fee_priority', 0) != new_fee_priority:
settings_changed = True
settings_cc['fee_priority'] = new_fee_priority
for coin, cc in self.coin_clients.items():
if cc['name'] == coin_name:
cc['fee_priority'] = new_fee_priority
self.ci(coin).setFeePriority(new_fee_priority)
break
if 'conf_target' in data:
new_conf_target = data['conf_target']
assert(new_conf_target >= 1 and new_conf_target < 33), 'Invalid conf_target'
if settings_cc.get('conf_target', 2) != new_conf_target:
settings_changed = True
settings_cc['conf_target'] = new_conf_target
for coin, cc in self.coin_clients.items():
if cc['name'] == coin_name:
cc['conf_target'] = new_conf_target
self.ci(coin).setConfTarget(new_conf_target)
break
if settings_changed:
settings_path = os.path.join(self.data_dir, cfg.CONFIG_FILENAME)
shutil.copyfile(settings_path, settings_path + '.last')
with open(settings_path, 'w') as fp:
json.dump(self.settings, fp, indent=4)
return settings_changed
5 years ago
def getSummary(self, opts=None):
num_watched_outputs = 0
for c, v in self.coin_clients.items():
num_watched_outputs += len(v['watched_outputs'])
bids_sent = 0
bids_received = 0
q = self.engine.execute('SELECT was_sent, was_received, COUNT(*) FROM bids GROUP BY was_sent, was_received ')
for r in q:
if r[0]:
bids_sent += r[2]
if r[1]:
bids_received += r[2]
now = int(time.time())
q = self.engine.execute('SELECT COUNT(*) FROM offers WHERE active_ind = 1 AND expire_at > {}'.format(now)).first()
5 years ago
num_offers = q[0]
q = self.engine.execute('SELECT COUNT(*) FROM offers WHERE was_sent = 1').first()
5 years ago
num_sent_offers = q[0]
rv = {
'network': self.chain,
'num_swapping': len(self.swaps_in_progress),
'num_network_offers': num_offers,
'num_sent_offers': num_sent_offers,
'num_recv_bids': bids_received,
'num_sent_bids': bids_sent,
'num_watched_outputs': num_watched_outputs,
}
return rv
def getWalletInfo(self, coin):
ci = self.ci(coin)
blockchaininfo = ci.getBlockchainInfo()
walletinfo = ci.getWalletInfo()
scale = chainparams[coin]['decimal_places']
5 years ago
rv = {
'version': self.coin_clients[coin]['core_version'],
5 years ago
'deposit_address': self.getCachedAddressForCoin(coin),
'name': chainparams[coin]['name'].capitalize(),
'blocks': blockchaininfo['blocks'],
'balance': format_amount(make_int(walletinfo['balance'], scale), scale),
'unconfirmed': format_amount(make_int(walletinfo.get('unconfirmed_balance'), scale), scale),
5 years ago
'synced': '{0:.2f}'.format(round(blockchaininfo['verificationprogress'], 2)),
'expected_seed': ci.knownWalletSeed(),
5 years ago
}
return rv
def getWalletsInfo(self, opts=None):
rv = {}
for c in Coins:
if self.coin_clients[c]['connection_type'] == 'rpc':
try:
rv[c] = self.getWalletInfo(c)
except Exception as ex:
rv[c] = {'name': chainparams[c]['name'].capitalize(), 'error': str(ex)}
5 years ago
return rv
def countAcceptedBids(self, offer_id=None):
self.mxDB.acquire()
try:
session = scoped_session(self.session_factory)
if offer_id:
q = session.execute('SELECT COUNT(*) FROM bids WHERE state >= {} AND offer_id = x\'{}\''.format(BidStates.BID_ACCEPTED, offer_id.hex())).first()
5 years ago
else:
q = session.execute('SELECT COUNT(*) FROM bids WHERE state >= {}'.format(BidStates.BID_ACCEPTED)).first()
5 years ago
return q[0]
finally:
session.close()
session.remove()
self.mxDB.release()
def listOffers(self, sent=False, filters={}):
5 years ago
self.mxDB.acquire()
try:
rv = []
now = int(time.time())
session = scoped_session(self.session_factory)
if sent:
q = session.query(Offer).filter(Offer.was_sent == True) # noqa: E712
5 years ago
else:
q = session.query(Offer).filter(sa.and_(Offer.expire_at > now, Offer.active_ind == 1))
4 years ago
filter_offer_id = filters.get('offer_id', None)
if filter_offer_id is not None:
q = q.filter(Offer.offer_id == filter_offer_id)
filter_coin_from = filters.get('coin_from', None)
if filter_coin_from and filter_coin_from > -1:
q = q.filter(Offer.coin_from == int(filter_coin_from))
filter_coin_to = filters.get('coin_to', None)
if filter_coin_to and filter_coin_to > -1:
q = q.filter(Offer.coin_to == int(filter_coin_to))
order_dir = filters.get('sort_dir', 'desc')
order_by = filters.get('sort_by', 'created_at')
if order_by == 'created_at':
q = q.order_by(Offer.created_at.desc() if order_dir == 'desc' else Offer.created_at.asc())
elif order_by == 'rate':
q = q.order_by(Offer.rate.desc() if order_dir == 'desc' else Offer.rate.asc())
limit = filters.get('limit', None)
if limit is not None:
q = q.limit(limit)
offset = filters.get('offset', None)
if offset is not None:
q = q.offset(offset)
5 years ago
for row in q:
rv.append(row)
return rv
finally:
session.close()
session.remove()
self.mxDB.release()
def listBids(self, sent=False, offer_id=None, for_html=False):
self.mxDB.acquire()
try:
rv = []
now = int(time.time())
session = scoped_session(self.session_factory)
query_str = 'SELECT bids.created_at, bids.bid_id, bids.offer_id, bids.amount, bids.state, bids.was_received, tx1.state, tx2.state, offers.coin_from FROM bids ' + \
'LEFT JOIN offers ON offers.offer_id = bids.offer_id ' + \
'LEFT JOIN transactions AS tx1 ON tx1.bid_id = bids.bid_id AND tx1.tx_type = {} '.format(TxTypes.ITX) + \
'LEFT JOIN transactions AS tx2 ON tx2.bid_id = bids.bid_id AND tx2.tx_type = {} '.format(TxTypes.PTX)
5 years ago
if offer_id is not None:
query_str += 'WHERE bids.offer_id = x\'{}\' '.format(offer_id.hex())
5 years ago
elif sent:
query_str += 'WHERE bids.was_sent = 1 '
5 years ago
else:
query_str += 'WHERE bids.was_received = 1 '
query_str += 'ORDER BY bids.created_at DESC'
q = session.execute(query_str)
5 years ago
for row in q:
rv.append(row)
return rv
finally:
session.close()
session.remove()
self.mxDB.release()
def listSwapsInProgress(self, for_html=False):
self.mxDB.acquire()
try:
rv = []
for k, v in self.swaps_in_progress.items():
rv.append((k, v[0].offer_id.hex(), v[0].state, v[0].getITxState(), v[0].getPTxState()))
5 years ago
return rv
finally:
self.mxDB.release()
def listWatchedOutputs(self):
self.mxDB.acquire()
try:
rv = []
rv_heights = []
for c, v in self.coin_clients.items():
if self.coin_clients[c]['connection_type'] == 'rpc':
rv_heights.append((c, v['last_height_checked']))
5 years ago
for o in v['watched_outputs']:
rv.append((c, o.bid_id, o.txid_hex, o.vout, o.tx_type))
5 years ago
return (rv, rv_heights)
finally:
self.mxDB.release()
def listSmsgAddresses(self, use_type_str):
use_type = MessageTypes.OFFER if use_type_str == 'offer' else MessageTypes.BID
self.mxDB.acquire()
try:
session = scoped_session(self.session_factory)
rv = []
q = session.execute('SELECT addr FROM smsgaddresses WHERE use_type = {} ORDER BY addr_id DESC'.format(use_type))
for row in q:
rv.append(row[0])
return rv
finally:
session.close()
session.remove()
self.mxDB.release()
def addLockRefundSigs(self, xmr_swap, ci):
self.log.debug('Setting lock refund tx sigs')
witness_stack = [
b'',
xmr_swap.al_lock_refund_tx_sig,
xmr_swap.af_lock_refund_tx_sig,
xmr_swap.a_lock_tx_script,
]
signed_tx = ci.setTxSignature(xmr_swap.a_lock_refund_tx, witness_stack)
assert(signed_tx), 'setTxSignature failed'
xmr_swap.a_lock_refund_tx = signed_tx
def createCoinALockRefundSwipeTx(self, ci, bid, offer, xmr_swap, xmr_offer):
self.log.debug('Creating %s lock refund swipe tx', ci.coin_name())
pkh_dest = ci.decodeAddress(self.getReceiveAddressForCoin(ci.coin_type()))
spend_tx = ci.createScriptLockRefundSpendToFTx(
xmr_swap.a_lock_refund_tx, xmr_swap.a_lock_refund_tx_script,
pkh_dest,
xmr_offer.a_fee_rate
)
vkaf = self.getPathKey(offer.coin_from, offer.coin_to, bid.created_at, xmr_swap.contract_count, 3)
sig = ci.signTx(vkaf, spend_tx, 0, xmr_swap.a_lock_refund_tx_script, xmr_swap.a_swap_refund_value)
witness_stack = [
sig,
b'',
xmr_swap.a_lock_refund_tx_script,
]
xmr_swap.a_lock_refund_swipe_tx = ci.setTxSignature(spend_tx, witness_stack)
def setBidDebugInd(self, bid_id, debug_ind):
self.log.debug('Bid %s Setting debug flag: %s', bid_id.hex(), debug_ind)
bid = self.getBid(bid_id)
bid.debug_ind = debug_ind
self.saveBid(bid_id, bid)
def storeOfferRevoke(self, offer_id, sig):
self.log.debug('Storing revoke request for offer: %s', offer_id.hex())
for pair in self._possibly_revoked_offers:
if offer_id == pair[0]:
return False
self._possibly_revoked_offers.appendleft((offer_id, sig))
return True
def isOfferRevoked(self, offer_id, offer_addr_from):
for pair in self._possibly_revoked_offers:
if offer_id == pair[0]:
signature_enc = base64.b64encode(pair[1]).decode('utf-8')
passed = self.callcoinrpc(Coins.PART, 'verifymessage', [offer_addr_from, signature_enc, offer_id.hex() + '_revoke'])
return True if passed is True else False # _possibly_revoked_offers should not contain duplicates
return False
def updateBidInProgress(self, bid):
swap_in_progress = self.swaps_in_progress.get(bid.bid_id, None)
if swap_in_progress is None:
return
self.swaps_in_progress[bid.bid_id] = (bid, swap_in_progress[1])
def add_connection(self, host, port, peer_pubkey):
self.log.info('add_connection %s %d %s', host, port, peer_pubkey.hex())
self._network.add_connection(host, port, peer_pubkey)
def get_network_info(self):
if not self._network:
return {'Error': 'Not Initialised'}
return self._network.get_info()