From 8e5a499a3afa51ea7c2c29ba417d1277d6bb4d5c Mon Sep 17 00:00:00 2001 From: barrystyle Date: Sat, 20 Feb 2021 05:04:51 +0800 Subject: [PATCH] port extended functionality and call structure (bet, orders and cancels) --- aspirelib/lib/api.py | 40 +- aspirelib/lib/blocks.py | 20 +- aspirelib/lib/log.py | 53 ++- aspirelib/lib/messages/bet.py | 595 +++++++++++++++++++++++ aspirelib/lib/messages/cancel.py | 131 ++++++ aspirelib/lib/messages/order.py | 786 +++++++++++++++++++++++++++++++ aspirelib/lib/util.py | 24 + 7 files changed, 1636 insertions(+), 13 deletions(-) create mode 100644 aspirelib/lib/messages/bet.py create mode 100644 aspirelib/lib/messages/cancel.py create mode 100644 aspirelib/lib/messages/order.py diff --git a/aspirelib/lib/api.py b/aspirelib/lib/api.py index ba16befb..cba47449 100644 --- a/aspirelib/lib/api.py +++ b/aspirelib/lib/api.py @@ -37,7 +37,10 @@ from aspirelib.lib import message_type from aspirelib.lib.messages import send from aspirelib.lib.messages import issuance +from aspirelib.lib.messages import order from aspirelib.lib.messages import broadcast +from aspirelib.lib.messages import bet +from aspirelib.lib.messages import cancel from aspirelib.lib.messages import dividend from aspirelib.lib.messages import proofofwork from aspirelib.lib.messages import publish @@ -45,13 +48,14 @@ D = decimal.Decimal -API_TABLES = ['assets', 'balances', 'credits', 'debits', - 'broadcasts', 'proofofwork', +API_TABLES = ['assets', 'balances', 'credits', 'debits', 'bets', 'bet_matches', + 'broadcasts', 'proofofwork', 'cancels', 'bet_expirations', 'order_expirations', 'bet_match_expirations', + 'order_match_expirations', 'bet_match_resolutions', 'dividends', 'issuances', 'sends', 'mempool'] -API_TRANSACTIONS = ['broadcast', 'proofofwork', - 'dividend', 'issuance', 'send', +API_TRANSACTIONS = [ 'bet', 'broadcast', 'proofofwork', 'cancel', + 'dividend', 'issuance', 'send', 'order' 'publish', 'execute'] COMMONS_ARGS = ['encoding', 'fee_per_kb', 'regular_dust_size', @@ -208,13 +212,20 @@ def value_to_marker(value): bindings.append(filter_['value']) # AND filters more_conditions = [] - if table not in ['balances']: - if start_block is not None: + if table not in ['balances', 'order_matches', 'bet_matches']: + if start_block != None: more_conditions.append('''block_index >= ?''') bindings.append(start_block) if end_block is not None: more_conditions.append('''block_index <= ?''') bindings.append(end_block) + elif table in ['order_matches', 'bet_matches']: + if start_block != None: + more_conditions.append('''tx0_block_index >= ?''') + bindings.append(start_block) + if end_block != None: + more_conditions.append('''tx1_block_index <= ?''') + bindings.append(end_block) # status if isinstance(status, list) and len(status) > 0: @@ -224,6 +235,12 @@ def value_to_marker(value): more_conditions.append('''status == ?''') bindings.append(status) + # legacy filters + if not show_expired and table == 'orders': + #Ignore BTC orders one block early. + expire_index = util.CURRENT_BLOCK_INDEX + 1 + more_conditions.append('''((give_asset == ? AND expire_index > ?) OR give_asset != ?)''') + bindings += [config.BTC, expire_index, config.BTC] if (len(conditions) + len(more_conditions)) > 0: statement += ''' WHERE''' @@ -235,12 +252,12 @@ def value_to_marker(value): statement += ''' {}'''.format(''' AND '''.join(all_conditions)) # ORDER BY - if order_by is not None: + if order_by != None: statement += ''' ORDER BY {}'''.format(order_by) - if order_dir is not None: + if order_dir != None: statement += ''' {}'''.format(order_dir.upper()) # LIMIT - if limit: + if limit and limit > 0: statement += ''' LIMIT {}'''.format(limit) if offset: statement += ''' OFFSET {}'''.format(offset) @@ -698,8 +715,9 @@ def get_running_info(): def get_element_counts(): counts = {} cursor = db.cursor() - for element in ['transactions', 'blocks', 'debits', 'credits', 'balances', 'sends', - 'issuances', 'broadcasts', 'dividends', 'proofofwork', 'messages']: + for element in ['transactions', 'blocks', 'debits', 'credits', 'balances', 'sends', 'orders', + 'order_matches', 'issuances', 'broadcasts', 'dividends', 'proofofwork', 'bets', 'bet_matches', 'cancels', + 'order_expirations', 'bet_expirations', 'order_match_expirations', 'bet_match_expirations', 'messages']: cursor.execute("SELECT COUNT(*) AS count FROM %s" % element) count_list = cursor.fetchall() assert len(count_list) == 1 diff --git a/aspirelib/lib/blocks.py b/aspirelib/lib/blocks.py index 047bd7e3..06ac2aab 100644 --- a/aspirelib/lib/blocks.py +++ b/aspirelib/lib/blocks.py @@ -51,7 +51,9 @@ # Order matters for FOREIGN KEY constraints. TABLES = ['credits', 'debits', 'messages'] + \ - ['broadcasts', 'proofofwork', + ['bet_match_resolutions', 'order_match_expirations', 'order_matches', + 'order_expirations', 'orders', 'bet_match_expirations', 'bet_matches', + 'bet_expirations', 'bets', 'broadcasts', 'proofofwork', 'cancels', 'dividends', 'issuances', 'sends', 'executions', 'storage', 'suicides', 'nonces', 'postqueue', 'contracts', 'destructions', 'assets', 'addresses'] @@ -92,14 +94,22 @@ def parse_tx(db, tx): send.parse(db, tx, message) elif message_type_id == enhanced_send.ID and util.enabled('enhanced_sends', block_index=tx['block_index']): enhanced_send.parse(db, tx, message) + elif message_type_id == order.ID: + order.parse(db, tx, message) + elif message_type_id == btcpay.ID: + btcpay.parse(db, tx, message) elif message_type_id == issuance.ID: issuance.parse(db, tx, message, message_type_id) elif message_type_id == issuance.SUBASSET_ID and util.enabled('subassets', block_index=tx['block_index']): issuance.parse(db, tx, message, message_type_id) elif message_type_id == broadcast.ID: broadcast.parse(db, tx, message) + elif message_type_id == bet.ID: + bet.parse(db, tx, message) elif message_type_id == dividend.ID: dividend.parse(db, tx, message) + elif message_type_id == cancel.ID: + cancel.parse(db, tx, message) elif message_type_id == publish.ID and tx['block_index'] != config.MEMPOOL_BLOCK_INDEX: publish.parse(db, tx, message) elif message_type_id == execute.ID and tx['block_index'] != config.MEMPOOL_BLOCK_INDEX: @@ -153,6 +163,10 @@ def parse_block(db, block_index, block_time, undolog_cursor.execute('''INSERT OR REPLACE INTO undolog_block(block_index, first_undo_index) VALUES(?,?)''', (block_index, 1,)) undolog_cursor.close() + # Expire orders, jgoregjorgjeoe + order.expire(db, block_index) + bet.expire(db, block_index, block_time) + # Parse transactions, sorting them by type. cursor = db.cursor() cursor.execute('''SELECT * FROM transactions WHERE block_index=? ORDER BY tx_index''', (block_index,)) @@ -351,11 +365,15 @@ def initialise(db): # Consolidated send.initialise(db) destroy.initialise(db) + order.initialise(db) + btcpay.initialise(db) issuance.initialise(db) broadcast.initialise(db) + bet.initialise(db) publish.initialise(db) execute.initialise(db) dividend.initialise(db) + cancel.initialise(db) proofofwork.initialise(db) # Messages diff --git a/aspirelib/lib/log.py b/aspirelib/lib/log.py index 7908dac0..0a229527 100644 --- a/aspirelib/lib/log.py +++ b/aspirelib/lib/log.py @@ -220,7 +220,15 @@ def output(quantity, asset): return '' if command == 'update': - if category == 'proofofwork': + if category == 'order': + logger.debug('Database: set status of order {} to {}.'.format(bindings['tx_hash'], bindings['status'])) + elif category == 'bet': + logger.debug('Database: set status of bet {} to {}.'.format(bindings['tx_hash'], bindings['status'])) + elif category == 'order_matches': + logger.debug('Database: set status of order_match {} to {}.'.format(bindings['order_match_id'], bindings['status'])) + elif category == 'bet_matches': + logger.debug('Database: set status of bet_match {} to {}.'.format(bindings['bet_match_id'], bindings['status'])) + elif category == 'proofofwork': logger.info('POW: block {} {}'.format(bindings['block_index'], bindings['status'])) # TODO: elif category == 'balances': @@ -237,6 +245,12 @@ def output(quantity, asset): elif category == 'sends': logger.info('Send: {} from {} to {} ({}) [{}]'.format(output(bindings['quantity'], bindings['asset']), bindings['source'], bindings['destination'], bindings['tx_hash'], bindings['status'])) + elif category == 'orders': + logger.info('Order: {} ordered {} for {} in {} blocks, with a provided fee of {:.8f} {} and a required fee of {:.8f} {} ({}) [{}]'.format(bindings['source'], output(bindings['give_quantity'], bindings['give_asset']), output(bindings['get_quantity'], bindings['get_asset']), bindings['expiration'], bindings['fee_provided'] / config.UNIT, config.BTC, bindings['fee_required'] / config.UNIT, config.BTC, bindings['tx_hash'], bindings['status'])) + + elif category == 'order_matches': + logger.info('Order Match: {} for {} ({}) [{}]'.format(output(bindings['forward_quantity'], bindings['forward_asset']), output(bindings['backward_quantity'], bindings['backward_asset']), bindings['id'], bindings['status'])) + elif category == 'issuances': if bindings['transfer']: logger.info('Issuance: {} transfered asset {} to {} ({}) [{}]'.format(bindings['source'], bindings['asset'], bindings['issuer'], bindings['tx_hash'], bindings['status'])) @@ -262,6 +276,17 @@ def output(quantity, asset): else: logger.info('Broadcast: ' + bindings['source'] + ' at ' + isodt(bindings['timestamp']) + ' with a fee of {}%'.format(output(D(bindings['fee_fraction_int'] / 1e8) * D(100), 'fraction')) + ' (' + bindings['tx_hash'] + ')' + ' [{}]'.format(bindings['status'])) + elif category == 'bets': + logger.info('Bet: {} against {}, by {}, on {}'.format(output(bindings['wager_quantity'], config.XCP), output(bindings['counterwager_quantity'], config.XCP), bindings['source'], bindings['feed_address'])) + + elif category == 'bet_matches': + placeholder = '' + if bindings['target_value'] >= 0: # Only non‐negative values are valid. + placeholder = ' that ' + str(output(bindings['target_value'], 'value')) + if bindings['leverage']: + placeholder += ', leveraged {}x'.format(output(bindings['leverage'] / 5040, 'leverage')) + logger.info('Bet Match: {} for {} against {} for {} on {} at {}{} ({}) [{}]'.format(util.BET_TYPE_NAME[bindings['tx0_bet_type']], output(bindings['forward_quantity'], config.XCP), util.BET_TYPE_NAME[bindings['tx1_bet_type']], output(bindings['backward_quantity'], config.XCP), bindings['feed_address'], isodt(bindings['deadline']), placeholder, bindings['id'], bindings['status'])) + elif category == 'dividends': logger.info('Dividend: {} paid {} per unit of {} ({}) [{}]'.format(bindings['source'], output(bindings['quantity_per_unit'], bindings['dividend_asset']), bindings['asset'], bindings['tx_hash'], bindings['status'])) @@ -271,6 +296,32 @@ def output(quantity, asset): elif category == 'cancels': logger.info('Cancel: {} ({}) [{}]'.format(bindings['offer_hash'], bindings['tx_hash'], bindings['status'])) + elif category == 'order_expirations': + logger.info('Expired order: {}'.format(bindings['order_hash'])) + + elif category == 'order_match_expirations': + logger.info('Expired Order Match awaiting payment: {}'.format(bindings['order_match_id'])) + + elif category == 'bet_expirations': + logger.info('Expired bet: {}'.format(bindings['bet_hash'])) + + elif category == 'bet_match_expirations': + logger.info('Expired Bet Match: {}'.format(bindings['bet_match_id'])) + + elif category == 'bet_match_resolutions': + # DUPE + cfd_type_id = util.BET_TYPE_ID['BullCFD'] + util.BET_TYPE_ID['BearCFD'] + equal_type_id = util.BET_TYPE_ID['Equal'] + util.BET_TYPE_ID['NotEqual'] + + if bindings['bet_match_type_id'] == cfd_type_id: + if bindings['settled']: + logger.info('Bet Match Settled: {} credited to the bull, {} credited to the bear, and {} credited to the feed address ({})'.format(output(bindings['bull_credit'], config.XCP), output(bindings['bear_credit'], config.XCP), output(bindings['fee'], config.XCP), bindings['bet_match_id'])) + else: + logger.info('Bet Match Force‐Liquidated: {} credited to the bull, {} credited to the bear, and {} credited to the feed address ({})'.format(output(bindings['bull_credit'], config.XCP), output(bindings['bear_credit'], config.XCP), output(bindings['fee'], config.XCP), bindings['bet_match_id'])) + + elif bindings['bet_match_type_id'] == equal_type_id: + logger.info('Bet Match Settled: {} won the pot of {}; {} credited to the feed address ({})'.format(bindings['winner'], output(bindings['escrow_less_fee'], config.XCP), output(bindings['fee'], config.XCP), bindings['bet_match_id'])) + elif category == 'contracts': logger.info('New Contract: {}'.format(bindings['contract_id'])) diff --git a/aspirelib/lib/messages/bet.py b/aspirelib/lib/messages/bet.py new file mode 100644 index 00000000..fdf9aa54 --- /dev/null +++ b/aspirelib/lib/messages/bet.py @@ -0,0 +1,595 @@ +#! /usr/bin/python3 + +""" +Datastreams are identified by the address that publishes them, and referenced +in transaction outputs. + +For CFD leverage, 1x = 5040, 2x = 10080, etc.: 5040 is a superior highly +composite number and a colossally abundant number, and has 1-10, 12 as factors. + +All wagers are in XCP. + +Expiring a bet match doesn’t re‐open the constituent bets. (So all bets may be ‘filled’.) +""" + +import struct +import decimal +import json +D = decimal.Decimal +import time +import logging +logger = logging.getLogger(__name__) + +from aspirelib.lib import config +from aspirelib.lib import exceptions +from aspirelib.lib import util +from aspirelib.lib import log +from aspirelib.lib import message_type + +FORMAT = '>HIQQdII' +LENGTH = 2 + 4 + 8 + 8 + 8 + 4 + 4 +ID = 40 + +def initialise (db): + cursor = db.cursor() + + # Bets. + cursor.execute('''CREATE TABLE IF NOT EXISTS bets( + tx_index INTEGER UNIQUE, + tx_hash TEXT UNIQUE, + block_index INTEGER, + source TEXT, + feed_address TEXT, + bet_type INTEGER, + deadline INTEGER, + wager_quantity INTEGER, + wager_remaining INTEGER, + counterwager_quantity INTEGER, + counterwager_remaining INTEGER, + target_value REAL, + leverage INTEGER, + expiration INTEGER, + expire_index INTEGER, + fee_fraction_int INTEGER, + status TEXT, + FOREIGN KEY (tx_index, tx_hash, block_index) REFERENCES transactions(tx_index, tx_hash, block_index), + PRIMARY KEY (tx_index, tx_hash)) + ''') + cursor.execute('''CREATE INDEX IF NOT EXISTS + block_index_idx ON bets (block_index) + ''') + cursor.execute('''CREATE INDEX IF NOT EXISTS + index_hash_idx ON bets (tx_index, tx_hash) + ''') + cursor.execute('''CREATE INDEX IF NOT EXISTS + expire_idx ON bets (status, expire_index) + ''') + cursor.execute('''CREATE INDEX IF NOT EXISTS + feed_valid_bettype_idx ON bets (feed_address, status, bet_type) + ''') + cursor.execute('''CREATE INDEX IF NOT EXISTS + source_idx ON bets (source) + ''') + cursor.execute('''CREATE INDEX IF NOT EXISTS + status_idx ON bets (status) + ''') + + # Bet Matches + cursor.execute('''CREATE TABLE IF NOT EXISTS bet_matches( + id TEXT PRIMARY KEY, + tx0_index INTEGER, + tx0_hash TEXT, + tx0_address TEXT, + tx1_index INTEGER, + tx1_hash TEXT, + tx1_address TEXT, + tx0_bet_type INTEGER, + tx1_bet_type INTEGER, + feed_address TEXT, + initial_value INTEGER, + deadline INTEGER, + target_value REAL, + leverage INTEGER, + forward_quantity INTEGER, + backward_quantity INTEGER, + tx0_block_index INTEGER, + tx1_block_index INTEGER, + block_index INTEGER, + tx0_expiration INTEGER, + tx1_expiration INTEGER, + match_expire_index INTEGER, + fee_fraction_int INTEGER, + status TEXT, + FOREIGN KEY (tx0_index, tx0_hash, tx0_block_index) REFERENCES transactions(tx_index, tx_hash, block_index), + FOREIGN KEY (tx1_index, tx1_hash, tx1_block_index) REFERENCES transactions(tx_index, tx_hash, block_index)) + ''') + cursor.execute('''CREATE INDEX IF NOT EXISTS + match_expire_idx ON bet_matches (status, match_expire_index) + ''') + cursor.execute('''CREATE INDEX IF NOT EXISTS + valid_feed_idx ON bet_matches (feed_address, status) + ''') + cursor.execute('''CREATE INDEX IF NOT EXISTS + id_idx ON bet_matches (id) + ''') + cursor.execute('''CREATE INDEX IF NOT EXISTS + tx0_address_idx ON bet_matches (tx0_address) + ''') + cursor.execute('''CREATE INDEX IF NOT EXISTS + tx1_address_idx ON bet_matches (tx1_address) + ''') + cursor.execute('''CREATE INDEX IF NOT EXISTS + status_idx ON bet_matches (status) + ''') + + # Bet Expirations + cursor.execute('''CREATE TABLE IF NOT EXISTS bet_expirations( + bet_index INTEGER PRIMARY KEY, + bet_hash TEXT UNIQUE, + source TEXT, + block_index INTEGER, + FOREIGN KEY (block_index) REFERENCES blocks(block_index), + FOREIGN KEY (bet_index, bet_hash) REFERENCES bets(tx_index, tx_hash)) + ''') + cursor.execute('''CREATE INDEX IF NOT EXISTS + block_index_idx ON bet_expirations (block_index) + ''') + cursor.execute('''CREATE INDEX IF NOT EXISTS + source_idx ON bet_expirations (source) + ''') + + # Bet Match Expirations + cursor.execute('''CREATE TABLE IF NOT EXISTS bet_match_expirations( + bet_match_id TEXT PRIMARY KEY, + tx0_address TEXT, + tx1_address TEXT, + block_index INTEGER, + FOREIGN KEY (bet_match_id) REFERENCES bet_matches(id), + FOREIGN KEY (block_index) REFERENCES blocks(block_index)) + ''') + cursor.execute('''CREATE INDEX IF NOT EXISTS + block_index_idx ON bet_match_expirations (block_index) + ''') + cursor.execute('''CREATE INDEX IF NOT EXISTS + tx0_address_idx ON bet_match_expirations (tx0_address) + ''') + cursor.execute('''CREATE INDEX IF NOT EXISTS + tx1_address_idx ON bet_match_expirations (tx1_address) + ''') + + # Bet Match Resolutions + cursor.execute('''CREATE TABLE IF NOT EXISTS bet_match_resolutions( + bet_match_id TEXT PRIMARY KEY, + bet_match_type_id INTEGER, + block_index INTEGER, + winner TEXT, + settled BOOL, + bull_credit INTEGER, + bear_credit INTEGER, + escrow_less_fee INTEGER, + fee INTEGER, + FOREIGN KEY (bet_match_id) REFERENCES bet_matches(id), + FOREIGN KEY (block_index) REFERENCES blocks(block_index)) + ''') + +def cancel_bet (db, bet, status, block_index): + cursor = db.cursor() + + # Update status of bet. + bindings = { + 'status': status, + 'tx_hash': bet['tx_hash'] + } + sql='update bets set status = :status where tx_hash = :tx_hash' + cursor.execute(sql, bindings) + log.message(db, block_index, 'update', 'bets', bindings) + + util.credit(db, bet['source'], config.XCP, bet['wager_remaining'], action='recredit wager remaining', event=bet['tx_hash']) + + cursor = db.cursor() + +def cancel_bet_match (db, bet_match, status, block_index): + # Does not re‐open, re‐fill, etc. constituent bets. + + cursor = db.cursor() + + # Recredit tx0 address. + util.credit(db, bet_match['tx0_address'], config.XCP, + bet_match['forward_quantity'], action='recredit forward quantity', event=bet_match['id']) + + # Recredit tx1 address. + util.credit(db, bet_match['tx1_address'], config.XCP, + bet_match['backward_quantity'], action='recredit backward quantity', event=bet_match['id']) + + # Update status of bet match. + bindings = { + 'status': status, + 'bet_match_id': bet_match['id'] + } + sql='update bet_matches set status = :status where id = :bet_match_id' + cursor.execute(sql, bindings) + log.message(db, block_index, 'update', 'bet_matches', bindings) + + cursor.close() + + +def get_fee_fraction (db, feed_address): + '''Get fee fraction from last broadcast from the feed_address address. + ''' + cursor = db.cursor() + broadcasts = list(cursor.execute('''SELECT * FROM broadcasts WHERE (status = ? AND source = ?) ORDER BY tx_index ASC''', ('valid', feed_address))) + cursor.close() + if broadcasts: + last_broadcast = broadcasts[-1] + fee_fraction_int = last_broadcast['fee_fraction_int'] + if fee_fraction_int: return fee_fraction_int / 1e8 + else: return 0 + else: + return 0 + +def validate (db, source, feed_address, bet_type, deadline, wager_quantity, + counterwager_quantity, target_value, leverage, expiration, block_index): + problems = [] + + if leverage is None: leverage = 5040 + + # For SQLite3 + if wager_quantity > config.MAX_INT or counterwager_quantity > config.MAX_INT or bet_type > config.MAX_INT \ + or deadline > config.MAX_INT or leverage > config.MAX_INT or block_index + expiration > config.MAX_INT: + problems.append('integer overflow') + + # Look at feed to be bet on. + cursor = db.cursor() + broadcasts = list(cursor.execute('''SELECT * FROM broadcasts WHERE (status = ? AND source = ?) ORDER BY tx_index ASC''', ('valid', feed_address))) + cursor.close() + if not broadcasts: + problems.append('feed doesn’t exist') + elif not broadcasts[-1]['text']: + problems.append('feed is locked') + elif broadcasts[-1]['timestamp'] >= deadline: + problems.append('deadline in that feed’s past') + + if not bet_type in (0, 1, 2, 3): + problems.append('unknown bet type') + + # Valid leverage level? + if leverage != 5040 and bet_type in (2,3): # Equal, NotEqual + problems.append('leverage used with Equal or NotEqual') + if leverage < 5040 and not bet_type in (0,1): # BullCFD, BearCFD (fractional leverage makes sense precisely with CFDs) + problems.append('leverage level too low') + + if bet_type in (0,1): # BullCFD, BearCFD + if block_index >= 312350: # Protocol change. + problems.append('CFDs temporarily disabled') + + if not isinstance(wager_quantity, int): + problems.append('wager_quantity must be in satoshis') + return problems, leverage + if not isinstance(counterwager_quantity, int): + problems.append('counterwager_quantity must be in satoshis') + return problems, leverage + if not isinstance(expiration, int): + problems.append('expiration must be expressed as an integer block delta') + return problems, leverage + + if wager_quantity <= 0: problems.append('non‐positive wager') + if counterwager_quantity <= 0: problems.append('non‐positive counterwager') + if deadline < 0: problems.append('negative deadline') + if expiration < 0: problems.append('negative expiration') + if expiration == 0 and not (block_index >= 317500 or config.TESTNET or config.REGTEST): # Protocol change. + problems.append('zero expiration') + + if target_value: + if bet_type in (0,1): # BullCFD, BearCFD + problems.append('CFDs have no target value') + if target_value < 0: + problems.append('negative target value') + + if expiration > config.MAX_EXPIRATION: + problems.append('expiration overflow') + + return problems, leverage + +def compose (db, source, feed_address, bet_type, deadline, wager_quantity, + counterwager_quantity, target_value, leverage, expiration): + + if util.get_balance(db, source, config.XCP) < wager_quantity: + raise exceptions.ComposeError('insufficient funds') + + problems, leverage = validate(db, source, feed_address, bet_type, deadline, wager_quantity, + counterwager_quantity, target_value, leverage, expiration, util.CURRENT_BLOCK_INDEX) + if util.date_passed(deadline): + problems.append('deadline passed') + if problems: raise exceptions.ComposeError(problems) + + data = message_type.pack(ID) + data += struct.pack(FORMAT, bet_type, deadline, + wager_quantity, counterwager_quantity, target_value, + leverage, expiration) + return (source, [(feed_address, None)], data) + +def parse (db, tx, message): + bet_parse_cursor = db.cursor() + + # Unpack message. + try: + if len(message) != LENGTH: + raise exceptions.UnpackError + (bet_type, deadline, wager_quantity, + counterwager_quantity, target_value, leverage, + expiration) = struct.unpack(FORMAT, message) + status = 'open' + except (exceptions.UnpackError, struct.error): + (bet_type, deadline, wager_quantity, + counterwager_quantity, target_value, leverage, + expiration, fee_fraction_int) = 0, 0, 0, 0, 0, 0, 0, 0 + status = 'invalid: could not unpack' + + odds, fee_fraction = 0, 0 + feed_address = tx['destination'] + if status == 'open': + try: + odds = util.price(wager_quantity, counterwager_quantity) + except ZeroDivisionError: + odds = 0 + + fee_fraction = get_fee_fraction(db, feed_address) + + # Overbet + bet_parse_cursor.execute('''SELECT * FROM balances \ + WHERE (address = ? AND asset = ?)''', (tx['source'], config.XCP)) + balances = list(bet_parse_cursor) + if not balances: + wager_quantity = 0 + else: + balance = balances[0]['quantity'] + if balance < wager_quantity: + wager_quantity = balance + counterwager_quantity = int(util.price(wager_quantity, odds)) + + problems, leverage = validate(db, tx['source'], feed_address, bet_type, deadline, wager_quantity, + counterwager_quantity, target_value, leverage, expiration, tx['block_index']) + if problems: status = 'invalid: ' + '; '.join(problems) + + # Debit quantity wagered. (Escrow.) + if status == 'open': + util.debit(db, tx['source'], config.XCP, wager_quantity, action='bet', event=tx['tx_hash']) + + # Add parsed transaction to message-type–specific table. + bindings = { + 'tx_index': tx['tx_index'], + 'tx_hash': tx['tx_hash'], + 'block_index': tx['block_index'], + 'source': tx['source'], + 'feed_address': feed_address, + 'bet_type': bet_type, + 'deadline': deadline, + 'wager_quantity': wager_quantity, + 'wager_remaining': wager_quantity, + 'counterwager_quantity': counterwager_quantity, + 'counterwager_remaining': counterwager_quantity, + 'target_value': target_value, + 'leverage': leverage, + 'expiration': expiration, + 'expire_index': tx['block_index'] + expiration, + 'fee_fraction_int': fee_fraction * 1e8, + 'status': status, + } + if "integer overflow" not in status: + sql = 'insert into bets values(:tx_index, :tx_hash, :block_index, :source, :feed_address, :bet_type, :deadline, :wager_quantity, :wager_remaining, :counterwager_quantity, :counterwager_remaining, :target_value, :leverage, :expiration, :expire_index, :fee_fraction_int, :status)' + bet_parse_cursor.execute(sql, bindings) + else: + logger.warn("Not storing [bet] tx [%s]: %s" % (tx['tx_hash'], status)) + logger.debug("Bindings: %s" % (json.dumps(bindings), )) + + # Match. + if status == 'open' and tx['block_index'] != config.MEMPOOL_BLOCK_INDEX: + match(db, tx) + + bet_parse_cursor.close() + +def match (db, tx): + + cursor = db.cursor() + + # Get bet in question. + bets = list(cursor.execute('''SELECT * FROM bets\ + WHERE (tx_index = ? AND status = ?)''', (tx['tx_index'], 'open'))) + if not bets: + cursor.close() + return + else: + assert len(bets) == 1 + tx1 = bets[0] + + # Get counterbet_type. + if tx1['bet_type'] % 2: counterbet_type = tx1['bet_type'] - 1 + else: counterbet_type = tx1['bet_type'] + 1 + + feed_address = tx1['feed_address'] + + cursor.execute('''SELECT * FROM bets\ + WHERE (feed_address=? AND status=? AND bet_type=?)''', + (tx1['feed_address'], 'open', counterbet_type)) + tx1_wager_remaining = tx1['wager_remaining'] + tx1_counterwager_remaining = tx1['counterwager_remaining'] + bet_matches = cursor.fetchall() + if tx['block_index'] > 284500 or config.TESTNET or config.REGTEST: # Protocol change. + sorted(bet_matches, key=lambda x: x['tx_index']) # Sort by tx index second. + sorted(bet_matches, key=lambda x: util.price(x['wager_quantity'], x['counterwager_quantity'])) # Sort by price first. + + tx1_status = tx1['status'] + for tx0 in bet_matches: + if tx1_status != 'open': break + + logger.debug('Considering: ' + tx0['tx_hash']) + tx0_wager_remaining = tx0['wager_remaining'] + tx0_counterwager_remaining = tx0['counterwager_remaining'] + + # Bet types must be opposite. + if counterbet_type != tx0['bet_type']: + logger.debug('Skipping: bet types disagree.') + continue + + # Leverages must agree exactly + if tx0['leverage'] != tx1['leverage']: + logger.debug('Skipping: leverages disagree.') + continue + + # Target values must agree exactly. + if tx0['target_value'] != tx1['target_value']: + logger.debug('Skipping: target values disagree.') + continue + + # Fee fractions must agree exactly. + if tx0['fee_fraction_int'] != tx1['fee_fraction_int']: + logger.debug('Skipping: fee fractions disagree.') + continue + + # Deadlines must agree exactly. + if tx0['deadline'] != tx1['deadline']: + logger.debug('Skipping: deadlines disagree.') + continue + + # If the odds agree, make the trade. The found order sets the odds, + # and they trade as much as they can. + tx0_odds = util.price(tx0['wager_quantity'], tx0['counterwager_quantity']) + tx0_inverse_odds = util.price(tx0['counterwager_quantity'], tx0['wager_quantity']) + tx1_odds = util.price(tx1['wager_quantity'], tx1['counterwager_quantity']) + + if tx['block_index'] < 286000: tx0_inverse_odds = util.price(1, tx0_odds) # Protocol change. + + logger.debug('Tx0 Inverse Odds: {}; Tx1 Odds: {}'.format(float(tx0_inverse_odds), float(tx1_odds))) + if tx0_inverse_odds > tx1_odds: + logger.debug('Skipping: price mismatch.') + else: + logger.debug('Potential forward quantities: {}, {}'.format(tx0_wager_remaining, int(util.price(tx1_wager_remaining, tx1_odds)))) + forward_quantity = int(min(tx0_wager_remaining, int(util.price(tx1_wager_remaining, tx1_odds)))) + logger.debug('Forward Quantity: {}'.format(forward_quantity)) + backward_quantity = round(forward_quantity / tx0_odds) + logger.debug('Backward Quantity: {}'.format(backward_quantity)) + + if not forward_quantity: + logger.debug('Skipping: zero forward quantity.') + continue + if tx1['block_index'] >= 286500 or config.TESTNET or config.REGTEST: # Protocol change. + if not backward_quantity: + logger.debug('Skipping: zero backward quantity.') + continue + + bet_match_id = util.make_id(tx0['tx_hash'], tx1['tx_hash']) + + # Debit the order. + # Counterwager remainings may be negative. + tx0_wager_remaining = tx0_wager_remaining - forward_quantity + tx0_counterwager_remaining = tx0_counterwager_remaining - backward_quantity + tx1_wager_remaining = tx1_wager_remaining - backward_quantity + tx1_counterwager_remaining = tx1_counterwager_remaining - forward_quantity + + # tx0 + tx0_status = 'open' + if tx0_wager_remaining <= 0 or tx0_counterwager_remaining <= 0: + # Fill order, and recredit give_remaining. + tx0_status = 'filled' + util.credit(db, tx0['source'], config.XCP, tx0_wager_remaining, event=tx1['tx_hash'], action='filled') + bindings = { + 'wager_remaining': tx0_wager_remaining, + 'counterwager_remaining': tx0_counterwager_remaining, + 'status': tx0_status, + 'tx_hash': tx0['tx_hash'] + } + sql='update bets set wager_remaining = :wager_remaining, counterwager_remaining = :counterwager_remaining, status = :status where tx_hash = :tx_hash' + cursor.execute(sql, bindings) + log.message(db, tx['block_index'], 'update', 'bets', bindings) + + if tx1['block_index'] >= 292000 or config.TESTNET or config.REGTEST: # Protocol change + if tx1_wager_remaining <= 0 or tx1_counterwager_remaining <= 0: + # Fill order, and recredit give_remaining. + tx1_status = 'filled' + util.credit(db, tx1['source'], config.XCP, tx1_wager_remaining, event=tx1['tx_hash'], action='filled') + # tx1 + bindings = { + 'wager_remaining': tx1_wager_remaining, + 'counterwager_remaining': tx1_counterwager_remaining, + 'status': tx1_status, + 'tx_hash': tx1['tx_hash'] + } + sql='update bets set wager_remaining = :wager_remaining, counterwager_remaining = :counterwager_remaining, status = :status where tx_hash = :tx_hash' + cursor.execute(sql, bindings) + log.message(db, tx['block_index'], 'update', 'bets', bindings) + + # Get last value of feed. + broadcasts = list(cursor.execute('''SELECT * FROM broadcasts WHERE (status = ? AND source = ?) ORDER BY tx_index ASC''', ('valid', feed_address))) + initial_value = broadcasts[-1]['value'] + + # Record bet fulfillment. + bindings = { + 'id': util.make_id(tx0['tx_hash'], tx['tx_hash']), + 'tx0_index': tx0['tx_index'], + 'tx0_hash': tx0['tx_hash'], + 'tx0_address': tx0['source'], + 'tx1_index': tx1['tx_index'], + 'tx1_hash': tx1['tx_hash'], + 'tx1_address': tx1['source'], + 'tx0_bet_type': tx0['bet_type'], + 'tx1_bet_type': tx1['bet_type'], + 'feed_address': tx1['feed_address'], + 'initial_value': initial_value, + 'deadline': tx1['deadline'], + 'target_value': tx1['target_value'], + 'leverage': tx1['leverage'], + 'forward_quantity': forward_quantity, + 'backward_quantity': backward_quantity, + 'tx0_block_index': tx0['block_index'], + 'tx1_block_index': tx1['block_index'], + 'block_index': max(tx0['block_index'], tx1['block_index']), + 'tx0_expiration': tx0['expiration'], + 'tx1_expiration': tx1['expiration'], + 'match_expire_index': min(tx0['expire_index'], tx1['expire_index']), + 'fee_fraction_int': tx1['fee_fraction_int'], + 'status': 'pending', + } + sql='insert into bet_matches values(:id, :tx0_index, :tx0_hash, :tx0_address, :tx1_index, :tx1_hash, :tx1_address, :tx0_bet_type, :tx1_bet_type, :feed_address, :initial_value, :deadline, :target_value, :leverage, :forward_quantity, :backward_quantity, :tx0_block_index, :tx1_block_index, :block_index, :tx0_expiration, :tx1_expiration, :match_expire_index, :fee_fraction_int, :status)' + cursor.execute(sql, bindings) + + cursor.close() + return + +def expire (db, block_index, block_time): + cursor = db.cursor() + + # Expire bets and give refunds for the quantity wager_remaining. + cursor.execute('''SELECT * FROM bets \ + WHERE (status = ? AND expire_index < ?)''', ('open', block_index)) + for bet in cursor.fetchall(): + cancel_bet(db, bet, 'expired', block_index) + + # Record bet expiration. + bindings = { + 'bet_index': bet['tx_index'], + 'bet_hash': bet['tx_hash'], + 'source': bet['source'], + 'block_index': block_index + } + sql='insert into bet_expirations values(:bet_index, :bet_hash, :source, :block_index)' + cursor.execute(sql, bindings) + + # Expire bet matches whose deadline is more than two weeks before the current block time. + cursor.execute('''SELECT * FROM bet_matches \ + WHERE (status = ? AND deadline < ?)''', ('pending', block_time - config.TWO_WEEKS)) + for bet_match in cursor.fetchall(): + cancel_bet_match(db, bet_match, 'expired', block_index) + + # Record bet match expiration. + bindings = { + 'bet_match_id': bet_match['id'], + 'tx0_address': bet_match['tx0_address'], + 'tx1_address': bet_match['tx1_address'], + 'block_index': block_index + } + sql='insert into bet_match_expirations values(:bet_match_id, :tx0_address, :tx1_address, :block_index)' + cursor.execute(sql, bindings) + + cursor.close() + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/aspirelib/lib/messages/cancel.py b/aspirelib/lib/messages/cancel.py new file mode 100644 index 00000000..5c2a6131 --- /dev/null +++ b/aspirelib/lib/messages/cancel.py @@ -0,0 +1,131 @@ +#! /usr/bin/python3 + +""" +offer_hash is the hash of either a bet or an order. +""" + +import binascii +import struct +import json +import logging +logger = logging.getLogger(__name__) + +from aspirelib.lib import (config, exceptions, util, message_type) +from . import (order, bet, rps) + +FORMAT = '>32s' +LENGTH = 32 +ID = 70 + +def initialise (db): + cursor = db.cursor() + cursor.execute('''CREATE TABLE IF NOT EXISTS cancels( + tx_index INTEGER PRIMARY KEY, + tx_hash TEXT UNIQUE, + block_index INTEGER, + source TEXT, + offer_hash TEXT, + status TEXT, + FOREIGN KEY (tx_index, tx_hash, block_index) REFERENCES transactions(tx_index, tx_hash, block_index)) + ''') + # Offer hash is not a foreign key. (And it cannot be, because of some invalid cancels.) + cursor.execute('''CREATE INDEX IF NOT EXISTS + cancels_block_index_idx ON cancels (block_index) + ''') + cursor.execute('''CREATE INDEX IF NOT EXISTS + source_idx ON cancels (source) + ''') + +def validate (db, source, offer_hash): + problems = [] + + # TODO: make query only if necessary + cursor = db.cursor() + cursor.execute('''SELECT * from orders WHERE tx_hash = ?''', (offer_hash,)) + orders = list(cursor) + cursor.execute('''SELECT * from bets WHERE tx_hash = ?''', (offer_hash,)) + bets = list(cursor) + cursor.execute('''SELECT * from rps WHERE tx_hash = ?''', (offer_hash,)) + rps = list(cursor) + cursor.close() + + offer_type = None + if orders: offer_type = 'order' + elif bets: offer_type = 'bet' + elif rps: offer_type = 'rps' + else: problems = ['no open offer with that hash'] + + offer = None + if offer_type: + offers = orders + bets + rps + offer = offers[0] + if offer['source'] != source: + problems.append('incorrect source address') + if offer['status'] != 'open': + problems.append('offer not open') + + return offer, offer_type, problems + +def compose (db, source, offer_hash): + + # Check that offer exists. + offer, offer_type, problems = validate(db, source, offer_hash) + if problems: raise exceptions.ComposeError(problems) + + offer_hash_bytes = binascii.unhexlify(bytes(offer_hash, 'utf-8')) + data = message_type.pack(ID) + data += struct.pack(FORMAT, offer_hash_bytes) + return (source, [], data) + +def parse (db, tx, message): + cursor = db.cursor() + + # Unpack message. + try: + if len(message) != LENGTH: + raise exceptions.UnpackError + offer_hash_bytes = struct.unpack(FORMAT, message)[0] + offer_hash = binascii.hexlify(offer_hash_bytes).decode('utf-8') + status = 'valid' + except (exceptions.UnpackError, struct.error) as e: + offer_hash = None + status = 'invalid: could not unpack' + + if status == 'valid': + offer, offer_type, problems = validate(db, tx['source'], offer_hash) + if problems: + status = 'invalid: ' + '; '.join(problems) + + if status == 'valid': + # Cancel if order. + if offer_type == 'order': + order.cancel_order(db, offer, 'cancelled', tx['block_index']) + # Cancel if bet. + elif offer_type == 'bet': + bet.cancel_bet(db, offer, 'cancelled', tx['block_index']) + # Cancel if rps. + elif offer_type == 'rps': + rps.cancel_rps(db, offer, 'cancelled', tx['block_index']) + # If neither order or bet, mark as invalid. + else: + assert False + + # Add parsed transaction to message-type–specific table. + bindings = { + 'tx_index': tx['tx_index'], + 'tx_hash': tx['tx_hash'], + 'block_index': tx['block_index'], + 'source': tx['source'], + 'offer_hash': offer_hash, + 'status': status, + } + if "integer overflow" not in status: + sql='INSERT INTO cancels VALUES (:tx_index, :tx_hash, :block_index, :source, :offer_hash, :status)' + cursor.execute(sql, bindings) + else: + logger.warn("Not storing [cancel] tx [%s]: %s" % (tx['tx_hash'], status)) + logger.debug("Bindings: %s" % (json.dumps(bindings), )) + + cursor.close() + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/aspirelib/lib/messages/order.py b/aspirelib/lib/messages/order.py new file mode 100644 index 00000000..2a52704c --- /dev/null +++ b/aspirelib/lib/messages/order.py @@ -0,0 +1,786 @@ +#! /usr/bin/python3 + +# Filled orders may not be re‐opened, so only orders not involving BTC (and so +# which cannot have expired order matches) may be filled. +import json +import struct +import decimal +D = decimal.Decimal +import logging +logger = logging.getLogger(__name__) + +from aspirelib.lib import config +from aspirelib.lib import exceptions +from aspirelib.lib import util +from aspirelib.lib import backend +from aspirelib.lib import log +from aspirelib.lib import message_type + +FORMAT = '>QQQQHQ' +LENGTH = 8 + 8 + 8 + 8 + 2 + 8 +ID = 10 + +def initialise(db): + cursor = db.cursor() + + # Orders + cursor.execute('''CREATE TABLE IF NOT EXISTS orders( + tx_index INTEGER UNIQUE, + tx_hash TEXT UNIQUE, + block_index INTEGER, + source TEXT, + give_asset TEXT, + give_quantity INTEGER, + give_remaining INTEGER, + get_asset TEXT, + get_quantity INTEGER, + get_remaining INTEGER, + expiration INTEGER, + expire_index INTEGER, + fee_required INTEGER, + fee_required_remaining INTEGER, + fee_provided INTEGER, + fee_provided_remaining INTEGER, + status TEXT, + FOREIGN KEY (tx_index, tx_hash, block_index) REFERENCES transactions(tx_index, tx_hash, block_index), + PRIMARY KEY (tx_index, tx_hash)) + ''') + cursor.execute('''CREATE INDEX IF NOT EXISTS + block_index_idx ON orders (block_index) + ''') + cursor.execute('''CREATE INDEX IF NOT EXISTS + index_hash_idx ON orders (tx_index, tx_hash) + ''') + cursor.execute('''CREATE INDEX IF NOT EXISTS + expire_idx ON orders (expire_index, status) + ''') + cursor.execute('''CREATE INDEX IF NOT EXISTS + give_status_idx ON orders (give_asset, status) + ''') + cursor.execute('''CREATE INDEX IF NOT EXISTS + source_give_status_idx ON orders (source, give_asset, status) + ''') + cursor.execute('''CREATE INDEX IF NOT EXISTS + give_get_status_idx ON orders (get_asset, give_asset, status) + ''') + cursor.execute('''CREATE INDEX IF NOT EXISTS + source_idx ON orders (source) + ''') + cursor.execute('''CREATE INDEX IF NOT EXISTS + give_asset_idx ON orders (give_asset) + ''') + + # Order Matches + cursor.execute('''CREATE TABLE IF NOT EXISTS order_matches( + id TEXT PRIMARY KEY, + tx0_index INTEGER, + tx0_hash TEXT, + tx0_address TEXT, + tx1_index INTEGER, + tx1_hash TEXT, + tx1_address TEXT, + forward_asset TEXT, + forward_quantity INTEGER, + backward_asset TEXT, + backward_quantity INTEGER, + tx0_block_index INTEGER, + tx1_block_index INTEGER, + block_index INTEGER, + tx0_expiration INTEGER, + tx1_expiration INTEGER, + match_expire_index INTEGER, + fee_paid INTEGER, + status TEXT, + FOREIGN KEY (tx0_index, tx0_hash, tx0_block_index) REFERENCES transactions(tx_index, tx_hash, block_index), + FOREIGN KEY (tx1_index, tx1_hash, tx1_block_index) REFERENCES transactions(tx_index, tx_hash, block_index)) + ''') + cursor.execute('''CREATE INDEX IF NOT EXISTS + match_expire_idx ON order_matches (status, match_expire_index) + ''') + cursor.execute('''CREATE INDEX IF NOT EXISTS + forward_status_idx ON order_matches (forward_asset, status) + ''') + cursor.execute('''CREATE INDEX IF NOT EXISTS + backward_status_idx ON order_matches (backward_asset, status) + ''') + cursor.execute('''CREATE INDEX IF NOT EXISTS + id_idx ON order_matches (id) + ''') + cursor.execute('''CREATE INDEX IF NOT EXISTS + tx0_address_idx ON order_matches (tx0_address) + ''') + cursor.execute('''CREATE INDEX IF NOT EXISTS + tx1_address_idx ON order_matches (tx1_address) + ''') + + # Order Expirations + cursor.execute('''CREATE TABLE IF NOT EXISTS order_expirations( + order_index INTEGER PRIMARY KEY, + order_hash TEXT UNIQUE, + source TEXT, + block_index INTEGER, + FOREIGN KEY (block_index) REFERENCES blocks(block_index), + FOREIGN KEY (order_index, order_hash) REFERENCES orders(tx_index, tx_hash)) + ''') + cursor.execute('''CREATE INDEX IF NOT EXISTS + block_index_idx ON order_expirations (block_index) + ''') + cursor.execute('''CREATE INDEX IF NOT EXISTS + source_idx ON order_expirations (source) + ''') + + # Order Match Expirations + cursor.execute('''CREATE TABLE IF NOT EXISTS order_match_expirations( + order_match_id TEXT PRIMARY KEY, + tx0_address TEXT, + tx1_address TEXT, + block_index INTEGER, + FOREIGN KEY (order_match_id) REFERENCES order_matches(id), + FOREIGN KEY (block_index) REFERENCES blocks(block_index)) + ''') + cursor.execute('''CREATE INDEX IF NOT EXISTS + block_index_idx ON order_match_expirations (block_index) + ''') + cursor.execute('''CREATE INDEX IF NOT EXISTS + tx0_address_idx ON order_match_expirations (tx0_address) + ''') + cursor.execute('''CREATE INDEX IF NOT EXISTS + tx1_address_idx ON order_match_expirations (tx1_address) + ''') + +def exact_penalty (db, address, block_index, order_match_id): + # Penalize addresses that don’t make BTC payments. If an address lets an + # order match expire, expire sell BTC orders from that address. + cursor = db.cursor() + + # Orders. + bad_orders = list(cursor.execute('''SELECT * FROM orders \ + WHERE (source = ? AND give_asset = ? AND status = ?)''', + (address, config.BTC, 'open'))) + for bad_order in bad_orders: + cancel_order(db, bad_order, 'expired', block_index) + + if not (block_index >= 314250 or config.TESTNET or config.REGTEST): # Protocol change. + # Order matches. + bad_order_matches = list(cursor.execute('''SELECT * FROM order_matches \ + WHERE ((tx0_address = ? AND forward_asset = ?) OR (tx1_address = ? AND backward_asset = ?)) AND (status = ?)''', + (address, config.BTC, address, config.BTC, 'pending'))) + for bad_order_match in bad_order_matches: + cancel_order_match(db, bad_order_match, 'expired', block_index) + + cursor.close() + return + + +def cancel_order (db, order, status, block_index): + cursor = db.cursor() + + # Update status of order. + bindings = { + 'status': status, + 'tx_hash': order['tx_hash'] + } + sql='update orders set status = :status where tx_hash = :tx_hash' + cursor.execute(sql, bindings) + log.message(db, block_index, 'update', 'orders', bindings) + + if order['give_asset'] != config.BTC: # Can’t credit BTC. + util.credit(db, order['source'], order['give_asset'], order['give_remaining'], action='cancel order', event=order['tx_hash']) + + if status == 'expired': + # Record offer expiration. + bindings = { + 'order_index': order['tx_index'], + 'order_hash': order['tx_hash'], + 'source': order['source'], + 'block_index': block_index + } + sql='insert into order_expirations values(:order_index, :order_hash, :source, :block_index)' + cursor.execute(sql, bindings) + + cursor.close() + +def cancel_order_match (db, order_match, status, block_index): + '''The only cancelling is an expiration. + ''' + + cursor = db.cursor() + + # Skip order matches just expired as a penalty. (Not very efficient.) + if not (block_index >= 314250 or config.TESTNET or config.REGTEST): # Protocol change. + order_matches = list(cursor.execute('''SELECT * FROM order_matches \ + WHERE (id = ? AND status = ?)''', + (order_match['id'], 'expired'))) + if order_matches: + cursor.close() + return + + # Update status of order match. + bindings = { + 'status': status, + 'order_match_id': order_match['id'] + } + sql='update order_matches set status = :status where id = :order_match_id' + cursor.execute(sql, bindings) + log.message(db, block_index, 'update', 'order_matches', bindings) + + order_match_id = util.make_id(order_match['tx0_hash'], order_match['tx1_hash']) + + # If tx0 is dead, credit address directly; if not, replenish give remaining, get remaining, and fee required remaining. + orders = list(cursor.execute('''SELECT * FROM orders \ + WHERE tx_index = ?''', + (order_match['tx0_index'],))) + assert len(orders) == 1 + tx0_order = orders[0] + if tx0_order['status'] in ('expired', 'cancelled'): + tx0_order_status = tx0_order['status'] + if order_match['forward_asset'] != config.BTC: + util.credit(db, order_match['tx0_address'], + order_match['forward_asset'], + order_match['forward_quantity'], action='order {}'.format(tx0_order_status), event=order_match['id']) + else: + tx0_give_remaining = tx0_order['give_remaining'] + order_match['forward_quantity'] + tx0_get_remaining = tx0_order['get_remaining'] + order_match['backward_quantity'] + if tx0_order['get_asset'] == config.BTC and (block_index >= 297000 or config.TESTNET or config.REGTEST): # Protocol change. + tx0_fee_required_remaining = tx0_order['fee_required_remaining'] + order_match['fee_paid'] + else: + tx0_fee_required_remaining = tx0_order['fee_required_remaining'] + tx0_order_status = tx0_order['status'] + bindings = { + 'give_remaining': tx0_give_remaining, + 'get_remaining': tx0_get_remaining, + 'status': tx0_order_status, + 'fee_required_remaining': tx0_fee_required_remaining, + 'tx_hash': order_match['tx0_hash'] + } + sql='update orders set give_remaining = :give_remaining, get_remaining = :get_remaining, fee_required_remaining = :fee_required_remaining where tx_hash = :tx_hash' + cursor.execute(sql, bindings) + log.message(db, block_index, 'update', 'orders', bindings) + + # If tx1 is dead, credit address directly; if not, replenish give remaining, get remaining, and fee required remaining. + orders = list(cursor.execute('''SELECT * FROM orders \ + WHERE tx_index = ?''', + (order_match['tx1_index'],))) + assert len(orders) == 1 + tx1_order = orders[0] + if tx1_order['status'] in ('expired', 'cancelled'): + tx1_order_status = tx1_order['status'] + if order_match['backward_asset'] != config.BTC: + util.credit(db, order_match['tx1_address'], + order_match['backward_asset'], + order_match['backward_quantity'], action='order {}'.format(tx1_order_status), event=order_match['id']) + else: + tx1_give_remaining = tx1_order['give_remaining'] + order_match['backward_quantity'] + tx1_get_remaining = tx1_order['get_remaining'] + order_match['forward_quantity'] + if tx1_order['get_asset'] == config.BTC and (block_index >= 297000 or config.TESTNET or config.REGTEST): # Protocol change. + tx1_fee_required_remaining = tx1_order['fee_required_remaining'] + order_match['fee_paid'] + else: + tx1_fee_required_remaining = tx1_order['fee_required_remaining'] + tx1_order_status = tx1_order['status'] + bindings = { + 'give_remaining': tx1_give_remaining, + 'get_remaining': tx1_get_remaining, + 'status': tx1_order_status, + 'fee_required_remaining': tx1_fee_required_remaining, + 'tx_hash': order_match['tx1_hash'] + } + sql='update orders set give_remaining = :give_remaining, get_remaining = :get_remaining, fee_required_remaining = :fee_required_remaining where tx_hash = :tx_hash' + cursor.execute(sql, bindings) + log.message(db, block_index, 'update', 'orders', bindings) + + if block_index < 286500: # Protocol change. + # Sanity check: one of the two must have expired. + tx0_order_time_left = tx0_order['expire_index'] - block_index + tx1_order_time_left = tx1_order['expire_index'] - block_index + assert tx0_order_time_left or tx1_order_time_left + + # Penalize tardiness. + if block_index >= 313900 or config.TESTNET or config.REGTEST: # Protocol change. + if tx0_order['status'] == 'expired' and order_match['forward_asset'] == config.BTC: + exact_penalty(db, order_match['tx0_address'], block_index, order_match['id']) + if tx1_order['status'] == 'expired' and order_match['backward_asset'] == config.BTC: + exact_penalty(db, order_match['tx1_address'], block_index, order_match['id']) + + # Re‐match. + if block_index >= 310000 or config.TESTNET or config.REGTEST: # Protocol change. + if not (block_index >= 315000 or config.TESTNET or config.REGTEST): # Protocol change. + cursor.execute('''SELECT * FROM transactions\ + WHERE tx_hash = ?''', (tx0_order['tx_hash'],)) + match(db, list(cursor)[0], block_index) + cursor.execute('''SELECT * FROM transactions\ + WHERE tx_hash = ?''', (tx1_order['tx_hash'],)) + match(db, list(cursor)[0], block_index) + + if status == 'expired': + # Record order match expiration. + bindings = { + 'order_match_id': order_match['id'], + 'tx0_address': order_match['tx0_address'], + 'tx1_address': order_match['tx1_address'], + 'block_index': block_index + } + sql='insert into order_match_expirations values(:order_match_id, :tx0_address, :tx1_address, :block_index)' + cursor.execute(sql, bindings) + + cursor.close() + + +def validate (db, source, give_asset, give_quantity, get_asset, get_quantity, expiration, fee_required, block_index): + problems = [] + cursor = db.cursor() + + # For SQLite3 + if give_quantity > config.MAX_INT or get_quantity > config.MAX_INT or fee_required > config.MAX_INT or block_index + expiration > config.MAX_INT: + problems.append('integer overflow') + + if give_asset == config.BTC and get_asset == config.BTC: + problems.append('cannot trade {} for itself'.format(config.BTC)) + + if not isinstance(give_quantity, int): + problems.append('give_quantity must be in satoshis') + return problems + if not isinstance(get_quantity, int): + problems.append('get_quantity must be in satoshis') + return problems + if not isinstance(fee_required, int): + problems.append('fee_required must be in satoshis') + return problems + if not isinstance(expiration, int): + problems.append('expiration must be expressed as an integer block delta') + return problems + + if give_quantity <= 0: problems.append('non‐positive give quantity') + if get_quantity <= 0: problems.append('non‐positive get quantity') + if fee_required < 0: problems.append('negative fee_required') + if expiration < 0: problems.append('negative expiration') + if expiration == 0 and not (block_index >= 317500 or config.TESTNET or config.REGTEST): # Protocol change. + problems.append('zero expiration') + + if not give_quantity or not get_quantity: + problems.append('zero give or zero get') + cursor.execute('select * from issuances where (status = ? and asset = ?)', ('valid', give_asset)) + if give_asset not in (config.BTC, config.XCP) and not cursor.fetchall(): + problems.append('no such asset to give ({})'.format(give_asset)) + cursor.execute('select * from issuances where (status = ? and asset = ?)', ('valid', get_asset)) + if get_asset not in (config.BTC, config.XCP) and not cursor.fetchall(): + problems.append('no such asset to get ({})'.format(get_asset)) + if expiration > config.MAX_EXPIRATION: + problems.append('expiration overflow') + + cursor.close() + return problems + +def compose (db, source, give_asset, give_quantity, get_asset, get_quantity, expiration, fee_required): + cursor = db.cursor() + + # resolve subassets + give_asset = util.resolve_subasset_longname(db, give_asset) + get_asset = util.resolve_subasset_longname(db, get_asset) + + # Check balance. + if give_asset != config.BTC: + balances = list(cursor.execute('''SELECT * FROM balances WHERE (address = ? AND asset = ?)''', (source, give_asset))) + if (not balances or balances[0]['quantity'] < give_quantity): + raise exceptions.ComposeError('insufficient funds') + + problems = validate(db, source, give_asset, give_quantity, get_asset, get_quantity, expiration, fee_required, util.CURRENT_BLOCK_INDEX) + if problems: raise exceptions.ComposeError(problems) + + give_id = util.get_asset_id(db, give_asset, util.CURRENT_BLOCK_INDEX) + get_id = util.get_asset_id(db, get_asset, util.CURRENT_BLOCK_INDEX) + data = message_type.pack(ID) + data += struct.pack(FORMAT, give_id, give_quantity, get_id, get_quantity, + expiration, fee_required) + cursor.close() + return (source, [], data) + +def parse (db, tx, message): + order_parse_cursor = db.cursor() + + # Unpack message. + try: + if len(message) != LENGTH: + raise exceptions.UnpackError + give_id, give_quantity, get_id, get_quantity, expiration, fee_required = struct.unpack(FORMAT, message) + give_asset = util.get_asset_name(db, give_id, tx['block_index']) + get_asset = util.get_asset_name(db, get_id, tx['block_index']) + status = 'open' + except (exceptions.UnpackError, exceptions.AssetNameError, struct.error) as e: + give_asset, give_quantity, get_asset, get_quantity, expiration, fee_required = 0, 0, 0, 0, 0, 0 + status = 'invalid: could not unpack' + + price = 0 + if status == 'open': + try: + price = util.price(get_quantity, give_quantity) + except ZeroDivisionError: + price = 0 + + # Overorder + order_parse_cursor.execute('''SELECT * FROM balances \ + WHERE (address = ? AND asset = ?)''', (tx['source'], give_asset)) + balances = list(order_parse_cursor) + if give_asset != config.BTC: + if not balances: + give_quantity = 0 + else: + balance = balances[0]['quantity'] + if balance < give_quantity: + give_quantity = balance + get_quantity = int(price * give_quantity) + + problems = validate(db, tx['source'], give_asset, give_quantity, get_asset, get_quantity, expiration, fee_required, tx['block_index']) + if problems: status = 'invalid: ' + '; '.join(problems) + + if util.enabled('btc_order_minimum'): + min_btc_quantity = 0.001 * config.UNIT # 0.001 BTC + if util.enabled('btc_order_minimum_adjustment_1'): + min_btc_quantity = 0.00001 * config.UNIT # 0.00001 BTC + + if (give_asset == config.BTC and give_quantity < min_btc_quantity) or (get_asset == config.BTC and get_quantity < min_btc_quantity): + if problems: + status += '; btc order below minimum' + else: + status = 'invalid: btc order below minimum' + + # Debit give quantity. (Escrow.) + if status == 'open': + if give_asset != config.BTC: # No need (or way) to debit BTC. + util.debit(db, tx['source'], give_asset, give_quantity, action='open order', event=tx['tx_hash']) + + # Add parsed transaction to message-type–specific table. + bindings = { + 'tx_index': tx['tx_index'], + 'tx_hash': tx['tx_hash'], + 'block_index': tx['block_index'], + 'source': tx['source'], + 'give_asset': give_asset, + 'give_quantity': give_quantity, + 'give_remaining': give_quantity, + 'get_asset': get_asset, + 'get_quantity': get_quantity, + 'get_remaining': get_quantity, + 'expiration': expiration, + 'expire_index': tx['block_index'] + expiration, + 'fee_required': fee_required, + 'fee_required_remaining': fee_required, + 'fee_provided': tx['fee'], + 'fee_provided_remaining': tx['fee'], + 'status': status, + } + if "integer overflow" not in status: + sql = 'insert into orders values(:tx_index, :tx_hash, :block_index, :source, :give_asset, :give_quantity, :give_remaining, :get_asset, :get_quantity, :get_remaining, :expiration, :expire_index, :fee_required, :fee_required_remaining, :fee_provided, :fee_provided_remaining, :status)' + order_parse_cursor.execute(sql, bindings) + else: + logger.warn("Not storing [order] tx [%s]: %s" % (tx['tx_hash'], status)) + logger.debug("Bindings: %s" % (json.dumps(bindings), )) + + # Match. + if status == 'open' and tx['block_index'] != config.MEMPOOL_BLOCK_INDEX: + match(db, tx) + + order_parse_cursor.close() + +def match (db, tx, block_index=None): + + cursor = db.cursor() + + # Get order in question. + orders = list(cursor.execute('''SELECT * FROM orders\ + WHERE (tx_index = ? AND status = ?)''', (tx['tx_index'], 'open'))) + if not orders: + cursor.close() + return + else: + assert len(orders) == 1 + tx1 = orders[0] + + cursor.execute('''SELECT * FROM orders \ + WHERE (give_asset=? AND get_asset=? AND status=? AND tx_hash != ?)''', + (tx1['get_asset'], tx1['give_asset'], 'open', tx1['tx_hash'])) + + tx1_give_remaining = tx1['give_remaining'] + tx1_get_remaining = tx1['get_remaining'] + + order_matches = cursor.fetchall() + if tx['block_index'] > 284500 or config.TESTNET or config.REGTEST: # Protocol change. + order_matches = sorted(order_matches, key=lambda x: x['tx_index']) # Sort by tx index second. + order_matches = sorted(order_matches, key=lambda x: util.price(x['get_quantity'], x['give_quantity'])) # Sort by price first. + + # Get fee remaining. + tx1_fee_required_remaining = tx1['fee_required_remaining'] + tx1_fee_provided_remaining = tx1['fee_provided_remaining'] + + tx1_status = tx1['status'] + for tx0 in order_matches: + order_match_id = util.make_id(tx0['tx_hash'], tx1['tx_hash']) + if not block_index: + block_index = max(tx0['block_index'], tx1['block_index']) + if tx1_status != 'open': break + + logger.debug('Considering: ' + tx0['tx_hash']) + tx0_give_remaining = tx0['give_remaining'] + tx0_get_remaining = tx0['get_remaining'] + + # Ignore previous matches. (Both directions, just to be sure.) + cursor.execute('''SELECT * FROM order_matches + WHERE id = ? ''', (util.make_id(tx0['tx_hash'], tx1['tx_hash']), )) + if list(cursor): + logger.debug('Skipping: previous match') + continue + cursor.execute('''SELECT * FROM order_matches + WHERE id = ? ''', (util.make_id(tx1['tx_hash'], tx0['tx_hash']), )) + if list(cursor): + logger.debug('Skipping: previous match') + continue + + # Get fee provided remaining. + tx0_fee_required_remaining = tx0['fee_required_remaining'] + tx0_fee_provided_remaining = tx0['fee_provided_remaining'] + + # Make sure that that both orders still have funds remaining (if order involves BTC, and so cannot be ‘filled’). + if tx0['give_asset'] == config.BTC or tx0['get_asset'] == config.BTC: # Gratuitous + if tx0_give_remaining <= 0 or tx1_give_remaining <= 0: + logger.debug('Skipping: negative give quantity remaining') + continue + if block_index >= 292000 and block_index <= 310500 and not config.TESTNET or config.REGTEST: # Protocol changes + if tx0_get_remaining <= 0 or tx1_get_remaining <= 0: + logger.debug('Skipping: negative get quantity remaining') + continue + + if block_index >= 294000 or config.TESTNET or config.REGTEST: # Protocol change. + if tx0['fee_required_remaining'] < 0: + logger.debug('Skipping: negative tx0 fee required remaining') + continue + if tx0['fee_provided_remaining'] < 0: + logger.debug('Skipping: negative tx0 fee provided remaining') + continue + if tx1_fee_provided_remaining < 0: + logger.debug('Skipping: negative tx1 fee provided remaining') + continue + if tx1_fee_required_remaining < 0: + logger.debug('Skipping: negative tx1 fee required remaining') + continue + + # If the prices agree, make the trade. The found order sets the price, + # and they trade as much as they can. + tx0_price = util.price(tx0['get_quantity'], tx0['give_quantity']) + tx1_price = util.price(tx1['get_quantity'], tx1['give_quantity']) + tx1_inverse_price = util.price(tx1['give_quantity'], tx1['get_quantity']) + + # Protocol change. + if tx['block_index'] < 286000: tx1_inverse_price = util.price(1, tx1_price) + + logger.debug('Tx0 Price: {}; Tx1 Inverse Price: {}'.format(float(tx0_price), float(tx1_inverse_price))) + if tx0_price > tx1_inverse_price: + logger.debug('Skipping: price mismatch.') + else: + logger.debug('Potential forward quantities: {}, {}'.format(tx0_give_remaining, int(util.price(tx1_give_remaining, tx0_price)))) + forward_quantity = int(min(tx0_give_remaining, int(util.price(tx1_give_remaining, tx0_price)))) + logger.debug('Forward Quantity: {}'.format(forward_quantity)) + backward_quantity = round(forward_quantity * tx0_price) + logger.debug('Backward Quantity: {}'.format(backward_quantity)) + + if not forward_quantity: + logger.debug('Skipping: zero forward quantity.') + continue + if block_index >= 286500 or config.TESTNET or config.REGTEST: # Protocol change. + if not backward_quantity: + logger.debug('Skipping: zero backward quantity.') + continue + + forward_asset, backward_asset = tx1['get_asset'], tx1['give_asset'] + + if block_index >= 313900 or config.TESTNET or config.REGTEST: # Protocol change. + min_btc_quantity = 0.001 * config.UNIT # 0.001 BTC + if (forward_asset == config.BTC and forward_quantity <= min_btc_quantity) or (backward_asset == config.BTC and backward_quantity <= min_btc_quantity): + logger.debug('Skipping: below minimum {} quantity'.format(config.BTC)) + continue + + # Check and update fee remainings. + fee = 0 + if block_index >= 286500 or config.TESTNET or config.REGTEST: # Protocol change. Deduct fee_required from provided_remaining, etc., if possible (else don’t match). + if tx1['get_asset'] == config.BTC: + + if block_index >= 310500 or config.TESTNET or config.REGTEST: # Protocol change. + fee = int(tx1['fee_required'] * util.price(backward_quantity, tx1['give_quantity'])) + else: + fee = int(tx1['fee_required_remaining'] * util.price(forward_quantity, tx1_get_remaining)) + + logger.debug('Tx0 fee provided remaining: {}; required fee: {}'.format(tx0_fee_provided_remaining / config.UNIT, fee / config.UNIT)) + if tx0_fee_provided_remaining < fee: + logger.debug('Skipping: tx0 fee provided remaining is too low.') + continue + else: + tx0_fee_provided_remaining -= fee + if block_index >= 287800 or config.TESTNET or config.REGTEST: # Protocol change. + tx1_fee_required_remaining -= fee + + elif tx1['give_asset'] == config.BTC: + + if block_index >= 310500 or config.TESTNET or config.REGTEST: # Protocol change. + fee = int(tx0['fee_required'] * util.price(backward_quantity, tx0['give_quantity'])) + else: + fee = int(tx0['fee_required_remaining'] * util.price(backward_quantity, tx0_get_remaining)) + + logger.debug('Tx1 fee provided remaining: {}; required fee: {}'.format(tx1_fee_provided_remaining / config.UNIT, fee / config.UNIT)) + if tx1_fee_provided_remaining < fee: + logger.debug('Skipping: tx1 fee provided remaining is too low.') + continue + else: + tx1_fee_provided_remaining -= fee + if block_index >= 287800 or config.TESTNET or config.REGTEST: # Protocol change. + tx0_fee_required_remaining -= fee + + else: # Don’t deduct. + if tx1['get_asset'] == config.BTC: + if tx0_fee_provided_remaining < tx1['fee_required']: continue + elif tx1['give_asset'] == config.BTC: + if tx1_fee_provided_remaining < tx0['fee_required']: continue + + if config.BTC in (tx1['give_asset'], tx1['get_asset']): + status = 'pending' + else: + status = 'completed' + # Credit. + util.credit(db, tx1['source'], tx1['get_asset'], + forward_quantity, action='order match', event=order_match_id) + util.credit(db, tx0['source'], tx0['get_asset'], + backward_quantity, action='order match', event=order_match_id) + + # Debit the order, even if it involves giving bitcoins, and so one + # can't debit the sending account. + # Get remainings may be negative. + tx0_give_remaining -= forward_quantity + tx0_get_remaining -= backward_quantity + tx1_give_remaining -= backward_quantity + tx1_get_remaining -= forward_quantity + + # Update give_remaining, get_remaining. + # tx0 + tx0_status = 'open' + if tx0_give_remaining <= 0 or (tx0_get_remaining <= 0 and (block_index >= 292000 or config.TESTNET or config.REGTEST)): # Protocol change + if tx0['give_asset'] != config.BTC and tx0['get_asset'] != config.BTC: + # Fill order, and recredit give_remaining. + tx0_status = 'filled' + util.credit(db, tx0['source'], tx0['give_asset'], tx0_give_remaining, event=tx1['tx_hash'], action='filled') + bindings = { + 'give_remaining': tx0_give_remaining, + 'get_remaining': tx0_get_remaining, + 'fee_required_remaining': tx0_fee_required_remaining, + 'fee_provided_remaining': tx0_fee_provided_remaining, + 'status': tx0_status, + 'tx_hash': tx0['tx_hash'] + } + sql='update orders set give_remaining = :give_remaining, get_remaining = :get_remaining, fee_required_remaining = :fee_required_remaining, fee_provided_remaining = :fee_provided_remaining, status = :status where tx_hash = :tx_hash' + cursor.execute(sql, bindings) + log.message(db, block_index, 'update', 'orders', bindings) + # tx1 + if tx1_give_remaining <= 0 or (tx1_get_remaining <= 0 and (block_index >= 292000 or config.TESTNET or config.REGTEST)): # Protocol change + if tx1['give_asset'] != config.BTC and tx1['get_asset'] != config.BTC: + # Fill order, and recredit give_remaining. + tx1_status = 'filled' + util.credit(db, tx1['source'], tx1['give_asset'], tx1_give_remaining, event=tx0['tx_hash'], action='filled') + bindings = { + 'give_remaining': tx1_give_remaining, + 'get_remaining': tx1_get_remaining, + 'fee_required_remaining': tx1_fee_required_remaining, + 'fee_provided_remaining': tx1_fee_provided_remaining, + 'status': tx1_status, + 'tx_hash': tx1['tx_hash'] + } + sql='update orders set give_remaining = :give_remaining, get_remaining = :get_remaining, fee_required_remaining = :fee_required_remaining, fee_provided_remaining = :fee_provided_remaining, status = :status where tx_hash = :tx_hash' + cursor.execute(sql, bindings) + log.message(db, block_index, 'update', 'orders', bindings) + + # Calculate when the match will expire. + if block_index >= 308000 or config.TESTNET or config.REGTEST: # Protocol change. + match_expire_index = block_index + 20 + elif block_index >= 286500 or config.TESTNET or config.REGTEST: # Protocol change. + match_expire_index = block_index + 10 + else: + match_expire_index = min(tx0['expire_index'], tx1['expire_index']) + + # Record order match. + bindings = { + 'id': util.make_id(tx0['tx_hash'], tx['tx_hash']), + 'tx0_index': tx0['tx_index'], + 'tx0_hash': tx0['tx_hash'], + 'tx0_address': tx0['source'], + 'tx1_index': tx1['tx_index'], + 'tx1_hash': tx1['tx_hash'], + 'tx1_address': tx1['source'], + 'forward_asset': forward_asset, + 'forward_quantity': forward_quantity, + 'backward_asset': backward_asset, + 'backward_quantity': backward_quantity, + 'tx0_block_index': tx0['block_index'], + 'tx1_block_index': tx1['block_index'], + 'block_index': block_index, + 'tx0_expiration': tx0['expiration'], + 'tx1_expiration': tx1['expiration'], + 'match_expire_index': match_expire_index, + 'fee_paid': fee, + 'status': status, + } + sql='insert into order_matches values(:id, :tx0_index, :tx0_hash, :tx0_address, :tx1_index, :tx1_hash, :tx1_address, :forward_asset, :forward_quantity, :backward_asset, :backward_quantity, :tx0_block_index, :tx1_block_index, :block_index, :tx0_expiration, :tx1_expiration, :match_expire_index, :fee_paid, :status)' + cursor.execute(sql, bindings) + + if tx1_status == 'filled': + break + + cursor.close() + return + +def expire (db, block_index): + cursor = db.cursor() + + # Expire orders and give refunds for the quantity give_remaining (if non-zero; if not BTC). + cursor.execute('''SELECT * FROM orders \ + WHERE (status = ? AND expire_index < ?)''', ('open', block_index)) + orders = list(cursor) + for order in orders: + cancel_order(db, order, 'expired', block_index) + + # Expire order_matches for BTC with no BTC. + cursor.execute('''SELECT * FROM order_matches \ + WHERE (status = ? and match_expire_index < ?)''', ('pending', block_index)) + order_matches = list(cursor) + for order_match in order_matches: + cancel_order_match(db, order_match, 'expired', block_index) + + # Expire btc sell order if match expires + if util.enabled('btc_sell_expire_on_match_expire'): + # Check for other pending order matches involving either tx0_hash or tx1_hash + bindings = { + 'status': 'pending', + 'tx0_hash': order_match['tx0_hash'], + 'tx1_hash': order_match['tx1_hash'] + } + sql='select * from order_matches where status = :status and ((tx0_hash in (:tx0_hash, :tx1_hash)) or ((tx1_hash in (:tx0_hash, :tx1_hash))))' + cursor.execute(sql, bindings) + order_matches_pending = cursor.fetchall() + # Set BTC sell order status as expired only if there are no pending order matches + if len(order_matches_pending) == 0: + if order_match['backward_asset'] == "BTC" and order_match['status'] == "expired": + cursor.execute('''SELECT * FROM orders \ + WHERE tx_hash = ?''', (order_match['tx1_hash'],)) + cancel_order(db, list(cursor)[0], 'expired', block_index) + if order_match['forward_asset'] == "BTC" and order_match['status'] == "expired": + cursor.execute('''SELECT * FROM orders \ + WHERE tx_hash = ?''', (order_match['tx0_hash'],)) + cancel_order(db, list(cursor)[0], 'expired', block_index) + + if block_index >= 315000 or config.TESTNET or config.REGTEST: # Protocol change. + # Re‐match. + for order_match in order_matches: + cursor.execute('''SELECT * FROM transactions\ + WHERE tx_hash = ?''', (order_match['tx0_hash'],)) + match(db, list(cursor)[0], block_index) + cursor.execute('''SELECT * FROM transactions\ + WHERE tx_hash = ?''', (order_match['tx1_hash'],)) + match(db, list(cursor)[0], block_index) + + cursor.close() + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/aspirelib/lib/util.py b/aspirelib/lib/util.py index 3dc5205b..3743914e 100644 --- a/aspirelib/lib/util.py +++ b/aspirelib/lib/util.py @@ -551,9 +551,33 @@ def holders(db, asset): ORDER BY quantity DESC''', (asset,)) for balance in list(cursor): holders.append({'address': balance['address'], 'address_quantity': balance['quantity'], 'escrow': None}) + # Funds escrowed in orders. (Protocol change.) + cursor.execute('''SELECT * FROM orders \ + WHERE give_asset = ? AND status = ?''', (asset, 'open')) + for order in list(cursor): + holders.append({'address': order['source'], 'address_quantity': order['give_remaining'], 'escrow': order['tx_hash']}) + # Funds escrowed in pending order matches. (Protocol change.) + cursor.execute('''SELECT * FROM order_matches \ + WHERE (forward_asset = ? AND status = ?)''', (asset, 'pending')) + for order_match in list(cursor): + holders.append({'address': order_match['tx0_address'], 'address_quantity': order_match['forward_quantity'], 'escrow': order_match['id']}) + cursor.execute('''SELECT * FROM order_matches \ + WHERE (backward_asset = ? AND status = ?)''', (asset, 'pending')) + for order_match in list(cursor): + holders.append({'address': order_match['tx1_address'], 'address_quantity': order_match['backward_quantity'], 'escrow': order_match['id']}) # escrow ASP if asset == config.XCP: + cursor.execute('''SELECT * FROM bets \ + WHERE status = ?''', ('open',)) + for bet in list(cursor): + holders.append({'address': bet['source'], 'address_quantity': bet['wager_remaining'], 'escrow': bet['tx_hash']}) + cursor.execute('''SELECT * FROM bet_matches \ + WHERE status = ?''', ('pending',)) + for bet_match in list(cursor): + holders.append({'address': bet_match['tx0_address'], 'address_quantity': bet_match['forward_quantity'], 'escrow': bet_match['id']}) + holders.append({'address': bet_match['tx1_address'], 'address_quantity': bet_match['backward_quantity'], 'escrow': bet_match['id']}) + cursor.execute('''SELECT * FROM executions WHERE status IN (?,?)''', ('valid', 'out of gas')) for execution in list(cursor): holders.append({'address': execution['source'], 'address_quantity': execution['gas_cost'], 'escrow': None})