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.
4641 lines
202 KiB
4641 lines
202 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() |
|
ACCEPT_XMR_BID = auto() |
|
SIGN_XMR_SWAP_LOCK_TX_A = auto() |
|
SEND_XMR_SWAP_LOCK_TX_A = auto() |
|
SEND_XMR_SWAP_LOCK_TX_B = auto() |
|
SEND_XMR_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_LOCK_PUBLISH = auto() |
|
LOCK_TX_A_PUBLISHED = auto() |
|
LOCK_TX_B_PUBLISHED = auto() |
|
FAILED_TX_B_SPEND = 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 strTxType(tx_type): |
|
if tx_type == TxTypes.XMR_SWAP_A_LOCK: |
|
return 'Chain A Lock Tx' |
|
if tx_type == TxTypes.XMR_SWAP_A_LOCK_SPEND: |
|
return 'Chain A Lock Spend Tx' |
|
if tx_type == TxTypes.XMR_SWAP_A_LOCK_REFUND: |
|
return 'Chain A Lock Refund Tx' |
|
if tx_type == TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND: |
|
return 'Chain A Lock Refund Spend Tx' |
|
if tx_type == TxTypes.XMR_SWAP_A_LOCK_REFUND_SWIPE: |
|
return 'Chain A Lock Refund Swipe Tx' |
|
if tx_type == TxTypes.XMR_SWAP_B_LOCK: |
|
return 'Chain B Lock Tx' |
|
return 'Unknown' |
|
|
|
|
|
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 describeEventEntry(event_type, event_msg): |
|
if event_type == EventLogTypes.FAILED_TX_B_LOCK_PUBLISH: |
|
return 'Failed to publish lock tx b' |
|
if event_type == EventLogTypes.FAILED_TX_B_LOCK_PUBLISH: |
|
return 'Failed to publish lock tx b' |
|
if event_type == EventLogTypes.LOCK_TX_A_PUBLISHED: |
|
return 'Lock tx a published' |
|
if event_type == EventLogTypes.LOCK_TX_B_PUBLISHED: |
|
return 'Lock tx b published' |
|
if event_type == EventLogTypes.FAILED_TX_B_SPEND: |
|
return 'Failed to publish lock tx b spend' |
|
|
|
|
|
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', 60) |
|
self.max_delay_retry = self.settings.get('max_delay_retry', 5 * 60) |
|
|
|
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, |
|
'rpchost': chain_client_settings.get('rpchost', '127.0.0.1'), |
|
'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'), |
|
'restore_height': chain_client_settings.get('restore_height', 0), |
|
} |
|
|
|
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) |
|
ci = self.ci(c) |
|
core_version = ci.getDaemonVersion() |
|
self.log.info('%s Core version %d', ci.coin_name(), core_version) |
|
self.coin_clients[c]['core_version'] = core_version |
|
|
|
if c == Coins.PART: |
|
self.coin_clients[c]['have_spent_index'] = ci.haveSpentIndex() |
|
|
|
# Sanity checks |
|
if self.callcoinrpc(c, 'getstakinginfo')['enabled'] is not False: |
|
self.log.warning('%s staking is not disabled.', ci.coin_name()) |
|
elif c == Coins.XMR: |
|
ci.ensureWalletExists() |
|
|
|
expect_address = self.getStringKV('main_wallet_addr_' + chainparams[c]['name']) |
|
if expect_address is None: |
|
self.log.warning('Can\'t find expected main wallet address for coin {}'.format(ci.coin_name())) |
|
else: |
|
if expect_address == ci.getMainWalletAddress(): |
|
ci.setWalletSeedWarning(False) |
|
else: |
|
self.log.warning('Wallet for coin {} not derived from swap seed.'.format(ci.coin_name())) |
|
else: |
|
expect_seedid = self.getStringKV('main_wallet_seedid_' + chainparams[c]['name']) |
|
if expect_seedid is None: |
|
self.log.warning('Can\'t find expected wallet seed id for coin {}'.format(ci.coin_name())) |
|
else: |
|
if expect_seedid == ci.getWalletSeedID(): |
|
ci.setWalletSeedWarning(False) |
|
else: |
|
self.log.warning('Wallet for coin {} not derived from swap seed.'.format(ci.coin_name())) |
|
|
|
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): |
|
if coin_type == Coins.PART: |
|
return |
|
ci = self.ci(coin_type) |
|
self.log.info('Initialising {} wallet.'.format(ci.coin_name())) |
|
|
|
if coin_type == Coins.XMR: |
|
key_view = self.getWalletKey(coin_type, 1, for_ed25519=True) |
|
key_spend = self.getWalletKey(coin_type, 2, for_ed25519=True) |
|
ci.initialiseWallet(key_view, key_spend) |
|
root_address = ci.getAddressFromKeys(key_view, key_spend) |
|
|
|
key_str = 'main_wallet_addr_' + chainparams[coin_type]['name'] |
|
self.setStringKV(key_str, root_address) |
|
return |
|
|
|
root_key = self.getWalletKey(coin_type, 1) |
|
root_hash = ci.getAddressHashFromKey(root_key) |
|
ci.initialiseWallet(root_key) |
|
|
|
key_str = 'main_wallet_seedid_' + chainparams[coin_type]['name'] |
|
self.setStringKV(key_str, root_hash.hex()) |
|
|
|
def setIntKV(self, str_key, int_val): |
|
self.mxDB.acquire() |
|
try: |
|
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) |
|
else: |
|
kv.value = int_val |
|
session.add(kv) |
|
session.commit() |
|
session.close() |
|
session.remove() |
|
finally: |
|
self.mxDB.release() |
|
|
|
def setStringKV(self, str_key, str_val): |
|
self.mxDB.acquire() |
|
try: |
|
session = scoped_session(self.session_factory) |
|
kv = session.query(DBKVString).filter_by(key=str_key).first() |
|
if not kv: |
|
kv = DBKVString(key=str_key, value=str_val) |
|
else: |
|
kv.value = str_val |
|
session.add(kv) |
|
session.commit() |
|
session.close() |
|
session.remove() |
|
finally: |
|
self.mxDB.release() |
|
|
|
def getStringKV(self, str_key): |
|
self.mxDB.acquire() |
|
try: |
|
session = scoped_session(self.session_factory) |
|
v = session.query(DBKVString).filter_by(key=str_key).first() |
|
if not v: |
|
return None |
|
return v.value |
|
finally: |
|
session.close() |
|
session.remove() |
|
self.mxDB.release() |
|
|
|
def activateBid(self, session, bid): |
|
if bid.bid_id in self.swaps_in_progress: |
|
self.log.debug('Bid %s is already in progress', bid.bid_id.hex()) |
|
|
|
self.log.debug('Loading active bid %s', bid.bid_id.hex()) |
|
|
|
offer = session.query(Offer).filter_by(offer_id=bid.offer_id).first() |
|
assert(offer), 'Offer not found' |
|
|
|
if offer.swap_type == SwapTypes.XMR_SWAP: |
|
xmr_swap = session.query(XmrSwap).filter_by(bid_id=bid.bid_id).first() |
|
self.loadBidTxns(bid, session) |
|
self.watchXmrSwap(bid, offer, xmr_swap) |
|
else: |
|
bid.initiate_tx = session.query(SwapTx).filter(sa.and_(SwapTx.bid_id == bid.bid_id, SwapTx.tx_type == TxTypes.ITX)).first() |
|
bid.participate_tx = session.query(SwapTx).filter(sa.and_(SwapTx.bid_id == bid.bid_id, SwapTx.tx_type == TxTypes.PTX)).first() |
|
|
|
self.swaps_in_progress[bid.bid_id] = (bid, offer) |
|
|
|
coin_from = Coins(offer.coin_from) |
|
coin_to = Coins(offer.coin_to) |
|
if bid.initiate_tx and bid.initiate_tx.txid: |
|
self.addWatchedOutput(coin_from, bid.bid_id, bid.initiate_tx.txid.hex(), bid.initiate_tx.vout, BidStates.SWAP_INITIATED) |
|
if bid.participate_tx and bid.participate_tx.txid: |
|
self.addWatchedOutput(coin_to, bid.bid_id, bid.participate_tx.txid.hex(), bid.participate_tx.vout, BidStates.SWAP_PARTICIPATING) |
|
|
|
if self.coin_clients[coin_from]['last_height_checked'] < 1: |
|
if bid.initiate_tx and bid.initiate_tx.chain_height: |
|
self.coin_clients[coin_from]['last_height_checked'] = bid.initiate_tx.chain_height |
|
if self.coin_clients[coin_to]['last_height_checked'] < 1: |
|
if bid.participate_tx and bid.participate_tx.chain_height: |
|
self.coin_clients[coin_to]['last_height_checked'] = bid.participate_tx.chain_height |
|
|
|
# TODO process addresspool if bid has previously been abandoned |
|
|
|
def deactivateBid(self, session, offer, bid): |
|
# Remove from in progress |
|
self.log.debug('Removing bid from in-progress: %s', bid.bid_id.hex()) |
|
self.swaps_in_progress.pop(bid.bid_id, None) |
|
|
|
bid.in_progress = 0 |
|
if session is None: |
|
self.saveBid(bid.bid_id, bid) |
|
|
|
# Remove any watched outputs |
|
self.removeWatchedOutput(Coins(offer.coin_from), bid.bid_id, None) |
|
self.removeWatchedOutput(Coins(offer.coin_to), bid.bid_id, None) |
|
|
|
if bid.state == BidStates.BID_ABANDONED or bid.state == BidStates.SWAP_COMPLETED: |
|
# Return unused addrs to pool |
|
itx_state = bid.getITxState() |
|
ptx_state = bid.getPTxState() |
|
if itx_state is not None and itx_state != TxStates.TX_REDEEMED: |
|
self.returnAddressToPool(bid.bid_id, TxTypes.ITX_REDEEM) |
|
if itx_state is not None and itx_state != TxStates.TX_REFUNDED: |
|
self.returnAddressToPool(bid.bid_id, TxTypes.ITX_REFUND) |
|
if ptx_state is not None and ptx_state != TxStates.TX_REDEEMED: |
|
self.returnAddressToPool(bid.bid_id, TxTypes.PTX_REDEEM) |
|
if ptx_state is not None and ptx_state != TxStates.TX_REFUNDED: |
|
self.returnAddressToPool(bid.bid_id, TxTypes.PTX_REFUND) |
|
|
|
def loadFromDB(self): |
|
self.log.info('Loading data from db') |
|
self.mxDB.acquire() |
|
try: |
|
session = scoped_session(self.session_factory) |
|
for bid in session.query(Bid): |
|
if bid.in_progress == 1 or (bid.state and bid.state > BidStates.BID_RECEIVED and bid.state < BidStates.SWAP_COMPLETED): |
|
self.activateBid(session, bid) |
|
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 validateSwapType(self, coin_from, coin_to, swap_type): |
|
if coin_from == Coins.XMR: |
|
raise ValueError('TODO: xmr coin_from') |
|
if coin_to == Coins.XMR and swap_type != SwapTypes.XMR_SWAP: |
|
raise ValueError('Invalid swap type for XMR') |
|
|
|
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.validateSwapType(coin_from_t, coin_to_t, swap_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: max fee warning? |
|
chain_client_settings = self.getChainClientSettings(coin_from) |
|
lock_tx_fee_premium = chain_client_settings.get('lock_tx_fee_premium', 0.0) |
|
|
|
xmr_offer.a_fee_rate = make_int(self.getFeeRateForCoin(coin_from) + lock_tx_fee_premium, self.ci(coin_from).exp()) |
|
|
|
# Unused: TODO - Set priority? |
|
# xmr_offer.b_fee_rate = make_int(0.0012595, self.ci(coin_to).exp()) |
|
xmr_offer.b_fee_rate = make_int(0.00002, self.ci(coin_to).exp()) # abs fee |
|
|
|
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, |
|
active_ind=1, |
|
|
|
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,E711 |
|
if not record: |
|
address = self.getReceiveAddressForCoin(coin_type) |
|
record = PooledAddress( |
|
addr=address, |
|
coin_type=int(coin_type)) |
|
record.bid_id = bid_id |
|
record.tx_type = tx_type |
|
addr = record.addr |
|
session.add(record) |
|
session.commit() |
|
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): |
|
override_feerate = self.coin_clients[coin_type].get('override_feerate', None) |
|
if override_feerate: |
|
self.log.debug('Fee rate override used for %s: %f', str(coin_type), override_feerate) |
|
return override_feerate |
|
|
|
return self.ci(coin_type).get_fee_rate() |
|
|
|
def estimateWithdrawFee(self, coin_type, fee_rate): |
|
if coin_type == Coins.XMR: |
|
self.log.error('TODO: estimateWithdrawFee XMR') |
|
return None |
|
tx_vsize = self.getContractSpendTxVSize(coin_type) |
|
est_fee = (fee_rate * tx_vsize) / 1000 |
|
return est_fee |
|
|
|
def withdrawCoin(self, coin_type, value, addr_to, subfee): |
|
self.log.info('withdrawCoin %s %s to %s %s', value, self.getTicker(coin_type), addr_to, ' subfee' if subfee else '') |
|
|
|
ci = self.ci(coin_type) |
|
return ci.withdrawCoin(value, addr_to, subfee) |
|
|
|
def cacheNewAddressForCoin(self, coin_type): |
|
self.log.debug('cacheNewAddressForCoin %s', coin_type) |
|
key_str = 'receive_addr_' + chainparams[coin_type]['name'] |
|
addr = self.getReceiveAddressForCoin(coin_type) |
|
self.setStringKV(key_str, addr) |
|
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)) |
|
|
|
offer = self.getOffer(offer_id) |
|
assert(offer), 'Offer not found: {}.'.format(offer_id.hex()) |
|
assert(offer.expire_at > int(time.time())), 'Offer has expired' |
|
|
|
if offer.swap_type == SwapTypes.XMR_SWAP: |
|
return self.postXmrBid(offer_id, amount, addr_send_from) |
|
|
|
self.mxDB.acquire() |
|
try: |
|
msg_buf = BidMessage() |
|
msg_buf.offer_msg_id = offer_id |
|
msg_buf.time_valid = 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 getXmrBidFromSession(self, session, bid_id, sent=False): |
|
bid = session.query(Bid).filter_by(bid_id=bid_id).first() |
|
xmr_swap = None |
|
if bid: |
|
xmr_swap = session.query(XmrSwap).filter_by(bid_id=bid_id).first() |
|
self.loadBidTxns(bid, session) |
|
return bid, xmr_swap |
|
|
|
def getXmrBid(self, bid_id, sent=False): |
|
self.mxDB.acquire() |
|
try: |
|
session = scoped_session(self.session_factory) |
|
return self.getXmrBidFromSession(session, bid_id, sent) |
|
finally: |
|
session.close() |
|
session.remove() |
|
self.mxDB.release() |
|
|
|
def getXmrOfferFromSession(self, session, offer_id, sent=False): |
|
offer = session.query(Offer).filter_by(offer_id=offer_id).first() |
|
xmr_offer = None |
|
if offer: |
|
xmr_offer = session.query(XmrOffer).filter_by(offer_id=offer_id).first() |
|
return offer, xmr_offer |
|
|
|
def getXmrOffer(self, offer_id, sent=False): |
|
self.mxDB.acquire() |
|
try: |
|
session = scoped_session(self.session_factory) |
|
return self.getXmrOfferFromSession(session, offer_id, sent) |
|
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() |
|
offer = None |
|
if bid: |
|
offer = session.query(Offer).filter_by(offer_id=bid.offer_id).first() |
|
if offer and offer.swap_type == SwapTypes.XMR_SWAP: |
|
self.loadBidTxns(bid, session) |
|
else: |
|
bid.initiate_tx = session.query(SwapTx).filter(sa.and_(SwapTx.bid_id == bid_id, SwapTx.tx_type == TxTypes.ITX)).first() |
|
bid.participate_tx = session.query(SwapTx).filter(sa.and_(SwapTx.bid_id == bid_id, SwapTx.tx_type == TxTypes.PTX)).first() |
|
return bid, offer |
|
finally: |
|
session.close() |
|
session.remove() |
|
self.mxDB.release() |
|
|
|
def list_bid_events(self, bid_id): |
|
self.mxDB.acquire() |
|
events = [] |
|
try: |
|
session = scoped_session(self.session_factory) |
|
query_str = 'SELECT created_at, event_type, event_msg FROM eventlog ' + \ |
|
'WHERE linked_type = {} AND linked_id = x\'{}\' '.format(TableTypes.BID, bid_id.hex()) |
|
q = self.engine.execute(query_str) |
|
|
|
for row in q: |
|
events.append({'at': row[0], 'desc': describeEventEntry(row[1], row[2])}) |
|
return events |
|
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' |
|
|
|
if offer.swap_type == SwapTypes.XMR_SWAP: |
|
return self.acceptXmrBid(bid_id) |
|
|
|
# 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.start_chain_a_height = ci_from.getChainHeight() |
|
xmr_swap.b_restore_height = ci_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) |
|
self.deactivateBid(session, offer, bid) |
|
session.add(bid) |
|
session.commit() |
|
finally: |
|
session.close() |
|
session.remove() |
|
self.mxDB.release() |
|
|
|
def setBidError(self, bid_id, bid, error_str, save_bid=True): |
|
self.log.error('Bid %s - Error: %s', bid_id.hex(), error_str) |
|
bid.setState(BidStates.BID_ERROR) |
|
bid.state_note = 'error msg: ' + error_str |
|
if save_bid: |
|
self.saveBid(bid_id, bid) |
|
|
|
def createInitiateTxn(self, coin_type, bid_id, bid, initiate_script): |
|
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 |
|
else: # not XMR_SWAP_A_LOCK_REFUND in bid.txns |
|
if len(xmr_swap.al_lock_refund_tx_sig) > 0 and len(xmr_swap.af_lock_refund_tx_sig) > 0: |
|
try: |
|
txid = ci_from.publishTx(xmr_swap.a_lock_refund_tx) |
|
|
|
self.log.info('Submitted coin a lock refund tx for bid {}'.format(bid_id.hex())) |
|
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 |
|
|
|
if bid.was_received: |
|
try: |
|
txn_hex = ci_from.getMempoolTx(xmr_swap.a_lock_spend_tx_id) |
|
self.log.info('Found lock spend txn in %s mempool, %s', ci_from.coin_name(), xmr_swap.a_lock_spend_tx_id.hex()) |
|
self.process_XMR_SWAP_A_LOCK_tx_spend(bid_id, xmr_swap.a_lock_spend_tx_id.hex(), txn_hex) |
|
except Exception as e: |
|
self.log.debug('getrawtransaction lock spend tx failed: %s', str(e)) |
|
elif state == BidStates.XMR_SWAP_NOSCRIPT_TX_REDEEMED: |
|
txid_hex = bid.xmr_b_lock_tx.spend_txid.hex() |
|
|
|
found_tx = ci_to.findTxnByHash(txid_hex) |
|
if found_tx is not None: |
|
self.log.info('Found coin b lock spend tx bid %s', bid_id.hex()) |
|
rv = True # Remove from swaps_in_progress |
|
bid.setState(BidStates.SWAP_COMPLETED) |
|
self.saveBidInSession(bid_id, bid, session, xmr_swap) |
|
session.commit() |
|
|
|
except Exception as ex: |
|
raise ex |
|
finally: |
|
if session: |
|
session.close() |
|
session.remove() |
|
self.mxDB.release() |
|
|
|
return rv |
|
|
|
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) |
|
|
|
watched = self.coin_clients[coin_type]['watched_outputs'] |
|
|
|
for wo in watched: |
|
if wo.bid_id == bid_id and wo.txid_hex == txid_hex and wo.vout == vout: |
|
self.log.debug('Output already being watched.') |
|
return |
|
|
|
watched.append(WatchedOutput(bid_id, txid_hex, vout, tx_type, swap_type)) |
|
|
|
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_hex): |
|
self.log.debug('Detected spend of XMR swap coin a lock tx for bid %s', bid_id.hex()) |
|
self.mxDB.acquire() |
|
try: |
|
session = scoped_session(self.session_factory) |
|
bid, xmr_swap = self.getXmrBidFromSession(session, bid_id) |
|
assert(bid), 'Bid not found: {}.'.format(bid_id.hex()) |
|
assert(xmr_swap), 'XMR swap not found: {}.'.format(bid_id.hex()) |
|
|
|
offer, xmr_offer = self.getXmrOfferFromSession(session, bid.offer_id, sent=False) |
|
assert(offer), 'Offer not found: {}.'.format(bid.offer_id.hex()) |
|
assert(xmr_offer), 'XMR offer not found: {}.'.format(bid.offer_id.hex()) |
|
coin_from = Coins(offer.coin_from) |
|
coin_to = Coins(offer.coin_to) |
|
|
|
state = BidStates(bid.state) |
|
spending_txid = bytes.fromhex(spend_txid_hex) |
|
|
|
if spending_txid == xmr_swap.a_lock_spend_tx_id: |
|
if state == BidStates.XMR_SWAP_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: |
|
bid.setState(BidStates.SWAP_DELAYING) |
|
delay = random.randrange(self.min_delay_event, self.max_delay_event) |
|
self.log.info('Redeeming coin b lock tx for bid %s in %d seconds', bid_id.hex(), delay) |
|
self.createEventInSession(delay, EventTypes.REDEEM_XMR_SWAP_LOCK_TX_B, bid_id, session) |
|
else: |
|
# Could already be processed if spend was detected in the mempool |
|
self.log.warning('Coin a lock tx spend ignored due to bid state for bid {}'.format(bid_id.hex())) |
|
|
|
elif spending_txid == xmr_swap.a_lock_refund_tx_id: |
|
self.log.debug('Coin a lock tx spent by lock refund tx.') |
|
pass |
|
else: |
|
self.setBidError(bid.bid_id, bid, 'Unexpected txn spent coin a lock tx: {}'.format(spend_txid_hex), save_bid=False) |
|
|
|
self.saveBidInSession(bid_id, bid, session, xmr_swap, save_in_progress=offer) |
|
session.commit() |
|
except Exception as ex: |
|
self.log.error('process_XMR_SWAP_A_LOCK_tx_spend %s', str(ex)) |
|
if self.debug: |
|
traceback.print_exc() |
|
finally: |
|
session.close() |
|
session.remove() |
|
self.mxDB.release() |
|
|
|
def process_XMR_SWAP_A_LOCK_REFUND_tx_spend(self, bid_id, spend_txid_hex, spend_txn): |
|
self.log.debug('Detected spend of XMR swap coin a lock refund tx for bid %s', bid_id.hex()) |
|
self.mxDB.acquire() |
|
try: |
|
session = scoped_session(self.session_factory) |
|
bid, xmr_swap = self.getXmrBidFromSession(session, bid_id) |
|
assert(bid), 'Bid not found: {}.'.format(bid_id.hex()) |
|
assert(xmr_swap), 'XMR swap not found: {}.'.format(bid_id.hex()) |
|
|
|
offer, xmr_offer = self.getXmrOfferFromSession(session, bid.offer_id, sent=False) |
|
assert(offer), 'Offer not found: {}.'.format(bid.offer_id.hex()) |
|
assert(xmr_offer), 'XMR offer not found: {}.'.format(bid.offer_id.hex()) |
|
coin_from = Coins(offer.coin_from) |
|
coin_to = Coins(offer.coin_to) |
|
|
|
state = BidStates(bid.state) |
|
spending_txid = bytes.fromhex(spend_txid_hex) |
|
|
|
if spending_txid == xmr_swap.a_lock_refund_spend_tx_id: |
|
self.log.info('Found coin a lock refund spend tx, bid {}'.format(bid_id.hex())) |
|
|
|
if bid.was_sent: |
|
xmr_swap.a_lock_refund_spend_tx = bytes.fromhex(spend_txn['hex']) # Replace with fully signed tx |
|
if TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND not in bid.txns: |
|
bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND] = SwapTx( |
|
bid_id=bid_id, |
|
tx_type=TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND, |
|
txid=xmr_swap.a_lock_refund_spend_tx_id, |
|
) |
|
if bid.xmr_b_lock_tx is not None: |
|
delay = random.randrange(self.min_delay_event, self.max_delay_event) |
|
self.log.info('Recovering xmr swap chain B lock tx for bid %s in %d seconds', bid_id.hex(), delay) |
|
self.createEventInSession(delay, EventTypes.RECOVER_XMR_SWAP_LOCK_TX_B, bid_id, session) |
|
else: |
|
bid.setState(BidStates.XMR_SWAP_FAILED_REFUNDED) |
|
|
|
if bid.was_received: |
|
if not bid.was_sent: |
|
bid.setState(BidStates.XMR_SWAP_FAILED_REFUNDED) |
|
|
|
else: |
|
self.log.info('Coin a lock refund spent by unknown tx, bid {}'.format(bid_id.hex())) |
|
bid.setState(BidStates.XMR_SWAP_FAILED_SWIPED) |
|
|
|
self.saveBidInSession(bid_id, bid, session, xmr_swap, save_in_progress=offer) |
|
session.commit() |
|
except Exception as ex: |
|
self.log.error('process_XMR_SWAP_A_LOCK_REFUND_tx_spend %s', str(ex)) |
|
if self.debug: |
|
traceback.print_exc() |
|
finally: |
|
session.close() |
|
session.remove() |
|
self.mxDB.release() |
|
|
|
def processSpentOutput(self, coin_type, watched_output, spend_txid_hex, spend_n, spend_txn): |
|
if watched_output.swap_type == SwapTypes.XMR_SWAP: |
|
if watched_output.tx_type == TxTypes.XMR_SWAP_A_LOCK: |
|
self.process_XMR_SWAP_A_LOCK_tx_spend(watched_output.bid_id, spend_txid_hex, spend_txn['hex']) |
|
elif watched_output.tx_type == TxTypes.XMR_SWAP_A_LOCK_REFUND: |
|
self.process_XMR_SWAP_A_LOCK_REFUND_tx_spend(watched_output.bid_id, spend_txid_hex, spend_txn) |
|
|
|
self.removeWatchedOutput(coin_type, watched_output.bid_id, watched_output.txid_hex) |
|
return |
|
|
|
if watched_output.tx_type == BidStates.SWAP_PARTICIPATING: |
|
self.participateTxnSpent(watched_output.bid_id, spend_txid_hex, spend_n, spend_txn) |
|
else: |
|
self.initiateTxnSpent(watched_output.bid_id, spend_txid_hex, spend_n, spend_txn) |
|
|
|
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]) |
|
try: |
|
block = self.callcoinrpc(coin_type, 'getblock', [block_hash, 2]) |
|
except Exception as e: |
|
if 'Block not available (pruned data)' in str(e): |
|
# TODO: Better solution? |
|
bci = self.callcoinrpc(coin_type, 'getblockchaininfo') |
|
self.log.error('Coin %s last_height_checked %d set to pruneheight %d', self.ci(coin_type).coin_name(), last_height_checked, bci['pruneheight']) |
|
last_height_checked = bci['pruneheight'] |
|
continue |
|
|
|
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.ACCEPT_XMR_BID: |
|
self.acceptXmrBid(row.linked_id) |
|
elif row.event_type == EventTypes.SIGN_XMR_SWAP_LOCK_TX_A: |
|
self.sendXmrBidTxnSigsFtoL(row.linked_id, session) |
|
elif row.event_type == EventTypes.SEND_XMR_SWAP_LOCK_TX_A: |
|
self.sendXmrBidCoinALockTx(row.linked_id, session) |
|
elif row.event_type == EventTypes.SEND_XMR_SWAP_LOCK_TX_B: |
|
self.sendXmrBidCoinBLockTx(row.linked_id, session) |
|
elif row.event_type == EventTypes.SEND_XMR_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 |
|
session = None |
|
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() |
|
finally: |
|
if session: |
|
session.close() |
|
session.remove() |
|
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.validateSwapType(coin_from, coin_to, offer_data.swap_type) |
|
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) |
|
|
|
if len(msg_data.offer_msg_id) != 28: |
|
raise ValueError('Invalid msg_id length') |
|
|
|
# TODO: Add to db in case received out of order, or add extra startup logic |
|
|
|
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), '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.getXmrOfferFromSession(session, bid.offer_id, sent=True) |
|
assert(offer and offer.was_sent), 'Offer not found: {}.'.format(bid.offer_id.hex()) |
|
assert(xmr_offer), 'XMR offer not found: {}.'.format(bid.offer_id.hex()) |
|
xmr_swap = session.query(XmrSwap).filter_by(bid_id=bid.bid_id).first() |
|
assert(xmr_swap), 'XMR swap not found: {}.'.format(bid.bid_id.hex()) |
|
|
|
ci_from = self.ci(Coins(offer.coin_from)) |
|
ci_to = self.ci(Coins(offer.coin_to)) |
|
|
|
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.') |
|
|
|
self.log.info('Received valid bid %s for xmr offer %s', bid.bid_id.hex(), bid.offer_id.hex()) |
|
|
|
bid.setState(BidStates.BID_RECEIVED) |
|
self.saveBidInSession(bid.bid_id, bid, session, xmr_swap) |
|
|
|
# Auto accept bid if set and no other non-abandoned bid for this order exists |
|
if offer.auto_accept_bids: |
|
if self.countAcceptedBids(bid.offer_id) > 0: |
|
self.log.info('Not auto accepting bid %s, already have', bid.bid_id.hex()) |
|
else: |
|
delay = random.randrange(self.min_delay_event, self.max_delay_event) |
|
self.log.info('Auto accepting xmr bid %s in %d seconds', bid.bid_id.hex(), delay) |
|
self.createEventInSession(delay, EventTypes.ACCEPT_XMR_BID, bid.bid_id, session) |
|
|
|
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()) |
|
xmr_swap = session.query(XmrSwap).filter_by(bid_id=bid.bid_id).first() |
|
assert(xmr_swap), 'XMR swap not found: {}.'.format(bid.bid_id.hex()) |
|
ci_from = self.ci(Coins(offer.coin_from)) |
|
ci_to = self.ci(Coins(offer.coin_to)) |
|
|
|
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()) |
|
ci_from = self.ci(Coins(offer.coin_from)) |
|
ci_to = self.ci(Coins(offer.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=ci_to.getChainHeight(), |
|
start_chain_a_height=ci_from.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()) |
|
ci_from = self.ci(Coins(offer.coin_from)) |
|
ci_to = self.ci(Coins(offer.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) |
|
|
|
coin_from = Coins(offer.coin_from) |
|
self.setLastHeightChecked(coin_from, xmr_swap.start_chain_a_height) |
|
self.addWatchedOutput(coin_from, bid.bid_id, bid.xmr_a_lock_tx.txid.hex(), bid.xmr_a_lock_tx.vout, TxTypes.XMR_SWAP_A_LOCK, SwapTypes.XMR_SWAP) |
|
self.addWatchedOutput(coin_from, bid.bid_id, xmr_swap.a_lock_refund_tx_id.hex(), 0, TxTypes.XMR_SWAP_A_LOCK_REFUND, SwapTypes.XMR_SWAP) |
|
bid.in_progress = 1 |
|
|
|
def sendXmrBidTxnSigsFtoL(self, bid_id, session): |
|
# F -> L: Sending MSG3L |
|
self.log.debug('Signing xmr bid lock txns %s', bid_id.hex()) |
|
|
|
bid, xmr_swap = self.getXmrBidFromSession(session, bid_id) |
|
assert(bid), 'Bid not found: {}.'.format(bid_id.hex()) |
|
assert(xmr_swap), 'XMR swap not found: {}.'.format(bid_id.hex()) |
|
|
|
offer, xmr_offer = self.getXmrOfferFromSession(session, bid.offer_id, sent=False) |
|
assert(offer), 'Offer not found: {}.'.format(bid.offer_id.hex()) |
|
assert(xmr_offer), 'XMR offer not found: {}.'.format(bid.offer_id.hex()) |
|
coin_from = Coins(offer.coin_from) |
|
coin_to = Coins(offer.coin_to) |
|
ci_from = self.ci(coin_from) |
|
ci_to = self.ci(coin_to) |
|
|
|
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.watchXmrSwap(bid, offer, xmr_swap) |
|
self.saveBidInSession(bid_id, bid, session, xmr_swap) |
|
except Exception as ex: |
|
if self.debug: |
|
traceback.print_exc() |
|
|
|
def sendXmrBidCoinALockTx(self, bid_id, session): |
|
# Send coin A lock tx and MSG4F L -> F |
|
self.log.debug('Sending coin A lock tx for xmr bid %s', bid_id.hex()) |
|
|
|
bid, xmr_swap = self.getXmrBidFromSession(session, bid_id) |
|
assert(bid), 'Bid not found: {}.'.format(bid_id.hex()) |
|
assert(xmr_swap), 'XMR swap not found: {}.'.format(bid_id.hex()) |
|
|
|
offer, xmr_offer = self.getXmrOfferFromSession(session, bid.offer_id, sent=False) |
|
assert(offer), 'Offer not found: {}.'.format(bid.offer_id.hex()) |
|
assert(xmr_offer), 'XMR offer not found: {}.'.format(bid.offer_id.hex()) |
|
coin_from = Coins(offer.coin_from) |
|
coin_to = Coins(offer.coin_to) |
|
ci_from = self.ci(coin_from) |
|
ci_to = self.ci(coin_to) |
|
|
|
kal = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, 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.watchXmrSwap(bid, offer, xmr_swap) |
|
self.logBidEvent(bid, EventLogTypes.LOCK_TX_A_PUBLISHED, '', session) |
|
|
|
self.saveBidInSession(bid_id, bid, session, xmr_swap) |
|
|
|
def sendXmrBidCoinBLockTx(self, bid_id, session): |
|
# Follower sending coin B lock tx |
|
self.log.debug('Sending coin B lock tx for xmr bid %s', bid_id.hex()) |
|
|
|
bid, xmr_swap = self.getXmrBidFromSession(session, bid_id) |
|
assert(bid), 'Bid not found: {}.'.format(bid_id.hex()) |
|
assert(xmr_swap), 'XMR swap not found: {}.'.format(bid_id.hex()) |
|
|
|
offer, xmr_offer = self.getXmrOfferFromSession(session, bid.offer_id, sent=False) |
|
assert(offer), 'Offer not found: {}.'.format(bid.offer_id.hex()) |
|
assert(xmr_offer), 'XMR offer not found: {}.'.format(bid.offer_id.hex()) |
|
coin_from = Coins(offer.coin_from) |
|
coin_to = Coins(offer.coin_to) |
|
ci_from = self.ci(coin_from) |
|
ci_to = self.ci(coin_to) |
|
|
|
if bid.debug_ind == DebugTypes.BID_STOP_AFTER_COIN_A_LOCK: |
|
self.log.debug('XMR bid %s: 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_LOCK_PUBLISH, session) |
|
if num_retries > 0: |
|
error_msg += ', retry no. {}'.format(num_retries) |
|
self.log.error(error_msg) |
|
|
|
str_error = str(ex) |
|
if num_retries < 5 and 'not enough unlocked money' in str_error or 'transaction was rejected by daemon' in str_error: |
|
delay = random.randrange(self.min_delay_retry, self.max_delay_retry) |
|
self.log.info('Retrying sending xmr swap chain B lock tx for bid %s in %d seconds', bid_id.hex(), delay) |
|
self.createEventInSession(delay, EventTypes.SEND_XMR_SWAP_LOCK_TX_B, bid_id, session) |
|
else: |
|
self.setBidError(bid_id, bid, 'publishBLockTx failed: ' + str(ex), save_bid=False) |
|
self.saveBidInSession(bid_id, bid, session, xmr_swap, save_in_progress=offer) |
|
|
|
self.logBidEvent(bid, EventLogTypes.FAILED_TX_B_LOCK_PUBLISH, str_error, session) |
|
return |
|
|
|
self.log.debug('Submitted lock txn %s to %s chain for bid %s', b_lock_tx_id.hex(), chainparams[coin_to]['name'], bid_id.hex()) |
|
bid.xmr_b_lock_tx = SwapTx( |
|
bid_id=bid_id, |
|
tx_type=TxTypes.XMR_SWAP_B_LOCK, |
|
txid=b_lock_tx_id, |
|
) |
|
bid.xmr_b_lock_tx.setState(TxStates.TX_NONE) |
|
self.logBidEvent(bid, EventLogTypes.LOCK_TX_B_PUBLISHED, '', session) |
|
|
|
self.saveBidInSession(bid_id, bid, session, xmr_swap, save_in_progress=offer) |
|
|
|
def 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.getXmrBidFromSession(session, bid_id) |
|
assert(bid), 'Bid not found: {}.'.format(bid_id.hex()) |
|
assert(xmr_swap), 'XMR swap not found: {}.'.format(bid_id.hex()) |
|
|
|
offer, xmr_offer = self.getXmrOfferFromSession(session, bid.offer_id, sent=False) |
|
assert(offer), 'Offer not found: {}.'.format(bid.offer_id.hex()) |
|
assert(xmr_offer), 'XMR offer not found: {}.'.format(bid.offer_id.hex()) |
|
coin_from = Coins(offer.coin_from) |
|
coin_to = Coins(offer.coin_to) |
|
|
|
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.getXmrBidFromSession(session, bid_id) |
|
assert(bid), 'Bid not found: {}.'.format(bid_id.hex()) |
|
assert(xmr_swap), 'XMR swap not found: {}.'.format(bid_id.hex()) |
|
|
|
offer, xmr_offer = self.getXmrOfferFromSession(session, bid.offer_id, sent=False) |
|
assert(offer), 'Offer not found: {}.'.format(bid.offer_id.hex()) |
|
assert(xmr_offer), 'XMR offer not found: {}.'.format(bid.offer_id.hex()) |
|
coin_from = Coins(offer.coin_from) |
|
coin_to = Coins(offer.coin_to) |
|
ci_from = self.ci(coin_from) |
|
ci_to = self.ci(coin_to) |
|
|
|
kbsf = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, 2, for_ed25519=True) |
|
kaf = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, 3) |
|
|
|
al_lock_spend_sig = ci_from.decryptOtVES(kbsf, xmr_swap.al_lock_spend_tx_esig) |
|
v = ci_from.verifyTxSig(xmr_swap.a_lock_spend_tx, al_lock_spend_sig, xmr_swap.pkal, 0, xmr_swap.a_lock_tx_script, bid.amount) |
|
assert(v) |
|
|
|
af_lock_spend_sig = ci_from.signTx(kaf, xmr_swap.a_lock_spend_tx, 0, xmr_swap.a_lock_tx_script, bid.amount) |
|
v = ci_from.verifyTxSig(xmr_swap.a_lock_spend_tx, af_lock_spend_sig, xmr_swap.pkaf, 0, xmr_swap.a_lock_tx_script, bid.amount) |
|
assert(v) |
|
|
|
witness_stack = [ |
|
b'', |
|
al_lock_spend_sig, |
|
af_lock_spend_sig, |
|
xmr_swap.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.getXmrBidFromSession(session, bid_id) |
|
assert(bid), 'Bid not found: {}.'.format(bid_id.hex()) |
|
assert(xmr_swap), 'XMR swap not found: {}.'.format(bid_id.hex()) |
|
|
|
offer, xmr_offer = self.getXmrOfferFromSession(session, bid.offer_id, sent=False) |
|
assert(offer), 'Offer not found: {}.'.format(bid.offer_id.hex()) |
|
assert(xmr_offer), 'XMR offer not found: {}.'.format(bid.offer_id.hex()) |
|
coin_from = Coins(offer.coin_from) |
|
coin_to = Coins(offer.coin_to) |
|
ci_from = self.ci(coin_from) |
|
ci_to = self.ci(coin_to) |
|
|
|
# Extract the leader's decrypted signature and use it to recover the follower's privatekey |
|
xmr_swap.al_lock_spend_tx_sig = ci_from.extractLeaderSig(xmr_swap.a_lock_spend_tx) |
|
|
|
kbsf = ci_from.recoverEncKey(xmr_swap.al_lock_spend_tx_esig, xmr_swap.al_lock_spend_tx_sig, xmr_swap.pkasf) |
|
assert(kbsf is not None) |
|
|
|
kbsl = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, 3, for_ed25519=True) |
|
vkbs = ci_to.sumKeys(kbsl, kbsf) |
|
|
|
address_to = ci_to.getMainWalletAddress() |
|
|
|
try: |
|
txid = ci_to.spendBLockTx(address_to, xmr_swap.vkbv, vkbs, bid.amount_to, xmr_offer.b_fee_rate, xmr_swap.b_restore_height) |
|
self.log.debug('Submitted lock B spend txn %s to %s chain for bid %s', txid.hex(), ci_to.coin_name(), bid_id.hex()) |
|
except Exception as ex: |
|
# TODO: Make min-conf 10? |
|
error_msg = 'spendBLockTx failed for bid {} with error {}'.format(bid_id.hex(), str(ex)) |
|
num_retries = self.countBidEvents(bid, EventLogTypes.FAILED_TX_B_SPEND, session) |
|
if num_retries > 0: |
|
error_msg += ', retry no. {}'.format(num_retries) |
|
self.log.error(error_msg) |
|
|
|
str_error = str(ex) |
|
if num_retries < 100 and 'Invalid unlocked_balance' in str_error: |
|
delay = random.randrange(self.min_delay_retry, self.max_delay_retry) |
|
self.log.info('Retrying sending xmr swap chain B spend tx for bid %s in %d seconds', bid_id.hex(), delay) |
|
self.createEventInSession(delay, EventTypes.REDEEM_XMR_SWAP_LOCK_TX_B, bid_id, session) |
|
else: |
|
self.setBidError(bid_id, bid, 'spendBLockTx failed: ' + str(ex), save_bid=False) |
|
self.saveBidInSession(bid_id, bid, session, xmr_swap, save_in_progress=offer) |
|
|
|
self.logBidEvent(bid, EventLogTypes.FAILED_TX_B_SPEND, str_error, session) |
|
return |
|
|
|
bid.xmr_b_lock_tx.spend_txid = txid |
|
bid.setState(BidStates.XMR_SWAP_NOSCRIPT_TX_REDEEMED) |
|
# TODO: Why does using bid.txns error here? |
|
self.saveBidInSession(bid_id, bid, session, xmr_swap, save_in_progress=offer) |
|
|
|
def recoverXmrBidCoinBLockTx(self, bid_id, session): |
|
# Follower recovering B lock tx |
|
self.log.debug('Recovering coin B lock tx for xmr bid %s', bid_id.hex()) |
|
|
|
bid, xmr_swap = self.getXmrBidFromSession(session, bid_id) |
|
assert(bid), 'Bid not found: {}.'.format(bid_id.hex()) |
|
assert(xmr_swap), 'XMR swap not found: {}.'.format(bid_id.hex()) |
|
|
|
offer, xmr_offer = self.getXmrOfferFromSession(session, bid.offer_id, sent=False) |
|
assert(offer), 'Offer not found: {}.'.format(bid.offer_id.hex()) |
|
assert(xmr_offer), 'XMR offer not found: {}.'.format(bid.offer_id.hex()) |
|
coin_from = Coins(offer.coin_from) |
|
coin_to = Coins(offer.coin_to) |
|
ci_from = self.ci(coin_from) |
|
ci_to = self.ci(coin_to) |
|
|
|
# Extract the follower's decrypted signature and use it to recover the leader's privatekey |
|
af_lock_refund_spend_tx_sig = ci_from.extractFollowerSig(xmr_swap.a_lock_refund_spend_tx) |
|
|
|
kbsl = ci_from.recoverEncKey(xmr_swap.af_lock_refund_spend_tx_esig, af_lock_refund_spend_tx_sig, xmr_swap.pkasl) |
|
assert(kbsl is not None) |
|
|
|
kbsf = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, 2, for_ed25519=True) |
|
vkbs = ci_to.sumKeys(kbsl, kbsf) |
|
|
|
address_to = ci_to.getMainWalletAddress() |
|
|
|
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()) |
|
ci_from = self.ci(Coins(offer.coin_from)) |
|
ci_to = self.ci(Coins(offer.coin_to)) |
|
|
|
try: |
|
xmr_swap.a_lock_spend_tx = msg_data.a_lock_spend_tx |
|
xmr_swap.a_lock_spend_tx_id = ci_from.getTxHash(xmr_swap.a_lock_spend_tx) |
|
xmr_swap.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} |
|
num_tries = 5 |
|
for i in range(num_tries + 1): |
|
try: |
|
msg = self.callrpc('smsg', [msg_id.hex(), options]) |
|
break |
|
except Exception as e: |
|
if 'Unknown message id' in str(e) and i < num_tries: |
|
time.sleep(1) |
|
else: |
|
raise e |
|
|
|
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(None, 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: |
|
activate_bid = False |
|
if offer.swap_type == SwapTypes.SELLER_FIRST: |
|
if bid.state and bid.state > BidStates.BID_RECEIVED and bid.state < BidStates.SWAP_COMPLETED: |
|
activate_bid = True |
|
else: |
|
self.log.debug('TODO - determine in-progress for manualBidUpdate') |
|
if offer.swap_type == SwapTypes.XMR_SWAP: |
|
if bid.state and bid.state in (BidStates.XMR_SWAP_SECRET_SHARED, BidStates.XMR_SWAP_NOSCRIPT_TX_REDEEMED): |
|
activate_bid = True |
|
|
|
if activate_bid: |
|
self.activateBid(session, bid) |
|
else: |
|
self.deactivateBid(session, offer, bid) |
|
|
|
self.saveBidInSession(bid_id, bid, session) |
|
session.commit() |
|
finally: |
|
session.close() |
|
session.remove() |
|
else: |
|
raise ValueError('No changes') |
|
finally: |
|
self.mxDB.release() |
|
|
|
def editSettings(self, coin_name, data): |
|
self.log.info('Updating settings %s', coin_name) |
|
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): |
|
|
|
ci = self.ci(coin) |
|
blockchaininfo = ci.getBlockchainInfo() |
|
walletinfo = ci.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)), |
|
'expected_seed': ci.knownWalletSeed(), |
|
} |
|
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)
|
|
|