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.
 
 
 
 
 
 

4412 lines
191 KiB

# -*- coding: utf-8 -*-
# Copyright (c) 2019-2020 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import os
import re
import zmq
import json
import time
import base64
import shutil
import random
import logging
import secrets
import hashlib
import datetime as dt
import traceback
import sqlalchemy as sa
from enum import IntEnum, auto
from sqlalchemy.orm import sessionmaker, scoped_session
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 . import __version__
from .util import (
COIN,
pubkeyToAddress,
format8,
format_amount,
encodeAddress,
decodeAddress,
SerialiseNum,
DeserialiseNum,
decodeWif,
toWIF,
getKeyID,
make_int,
)
from .chainparams import (
chainparams,
Coins,
)
from .script import (
OpCodes,
)
from .messages_pb2 import (
OfferMessage,
BidMessage,
BidAcceptMessage,
XmrBidMessage,
XmrBidAcceptMessage,
XmrSplitMessage,
XmrBidLockTxSigsMessage,
XmrBidLockSpendTxMessage,
XmrBidSecretMessage,
OfferRevokeMessage,
)
from .db import (
CURRENT_DB_VERSION,
TableTypes,
Base,
DBKVInt,
DBKVString,
Offer,
Bid,
SwapTx,
PooledAddress,
SentOffer,
SmsgAddress,
EventQueue,
EventLog,
XmrOffer,
XmrSwap,
XmrSplitData,
)
from .explorers import ExplorerInsight, ExplorerBitAps, ExplorerChainz
import basicswap.config as cfg
from .base import BaseApp
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()
XMR_BID_SPLIT = auto()
XMR_BID_ACCEPT_LF = auto()
XMR_BID_TXN_SIGS_FL = auto()
XMR_BID_LOCK_SPEND_TX_LF = auto()
XMR_BID_SECRET_LF = auto()
OFFER_REVOKE = auto()
class SwapTypes(IntEnum):
SELLER_FIRST = auto()
BUYER_FIRST = auto()
SELLER_FIRST_2MSG = auto()
BUYER_FIRST_2MSG = auto()
XMR_SWAP = auto()
class OfferStates(IntEnum):
OFFER_SENT = auto()
OFFER_RECEIVED = auto()
OFFER_ABANDONED = auto()
class BidStates(IntEnum):
BID_SENT = auto()
BID_RECEIVING = auto() # Partially received
BID_RECEIVED = auto()
BID_RECEIVING_ACC = auto() # Partially received accept message
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_SECRET_SHARED = 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()
SWAP_TIMEDOUT = auto()
BID_ABANDONED = auto() # Bid will no longer be processed
BID_ERROR = auto() # An error occurred
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()
class EventTypes(IntEnum):
ACCEPT_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_SECRET = auto()
REDEEM_XMR_SWAP_LOCK_TX_A = auto() # Follower
REDEEM_XMR_SWAP_LOCK_TX_B = auto() # Leader
RECOVER_XMR_SWAP_LOCK_TX_B = auto()
class EventLogTypes(IntEnum):
FAILED_TX_B_PUBLISH = auto()
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()
SEQUENCE_LOCK_BLOCKS = 1
SEQUENCE_LOCK_TIME = 2
ABS_LOCK_BLOCKS = 3
ABS_LOCK_TIME = 4
SEQUENCE_LOCKTIME_GRANULARITY = 9 # 512 seconds
SEQUENCE_LOCKTIME_TYPE_FLAG = (1 << 22)
SEQUENCE_LOCKTIME_MASK = 0x0000ffff
INITIATE_TX_TIMEOUT = 40 * 60 # TODO: make variable per coin
def strOfferState(state):
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):
if state == BidStates.BID_SENT:
return 'Sent'
if state == BidStates.BID_RECEIVING:
return 'Receiving'
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_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_SECRET_SHARED:
return 'Secret shared'
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'
return 'Unknown'
def strTxState(state):
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 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'
def getExpectedSequence(lockType, lockVal, coin_type):
assert(lockVal >= 1), 'Bad lockVal'
if lockType == SEQUENCE_LOCK_BLOCKS:
return lockVal
if lockType == SEQUENCE_LOCK_TIME:
secondsLocked = lockVal
# Ensure the locked time is never less than lockVal
if secondsLocked % (1 << SEQUENCE_LOCKTIME_GRANULARITY) != 0:
secondsLocked += (1 << SEQUENCE_LOCKTIME_GRANULARITY)
secondsLocked >>= SEQUENCE_LOCKTIME_GRANULARITY
return secondsLocked | SEQUENCE_LOCKTIME_TYPE_FLAG
raise ValueError('Unknown lock type')
def decodeSequence(lock_value):
# Return the raw value
if lock_value & SEQUENCE_LOCKTIME_TYPE_FLAG:
return (lock_value & SEQUENCE_LOCKTIME_MASK) << SEQUENCE_LOCKTIME_GRANULARITY
return lock_value & SEQUENCE_LOCKTIME_MASK
def buildContractScript(lock_val, secret_hash, pkh_redeem, pkh_refund, op_lock=OpCodes.OP_CHECKSEQUENCEVERIFY):
script = bytearray([
OpCodes.OP_IF,
OpCodes.OP_SIZE,
0x01, 0x20, # 32
OpCodes.OP_EQUALVERIFY,
OpCodes.OP_SHA256,
0x20]) \
+ secret_hash \
+ bytearray([
OpCodes.OP_EQUALVERIFY,
OpCodes.OP_DUP,
OpCodes.OP_HASH160,
0x14]) \
+ pkh_redeem \
+ bytearray([OpCodes.OP_ELSE, ]) \
+ SerialiseNum(lock_val) \
+ bytearray([
op_lock,
OpCodes.OP_DROP,
OpCodes.OP_DUP,
OpCodes.OP_HASH160,
0x14]) \
+ pkh_refund \
+ bytearray([
OpCodes.OP_ENDIF,
OpCodes.OP_EQUALVERIFY,
OpCodes.OP_CHECKSIG])
return script
def extractScriptSecretHash(script):
return script[7:39]
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 getP2SHScriptForHash(p2sh):
return bytearray([OpCodes.OP_HASH160, 0x14]) \
+ p2sh \
+ bytearray([OpCodes.OP_EQUAL])
def getP2WSH(script):
return bytearray([OpCodes.OP_0, 0x20]) + hashlib.sha256(script).digest()
def replaceAddrPrefix(addr, coin_type, chain_name, addr_type='pubkey_address'):
return encodeAddress(bytes((chainparams[coin_type][chain_name][addr_type],)) + decodeAddress(addr)[1:])
class WatchedOutput():
# Watch for spends
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():
# 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):
def __init__(self, fp, data_dir, settings, chain, log_name='BasicSwap'):
super().__init__(fp, data_dir, settings, chain, log_name)
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)
self.check_events_seconds = self.settings.get('check_events_seconds', 10)
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
# 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', 20)
self.max_delay_retry = self.settings.get('max_delay_retry', 120)
self.swaps_in_progress = dict()
if self.chain == 'regtest':
self.SMSG_SECONDS_IN_DAY = 600
self.SMSG_SECONDS_IN_HOUR = 60 * 2
else:
self.SMSG_SECONDS_IN_DAY = 86400
self.SMSG_SECONDS_IN_HOUR = 60 * 60
# 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']
self.network_addr = pubkeyToAddress(chainparams[Coins.PART][self.chain]['pubkey_address'], bytes.fromhex(self.network_pubkey))
# self.wallet = self.settings.get('wallet', None) # TODO: Move to coin_clients
self.sqlite_file = os.path.join(self.data_dir, 'db{}.sqlite'.format('' if self.chain == 'mainnet' else ('_' + self.chain)))
db_exists = os.path.exists(self.sqlite_file)
self.engine = sa.create_engine('sqlite:///' + self.sqlite_file) # , echo=True
if not db_exists:
Base.metadata.create_all(self.engine)
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:
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()
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)
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
random.seed(secrets.randbits(128))
def setCoinConnectParams(self, coin):
# Set anything that does not require the daemon to be running
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'])))
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)
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)
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()
self.coin_clients[coin] = {
'coin': coin,
'name': chainparams[coin]['name'],
'connection_type': connection_type,
'bindir': bindir,
'datadir': datadir,
'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),
'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'),
}
if self.coin_clients[coin]['connection_type'] == 'rpc':
if coin == Coins.XMR:
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
return self.coin_clients[coin]['interface']
def createInterface(self, coin):
if coin == Coins.PART:
return PARTInterface(self.coin_clients[coin], self.chain)
elif coin == Coins.BTC:
return BTCInterface(self.coin_clients[coin], self.chain)
elif coin == Coins.LTC:
return LTCInterface(self.coin_clients[coin], self.chain)
elif coin == Coins.NMC:
return NMCInterface(self.coin_clients[coin], self.chain)
elif coin == Coins.XMR:
xmr_i = XMRInterface(self.coin_clients[coin], self.chain)
chain_client_settings = self.getChainClientSettings(coin)
xmr_i.setWalletFilename(chain_client_settings['walletfile'])
return xmr_i
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)
def start(self):
self.log.info('Starting BasicSwap %s\n\n', __version__)
self.log.info('sqlalchemy version %s', sa.__version__)
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)
core_version = self.coin_clients[c]['interface'].getDaemonVersion()
self.log.info('%s Core version %d', chainparams[c]['name'].capitalize(), core_version)
self.coin_clients[c]['core_version'] = core_version
if c == Coins.XMR:
self.coin_clients[c]['interface'].ensureWalletExists()
if c == Coins.PART:
self.coin_clients[c]['have_spent_index'] = self.coin_clients[c]['interface'].haveSpentIndex()
# Sanity checks
if self.callcoinrpc(c, 'getstakinginfo')['enabled'] is not False:
self.log.warning('%s staking is not disabled.', chainparams[c]['name'].capitalize())
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)
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)
raise ValueError('Scripted database upgrade not found.')
def waitForDaemonRPC(self, coin_type):
for i in range(21):
if not self.is_running:
return
try:
self.coin_clients[coin_type]['interface'].testDaemonRPC()
return
except Exception as ex:
self.log.warning('Can\'t connect to %s RPC: %s. Trying again in %d second/s.', coin_type, str(ex), (1 + i))
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
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
synced = round(self.callcoinrpc(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):
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)
return
root_key = self.getWalletKey(coin_type, 1)
ci.initialiseWallet(root_key)
def setIntKV(self, str_key, int_val):
session = scoped_session(self.session_factory)
kv = session.query(DBKVInt).filter_by(key=str_key).first()
if not kv:
kv = DBKVInt(key=str_key, value=int_val)
session.add(kv)
session.commit()
session.close()
session.remove()
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'
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)
# TODO: watch for xmr bid outputs
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, 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)
# 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)
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.state and bid.state > BidStates.BID_RECEIVED and bid.state < BidStates.SWAP_COMPLETED:
self.activateBid(session, bid)
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'
# TODO: Ensure smsg is enabled for the active wallet.
self.loadFromDB()
# Scan inbox
options = {'encoding': 'hex'}
ro = self.callrpc('smsginbox', ['unread', '', options])
nm = 0
for msg in ro['messages']:
msg['hex'] += '00' # Add nullbtye to match output from 'smsg' cmd - TODO: make consistent
self.processMsg(msg)
nm += 1
self.log.info('Scanned %d unread messages.', nm)
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 = (amount * rate) // (10 ** chainparams[coin_from]['decimal_places'])
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 >= 2 * 60 * 60 and lock_value <= 96 * 60 * 60), '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?
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?
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')
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):
# 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)
except Exception:
raise ValueError('Unknown coin from type')
try:
coin_to_t = Coins(coin_to)
except Exception:
raise ValueError('Unknown coin to type')
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)
self.mxDB.acquire()
try:
self.checkSynced(coin_from_t, coin_to_t)
# TODO: require proof of funds on offers?
proof_addr, proof_sig = self.getProofOfFunds(coin_from_t, amount)
offer_addr = self.callrpc('getnewaddress') if addr_send_from is None else addr_send_from
offer_created_at = int(time.time())
msg_buf = OfferMessage()
msg_buf.coin_from = int(coin_from)
msg_buf.coin_to = int(coin_to)
msg_buf.amount_from = int(amount)
msg_buf.rate = int(rate)
msg_buf.min_bid_amount = int(min_bid_amount)
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 swap_type == SwapTypes.XMR_SWAP:
xmr_offer = XmrOffer()
# Delay before the chain a lock refund tx can be mined
xmr_offer.lock_time_1 = getExpectedSequence(lock_type, lock_value, coin_from)
# Delay before the follower can spend from the chain a lock refund tx
xmr_offer.lock_time_2 = getExpectedSequence(lock_type, lock_value, coin_from)
# TODO: Dynamic fee selection
xmr_offer.a_fee_rate = make_int(0.00032595, self.ci(coin_from).exp())
xmr_offer.b_fee_rate = make_int(0.0012595, self.ci(coin_to).exp())
msg_buf.fee_rate_from = xmr_offer.a_fee_rate
msg_buf.fee_rate_to = xmr_offer.b_fee_rate
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])
msg_id = ro['msgid']
offer_id = bytes.fromhex(msg_id)
session = scoped_session(self.session_factory)
offer = Offer(
offer_id=offer_id,
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,
was_sent=True,
auto_accept_bids=auto_accept_bids,)
offer.setState(OfferStates.OFFER_SENT)
if swap_type == SwapTypes.XMR_SWAP:
xmr_offer.offer_id = offer_id
session.add(xmr_offer)
session.add(offer)
session.add(SentOffer(offer_id=offer_id))
if addr_send_from is None:
session.add(SmsgAddress(addr=offer_addr, use_type=MessageTypes.OFFER))
session.commit()
session.close()
session.remove()
finally:
self.mxDB.release()
self.log.info('Sent OFFER %s', offer_id.hex())
return offer_id
def revokeOffer(self, offer_id):
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()
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):
account = self.callcoinrpc(Coins.PART, 'extkey', ['account'])
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):
account = self.callcoinrpc(Coins.PART, 'extkey', ['account'])
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:
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)
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
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()
session.close()
session.remove()
finally:
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
session.close()
session.remove()
finally:
self.mxDB.release()
def getReceiveAddressForCoin(self, coin_type):
new_addr = self.coin_clients[coin_type]['interface'].getNewAddress(self.coin_clients[coin_type]['use_segwit'])
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):
# TODO: Per coin settings to override feerate
override_feerate = self.coin_clients[coin_type].get('override_feerate', None)
if override_feerate:
return override_feerate
return self.ci(coin_type).get_fee_rate()
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 '')
params = [addr_to, value, '', '', subfee, True, self.coin_clients[coin_type]['conf_target']]
if coin_type == Coins.PART:
params.insert(5, '') # narration
return self.callcoinrpc(coin_type, 'sendtoaddress', params)
def cacheNewAddressForCoin(self, coin_type):
self.log.debug('cacheNewAddressForCoin %s', coin_type)
key_str = 'receive_addr_' + chainparams[coin_type]['name']
session = scoped_session(self.session_factory)
addr = self.getReceiveAddressForCoin(coin_type)
self.mxDB.acquire()
try:
session = scoped_session(self.session_factory)
try:
kv = session.query(DBKVString).filter_by(key=key_str).first()
kv.value = addr
except Exception:
kv = DBKVString(
key=key_str,
value=addr
)
session.add(kv)
session.commit()
session.close()
session.remove()
finally:
self.mxDB.release()
return addr
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()
session.close()
session.remove()
finally:
self.mxDB.release()
return addr
def getNewContractId(self):
self.mxDB.acquire()
try:
self._contract_count += 1
session = scoped_session(self.session_factory)
self.engine.execute('UPDATE kv_int SET value = {} WHERE KEY="contract_count"'.format(self._contract_count))
session.commit()
session.close()
session.remove()
finally:
self.mxDB.release()
return self._contract_count
def getProofOfFunds(self, coin_type, amount_for):
self.log.debug('getProofOfFunds %s %s', str(coin_type), format8(amount_for))
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.0) * COIN + u['amount'] * COIN
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'])
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)
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)
def saveBid(self, bid_id, bid, xmr_swap=None):
self.mxDB.acquire()
try:
session = scoped_session(self.session_factory)
self.saveBidInSession(bid_id, bid, session, xmr_swap)
session.commit()
session.close()
session.remove()
finally:
self.mxDB.release()
def saveToDB(self, db_record):
self.mxDB.acquire()
try:
session = scoped_session(self.session_factory)
session.add(db_record)
session.commit()
session.close()
session.remove()
finally:
self.mxDB.release()
def createEventInSession(self, delay, event_type, linked_id, session):
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())
self.mxDB.acquire()
try:
session = scoped_session(self.session_factory)
self.createEventInSession(delay, event_type, linked_id, session)
session.commit()
session.close()
session.remove()
finally:
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)
session.add(entry)
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):
# Bid to send bid.amount * offer.rate of coin_to in exchange for bid.amount of coin_from
self.log.debug('postBid %s %s', offer_id.hex(), format8(amount))
self.mxDB.acquire()
try:
offer = self.getOffer(offer_id)
assert(offer), 'Offer not found: {}.'.format(offer_id.hex())
assert(offer.expire_at > int(time.time())), 'Offer has expired'
msg_buf = BidMessage()
msg_buf.offer_msg_id = offer_id
msg_buf.time_valid = 60 * 10
msg_buf.amount = int(amount) # amount of coin_from
coin_from = Coins(offer.coin_from)
coin_to = Coins(offer.coin_to)
self.checkSynced(coin_from, coin_to)
contract_count = self.getNewContractId()
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))
proof_addr, proof_sig = self.getProofOfFunds(coin_to, msg_buf.amount)
msg_buf.proof_address = proof_addr
msg_buf.proof_signature = proof_sig
else:
raise ValueError('TODO')
bid_bytes = msg_buf.SerializeToString()
payload_hex = str.format('{:02x}', MessageTypes.BID) + bid_bytes.hex()
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']
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=(msg_buf.amount * offer.rate) // COIN,
expire_at=now + msg_buf.time_valid,
bid_addr=bid_addr,
was_sent=True,
)
bid.setState(BidStates.BID_SENT)
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()
session.close()
session.remove()
self.log.info('Sent BID %s', bid_id.hex())
return bid_id
finally:
self.mxDB.release()
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 getXmrBid(self, bid_id, sent=False):
self.mxDB.acquire()
try:
session = scoped_session(self.session_factory)
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
finally:
session.close()
session.remove()
self.mxDB.release()
def getXmrOffer(self, offer_id, sent=False):
self.mxDB.acquire()
try:
session = scoped_session(self.session_factory)
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
finally:
session.close()
session.remove()
self.mxDB.release()
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
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()
if bid:
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, session.query(Offer).filter_by(offer_id=bid.offer_id).first() if bid is not None else None
finally:
session.close()
session.remove()
self.mxDB.release()
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(BidStates(bid.state))
if bid.contract_count is None:
bid.contract_count = self.getNewContractId()
coin_from = Coins(offer.coin_from)
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:
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 = getExpectedSequence(offer.lock_type, offer.lock_value, coin_from)
script = 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
self.log.debug('Initiate %s lock_value %d %d', coin_from, offer.lock_value, lock_value)
script = buildContractScript(lock_value, secret_hash, bid.pkhash_buyer, pkhash_refund, OpCodes.OP_CHECKLOCKTIMEVERIFY)
p2sh = self.callcoinrpc(Coins.PART, 'decodescript', [script.hex()])['p2sh']
bid.pkhash_seller = pkhash_refund
txn = self.createInitiateTxn(coin_from, bid_id, bid, script)
# 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)
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)
# 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))
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])
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)
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('postBid %s %s', offer_id.hex(), format8(amount))
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)
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.b_restore_height = self.ci(coin_to).getChainHeight()
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)
kaf = self.getPathKey(coin_from, coin_to, bid_created_at, xmr_swap.contract_count, 3)
karf = self.getPathKey(coin_from, coin_to, bid_created_at, xmr_swap.contract_count, 4)
xmr_swap.vkbvf = kbvf
xmr_swap.pkbvf = ci_to.getPubkey(kbvf)
xmr_swap.pkbsf = ci_to.getPubkey(kbsf)
xmr_swap.pkaf = ci_from.getPubkey(kaf)
xmr_swap.pkarf = ci_from.getPubkey(karf)
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))
msg_buf.pkaf = xmr_swap.pkaf
msg_buf.pkarf = xmr_swap.pkarf
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()
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) // COIN,
expire_at=bid_created_at + msg_buf.time_valid,
bid_addr=bid_addr,
was_sent=True,
)
bid.setState(BidStates.BID_SENT)
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()
session.close()
session.remove()
self.log.info('Sent XMR_BID_FL %s', xmr_swap.bid_id.hex())
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), 'Offer has expired'
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()
contract_secret = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, 1)
kbvl = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, 2, for_ed25519=True)
kbsl = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, 3, for_ed25519=True)
kal = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, 4)
karl = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, 5)
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)
xmr_swap.sh = hashlib.sha256(contract_secret).digest()
xmr_swap.pkal = ci_from.getPubkey(kal)
xmr_swap.pkarl = ci_from.getPubkey(karl)
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.sh,
xmr_swap.pkal, xmr_swap.pkaf,
xmr_offer.lock_time_1,
xmr_swap.pkarl, xmr_swap.pkarf,
)
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)
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.pkarl, xmr_swap.pkarf,
xmr_offer.lock_time_2,
xmr_swap.pkaf,
xmr_offer.a_fee_rate
)
xmr_swap.a_lock_refund_tx_id = ci_from.getTxHash(xmr_swap.a_lock_refund_tx)
xmr_swap.al_lock_refund_tx_sig = ci_from.signTx(karl, 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.pkarl, 0, xmr_swap.a_lock_tx_script, bid.amount)
assert(v)
pkh_refund_to = ci_from.decodeAddress(self.getReceiveAddressForCoin(coin_from))
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,
xmr_offer.a_fee_rate
)
xmr_swap.a_lock_refund_spend_tx_id = ci_from.getTxHash(xmr_swap.a_lock_refund_spend_tx)
msg_buf = XmrBidAcceptMessage()
msg_buf.bid_msg_id = bid_id
msg_buf.sh = xmr_swap.sh
msg_buf.pkal = xmr_swap.pkal
msg_buf.pkarl = xmr_swap.pkarl
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()
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)
session = scoped_session(self.session_factory)
self.saveBidInSession(bid_id, bid, session, xmr_swap)
session.commit()
session.close()
session.remove()
# Add to swaps_in_progress only when waiting on txns
self.log.info('Sent XMR_BID_ACCEPT_LF %s', bid_id.hex())
return bid_id
finally:
self.mxDB.release()
def abandonOffer(self, offer_id):
self.log.info('Abandoning Offer %s', offer_id.hex())
self.mxDB.acquire()
try:
session = scoped_session(self.session_factory)
offer = session.query(Offer).filter_by(offer_id=offer_id).first()
assert(offer), 'Offer not found'
# TODO: abandon linked bids?
# Mark bid as abandoned, no further processing will be done
offer.setState(OfferStates.OFFER_ABANDONED)
session.commit()
finally:
session.close()
session.remove()
self.mxDB.release()
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)
session.commit()
self.deactivateBid(offer, bid)
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):
if self.coin_clients[coin_type]['connection_type'] != 'rpc':
return None
if self.coin_clients[coin_type]['use_segwit']:
addr_to = self.encodeSegwitP2WSH(coin_type, getP2WSH(initiate_script))
else:
addr_to = self.getScriptAddress(coin_type, initiate_script)
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: format8(bid.amount)}])
options = {
'lockUnspents': True,
'conf_target': self.coin_clients[coin_type]['conf_target'],
}
txn_funded = self.callcoinrpc(coin_type, 'fundrawtransaction', [txn, options])['hex']
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)
bid_date = dt.datetime.fromtimestamp(bid.created_at).date()
secret_hash = extractScriptSecretHash(bid.initiate_tx.script)
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 = getExpectedSequence(offer.lock_type, lock_value, coin_to)
participate_script = buildContractScript(sequence, secret_hash, pkhash_seller, pkhash_buyer_refund)
else:
# 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:
# 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):
self.log.debug('wtf %d', i)
prev_block = self.callcoinrpc(coin_to, 'getblock', [cblock_hash, ])
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:
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 = buildContractScript(contract_lock_value, secret_hash, pkhash_seller, pkhash_buyer_refund, OpCodes.OP_CHECKLOCKTIMEVERIFY)
return participate_script
def createParticipateTxn(self, bid_id, bid, offer, participate_script):
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
amount_to = bid.amount_to
# Check required?
assert(amount_to == (bid.amount * offer.rate) // COIN)
if self.coin_clients[coin_to]['use_segwit']:
p2wsh = getP2WSH(participate_script)
addr_to = self.encodeSegwitP2WSH(coin_to, p2wsh)
else:
addr_to = self.getScriptAddress(coin_to, participate_script)
txn = self.callcoinrpc(coin_to, 'createrawtransaction', [[], {addr_to: format8(amount_to)}])
options = {
'lockUnspents': True,
'conf_target': self.coin_clients[coin_to]['conf_target'],
}
txn_funded = self.callcoinrpc(coin_to, 'fundrawtransaction', [txn, options])['hex']
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)
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
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))
if for_txn_type == 'participate':
prev_txnid = bid.participate_tx.txid.hex()
prev_n = bid.participate_tx.vout
txn_script = bid.participate_tx.script
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
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': format8(prev_amount)}
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 = self.getFeeRateForCoin(coin_type)
tx_vsize = self.getContractSpendTxVSize(coin_type)
tx_fee = (fee_rate * tx_vsize) / 1000
self.log.debug('Redeem tx fee %s, rate %s', format8(tx_fee * COIN), str(fee_rate))
amount_out = prev_amount - tx_fee * COIN
assert(amount_out > 0), 'Amount out <= 0'
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)
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(format8(amount_out), addr_redeem_out)
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'
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):
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
prevout_s = ' in={}:{}:{}'.format(txjs['txid'], vout, sequence)
fee_rate = self.getFeeRateForCoin(coin_type)
tx_vsize = self.getContractSpendTxVSize(coin_type, False)
tx_fee = (fee_rate * tx_vsize) / 1000
self.log.debug('Refund tx fee %s, rate %s', format8(tx_fee * COIN), str(fee_rate))
amount_out = prev_amount * COIN - tx_fee * COIN
if amount_out <= 0:
raise ValueError('Refund amount out <= 0')
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'
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(format8(amount_out), addr_refund_out)
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))
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'
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)
# Seller first mode, buyer participates
participate_script = self.deriveParticipateScript(bid_id, bid, offer)
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())
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,
)
# bid saved in checkBidState
def setLastHeightChecked(self, coin_type, tx_height):
chain_name = chainparams[coin_type]['name']
if tx_height < 1:
tx_height = self.lookupChainHeight(coin_type)
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)
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)
# 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
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)
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):
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
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))
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': make_int(o['amount']),
}
else:
sum_unspent += o['amount'] * COIN
if sum_output:
return sum_unspent
return None
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: Abandoning bid for testing: %d.', bid_id.hex(), bid.debug_ind)
bid.setState(BidStates.BID_ABANDONED)
rv = True
self.saveBidInSession(bid_id, bid, session, xmr_swap)
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.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:
logging.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.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:
logging.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
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()))
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
a_lock_tx_dest = ci_from.getScriptDest(xmr_swap.a_lock_tx_script)
utxos = 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 utxo['depth'] >= ci_from.blocks_confirmed:
bid.xmr_a_lock_tx.setState(TxStates.TX_CONFIRMED)
bid.setState(BidStates.XMR_SWAP_SCRIPT_COIN_LOCKED)
self.saveBidInSession(bid_id, bid, session, xmr_swap)
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)
session.commit()
elif state == BidStates.XMR_SWAP_SCRIPT_COIN_LOCKED:
if bid.was_sent and bid.xmr_b_lock_tx is None:
return rv
found_tx = ci_to.findTxB(xmr_swap.vkbv, xmr_swap.pkbs, bid.amount_to, ci_to.blocks_confirmed, xmr_swap.b_restore_height)
if found_tx is not None:
self.log.debug('Found {} lock tx in chain'.format(ci_to.coin_name()))
if bid.xmr_b_lock_tx is None:
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,
)
bid.xmr_b_lock_tx.setState(TxStates.TX_CONFIRMED)
bid.setState(BidStates.XMR_SWAP_NOSCRIPT_COIN_LOCKED)
self.saveBidInSession(bid_id, bid, session, xmr_swap)
if bid.was_received:
delay = random.randrange(self.min_delay_event, self.max_delay_event)
self.log.info('Releasing xmr swap secret for bid %s in %d seconds', bid_id.hex(), delay)
self.createEventInSession(delay, EventTypes.SEND_XMR_SECRET, bid_id, session)
session.commit()
elif state == BidStates.XMR_SWAP_SECRET_SHARED:
# Wait for script spend tx to confirm
# TODO: Use explorer to get tx / block hash for getrawtransaction
pass
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
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))
if offer.swap_type == SwapTypes.XMR_SWAP:
return self.checkXmrBidState(bid_id, bid, offer)
save_bid = False
coin_from = Coins(offer.coin_from)
coin_to = Coins(offer.coin_to)
# TODO: Batch calls to scantxoutset
# TODO: timeouts
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)
index = None
tx_height = None
last_initiate_txn_conf = bid.initiate_tx.conf
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))
bid.initiate_tx.conf = initiate_txn['confirmations']
try:
tx_height = initiate_txn['height']
except Exception:
tx_height = -1
index = vout
except Exception:
pass
else:
if self.coin_clients[coin_from]['use_segwit']:
addr = self.encodeSegwitP2WSH(coin_from, getP2WSH(bid.initiate_tx.script))
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']
index = found['index']
tx_height = found['height']
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)
if bid.initiate_tx.vout is None:
bid.initiate_tx.vout = index
# 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
if bid.initiate_tx.conf >= self.coin_clients[coin_from]['blocks_confirmed']:
self.initiateTxnConfirmed(bid_id, bid, offer)
save_bid = True
# 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()):
self.log.info('Swap timed out waiting for initiate tx for bid %s', bid_id.hex())
bid.setState(BidStates.SWAP_TIMEDOUT)
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))
else:
addr = self.getScriptAddress(coin_to, bid.participate_tx.script)
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']
index = found['index']
if bid.participate_tx is None or bid.participate_tx.txid is None:
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
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']:
self.participateTxnConfirmed(bid_id, bid, offer)
save_bid = True
elif state == BidStates.SWAP_PARTICIPATING:
# Waiting for initiate txn spend
pass
elif state == BidStates.BID_ERROR:
# Wait for user input
pass
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):
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)
bid.setState(BidStates.SWAP_COMPLETED)
self.saveBid(bid_id, bid)
return True # Mark bid for archiving
if save_bid:
self.saveBid(bid_id, bid)
# Try refund, keep trying until sent tx is spent
if (bid.getITxState() == TxStates.TX_SENT or bid.getITxState() == TxStates.TX_CONFIRMED) \
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) \
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))
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'
return bytes.fromhex(spend_in['txinwitness'][2])
else:
script_sig = spend_in['scriptSig']['asm'].split(' ')
assert(len(script_sig) == 5), 'Bad witness size'
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)
self.coin_clients[coin_type]['watched_outputs'].append(WatchedOutput(bid_id, txid_hex, vout, tx_type, swap_type))
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):
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)
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
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)
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)
self.removeWatchedOutput(coin_from, bid_id, bid.initiate_tx.txid.hex())
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
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)
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)
if bid.was_sent:
txn = self.createRedeemTxn(coin_from, bid, for_txn_type='initiate')
txid = self.submitTxn(coin_from, txn)
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())
# TODO: Wait for depth? new state SWAP_TXI_REDEEM_SENT?
self.removeWatchedOutput(coin_to, bid_id, bid.participate_tx.txid.hex())
self.saveBid(bid_id, bid)
def process_XMR_SWAP_A_LOCK_tx_spend(self, bid_id, spend_txid_hex, spend_txn):
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.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)
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_SECRET_SHARED:
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:
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.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)
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)
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)
def checkForSpends(self, coin_type, c):
# assert(self.mxDB.locked()) self.log.debug('checkForSpends %s', coin_type)
self.log.debug('checkForSpends %s', coin_type)
if coin_type == Coins.PART and self.coin_clients[coin_type]['have_spent_index']:
# 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))
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'])
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)
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])
block = self.callcoinrpc(coin_type, 'getblock', [block_hash, 2])
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)
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)
def expireMessages(self):
self.mxDB.acquire()
try:
now = int(time.time())
options = {'encoding': 'none'}
ro = self.callrpc('smsginbox', ['all', '', options])
for msg in ro['messages']:
expire_at = msg['sent'] + msg['daysretention'] * self.SMSG_SECONDS_IN_DAY
if expire_at < now:
options = {'encoding': 'none', 'delete': True}
del_msg = self.callrpc('smsg', [msg['msgid'], options])
logging.debug('TODO: Expire records from db')
finally:
self.mxDB.release()
def checkEvents(self):
self.mxDB.acquire()
now = int(time.time())
session = None
try:
session = scoped_session(self.session_factory)
q = session.query(EventQueue).filter(sa.and_(EventQueue.active_ind == 1, EventQueue.trigger_at <= now))
for row in q:
try:
if row.event_type == EventTypes.ACCEPT_BID:
self.acceptBid(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_SECRET:
self.sendXmrBidSecret(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))
session.commit()
finally:
if session:
session.close()
session.remove()
self.mxDB.release()
def checkXmrSwaps(self):
self.mxDB.acquire()
now = int(time.time())
ttl_xmr_split_messages = 60 * 60
try:
session = scoped_session(self.session_factory)
q = session.query(Bid).filter(Bid.state == BidStates.BID_RECEIVING)
for bid in q:
q = self.engine.execute('SELECT COUNT(*) FROM xmr_split_data WHERE bid_id = x\'{}\' AND msg_type = {}'.format(bid.bid_id.hex(), XmrSplitMsgTypes.BID)).first()
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))
session.add(bid)
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 = self.engine.execute('SELECT COUNT(*) FROM xmr_split_data WHERE bid_id = x\'{}\' AND msg_type = {}'.format(bid.bid_id.hex(), XmrSplitMsgTypes.BID_ACCEPT)).first()
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))
session.add(bid)
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)
session.commit()
session.close()
session.remove()
finally:
self.mxDB.release()
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)
coin_to = Coins(offer_data.coin_to)
chain_from = chainparams[coin_from][self.chain]
assert(offer_data.coin_from != offer_data.coin_to), 'coin_from == coin_to'
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)
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'
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)
logging.debug('TODO - More restrictions')
else:
raise ValueError('Unknown swap type {}.'.format(offer_data.swap_type))
offer_id = bytes.fromhex(msg['msgid'])
session = scoped_session(self.session_factory)
# 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 = getExpectedSequence(offer_data.lock_type, offer_data.lock_value, coin_from)
xmr_offer.lock_time_2 = getExpectedSequence(offer_data.lock_type, offer_data.lock_value, coin_from)
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()
session.close()
session.remove()
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)
offer = session.query(Offer).filter_by(offer_id=msg_data.offer_msg_id).first()
if offer is None:
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()))
if len(msg_data.signature) != 65:
raise ValueError('Invalid signature length')
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), 'Proof of funds 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()
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'
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(now <= msg['sent'] + bid_data.time_valid), 'Bid expired'
# TODO: allow higher bids
# assert(bid_data.rate != offer['data'].rate), 'Bid rate mismatch'
coin_to = Coins(offer.coin_to)
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.callcoinrpc(Coins.PART, 'verifymessage', [bid_proof_address, bid_data.proof_signature, bid_data.proof_address + '_swap_proof', mm])
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, format8(sum_unspent))
assert(sum_unspent >= bid_data.amount), 'Proof of funds failed'
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=(bid_data.amount * offer.rate) // COIN,
expire_at=msg['sent'] + bid_data.time_valid,
bid_addr=msg['from'],
was_received=True,
)
else:
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())
else:
delay = random.randrange(self.min_delay_event, self.max_delay_event)
self.log.info('Auto accepting bid %s in %d seconds', bid_id.hex(), delay)
self.createEvent(delay, EventTypes.ACCEPT_BID, bid_id)
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)
# assert(bid.expire_at > now), 'Bid expired' # How much time over to accept
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))))
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()
assert(len(scriptvalues[0]) == 64), 'Bad secret_hash length'
assert(bytes.fromhex(scriptvalues[1]) == bid.pkhash_buyer), 'pkhash_buyer mismatch'
script_lock_value = int(scriptvalues[2])
if use_csv:
expect_sequence = getExpectedSequence(offer.lock_type, offer.lock_value, coin_from)
assert(script_lock_value == expect_sequence), 'sequence mismatch'
else:
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'
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,
)
bid.pkhash_seller = bytes.fromhex(scriptvalues[3])
bid.setState(BidStates.BID_ACCEPTED)
bid.setITxState(TxStates.TX_NONE)
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)
def receiveXmrBid(self, bid, session):
self.log.debug('Receiving xmr bid %s', bid.bid_id.hex())
now = int(time.time())
offer, xmr_offer = self.getXmrOffer(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())
coin_from = Coins(offer.coin_from)
coin_to = Coins(offer.coin_to)
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(coin_from)
ci_to = self.ci(coin_to)
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.')
if not ci_from.verifyPubkey(xmr_swap.pkaf):
raise ValueError('Invalid pubkey.')
if not ci_from.verifyPubkey(xmr_swap.pkarf):
raise ValueError('Invalid pubkey.')
bid.setState(BidStates.BID_RECEIVED)
self.saveBidInSession(bid.bid_id, bid, session, xmr_swap)
def receiveXmrBidAccept(self, bid, session):
# Follower receiving MSG1F and MSG2F
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())
coin_from = Coins(offer.coin_from)
coin_to = Coins(offer.coin_to)
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(coin_from)
ci_to = self.ci(coin_to)
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)
if not ci_from.verifyPubkey(xmr_swap.pkal):
raise ValueError('Invalid pubkey.')
if not ci_from.verifyPubkey(xmr_swap.pkarl):
raise ValueError('Invalid pubkey.')
bid.setState(BidStates.SWAP_DELAYING)
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)
def processXmrBid(self, msg):
# MSG1L
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())
coin_from = Coins(offer.coin_from)
coin_to = Coins(offer.coin_to)
ci_from = self.ci(coin_from)
ci_to = self.ci(coin_to)
logging.debug('TODO: xmr bid validation')
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) // COIN,
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,
pkaf=bid_data.pkaf,
pkarf=bid_data.pkarf,
vkbvf=bid_data.kbvf,
pkbvf=ci_to.getPubkey(bid_data.kbvf),
kbsf_dleag=bid_data.kbsf_dleag,
b_restore_height=self.ci(coin_to).getChainHeight(),
)
else:
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
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())
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())
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:
assert(len(msg_data.sh) == 32), 'Bad secret hash length'
xmr_swap.sh = msg_data.sh
xmr_swap.pkal = msg_data.pkal
xmr_swap.pkarl = msg_data.pkarl
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.sh,
xmr_swap.pkal, xmr_swap.pkaf,
xmr_offer.lock_time_1, xmr_offer.a_fee_rate,
xmr_swap.pkarl, xmr_swap.pkarf,
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.pkarl, xmr_swap.pkarf,
xmr_offer.lock_time_2,
xmr_swap.pkaf,
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
)
logging.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.pkarl, 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)
self.addWatchedOutput(Coins(offer.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(Coins(offer.coin_from), bid.bid_id, xmr_swap.a_lock_refund_tx_id.hex(), 0, TxTypes.XMR_SWAP_A_LOCK_REFUND, SwapTypes.XMR_SWAP)
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.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:
karf = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, 4)
xmr_swap.af_lock_refund_spend_tx_esig = ci_from.signTxOtVES(karf, 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(karf, 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.saveBidInSession(bid_id, bid, session, xmr_swap)
self.watchXmrSwap(bid, offer, 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.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)
kal = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, 4)
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
msg_buf = XmrBidLockSpendTxMessage(
bid_msg_id=bid_id,
a_lock_spend_tx=xmr_swap.a_lock_spend_tx,
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_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.saveBidInSession(bid_id, bid, session, xmr_swap)
self.watchXmrSwap(bid, offer, 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.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)
if bid.debug_ind == DebugTypes.BID_STOP_AFTER_COIN_A_LOCK:
self.log.debug('XMR bid %s: Abandoning bid for testing: %d.', bid_id.hex(), bid.debug_ind)
# bid.setState(BidStates.BID_ABANDONED) # TODO: Retry if state
return
if bid.debug_ind == DebugTypes.CREATE_INVALID_COIN_B_LOCK:
self.log.debug('XMR bid %s: Debug %d - Reducing lock b txn amount by 10%%.', bid_id.hex(), bid.debug_ind)
bid.amount_to -= int(bid.amount_to * 0.1)
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_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_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.saveBidInSession(bid_id, bid, session, xmr_swap, save_in_progress=offer)
def sendXmrBidSecret(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.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)
contract_secret = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, 1)
msg_buf = XmrBidSecretMessage(
bid_msg_id=bid_id,
secret_value=contract_secret)
msg_bytes = msg_buf.SerializeToString()
payload_hex = str.format('{:02x}', MessageTypes.XMR_BID_SECRET_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_SECRET_SHARED)
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.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)
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.sv,
bytes((1,)),
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.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)
# 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, 3, for_ed25519=True)
vkbs = ci_to.sumKeys(kbsl, kbsf)
address_to = ci_to.getMainWalletAddress()
txid = ci_to.spendBLockTx(address_to, xmr_swap.vkbv, vkbs, bid.amount_to, xmr_offer.b_fee_rate, xmr_swap.b_restore_height)
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.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)
# 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()
txid = ci_to.spendBLockTx(address_to, xmr_swap.vkbv, vkbs, bid.amount_to, xmr_offer.b_fee_rate, xmr_swap.b_restore_height)
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, 3, for_ed25519=True)
karl = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, 5)
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(karl, 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.pkarf, 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))
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())
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.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.al_lock_spend_tx_esig = msg_data.al_lock_spend_tx_esig
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)
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'
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)
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:
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
self.saveToDB(dbr)
def processXmrSecretMessage(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 = XmrBidSecretMessage()
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())
xmr_swap.sv = msg_data.secret_value
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_SECRET_SHARED)
self.saveBid(bid_id, bid, xmr_swap=xmr_swap)
self.swaps_in_progress[bid_id] = (bid, offer)
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:
self.processXmrBid(msg)
elif msg_type == MessageTypes.XMR_BID_ACCEPT_LF:
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)
elif msg_type == MessageTypes.XMR_BID_SPLIT:
self.processXmrSplitMessage(msg)
elif msg_type == MessageTypes.XMR_BID_SECRET_LF:
self.processXmrSecretMessage(msg)
if msg_type == MessageTypes.OFFER_REVOKE:
self.processOfferRevoke(msg)
except Exception as ex:
self.log.error('processMsg %s', str(ex))
if self.debug:
traceback.print_exc()
finally:
self.mxDB.release()
def processZmqSmsg(self):
message = self.zmqSubscriber.recv()
clear = self.zmqSubscriber.recv()
if message[0] == 3: # Paid smsg
return # TODO: Switch to paid?
msg_id = message[2:]
options = {'encoding': 'hex', 'setread': True}
msg = self.callrpc('smsg', [msg_id.hex(), options])
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:
pass
except Exception as ex:
self.log.error('smsg zmq %s', str(ex))
if self.debug:
traceback.print_exc()
self.mxDB.acquire()
try:
# TODO: Wait for blocks / txns, would need to check multiple coins
now = int(time.time())
if now - self._last_checked_progress >= self.check_progress_seconds:
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(offer, bid)
self._last_checked_progress = now
if now - self._last_checked_watched >= self.check_watched_seconds:
for k, c in self.coin_clients.items():
if len(c['watched_outputs']) > 0:
self.checkForSpends(k, c)
self._last_checked_watched = now
if now - self._last_checked_expired >= self.check_expired_seconds:
self.expireMessages()
self._last_checked_expired = now
if now - self._last_checked_events >= self.check_events_seconds:
self.checkEvents()
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
except Exception as ex:
self.log.error('update %s', str(ex))
if self.debug:
traceback.print_exc()
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:
self.saveBidInSession(bid_id, bid, session)
session.commit()
if bid.state and bid.state > BidStates.BID_RECEIVED and bid.state < BidStates.SWAP_COMPLETED:
self.activateBid(session, bid)
else:
self.deactivateBid(offer, bid)
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)
self.mxDB.acquire()
try:
if 'lookups' in data:
self.settings['chainclients'][coin_name]['chain_lookups'] = data['lookups']
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)
for c in self.coin_clients:
if c['name'] == coin_name:
c['chain_lookups'] = data['lookups']
finally:
self.mxDB.release()
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 expire_at > {}'.format(now)).first()
num_offers = q[0]
q = self.engine.execute('SELECT COUNT(*) FROM offers WHERE was_sent = 1').first()
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):
blockchaininfo = self.coin_clients[coin]['interface'].getBlockchainInfo()
walletinfo = self.coin_clients[coin]['interface'].getWalletInfo()
scale = chainparams[coin]['decimal_places']
rv = {
'version': self.coin_clients[coin]['core_version'],
'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),
'synced': '{0:.2f}'.format(round(blockchaininfo['verificationprogress'], 2)),
}
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)}
return rv
def countAcceptedBids(self, offer_id=None):
self.mxDB.acquire()
try:
session = scoped_session(self.session_factory)
if offer_id:
q = self.engine.execute('SELECT COUNT(*) FROM bids WHERE state >= {} AND offer_id = x\'{}\''.format(BidStates.BID_ACCEPTED, offer_id.hex())).first()
else:
q = self.engine.execute('SELECT COUNT(*) FROM bids WHERE state >= {}'.format(BidStates.BID_ACCEPTED)).first()
return q[0]
finally:
session.close()
session.remove()
self.mxDB.release()
def listOffers(self, sent=False, filters={}):
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
else:
q = session.query(Offer).filter(sa.and_(Offer.expire_at > now, Offer.active_ind == 1))
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('order_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)
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 FROM bids ' + \
'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)
if offer_id is not None:
query_str += 'WHERE bids.offer_id = x\'{}\' '.format(offer_id.hex())
elif sent:
query_str += 'WHERE bids.was_sent = 1 '
else:
query_str += 'WHERE bids.was_received = 1 '
query_str += 'ORDER BY bids.created_at DESC'
q = self.engine.execute(query_str)
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()))
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']))
for o in v['watched_outputs']:
rv.append((c, o.bid_id, o.txid_hex, o.vout, o.tx_type))
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 = self.engine.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,
b'',
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)