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: # 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'], tx1['block_index'])) # 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 = tx0['tx_hash'] + tx1['tx_hash'] if not block_index: block_index = max(tx0['block_index'], tx1['block_index']) if tx1_status != 'open': break logging.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 = ? ''', (tx0['tx_hash'] + tx1['tx_hash'], )) if list(cursor): logging.debug('Skipping: previous match') continue cursor.execute('''SELECT * FROM order_matches WHERE id = ? ''', (tx1['tx_hash'] + tx0['tx_hash'], )) if list(cursor): logging.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: logging.debug('Skipping: negative give quantity remaining') continue if block_index >= 292000 and block_index <= 310500 and not config.TESTNET: # Protocol changes if tx0_get_remaining <= 0 or tx1_get_remaining <= 0: logging.debug('Skipping: negative get quantity remaining') continue if block_index >= 294000 or config.TESTNET: # Protocol change. if tx0['fee_required_remaining'] < 0: logging.debug('Skipping: negative tx0 fee required remaining') continue if tx0['fee_provided_remaining'] < 0: logging.debug('Skipping: negative tx0 fee provided remaining') continue if tx1_fee_provided_remaining < 0: logging.debug('Skipping: negative tx1 fee provided remaining') continue if tx1_fee_required_remaining < 0: logging.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'], block_index) tx1_price = util.price(tx1['get_quantity'], tx1['give_quantity'], block_index) tx1_inverse_price = util.price(tx1['give_quantity'], tx1['get_quantity'], block_index) # Protocol change. if tx['block_index'] < 286000: tx1_inverse_price = util.price(1, tx1_price, block_index) logging.debug('Tx0 Price: {}; Tx1 Inverse Price: {}'.format(float(tx0_price), float(tx1_inverse_price))) if tx0_price > tx1_inverse_price: logging.debug('Skipping: price mismatch.') else: logging.debug('Potential forward quantities: {}, {}'.format(tx0_give_remaining, int(util.price(tx1_give_remaining, tx0_price, block_index)))) forward_quantity = int(min(tx0_give_remaining, int(util.price(tx1_give_remaining, tx0_price, block_index)))) logging.debug('Forward Quantity: {}'.format(forward_quantity)) backward_quantity = round(forward_quantity * tx0_price) logging.debug('Backward Quantity: {}'.format(backward_quantity)) if not forward_quantity: logging.debug('Skipping: zero forward quantity.') continue if block_index >= 286500 or config.TESTNET: # Protocol change. if not backward_quantity: logging.debug('Skipping: zero backward quantity.') continue forward_asset, backward_asset = tx1['get_asset'], tx1['give_asset'] if block_index >= 313900 or config.TESTNET: # 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): logging.debug('Skipping: below minimum {} quantity'.format(config.BTC)) continue # Check and update fee remainings. fee = 0 if block_index >= 286500 or config.TESTNET: # 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: # Protocol change. fee = int(tx1['fee_required'] * util.price(backward_quantity, tx1['give_quantity'], block_index)) else: fee = int(tx1['fee_required_remaining'] * util.price(forward_quantity, tx1_get_remaining, block_index)) logging.debug('Tx0 fee provided remaining: {}; required fee: {}'.format(tx0_fee_provided_remaining / config.UNIT, fee / config.UNIT)) if tx0_fee_provided_remaining < fee: logging.debug('Skipping: tx0 fee provided remaining is too low.') continue else: tx0_fee_provided_remaining -= fee if block_index >= 287800 or config.TESTNET: # Protocol change. tx1_fee_required_remaining -= fee elif tx1['give_asset'] == config.BTC: if block_index >= 310500 or config.TESTNET: # Protocol change. fee = int(tx0['fee_required'] * util.price(backward_quantity, tx0['give_quantity'], block_index)) else: fee = int(tx0['fee_required_remaining'] * util.price(backward_quantity, tx0_get_remaining, block_index)) logging.debug('Tx1 fee provided remaining: {}; required fee: {}'.format(tx1_fee_provided_remaining / config.UNIT, fee / config.UNIT)) if tx1_fee_provided_remaining < fee: logging.debug('Skipping: tx1 fee provided remaining is too low.') continue else: tx1_fee_provided_remaining -= fee if block_index >= 287800 or config.TESTNET: # 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, tx['block_index'], tx1['source'], tx1['get_asset'], forward_quantity, action='order match', event=order_match_id) util.credit(db, tx['block_index'], 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)): # 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, block_index, 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) util.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)): # 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, block_index, 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) util.message(db, block_index, 'update', 'orders', bindings) # Calculate when the match will expire. if block_index >= 308000 or config.TESTNET: # Protocol change. match_expire_index = block_index + 20 elif block_index >= 286500 or config.TESTNET: # Protocol change. match_expire_index = block_index + 10 else: match_expire_index = min(tx0['expire_index'], tx1['expire_index']) # Record order match. bindings = { '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 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: # 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'], tx1['block_index'])) # Sort by price first. tx1_status = tx1['status'] for tx0 in bet_matches: if tx1_status != 'open': break logging.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']: logging.debug('Skipping: bet types disagree.') continue # Leverages must agree exactly if tx0['leverage'] != tx1['leverage']: logging.debug('Skipping: leverages disagree.') continue # Target values must agree exactly. if tx0['target_value'] != tx1['target_value']: logging.debug('Skipping: target values disagree.') continue # Fee fractions must agree exactly. if tx0['fee_fraction_int'] != tx1['fee_fraction_int']: logging.debug('Skipping: fee fractions disagree.') continue # Deadlines must agree exactly. if tx0['deadline'] != tx1['deadline']: logging.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'], tx1['block_index']) tx0_inverse_odds = util.price(tx0['counterwager_quantity'], tx0['wager_quantity'], tx1['block_index']) tx1_odds = util.price(tx1['wager_quantity'], tx1['counterwager_quantity'], tx1['block_index']) if tx['block_index'] < 286000: tx0_inverse_odds = util.price(1, tx0_odds, tx1['block_index']) # Protocol change. logging.debug('Tx0 Inverse Odds: {}; Tx1 Odds: {}'.format(float(tx0_inverse_odds), float(tx1_odds))) if tx0_inverse_odds > tx1_odds: logging.debug('Skipping: price mismatch.') else: logging.debug('Potential forward quantities: {}, {}'.format(tx0_wager_remaining, int(util.price(tx1_wager_remaining, tx1_odds, tx1['block_index'])))) forward_quantity = int(min(tx0_wager_remaining, int(util.price(tx1_wager_remaining, tx1_odds, tx1['block_index'])))) logging.debug('Forward Quantity: {}'.format(forward_quantity)) backward_quantity = round(forward_quantity / tx0_odds) logging.debug('Backward Quantity: {}'.format(backward_quantity)) if not forward_quantity: logging.debug('Skipping: zero forward quantity.') continue if tx1['block_index'] >= 286500 or config.TESTNET: # Protocol change. if not backward_quantity: logging.debug('Skipping: zero backward quantity.') continue bet_match_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, tx1['block_index'], 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) util.message(db, tx1['block_index'], 'update', 'bets', bindings) if tx1['block_index'] >= 292000 or config.TESTNET: # 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['block_index'], 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) util.message(db, tx1['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': 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 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(give_id, tx['block_index']) get_asset = util.get_asset_name(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, tx['block_index']) 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) # Debit give quantity. (Escrow.) if status == 'open': if give_asset != config.BTC: # No need (or way) to debit BTC. util.debit(db, tx['block_index'], 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, } 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) # Match. if status == 'open' and tx['block_index'] != config.MEMPOOL_BLOCK_INDEX: match(db, tx) order_parse_cursor.close()
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, tx['block_index']) 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, tx['block_index'])) 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['block_index'], 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, } 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) # 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: # 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'], tx1['block_index']) ) # Sort by price first. tx1_status = tx1['status'] for tx0 in bet_matches: if tx1_status != 'open': break logging.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']: logging.debug('Skipping: bet types disagree.') continue # Leverages must agree exactly if tx0['leverage'] != tx1['leverage']: logging.debug('Skipping: leverages disagree.') continue # Target values must agree exactly. if tx0['target_value'] != tx1['target_value']: logging.debug('Skipping: target values disagree.') continue # Fee fractions must agree exactly. if tx0['fee_fraction_int'] != tx1['fee_fraction_int']: logging.debug('Skipping: fee fractions disagree.') continue # Deadlines must agree exactly. if tx0['deadline'] != tx1['deadline']: logging.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'], tx1['block_index']) tx0_inverse_odds = util.price(tx0['counterwager_quantity'], tx0['wager_quantity'], tx1['block_index']) tx1_odds = util.price(tx1['wager_quantity'], tx1['counterwager_quantity'], tx1['block_index']) if tx['block_index'] < 286000: tx0_inverse_odds = util.price( 1, tx0_odds, tx1['block_index']) # Protocol change. logging.debug('Tx0 Inverse Odds: {}; Tx1 Odds: {}'.format( float(tx0_inverse_odds), float(tx1_odds))) if tx0_inverse_odds > tx1_odds: logging.debug('Skipping: price mismatch.') else: logging.debug('Potential forward quantities: {}, {}'.format( tx0_wager_remaining, int( util.price(tx1_wager_remaining, tx1_odds, tx1['block_index'])))) forward_quantity = int( min( tx0_wager_remaining, int( util.price(tx1_wager_remaining, tx1_odds, tx1['block_index'])))) logging.debug('Forward Quantity: {}'.format(forward_quantity)) backward_quantity = round(forward_quantity / tx0_odds) logging.debug('Backward Quantity: {}'.format(backward_quantity)) if not forward_quantity: logging.debug('Skipping: zero forward quantity.') continue if tx1['block_index'] >= 286500 or config.TESTNET: # Protocol change. if not backward_quantity: logging.debug('Skipping: zero backward quantity.') continue bet_match_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, tx1['block_index'], 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) util.message(db, tx1['block_index'], 'update', 'bets', bindings) if tx1['block_index'] >= 292000 or config.TESTNET: # 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['block_index'], 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) util.message(db, tx1['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': 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 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, tx['block_index']) 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, tx['block_index'])) 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['block_index'], 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, } 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) # Match. if status == 'open' and tx['block_index'] != config.MEMPOOL_BLOCK_INDEX: match(db, tx) bet_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: # 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'], tx1['block_index'])) # 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: # 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: # 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'], block_index) tx1_price = util.price(tx1['get_quantity'], tx1['give_quantity'], block_index) tx1_inverse_price = util.price(tx1['give_quantity'], tx1['get_quantity'], block_index) # Protocol change. if tx['block_index'] < 286000: tx1_inverse_price = util.price(1, tx1_price, block_index) 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, block_index)))) forward_quantity = int(min(tx0_give_remaining, int(util.price(tx1_give_remaining, tx0_price, block_index)))) 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: # 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: # 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: # 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: # Protocol change. fee = int(tx1['fee_required'] * util.price(backward_quantity, tx1['give_quantity'], block_index)) else: fee = int(tx1['fee_required_remaining'] * util.price(forward_quantity, tx1_get_remaining, block_index)) 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: # Protocol change. tx1_fee_required_remaining -= fee elif tx1['give_asset'] == config.BTC: if block_index >= 310500 or config.TESTNET: # Protocol change. fee = int(tx0['fee_required'] * util.price(backward_quantity, tx0['give_quantity'], block_index)) else: fee = int(tx0['fee_required_remaining'] * util.price(backward_quantity, tx0_get_remaining, block_index)) 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: # 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, tx['block_index'], tx1['source'], tx1['get_asset'], forward_quantity, action='order match', event=order_match_id) util.credit(db, tx['block_index'], 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)): # 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, block_index, 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)): # 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, block_index, 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: # Protocol change. match_expire_index = block_index + 20 elif block_index >= 286500 or config.TESTNET: # 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 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, tx['block_index']) 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) # Debit give quantity. (Escrow.) if status == 'open': if give_asset != config.BTC: # No need (or way) to debit BTC. util.debit(db, tx['block_index'], 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, } 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) # Match. if status == 'open' and tx['block_index'] != config.MEMPOOL_BLOCK_INDEX: match(db, tx) order_parse_cursor.close()