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_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 dispense(db, tx): cursor = db.cursor() cursor.execute('SELECT * FROM dispensers WHERE source=:source AND status=:status', { 'source': tx['destination'], 'status': STATUS_OPEN }) dispensers = cursor.fetchall() for dispenser in dispensers: must_give = int(floor(tx['btc_amount'] / dispenser['satoshirate'])) remaining = int(floor(dispenser['give_remaining'] / dispenser['give_quantity'])) actually_given = min(must_give, remaining) * dispenser['give_quantity'] give_remaining = dispenser['give_remaining'] - actually_given assert give_remaining >= 0 util.credit(db, tx['source'], dispenser['asset'], actually_given, action='dispense', event=tx['tx_hash']) #print(cursor.execute('select * from credits where address=? and calling_function=?', (tx['source'],'dispense')).fetchall()) dispenser['give_remaining'] = give_remaining if give_remaining < dispenser['give_quantity']: # close the dispenser dispenser['give_remaining'] = 0 if give_remaining > 0: # return the remaining to the owner util.credit(db, dispenser['source'], dispenser['asset'], give_remaining, action='dispenser close', event=tx['tx_hash']) dispenser['status'] = STATUS_CLOSED cursor.execute('UPDATE DISPENSERS SET give_remaining=:give_remaining, status=:status \ WHERE source=:source AND asset=:asset AND satoshirate=:satoshirate AND give_quantity=:give_quantity', dispenser) cursor.close()
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 parse (db, tx, message): cursor = db.cursor() # Unpack message. try: if len(message) != LENGTH: raise exceptions.UnpackError tx0_hash_bytes, tx1_hash_bytes = struct.unpack(FORMAT, message) tx0_hash, tx1_hash = binascii.hexlify(tx0_hash_bytes).decode('utf-8'), binascii.hexlify(tx1_hash_bytes).decode('utf-8') order_match_id = util.make_id(tx0_hash, tx1_hash) status = 'valid' except (exceptions.UnpackError, struct.error) as e: tx0_hash, tx1_hash, order_match_id = None, None, None status = 'invalid: could not unpack' if status == 'valid': destination, btc_quantity, escrowed_asset, escrowed_quantity, order_match, problems = validate(db, tx['source'], order_match_id, tx['block_index']) if problems: order_match = None status = 'invalid: ' + '; '.join(problems) if status == 'valid': # BTC must be paid all at once. if tx['btc_amount'] >= btc_quantity: # Credit source address for the currency that he bought with the bitcoins. util.credit(db, tx['source'], escrowed_asset, escrowed_quantity, action='btcpay', event=tx['tx_hash']) status = 'valid' # Update order match. bindings = { 'status': 'completed', '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, tx['block_index'], 'update', 'order_matches', bindings) # 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'], 'destination': tx['destination'], 'btc_amount': tx['btc_amount'], 'order_match_id': order_match_id, 'status': status, } if "integer overflow" not in status: sql = 'insert into btcpays values(:tx_index, :tx_hash, :block_index, :source, :destination, :btc_amount, :order_match_id, :status)' cursor.execute(sql, bindings) else: logger.warn("Not storing [btcpay] tx [%s]: %s" % (tx['tx_hash'], status)) logger.debug("Bindings: %s" % (json.dumps(bindings), )) cursor.close()
def parse (db, tx, message): dividend_parse_cursor = db.cursor() # Unpack message. try: if (tx['block_index'] > 288150 or config.TESTNET) and len(message) == LENGTH_2: quantity_per_unit, asset_id, dividend_asset_id = struct.unpack(FORMAT_2, message) asset = util.get_asset_name(db, asset_id, tx['block_index']) dividend_asset = util.get_asset_name(db, dividend_asset_id, tx['block_index']) status = 'valid' elif len(message) == LENGTH_1: quantity_per_unit, asset_id = struct.unpack(FORMAT_1, message) asset = util.get_asset_name(db, asset_id, tx['block_index']) dividend_asset = config.XCP status = 'valid' else: raise exceptions.UnpackError except (exceptions.UnpackError, exceptions.AssetNameError, struct.error) as e: dividend_asset, quantity_per_unit, asset = None, None, None status = 'invalid: could not unpack' if dividend_asset == config.BTC: status = 'invalid: cannot pay {} dividends within protocol'.format(config.BTC) if status == 'valid': # For SQLite3 quantity_per_unit = min(quantity_per_unit, config.MAX_INT) dividend_total, outputs, problems, fee = validate(db, tx['source'], quantity_per_unit, asset, dividend_asset, block_index=tx['block_index']) if problems: status = 'invalid: ' + '; '.join(problems) if status == 'valid': # Debit. util.debit(db, tx['source'], dividend_asset, dividend_total, action='dividend', event=tx['tx_hash']) if tx['block_index'] >= 330000 or config.TESTNET: # Protocol change. util.debit(db, tx['source'], config.XCP, fee, action='dividend fee', event=tx['tx_hash']) # Credit. for output in outputs: util.credit(db, output['address'], dividend_asset, output['dividend_quantity'], action='dividend', 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'], 'asset': asset, 'dividend_asset': dividend_asset, 'quantity_per_unit': quantity_per_unit, 'fee_paid': fee, 'status': status, } sql='insert into dividends values(:tx_index, :tx_hash, :block_index, :source, :asset, :dividend_asset, :quantity_per_unit, :fee_paid, :status)' dividend_parse_cursor.execute(sql, bindings) dividend_parse_cursor.close()
def dispense(db, tx): cursor = db.cursor() cursor.execute('SELECT * FROM dispensers WHERE source=:source AND status=:status ORDER BY asset', { 'source': tx['destination'], 'status': STATUS_OPEN }) dispensers = cursor.fetchall() dispense_index = 0 for dispenser in dispensers: satoshirate = dispenser['satoshirate'] give_quantity = dispenser['give_quantity'] if satoshirate > 0 and give_quantity > 0: must_give = int(floor(tx['btc_amount'] / satoshirate)) remaining = int(floor(dispenser['give_remaining'] / give_quantity)) actually_given = min(must_give, remaining) * give_quantity give_remaining = dispenser['give_remaining'] - actually_given assert give_remaining >= 0 # Skip dispense if quantity is 0 if util.enabled('zero_quantity_value_adjustment_1') and actually_given==0: continue util.credit(db, tx['source'], dispenser['asset'], actually_given, action='dispense', event=tx['tx_hash']) dispenser['give_remaining'] = give_remaining if give_remaining < dispenser['give_quantity']: # close the dispenser dispenser['give_remaining'] = 0 if give_remaining > 0: # return the remaining to the owner util.credit(db, dispenser['source'], dispenser['asset'], give_remaining, action='dispenser close', event=tx['tx_hash']) dispenser['status'] = STATUS_CLOSED dispenser['block_index'] = tx['block_index'] dispenser['prev_status'] = STATUS_OPEN cursor.execute('UPDATE DISPENSERS SET give_remaining=:give_remaining, status=:status \ WHERE source=:source AND asset=:asset AND satoshirate=:satoshirate AND give_quantity=:give_quantity AND status=:prev_status', dispenser) bindings = { 'tx_index': tx['tx_index'], 'tx_hash': tx['tx_hash'], 'dispense_index': dispense_index, 'block_index': tx['block_index'], 'source': tx['destination'], 'destination': tx['source'], 'asset': dispenser['asset'], 'dispense_quantity': actually_given, 'dispenser_tx_hash': dispenser['tx_hash'] } sql = 'INSERT INTO dispenses(tx_index, dispense_index, tx_hash, block_index, source, destination, asset, dispense_quantity, dispenser_tx_hash) \ VALUES(:tx_index, :dispense_index, :tx_hash, :block_index, :source, :destination, :asset, :dispense_quantity, :dispenser_tx_hash);' cursor.execute(sql, bindings) dispense_index += 1 cursor.close()
def create_contract(self, code, endowment=0, sender=''): if not sender: sender = '82a978b3f5962a5b0957d9ee9eef472ee55b42f1' # PyEthereum uses ECDSA to derive this string from `sender = 0`. util.credit(db, sender, config.XCP, max(endowment*2, 100000000), action='unit test', event='facefeed') success, data = tester.state.do_send(self, sender, '', endowment, data=code) contract_id = data return contract_id
def dispense(db, tx): cursor = db.cursor() cursor.execute('SELECT * FROM dispensers WHERE source=:source AND status=:status', { 'source': tx['destination'], 'status': STATUS_OPEN }) dispensers = cursor.fetchall() dispense_logs = [] for dispenser in dispensers: # They should be rejected in `validate()`. assert dispenser['satoshirate'] > 0 assert dispenser['give_quantity'] > 0 must_give = int(floor(tx['btc_amount'] / dispenser['satoshirate'])) remaining = int(floor(dispenser['give_remaining'] / dispenser['give_quantity'])) actually_given = min(must_give, remaining) * dispenser['give_quantity'] give_remaining = dispenser['give_remaining'] - actually_given assert give_remaining >= 0 # Skip dispense if quantity is 0 if util.enabled('zero_quantity_value_adjustment_1') and actually_given==0: continue util.credit(db, tx['source'], dispenser['asset'], actually_given, action='dispense', event=tx['tx_hash']) dispense_logs.append({ "tx_index": tx['tx_index'], "tx_hash": tx['tx_hash'], "block_index": tx['block_index'], "source": tx['source'], "destination": tx['destination'], "asset": dispenser['asset'], "must_give": must_give, "remaining": remaining, "actually_given": actually_given, "satoshirate": dispenser['satoshirate'], "dispenser_tx_hash": dispenser['tx_hash'] }) dispenser['give_remaining'] = give_remaining if give_remaining < dispenser['give_quantity']: # close the dispenser dispenser['give_remaining'] = 0 if give_remaining > 0: # return the remaining to the owner util.credit(db, dispenser['source'], dispenser['asset'], give_remaining, action='dispenser close', event=tx['tx_hash']) dispenser['status'] = STATUS_CLOSED dispenser['block_index'] = tx['block_index'] dispenser['prev_status'] = STATUS_OPEN cursor.execute('UPDATE DISPENSERS SET give_remaining=:give_remaining, status=:status \ WHERE source=:source AND asset=:asset AND satoshirate=:satoshirate AND give_quantity=:give_quantity AND status=:prev_status', dispenser) for log in dispense_logs: cursor.execute('INSERT INTO DISPENSES VALUES (:tx_index, :tx_hash, :block_index, :source, :destination, :asset, :must_give, :remaining, :actually_given, :satoshirate, :dispenser_tx_hash)', log)
def send (self, sender, to, value, data=[]): # print('tuple', sender, to, value, data) # Encode data. data = serpent.encode_datalist(data) data = binascii.unhexlify(data) # Execute contract. # print('qux', data, type(data)) util.credit(db, sender, config.XCP, value + 100000000, action='unit test', event='facefeed') success, output = tester.state.do_send(self, sender, to, value, data=data) if output: return rlp.decode_datalist(bytes(output)) else: return []
def cancel_rps (db, rps, status, block_index): cursor = db.cursor() # Update status of rps. bindings = { 'status': status, 'tx_hash': rps['tx_hash'] } sql='''UPDATE rps SET status = :status WHERE tx_hash = :tx_hash''' cursor.execute(sql, bindings) log.message(db, block_index, 'update', 'rps', bindings) util.credit(db, rps['source'], 'XCP', rps['wager'], action='recredit wager', event=rps['tx_hash']) cursor.close()
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 create_contract(self, code, endowment=0, sender=''): if not sender: sender = '82a978b3f5962a5b0957d9ee9eef472ee55b42f1' # PyEthereum uses ECDSA to derive this string from `sender = 0`. util.credit(db, sender, config.XCP, max(endowment * 2, 100000000), action='unit test', event='facefeed') success, data = tester.state.do_send(self, sender, '', endowment, data=code) contract_id = data return contract_id
def transfer_value(self, tx, source, destination, quantity, asset=config.XCP): if source: util.debit(self.db, source, asset, quantity, action='transfer value', event=tx.tx_hash) if destination: util.credit(self.db, destination, asset, quantity, action='transfer value', event=tx.tx_hash) return True
def update_rps_match_status (db, rps_match, status, block_index): cursor = db.cursor() if status in ['expired', 'concluded: tie']: # Recredit tx0 address. util.credit(db, rps_match['tx0_address'], 'XCP', rps_match['wager'], action='recredit wager', event=rps_match['id']) # Recredit tx1 address. util.credit(db, rps_match['tx1_address'], 'XCP', rps_match['wager'], action='recredit wager', event=rps_match['id']) elif status.startswith('concluded'): # Credit the winner winner = rps_match['tx0_address'] if status == 'concluded: first player wins' else rps_match['tx1_address'] util.credit(db, winner, 'XCP', 2 * rps_match['wager'], action='wins', event=rps_match['id']) # Update status of rps match. bindings = { 'status': status, 'rps_match_id': rps_match['id'] } sql='UPDATE rps_matches SET status = :status WHERE id = :rps_match_id' cursor.execute(sql, bindings) log.message(db, block_index, 'update', 'rps_matches', bindings) cursor.close()
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 send(self, sender, to, value, data=[]): # print('tuple', sender, to, value, data) # Encode data. data = serpent.encode_datalist(data) data = binascii.unhexlify(data) # Execute contract. # print('qux', data, type(data)) util.credit(db, sender, config.XCP, value + 100000000, action='unit test', event='facefeed') success, output = tester.state.do_send(self, sender, to, value, data=data) if output: return rlp.decode_datalist(bytes(output)) else: return []
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 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 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'])) # 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']) 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: # 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'])) 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: # 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'])) 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: # 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)): # 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)): # 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: # 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): issuance_parse_cursor = db.cursor() # Unpack message. try: if (tx['block_index'] > 283271 or config.TESTNET) and len(message) >= LENGTH_2: # Protocol change. if len(message) - LENGTH_2 <= 42: curr_format = FORMAT_2 + '{}p'.format(len(message) - LENGTH_2) else: curr_format = FORMAT_2 + '{}s'.format(len(message) - LENGTH_2) asset_id, quantity, divisible, callable_, call_date, call_price, description = struct.unpack(curr_format, message) call_price = round(call_price, 6) # TODO: arbitrary try: description = description.decode('utf-8') except UnicodeDecodeError: description = '' else: if len(message) != LENGTH_1: raise exceptions.UnpackError asset_id, quantity, divisible = struct.unpack(FORMAT_1, message) callable_, call_date, call_price, description = False, 0, 0.0, '' try: asset = util.generate_asset_name(asset_id, tx['block_index']) except exceptions.AssetNameError: asset = None status = 'invalid: bad asset name' status = 'valid' except exceptions.UnpackError as e: asset, quantity, divisible, callable_, call_date, call_price, description = None, None, None, None, None, None, None status = 'invalid: could not unpack' fee = 0 if status == 'valid': call_date, call_price, problems, fee, description, divisible, reissuance = validate(db, tx['source'], tx['destination'], asset, quantity, divisible, callable_, call_date, call_price, description, block_index=tx['block_index']) if problems: status = 'invalid: ' + '; '.join(problems) if not util.enabled('integer_overflow_fix', block_index=tx['block_index']) and 'total quantity overflow' in problems: quantity = 0 if tx['destination']: issuer = tx['destination'] transfer = True quantity = 0 else: issuer = tx['source'] transfer = False # Debit fee. if status == 'valid': util.debit(db, tx['source'], config.XCP, fee, action="issuance fee", event=tx['tx_hash']) # Lock? lock = False if status == 'valid': if description and description.lower() == 'lock': lock = True cursor = db.cursor() issuances = list(cursor.execute('''SELECT * FROM issuances \ WHERE (status = ? AND asset = ?) ORDER BY tx_index ASC''', ('valid', asset))) cursor.close() description = issuances[-1]['description'] # Use last description. (Assume previous issuance exists because tx is valid.) timestamp, value_int, fee_fraction_int = None, None, None if not reissuance: # Add to table of assets. bindings= { 'asset_id': str(asset_id), 'asset_name': str(asset), 'block_index': tx['block_index'], } sql='insert into assets values(:asset_id, :asset_name, :block_index)' issuance_parse_cursor.execute(sql, bindings) # Add parsed transaction to message-type–specific table. bindings= { 'tx_index': tx['tx_index'], 'tx_hash': tx['tx_hash'], 'block_index': tx['block_index'], 'asset': asset, 'quantity': quantity, 'divisible': divisible, 'source': tx['source'], 'issuer': issuer, 'transfer': transfer, 'callable': callable_, 'call_date': call_date, 'call_price': call_price, 'description': description, 'fee_paid': fee, 'locked': lock, 'status': status, } if "integer overflow" not in status: sql='insert into issuances values(:tx_index, :tx_hash, :block_index, :asset, :quantity, :divisible, :source, :issuer, :transfer, :callable, :call_date, :call_price, :description, :fee_paid, :locked, :status)' issuance_parse_cursor.execute(sql, bindings) else: logger.warn("Not storing [issuance] tx [%s]: %s" % (tx['tx_hash'], status)) logger.debug("Bindings: %s" % (json.dumps(bindings), )) # Credit. if status == 'valid' and quantity: util.credit(db, tx['source'], asset, quantity, action="issuance", event=tx['tx_hash']) issuance_parse_cursor.close()
def parse(db, tx, message): issuance_parse_cursor = db.cursor() # Unpack message. try: if (tx['block_index'] > 283271 or config.TESTNET ) and len(message) >= LENGTH_2: # Protocol change. if len(message) - LENGTH_2 <= 42: curr_format = FORMAT_2 + '{}p'.format(len(message) - LENGTH_2) else: curr_format = FORMAT_2 + '{}s'.format(len(message) - LENGTH_2) asset_id, quantity, divisible, callable_, call_date, call_price, description = struct.unpack( curr_format, message) call_price = round(call_price, 6) # TODO: arbitrary try: description = description.decode('utf-8') except UnicodeDecodeError: description = '' else: if len(message) != LENGTH_1: raise exceptions.UnpackError asset_id, quantity, divisible = struct.unpack(FORMAT_1, message) callable_, call_date, call_price, description = False, 0, 0.0, '' try: asset = util.generate_asset_name(asset_id, tx['block_index']) except exceptions.AssetNameError: asset = None status = 'invalid: bad asset name' status = 'valid' except exceptions.UnpackError as e: asset, quantity, divisible, callable_, call_date, call_price, description = None, None, None, None, None, None, None status = 'invalid: could not unpack' fee = 0 if status == 'valid': call_date, call_price, problems, fee, description, divisible, reissuance = validate( db, tx['source'], tx['destination'], asset, quantity, divisible, callable_, call_date, call_price, description, block_index=tx['block_index']) if problems: status = 'invalid: ' + '; '.join(problems) if 'total quantity overflow' in problems: quantity = 0 if tx['destination']: issuer = tx['destination'] transfer = True quantity = 0 else: issuer = tx['source'] transfer = False # Debit fee. if status == 'valid': util.debit(db, tx['source'], config.XCP, fee, action="issuance fee", event=tx['tx_hash']) # Lock? lock = False if status == 'valid': if description and description.lower() == 'lock': lock = True cursor = db.cursor() issuances = list( cursor.execute( '''SELECT * FROM issuances \ WHERE (status = ? AND asset = ?) ORDER BY tx_index ASC''', ('valid', asset))) cursor.close() description = issuances[-1][ 'description'] # Use last description. (Assume previous issuance exists because tx is valid.) timestamp, value_int, fee_fraction_int = None, None, None if not reissuance: # Add to table of assets. bindings = { 'asset_id': str(asset_id), 'asset_name': str(asset), 'block_index': tx['block_index'], } sql = 'insert into assets values(:asset_id, :asset_name, :block_index)' issuance_parse_cursor.execute(sql, bindings) # Add parsed transaction to message-type–specific table. bindings = { 'tx_index': tx['tx_index'], 'tx_hash': tx['tx_hash'], 'block_index': tx['block_index'], 'asset': asset, 'quantity': quantity, 'divisible': divisible, 'source': tx['source'], 'issuer': issuer, 'transfer': transfer, 'callable': callable_, 'call_date': call_date, 'call_price': call_price, 'description': description, 'fee_paid': fee, 'locked': lock, 'status': status, } sql = 'insert into issuances values(:tx_index, :tx_hash, :block_index, :asset, :quantity, :divisible, :source, :issuer, :transfer, :callable, :call_date, :call_price, :description, :fee_paid, :locked, :status)' issuance_parse_cursor.execute(sql, bindings) # Credit. if status == 'valid' and quantity: util.credit(db, tx['source'], asset, quantity, action="issuance", event=tx['tx_hash']) issuance_parse_cursor.close()
def parse(db, tx, message): try: unpacked = unpack(db, message, tx['block_index']) status = 'valid' except (struct.error) as e: status = 'invalid: truncated message' except (exceptions.AssetNameError, exceptions.AssetIDError) as e: status = 'invalid: invalid asset name/id' except (Exception) as e: status = 'invalid: couldn\'t unpack; %s' % e cursor = db.cursor() plain_sends = [] all_debits = [] all_credits = [] if status == 'valid': for asset_id in unpacked: try: asset = util.get_asset_name(db, asset_id, tx['block_index']) except (exceptions.AssetNameError) as e: status = 'invalid: asset %s invalid at block index %i' % ( asset_id, tx['block_index']) break cursor.execute( '''SELECT * FROM balances \ WHERE (address = ? AND asset = ?)''', (tx['source'], asset_id)) balances = cursor.fetchall() if not balances: status = 'invalid: insufficient funds for asset %s, address %s has no balance' % ( asset_id, tx['source']) break credits = unpacked[asset_id] total_sent = reduce(lambda p, t: p + t[1], credits, 0) if balances[0]['quantity'] < total_sent: status = 'invalid: insufficient funds for asset %s, needs %i' % ( asset_id, total_sent) break if status == 'valid': plain_sends += map(lambda t: util.py34TupleAppend(asset_id, t), credits) all_credits += map( lambda t: { "asset": asset_id, "destination": t[0], "quantity": t[1] }, credits) all_debits.append({"asset": asset_id, "quantity": total_sent}) if status == 'valid': problems = validate(db, tx['source'], plain_sends, tx['block_index']) if problems: status = 'invalid:' + '; '.join(problems) if status == 'valid': for op in all_credits: util.credit(db, op['destination'], op['asset'], op['quantity'], action='mpma send', event=tx['tx_hash']) for op in all_debits: util.debit(db, tx['source'], op['asset'], op['quantity'], action='mpma send', event=tx['tx_hash']) # Enumeration of the plain sends needs to be deterministic, so we sort them by asset and then by address plain_sends = sorted(plain_sends, key=lambda x: ''.join([x[0], x[1]])) for i, op in enumerate(plain_sends): if len(op) > 3: memo_bytes = op[3] else: memo_bytes = None bindings = { 'tx_index': tx['tx_index'], 'tx_hash': tx['tx_hash'], 'block_index': tx['block_index'], 'source': tx['source'], 'asset': op[0], 'destination': op[1], 'quantity': op[2], 'status': status, 'memo': memo_bytes, 'msg_index': i } sql = 'insert into sends (tx_index, tx_hash, block_index, source, destination, asset, quantity, status, memo, msg_index) values(:tx_index, :tx_hash, :block_index, :source, :destination, :asset, :quantity, :status, :memo, :msg_index)' cursor.execute(sql, bindings) if status != 'valid': logger.warn("Not storing [mpma] tx [%s]: %s" % (tx['tx_hash'], status)) cursor.close()
def parse (db, tx, message): cursor = db.cursor() # Unpack message. try: unpacked = unpack(db, message, tx['block_index']) asset, quantity, destination, memo_bytes = unpacked['asset'], unpacked['quantity'], unpacked['address'], unpacked['memo'] status = 'valid' except (exceptions.UnpackError, exceptions.AssetNameError, struct.error) as e: asset, quantity, destination, memo_bytes = None, None, None, None status = 'invalid: could not unpack ({})'.format(e) except: asset, quantity, destination, memo_bytes = None, None, None, None status = 'invalid: could not unpack' if status == 'valid': # don't allow sends over MAX_INT at all if quantity and quantity > config.MAX_INT: status = 'invalid: quantity is too large' quantity = None if status == 'valid': problems = validate(db, tx['source'], destination, asset, quantity, memo_bytes, tx['block_index']) if problems: status = 'invalid: ' + '; '.join(problems) if status == 'valid': # verify balance is present cursor.execute('''SELECT * FROM balances \ WHERE (address = ? AND asset = ?)''', (tx['source'], asset)) balances = cursor.fetchall() if not balances or balances[0]['quantity'] < quantity: status = 'invalid: insufficient funds' if status == 'valid': util.debit(db, tx['source'], asset, quantity, action='send', event=tx['tx_hash']) util.credit(db, destination, asset, quantity, action='send', event=tx['tx_hash']) # log invalid transactions if status != 'valid': if quantity is None: logger.warn("Invalid send from %s with status %s. (%s)" % (tx['source'], status, tx['tx_hash'])) else: logger.warn("Invalid send of %s %s from %s to %s. status is %s. (%s)" % (quantity, asset, tx['source'], destination, status, 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'], 'destination': destination, 'asset': asset, 'quantity': quantity, 'status': status, 'memo': memo_bytes, } if "integer overflow" not in status and "quantity must be in satoshis" not in status: sql = 'insert into sends values(:tx_index, :tx_hash, :block_index, :source, :destination, :asset, :quantity, :status, :memo)' cursor.execute(sql, bindings) else: logger.warn("Not storing [send] tx [%s]: %s" % (tx['tx_hash'], status)) logger.debug("Bindings: %s" % (json.dumps(bindings), )) cursor.close()
def parse (db, tx, MAINNET_BURNS, message=None): burn_parse_cursor = db.cursor() if config.TESTNET or config.REGTEST: problems = [] status = 'valid' if status == 'valid': problems = validate(db, tx['source'], tx['destination'], tx['btc_amount'], tx['block_index'], overburn=False) if problems: status = 'invalid: ' + '; '.join(problems) if tx['btc_amount'] != None: sent = tx['btc_amount'] else: sent = 0 if status == 'valid': # Calculate quantity of XCP earned. (Maximum 1 BTC in total, ever.) cursor = db.cursor() cursor.execute('''SELECT * FROM burns WHERE (status = ? AND source = ?)''', ('valid', tx['source'])) burns = cursor.fetchall() already_burned = sum([burn['burned'] for burn in burns]) ONE = 1 * config.UNIT max_burn = ONE - already_burned if sent > max_burn: burned = max_burn # Exceeded maximum burn; earn what you can. else: burned = sent total_time = config.BURN_END - config.BURN_START partial_time = config.BURN_END - tx['block_index'] multiplier = (1000 + (500 * Fraction(partial_time, total_time))) earned = round(burned * multiplier) # Credit source address with earned XCP. util.credit(db, tx['source'], config.XCP, earned, action='burn', event=tx['tx_hash']) else: burned = 0 earned = 0 tx_index = tx['tx_index'] tx_hash = tx['tx_hash'] block_index = tx['block_index'] source = tx['source'] else: # Mainnet burns are hard‐coded. try: line = MAINNET_BURNS[tx['tx_hash']] except KeyError: return util.credit(db, line['source'], config.XCP, int(line['earned']), action='burn', event=line['tx_hash']) tx_index = tx['tx_index'] tx_hash = line['tx_hash'] block_index = line['block_index'] source = line['source'] burned = line['burned'] earned = line['earned'] status = 'valid' # Add parsed transaction to message-type–specific table. # TODO: store sent in table bindings = { 'tx_index': tx_index, 'tx_hash': tx_hash, 'block_index': block_index, 'source': source, 'burned': burned, 'earned': earned, 'status': status, } if "integer overflow" not in status: sql = 'insert into burns values(:tx_index, :tx_hash, :block_index, :source, :burned, :earned, :status)' burn_parse_cursor.execute(sql, bindings) else: logger.warn("Not storing [burn] tx [%s]: %s" % (tx['tx_hash'], status)) logger.debug("Bindings: %s" % (json.dumps(bindings), )) burn_parse_cursor.close()
def parse (db, tx, message): cursor = db.cursor() fee_paid = round(config.UNIT/2) # Unpack message. try: unpacked = unpack(db, message, tx['block_index']) destination, flags, memo_bytes = unpacked['destination'], unpacked['flags'], unpacked['memo'] status = 'valid' except (exceptions.UnpackError, exceptions.AssetNameError, struct.error) as e: destination, flags, memo_bytes = None, None, None status = 'invalid: could not unpack ({})'.format(e) except BalanceError: destination, flags, memo_bytes = None, None, None status = 'invalid: insufficient balance for antispam fee for sweep' except Exception as err: destination, flags, memo_bytes = None, None, None status = 'invalid: could not unpack, ' + str(err) if status == 'valid': problems = validate(db, tx['source'], destination, flags, memo_bytes, tx['block_index']) if problems: status = 'invalid: ' + '; '.join(problems) try: util.debit(db, tx['source'], config.XCP, fee_paid, action='sweep fee', event=tx['tx_hash']) except BalanceError: destination, flags, memo_bytes = None, None, None status = 'invalid: insufficient balance for antispam fee for sweep' if status == 'valid': cursor.execute('''SELECT * FROM balances WHERE address = ?''', (tx['source'],)) balances = cursor.fetchall() if flags & FLAG_BALANCES: for balance in balances: util.debit(db, tx['source'], balance['asset'], balance['quantity'], action='sweep', event=tx['tx_hash']) util.credit(db, destination, balance['asset'], balance['quantity'], action='sweep', event=tx['tx_hash']) if flags & FLAG_OWNERSHIP: for balance in balances: cursor.execute('''SELECT * FROM issuances \ WHERE (status = ? AND asset = ?) ORDER BY tx_index ASC''', ('valid', balance['asset'])) issuances = cursor.fetchall() if len(issuances) > 0: last_issuance = issuances[-1] if last_issuance['issuer'] == tx['source']: bindings= { 'tx_index': tx['tx_index'], 'tx_hash': tx['tx_hash'], 'block_index': tx['block_index'], 'asset': balance['asset'], 'quantity': 0, 'divisible': last_issuance['divisible'], 'source': last_issuance['source'], 'issuer': destination, 'transfer': True, 'callable': last_issuance['callable'], 'call_date': last_issuance['call_date'], 'call_price': last_issuance['call_price'], 'description': last_issuance['description'], 'fee_paid': 0, 'locked': last_issuance['locked'], 'status': status, 'asset_longname': last_issuance['asset_longname'], } sql='insert into issuances values(:tx_index, :tx_hash, :block_index, :asset, :quantity, :divisible, :source, :issuer, :transfer, :callable, :call_date, :call_price, :description, :fee_paid, :locked, :status, :asset_longname)' cursor.execute(sql, bindings) bindings = { 'tx_index': tx['tx_index'], 'tx_hash': tx['tx_hash'], 'block_index': tx['block_index'], 'source': tx['source'], 'destination': destination, 'flags': flags, 'status': status, 'memo': memo_bytes, 'fee_paid': fee_paid } sql = 'insert into sweeps values(:tx_index, :tx_hash, :block_index, :source, :destination, :flags, :status, :memo, :fee_paid)' cursor.execute(sql, bindings) cursor.close()
def parse(db, tx, message): cursor = db.cursor() # Unpack message. status = 'valid' # might be invalidate later. try: if util.enabled('broadcast_pack_text', tx['block_index']): timestamp, value, fee_fraction_int, rawtext = struct.unpack( FORMAT + '{}s'.format(len(message) - LENGTH), message) textlen = VarIntSerializer.deserialize(rawtext) if textlen == 0: text = b'' else: text = rawtext[-textlen:] if len(text) != textlen: status = 'invalid: text length mismatch' else: if len(message) - LENGTH <= 52: curr_format = FORMAT + '{}p'.format(len(message) - LENGTH) else: curr_format = FORMAT + '{}s'.format(len(message) - LENGTH) timestamp, value, fee_fraction_int, text = struct.unpack( curr_format, message) try: text = text.decode('utf-8') except UnicodeDecodeError: text = '' except (struct.error): timestamp, value, fee_fraction_int, text = 0, None, 0, None status = 'invalid: could not unpack' if status == 'valid': # For SQLite3 timestamp = min(timestamp, config.MAX_INT) value = min(value, config.MAX_INT) problems = validate(db, tx['source'], timestamp, value, fee_fraction_int, text, tx['block_index']) if problems: status = 'invalid: ' + '; '.join(problems) # Lock? lock = False if text and text.lower() == 'lock': lock = True timestamp, value, fee_fraction_int, text = 0, None, None, None else: lock = 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'], 'timestamp': timestamp, 'value': value, 'fee_fraction_int': fee_fraction_int, 'text': text, 'locked': lock, 'status': status, } if "integer overflow" not in status: sql = 'insert into broadcasts values(:tx_index, :tx_hash, :block_index, :source, :timestamp, :value, :fee_fraction_int, :text, :locked, :status)' cursor.execute(sql, bindings) else: logger.warn("Not storing [broadcast] tx [%s]: %s" % (tx['tx_hash'], status)) logger.debug("Bindings: %s" % (json.dumps(bindings), )) # stop processing if broadcast is invalid for any reason if util.enabled('broadcast_invalid_check') and status != 'valid': return # Options? Should not fail to parse due to above checks. if util.enabled('options_require_memo') and text and text.lower( ).startswith('options'): options = util.parse_options_from_string(text) if options is not False: op_bindings = { 'block_index': tx['block_index'], 'address': tx['source'], 'options': options } sql = 'insert or replace into addresses(address, options, block_index) values(:address, :options, :block_index)' cursor = db.cursor() cursor.execute(sql, op_bindings) # Negative values (default to ignore). if value is None or value < 0: # Cancel Open Bets? if value == -2: cursor.execute( '''SELECT * FROM bets \ WHERE (status = ? AND feed_address = ?)''', ('open', tx['source'])) for i in list(cursor): bet.cancel_bet(db, i, 'dropped', tx['block_index']) # Cancel Pending Bet Matches? if value == -3: cursor.execute( '''SELECT * FROM bet_matches \ WHERE (status = ? AND feed_address = ?)''', ('pending', tx['source'])) for bet_match in list(cursor): bet.cancel_bet_match(db, bet_match, 'dropped', tx['block_index']) return # stop processing if broadcast is invalid for any reason # @TODO: remove this check once broadcast_invalid_check has been activated if util.enabled('max_fee_fraction') and status != 'valid': return # Handle bet matches that use this feed. cursor.execute( '''SELECT * FROM bet_matches \ WHERE (status=? AND feed_address=?) ORDER BY tx1_index ASC, tx0_index ASC''', ('pending', tx['source'])) for bet_match in cursor.fetchall(): broadcast_bet_match_cursor = db.cursor() bet_match_id = util.make_id(bet_match['tx0_hash'], bet_match['tx1_hash']) bet_match_status = None # Calculate total funds held in escrow and total fee to be paid if # the bet match is settled. Escrow less fee is amount to be paid back # to betters. total_escrow = bet_match['forward_quantity'] + bet_match[ 'backward_quantity'] if util.enabled('inmutable_fee_fraction'): fee_fraction = bet_match['fee_fraction_int'] else: fee_fraction = fee_fraction_int fee = int(fee_fraction * total_escrow / config.UNIT) # Truncate. escrow_less_fee = total_escrow - fee # Get known bet match type IDs. 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'] # Get the bet match type ID of this bet match. bet_match_type_id = bet_match['tx0_bet_type'] + bet_match[ 'tx1_bet_type'] # Contract for difference, with determinate settlement date. if bet_match_type_id == cfd_type_id: # Recognise tx0, tx1 as the bull, bear (in the right direction). if bet_match['tx0_bet_type'] < bet_match['tx1_bet_type']: bull_address = bet_match['tx0_address'] bear_address = bet_match['tx1_address'] bear_escrow = bet_match['backward_quantity'] else: bull_address = bet_match['tx1_address'] bear_address = bet_match['tx0_address'] bear_escrow = bet_match['forward_quantity'] leverage = Fraction(bet_match['leverage'], 5040) initial_value = bet_match['initial_value'] bear_credit = bear_escrow - ( value - initial_value) * leverage * config.UNIT bull_credit = escrow_less_fee - bear_credit bear_credit = round(bear_credit) bull_credit = round(bull_credit) # Liquidate, as necessary. if bull_credit >= escrow_less_fee or bull_credit <= 0: if bull_credit >= escrow_less_fee: bull_credit = escrow_less_fee bear_credit = 0 bet_match_status = 'settled: liquidated for bull' util.credit(db, bull_address, config.XCP, bull_credit, action='bet {}'.format(bet_match_status), event=tx['tx_hash']) elif bull_credit <= 0: bull_credit = 0 bear_credit = escrow_less_fee bet_match_status = 'settled: liquidated for bear' util.credit(db, bear_address, config.XCP, bear_credit, action='bet {}'.format(bet_match_status), event=tx['tx_hash']) # Pay fee to feed. util.credit(db, bet_match['feed_address'], config.XCP, fee, action='feed fee', event=tx['tx_hash']) # For logging purposes. bindings = { 'bet_match_id': bet_match_id, 'bet_match_type_id': bet_match_type_id, 'block_index': tx['block_index'], 'settled': False, 'bull_credit': bull_credit, 'bear_credit': bear_credit, 'winner': None, 'escrow_less_fee': None, 'fee': fee } sql = 'insert into bet_match_resolutions values(:bet_match_id, :bet_match_type_id, :block_index, :settled, :bull_credit, :bear_credit, :winner, :escrow_less_fee, :fee)' cursor.execute(sql, bindings) # Settle (if not liquidated). elif timestamp >= bet_match['deadline']: bet_match_status = 'settled' util.credit(db, bull_address, config.XCP, bull_credit, action='bet {}'.format(bet_match_status), event=tx['tx_hash']) util.credit(db, bear_address, config.XCP, bear_credit, action='bet {}'.format(bet_match_status), event=tx['tx_hash']) # Pay fee to feed. util.credit(db, bet_match['feed_address'], config.XCP, fee, action='feed fee', event=tx['tx_hash']) # For logging purposes. bindings = { 'bet_match_id': bet_match_id, 'bet_match_type_id': bet_match_type_id, 'block_index': tx['block_index'], 'settled': True, 'bull_credit': bull_credit, 'bear_credit': bear_credit, 'winner': None, 'escrow_less_fee': None, 'fee': fee } sql = 'insert into bet_match_resolutions values(:bet_match_id, :bet_match_type_id, :block_index, :settled, :bull_credit, :bear_credit, :winner, :escrow_less_fee, :fee)' cursor.execute(sql, bindings) # Equal[/NotEqual] bet. elif bet_match_type_id == equal_type_id and timestamp >= bet_match[ 'deadline']: # Recognise tx0, tx1 as the bull, bear (in the right direction). if bet_match['tx0_bet_type'] < bet_match['tx1_bet_type']: equal_address = bet_match['tx0_address'] notequal_address = bet_match['tx1_address'] else: equal_address = bet_match['tx1_address'] notequal_address = bet_match['tx0_address'] # Decide who won, and credit appropriately. if value == bet_match['target_value']: winner = 'Equal' bet_match_status = 'settled: for equal' util.credit(db, equal_address, config.XCP, escrow_less_fee, action='bet {}'.format(bet_match_status), event=tx['tx_hash']) else: winner = 'NotEqual' bet_match_status = 'settled: for notequal' util.credit(db, notequal_address, config.XCP, escrow_less_fee, action='bet {}'.format(bet_match_status), event=tx['tx_hash']) # Pay fee to feed. util.credit(db, bet_match['feed_address'], config.XCP, fee, action='feed fee', event=tx['tx_hash']) # For logging purposes. bindings = { 'bet_match_id': bet_match_id, 'bet_match_type_id': bet_match_type_id, 'block_index': tx['block_index'], 'settled': None, 'bull_credit': None, 'bear_credit': None, 'winner': winner, 'escrow_less_fee': escrow_less_fee, 'fee': fee } sql = 'insert into bet_match_resolutions values(:bet_match_id, :bet_match_type_id, :block_index, :settled, :bull_credit, :bear_credit, :winner, :escrow_less_fee, :fee)' cursor.execute(sql, bindings) # Update the bet match’s status. if bet_match_status: bindings = { 'status': bet_match_status, 'bet_match_id': util.make_id(bet_match['tx0_hash'], bet_match['tx1_hash']) } sql = 'update bet_matches set status = :status where id = :bet_match_id' cursor.execute(sql, bindings) log.message(db, tx['block_index'], 'update', 'bet_matches', bindings)
def parse(db, tx, message): cursor = db.cursor() # Unpack message. try: action_address = tx['source'] assetid, give_quantity, escrow_quantity, mainchainrate, dispenser_status = struct.unpack( FORMAT, message[0:LENGTH]) if dispenser_status == STATUS_OPEN_EMPTY_ADDRESS: action_address = address.unpack(message[LENGTH:LENGTH + 21]) asset = util.generate_asset_name(assetid, util.CURRENT_BLOCK_INDEX) status = 'valid' except (exceptions.UnpackError, struct.error) as e: assetid, give_quantity, mainchainrate, asset = None, None, None, None status = 'invalid: could not unpack' if status == 'valid': if dispenser_status == STATUS_OPEN or dispenser_status == STATUS_OPEN_EMPTY_ADDRESS: cursor.execute( 'SELECT * FROM dispensers WHERE source=:source AND asset=:asset AND status=:status', { 'source': action_address, 'asset': asset, 'status': STATUS_OPEN }) existing = cursor.fetchall() if len(existing) == 0: # Create the new dispenser try: if dispenser_status == STATUS_OPEN_EMPTY_ADDRESS: cursor.execute( 'SELECT count(*) cnt FROM balances WHERE address=:address AND quantity > 0', {'address': action_address}) counts = cursor.fetchall()[0] if counts['cnt'] == 0: util.debit(db, tx['source'], asset, escrow_quantity, action='open dispenser empty addr', event=tx['tx_hash']) util.credit(db, action_address, asset, escrow_quantity, action='open dispenser empty addr', event=tx['tx_hash']) util.debit(db, action_address, asset, escrow_quantity, action='open dispenser empty addr', event=tx['tx_hash']) else: status = 'invalid: address not empty' else: util.debit(db, tx['source'], asset, escrow_quantity, action='open dispenser', event=tx['tx_hash']) except util.DebitError as e: status = 'invalid: insufficient funds' if status == 'valid': bindings = { 'tx_index': tx['tx_index'], 'tx_hash': tx['tx_hash'], 'block_index': tx['block_index'], 'source': action_address, 'asset': asset, 'give_quantity': give_quantity, 'escrow_quantity': escrow_quantity, 'satoshirate': mainchainrate, 'status': STATUS_OPEN, 'give_remaining': escrow_quantity } sql = 'insert into dispensers values(:tx_index, :tx_hash, :block_index, :source, :asset, :give_quantity, :escrow_quantity, :satoshirate, :status, :give_remaining)' cursor.execute(sql, bindings) elif len(existing) == 1 and existing[0][ 'satoshirate'] == mainchainrate and existing[0][ 'give_quantity'] == give_quantity: if tx["source"] == action_address: # Refill the dispenser by the given amount bindings = { 'source': tx['source'], 'asset': asset, 'status': dispenser_status, 'give_remaining': existing[0]['give_remaining'] + escrow_quantity, 'status': STATUS_OPEN, 'block_index': tx['block_index'] } try: util.debit(db, tx['source'], asset, escrow_quantity, action='refill dispenser', event=tx['tx_hash']) sql = 'UPDATE dispensers SET give_remaining=:give_remaining \ WHERE source=:source AND asset=:asset AND status=:status' cursor.execute(sql, bindings) except (util.DebitError): status = 'insufficient funds' else: status = 'invalid: can only refill dispenser from source' else: status = 'can only have one open dispenser per asset per address' elif dispenser_status == STATUS_CLOSED: cursor.execute( 'SELECT give_remaining FROM dispensers WHERE source=:source AND asset=:asset AND status=:status', { 'source': tx['source'], 'asset': asset, 'status': STATUS_OPEN }) existing = cursor.fetchall() if len(existing) == 1: util.credit(db, tx['source'], asset, existing[0]['give_remaining'], action='close dispenser', event=tx['tx_hash']) bindings = { 'source': tx['source'], 'asset': asset, 'status': STATUS_CLOSED, 'block_index': tx['block_index'] } sql = 'UPDATE dispensers SET give_remaining=0, status=:status WHERE source=:source AND asset=:asset' cursor.execute(sql, bindings) else: status = 'dispenser inexistent' else: status = 'invalid: status must be one of OPEN or CLOSE' if status != 'valid': logger.warn("Not storing [dispenser] tx [%s]: %s" % (tx['tx_hash'], status)) cursor.close()
def parse (db, tx, message): cursor = db.cursor() # Unpack message. try: if len(message) != LENGTH: raise exceptions.UnpackError assetid, give_quantity, escrow_quantity, mainchainrate, dispenser_status = struct.unpack(FORMAT, message) asset = util.generate_asset_name(assetid, util.CURRENT_BLOCK_INDEX) status = 'valid' except (exceptions.UnpackError, struct.error): assetid, give_quantity, mainchainrate, asset = None, None, None, None status = 'invalid: could not unpack' if status == 'valid': assetid, problems = validate (db, tx['source'], asset, give_quantity, escrow_quantity, mainchainrate, dispenser_status, tx['block_index']) if problems: status = 'invalid: ' + '; '.join(problems) if status == 'valid': if dispenser_status == STATUS_OPEN: cursor.execute('SELECT * FROM dispensers WHERE source=:source AND asset=:asset AND status=:status', { 'source': tx['source'], 'asset': asset, 'status': STATUS_OPEN }) existing = cursor.fetchall() if len(existing) == 0: # Create the new dispenser bindings = { 'tx_index': tx['tx_index'], 'tx_hash': tx['tx_hash'], 'block_index': tx['block_index'], 'source': tx['source'], 'asset': asset, 'give_quantity': give_quantity, 'escrow_quantity': escrow_quantity, 'satoshirate': mainchainrate, 'status': dispenser_status, 'give_remaining': escrow_quantity } try: util.debit(db, tx['source'], asset, escrow_quantity, action='open dispenser', event=tx['tx_hash']) sql = 'insert into dispensers values(:tx_index, :tx_hash, :block_index, :source, :asset, :give_quantity, :escrow_quantity, :satoshirate, :status, :give_remaining)' cursor.execute(sql, bindings) except (util.DebitError): status = 'insufficient funds' elif len(existing) == 1 and existing[0]['satoshirate'] == mainchainrate and existing[0]['give_quantity'] == give_quantity: # Refill the dispenser by the given amount bindings = { 'source': tx['source'], 'asset': asset, 'give_remaining': existing[0]['give_remaining'] + escrow_quantity, 'status': STATUS_OPEN, 'block_index': tx['block_index'] } try: util.debit(db, tx['source'], asset, escrow_quantity, action='refill dispenser', event=tx['tx_hash']) sql = 'UPDATE dispensers SET give_remaining=:give_remaining \ WHERE source=:source AND asset=:asset AND status=:status' cursor.execute(sql, bindings) except (util.DebitError): status = 'insufficient funds' else: status = 'can only have one open dispenser per asset per address' elif dispenser_status == STATUS_CLOSED: cursor.execute('SELECT give_remaining FROM dispensers WHERE source=:source AND asset=:asset AND status=:status', { 'source': tx['source'], 'asset': asset, 'status': STATUS_OPEN }) existing = cursor.fetchall() if len(existing) == 1: util.credit(db, tx['source'], asset, existing[0]['give_remaining'], action='close dispenser', event=tx['tx_hash']) bindings = { 'source': tx['source'], 'asset': asset, 'status': STATUS_CLOSED, 'block_index': tx['block_index'] } sql = 'UPDATE dispensers SET give_remaining=0, status=:status WHERE source=:source AND asset=:asset' cursor.execute(sql, bindings) else: status = 'dispenser inexistent' else: status = 'invalid: status must be one of OPEN or CLOSE'
def parse(db, tx, message): cursor = db.cursor() # Unpack message. try: unpacked = unpack(db, message, tx['block_index']) asset, quantity, destination, memo_bytes = unpacked['asset'], unpacked[ 'quantity'], unpacked['address'], unpacked['memo'] status = 'valid' except (exceptions.UnpackError, exceptions.AssetNameError, struct.error) as e: asset, quantity, destination, memo_bytes = None, None, None, None status = 'invalid: could not unpack ({})'.format(e) except: asset, quantity, destination, memo_bytes = None, None, None, None status = 'invalid: could not unpack' if status == 'valid': # don't allow sends over MAX_INT at all if quantity and quantity > config.MAX_INT: status = 'invalid: quantity is too large' quantity = None if status == 'valid': problems = validate(db, tx['source'], destination, asset, quantity, memo_bytes, tx['block_index']) if problems: status = 'invalid: ' + '; '.join(problems) if status == 'valid': # verify balance is present cursor.execute( '''SELECT * FROM balances \ WHERE (address = ? AND asset = ?)''', (tx['source'], asset)) balances = cursor.fetchall() if not balances or balances[0]['quantity'] < quantity: status = 'invalid: insufficient funds' if status == 'valid': util.debit(db, tx['source'], asset, quantity, action='send', event=tx['tx_hash']) util.credit(db, destination, asset, quantity, action='send', event=tx['tx_hash']) # log invalid transactions if status != 'valid': if quantity is None: logger.warn("Invalid send from %s with status %s. (%s)" % (tx['source'], status, tx['tx_hash'])) else: logger.warn( "Invalid send of %s %s from %s to %s. status is %s. (%s)" % (quantity, asset, tx['source'], destination, status, 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'], 'destination': destination, 'asset': asset, 'quantity': quantity, 'status': status, 'memo': memo_bytes, } if "integer overflow" not in status and "quantity must be in satoshis" not in status: sql = 'insert into sends values(:tx_index, :tx_hash, :block_index, :source, :destination, :asset, :quantity, :status, :memo)' cursor.execute(sql, bindings) else: logger.warn("Not storing [send] tx [%s]: %s" % (tx['tx_hash'], status)) logger.debug("Bindings: %s" % (json.dumps(bindings), )) cursor.close()
def parse (db, tx, message, message_type_id): issuance_parse_cursor = db.cursor() # Unpack message. try: subasset_longname = None if message_type_id == SUBASSET_ID: if not util.enabled('subassets', block_index=tx['block_index']): logger.warn("subassets are not enabled at block %s" % tx['block_index']) raise exceptions.UnpackError # parse a subasset original issuance message asset_id, quantity, divisible, compacted_subasset_length = struct.unpack(SUBASSET_FORMAT, message[0:SUBASSET_FORMAT_LENGTH]) description_length = len(message) - SUBASSET_FORMAT_LENGTH - compacted_subasset_length if description_length < 0: logger.warn("invalid subasset length: [issuance] tx [%s]: %s" % (tx['tx_hash'], compacted_subasset_length)) raise exceptions.UnpackError messages_format = '>{}s{}s'.format(compacted_subasset_length, description_length) compacted_subasset_longname, description = struct.unpack(messages_format, message[SUBASSET_FORMAT_LENGTH:]) subasset_longname = util.expand_subasset_longname(compacted_subasset_longname) callable_, call_date, call_price = False, 0, 0.0 try: description = description.decode('utf-8') except UnicodeDecodeError: description = '' elif (tx['block_index'] > 283271 or config.TESTNET or config.REGTEST) and len(message) >= LENGTH_2: # Protocol change. if len(message) - LENGTH_2 <= 42: curr_format = FORMAT_2 + '{}p'.format(len(message) - LENGTH_2) else: curr_format = FORMAT_2 + '{}s'.format(len(message) - LENGTH_2) asset_id, quantity, divisible, callable_, call_date, call_price, description = struct.unpack(curr_format, message) call_price = round(call_price, 6) # TODO: arbitrary try: description = description.decode('utf-8') except UnicodeDecodeError: description = '' else: if len(message) != LENGTH_1: raise exceptions.UnpackError asset_id, quantity, divisible = struct.unpack(FORMAT_1, message) callable_, call_date, call_price, description = False, 0, 0.0, '' try: asset = util.generate_asset_name(asset_id, tx['block_index']) status = 'valid' except exceptions.AssetIDError: asset = None status = 'invalid: bad asset name' except exceptions.UnpackError as e: asset, quantity, divisible, callable_, call_date, call_price, description = None, None, None, None, None, None, None status = 'invalid: could not unpack' # parse and validate the subasset from the message subasset_parent = None if status == 'valid' and subasset_longname is not None: # Protocol change. try: # ensure the subasset_longname is valid util.validate_subasset_longname(subasset_longname) subasset_parent, subasset_longname = util.parse_subasset_from_asset_name(subasset_longname) except exceptions.AssetNameError as e: asset = None status = 'invalid: bad subasset name' reissuance = None fee = 0 if status == 'valid': call_date, call_price, problems, fee, description, divisible, reissuance, reissued_asset_longname = validate(db, tx['source'], tx['destination'], asset, quantity, divisible, callable_, call_date, call_price, description, subasset_parent, subasset_longname, block_index=tx['block_index']) if problems: status = 'invalid: ' + '; '.join(problems) if not util.enabled('integer_overflow_fix', block_index=tx['block_index']) and 'total quantity overflow' in problems: quantity = 0 if tx['destination']: issuer = tx['destination'] transfer = True quantity = 0 else: issuer = tx['source'] transfer = False # Debit fee. if status == 'valid': util.debit(db, tx['source'], config.XCP, fee, action="issuance fee", event=tx['tx_hash']) # Lock? lock = False if status == 'valid': if description and description.lower() == 'lock': lock = True cursor = db.cursor() issuances = list(cursor.execute('''SELECT * FROM issuances \ WHERE (status = ? AND asset = ?) ORDER BY tx_index ASC''', ('valid', asset))) cursor.close() description = issuances[-1]['description'] # Use last description. (Assume previous issuance exists because tx is valid.) timestamp, value_int, fee_fraction_int = None, None, None if not reissuance: # Add to table of assets. bindings= { 'asset_id': str(asset_id), 'asset_name': str(asset), 'block_index': tx['block_index'], 'asset_longname': subasset_longname, } sql='insert into assets values(:asset_id, :asset_name, :block_index, :asset_longname)' issuance_parse_cursor.execute(sql, bindings) if status == 'valid' and reissuance: # when reissuing, add the asset_longname to the issuances table for API lookups asset_longname = reissued_asset_longname else: asset_longname = subasset_longname # Add parsed transaction to message-type–specific table. bindings= { 'tx_index': tx['tx_index'], 'tx_hash': tx['tx_hash'], 'block_index': tx['block_index'], 'asset': asset, 'quantity': quantity, 'divisible': divisible, 'source': tx['source'], 'issuer': issuer, 'transfer': transfer, 'callable': callable_, 'call_date': call_date, 'call_price': call_price, 'description': description, 'fee_paid': fee, 'locked': lock, 'status': status, 'asset_longname': asset_longname, } if "integer overflow" not in status: sql='insert into issuances values(:tx_index, :tx_hash, :block_index, :asset, :quantity, :divisible, :source, :issuer, :transfer, :callable, :call_date, :call_price, :description, :fee_paid, :locked, :status, :asset_longname)' issuance_parse_cursor.execute(sql, bindings) else: logger.warn("Not storing [issuance] tx [%s]: %s" % (tx['tx_hash'], status)) logger.debug("Bindings: %s" % (json.dumps(bindings), )) # Credit. if status == 'valid' and quantity: util.credit(db, tx['source'], asset, quantity, action="issuance", event=tx['tx_hash']) issuance_parse_cursor.close()
def parse(db, tx, message, message_type_id): issuance_parse_cursor = db.cursor() # Unpack message. try: subasset_longname = None if message_type_id == SUBASSET_ID: if not util.enabled('subassets', block_index=tx['block_index']): logger.warn("subassets are not enabled at block %s" % tx['block_index']) raise exceptions.UnpackError # parse a subasset original issuance message asset_id, quantity, divisible, compacted_subasset_length = struct.unpack( SUBASSET_FORMAT, message[0:SUBASSET_FORMAT_LENGTH]) description_length = len( message) - SUBASSET_FORMAT_LENGTH - compacted_subasset_length if description_length < 0: logger.warn("invalid subasset length: [issuance] tx [%s]: %s" % (tx['tx_hash'], compacted_subasset_length)) raise exceptions.UnpackError messages_format = '>{}s{}s'.format(compacted_subasset_length, description_length) compacted_subasset_longname, description = struct.unpack( messages_format, message[SUBASSET_FORMAT_LENGTH:]) subasset_longname = util.expand_subasset_longname( compacted_subasset_longname) callable_, call_date, call_price = False, 0, 0.0 try: description = description.decode('utf-8') except UnicodeDecodeError: description = '' elif (tx['block_index'] > 283271 or config.TESTNET) and len(message) >= LENGTH_2: # Protocol change. if len(message) - LENGTH_2 <= 42: curr_format = FORMAT_2 + '{}p'.format(len(message) - LENGTH_2) else: curr_format = FORMAT_2 + '{}s'.format(len(message) - LENGTH_2) asset_id, quantity, divisible, callable_, call_date, call_price, description = struct.unpack( curr_format, message) call_price = round(call_price, 6) # TODO: arbitrary try: description = description.decode('utf-8') except UnicodeDecodeError: description = '' else: if len(message) != LENGTH_1: raise exceptions.UnpackError asset_id, quantity, divisible = struct.unpack(FORMAT_1, message) callable_, call_date, call_price, description = False, 0, 0.0, '' try: asset = util.generate_asset_name(asset_id, tx['block_index']) except exceptions.AssetNameError: asset = None status = 'invalid: bad asset name' status = 'valid' except exceptions.UnpackError as e: asset, quantity, divisible, callable_, call_date, call_price, description = None, None, None, None, None, None, None status = 'invalid: could not unpack' # parse and validate the subasset from the message subasset_parent = None if status == 'valid' and subasset_longname is not None: # Protocol change. try: # ensure the subasset_longname is valid util.validate_subasset_longname(subasset_longname) subasset_parent, subasset_longname = util.parse_subasset_from_asset_name( subasset_longname) except exceptions.AssetNameError as e: asset = None status = 'invalid: bad subasset name' reissuance = None fee = 0 if status == 'valid': call_date, call_price, problems, fee, description, divisible, reissuance, reissued_asset_longname = validate( db, tx['source'], tx['destination'], asset, quantity, divisible, callable_, call_date, call_price, description, subasset_parent, subasset_longname, block_index=tx['block_index']) if problems: status = 'invalid: ' + '; '.join(problems) if not util.enabled('integer_overflow_fix', block_index=tx['block_index'] ) and 'total quantity overflow' in problems: quantity = 0 if tx['destination']: issuer = tx['destination'] transfer = True quantity = 0 else: issuer = tx['source'] transfer = False # Debit fee. if status == 'valid': util.debit(db, tx['source'], config.XCP, fee, action="issuance fee", event=tx['tx_hash']) # Lock? lock = False if status == 'valid': if description and description.lower() == 'lock': lock = True cursor = db.cursor() issuances = list( cursor.execute( '''SELECT * FROM issuances \ WHERE (status = ? AND asset = ?) ORDER BY tx_index ASC''', ('valid', asset))) cursor.close() description = issuances[-1][ 'description'] # Use last description. (Assume previous issuance exists because tx is valid.) timestamp, value_int, fee_fraction_int = None, None, None if not reissuance: # Add to table of assets. bindings = { 'asset_id': str(asset_id), 'asset_name': str(asset), 'block_index': tx['block_index'], 'asset_longname': subasset_longname, } sql = 'insert into assets values(:asset_id, :asset_name, :block_index, :asset_longname)' issuance_parse_cursor.execute(sql, bindings) if status == 'valid' and reissuance: # when reissuing, add the asset_longname to the issuances table for API lookups asset_longname = reissued_asset_longname else: asset_longname = subasset_longname # Add parsed transaction to message-type–specific table. bindings = { 'tx_index': tx['tx_index'], 'tx_hash': tx['tx_hash'], 'block_index': tx['block_index'], 'asset': asset, 'quantity': quantity, 'divisible': divisible, 'source': tx['source'], 'issuer': issuer, 'transfer': transfer, 'callable': callable_, 'call_date': call_date, 'call_price': call_price, 'description': description, 'fee_paid': fee, 'locked': lock, 'status': status, 'asset_longname': asset_longname, } if "integer overflow" not in status: sql = 'insert into issuances values(:tx_index, :tx_hash, :block_index, :asset, :quantity, :divisible, :source, :issuer, :transfer, :callable, :call_date, :call_price, :description, :fee_paid, :locked, :status, :asset_longname)' issuance_parse_cursor.execute(sql, bindings) else: logger.warn("Not storing [issuance] tx [%s]: %s" % (tx['tx_hash'], status)) logger.debug("Bindings: %s" % (json.dumps(bindings), )) # Credit. if status == 'valid' and quantity: util.credit(db, tx['source'], asset, quantity, action="issuance", event=tx['tx_hash']) issuance_parse_cursor.close()
def parse(db, tx, message): cursor = db.cursor() # Unpack message. try: if len(message) - LENGTH <= 52: curr_format = FORMAT + '{}p'.format(len(message) - LENGTH) else: curr_format = FORMAT + '{}s'.format(len(message) - LENGTH) timestamp, value, fee_fraction_int, text = struct.unpack( curr_format, message) try: text = text.decode('utf-8') except UnicodeDecodeError: text = '' status = 'valid' except (struct.error) as e: timestamp, value, fee_fraction_int, text = 0, None, 0, None status = 'invalid: could not unpack' if status == 'valid': # For SQLite3 timestamp = min(timestamp, config.MAX_INT) value = min(value, config.MAX_INT) problems = validate(db, tx['source'], timestamp, value, fee_fraction_int, text, tx['block_index']) if problems: status = 'invalid: ' + '; '.join(problems) # Lock? lock = False if text and text.lower() == 'lock': lock = True timestamp, value, fee_fraction_int, text = 0, None, None, None else: lock = 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'], 'timestamp': timestamp, 'value': value, 'fee_fraction_int': fee_fraction_int, 'text': text, 'locked': lock, 'status': status, } sql = 'insert into broadcasts values(:tx_index, :tx_hash, :block_index, :source, :timestamp, :value, :fee_fraction_int, :text, :locked, :status)' cursor.execute(sql, bindings) # Negative values (default to ignore). if value == None or value < 0: # Cancel Open Bets? if value == -2: cursor.execute( '''SELECT * FROM bets \ WHERE (status = ? AND feed_address = ?)''', ('open', tx['source'])) for i in list(cursor): bet.cancel_bet(db, i, 'dropped', tx['block_index']) # Cancel Pending Bet Matches? if value == -3: cursor.execute( '''SELECT * FROM bet_matches \ WHERE (status = ? AND feed_address = ?)''', ('pending', tx['source'])) for bet_match in list(cursor): bet.cancel_bet_match(db, bet_match, 'dropped', tx['block_index']) cursor.close() return # Handle bet matches that use this feed. cursor.execute( '''SELECT * FROM bet_matches \ WHERE (status=? AND feed_address=?) ORDER BY tx1_index ASC, tx0_index ASC''', ('pending', tx['source'])) for bet_match in cursor.fetchall(): broadcast_bet_match_cursor = db.cursor() bet_match_id = util.make_id(bet_match['tx0_hash'], bet_match['tx1_hash']) bet_match_status = None # Calculate total funds held in escrow and total fee to be paid if # the bet match is settled. Escrow less fee is amount to be paid back # to betters. total_escrow = bet_match['forward_quantity'] + bet_match[ 'backward_quantity'] fee_fraction = fee_fraction_int / config.UNIT fee = int(fee_fraction * total_escrow) # Truncate. escrow_less_fee = total_escrow - fee # Get known bet match type IDs. 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'] # Get the bet match type ID of this bet match. bet_match_type_id = bet_match['tx0_bet_type'] + bet_match[ 'tx1_bet_type'] # Contract for difference, with determinate settlement date. if bet_match_type_id == cfd_type_id: # Recognise tx0, tx1 as the bull, bear (in the right direction). if bet_match['tx0_bet_type'] < bet_match['tx1_bet_type']: bull_address = bet_match['tx0_address'] bear_address = bet_match['tx1_address'] bull_escrow = bet_match['forward_quantity'] bear_escrow = bet_match['backward_quantity'] else: bull_address = bet_match['tx1_address'] bear_address = bet_match['tx0_address'] bull_escrow = bet_match['backward_quantity'] bear_escrow = bet_match['forward_quantity'] leverage = Fraction(bet_match['leverage'], 5040) initial_value = bet_match['initial_value'] bear_credit = bear_escrow - ( value - initial_value) * leverage * config.UNIT bull_credit = escrow_less_fee - bear_credit bear_credit = round(bear_credit) bull_credit = round(bull_credit) # Liquidate, as necessary. if bull_credit >= escrow_less_fee or bull_credit <= 0: if bull_credit >= escrow_less_fee: bull_credit = escrow_less_fee bear_credit = 0 bet_match_status = 'settled: liquidated for bull' util.credit(db, bull_address, config.XCP, bull_credit, action='bet {}'.format(bet_match_status), event=tx['tx_hash']) elif bull_credit <= 0: bull_credit = 0 bear_credit = escrow_less_fee bet_match_status = 'settled: liquidated for bear' util.credit(db, bear_address, config.XCP, bear_credit, action='bet {}'.format(bet_match_status), event=tx['tx_hash']) # Pay fee to feed. util.credit(db, bet_match['feed_address'], config.XCP, fee, action='feed fee', event=tx['tx_hash']) # For logging purposes. bindings = { 'bet_match_id': bet_match_id, 'bet_match_type_id': bet_match_type_id, 'block_index': tx['block_index'], 'settled': False, 'bull_credit': bull_credit, 'bear_credit': bear_credit, 'winner': None, 'escrow_less_fee': None, 'fee': fee } sql = 'insert into bet_match_resolutions values(:bet_match_id, :bet_match_type_id, :block_index, :settled, :bull_credit, :bear_credit, :winner, :escrow_less_fee, :fee)' cursor.execute(sql, bindings) # Settle (if not liquidated). elif timestamp >= bet_match['deadline']: bet_match_status = 'settled' util.credit(db, bull_address, config.XCP, bull_credit, action='bet {}'.format(bet_match_status), event=tx['tx_hash']) util.credit(db, bear_address, config.XCP, bear_credit, action='bet {}'.format(bet_match_status), event=tx['tx_hash']) # Pay fee to feed. util.credit(db, bet_match['feed_address'], config.XCP, fee, action='feed fee', event=tx['tx_hash']) # For logging purposes. bindings = { 'bet_match_id': bet_match_id, 'bet_match_type_id': bet_match_type_id, 'block_index': tx['block_index'], 'settled': True, 'bull_credit': bull_credit, 'bear_credit': bear_credit, 'winner': None, 'escrow_less_fee': None, 'fee': fee } sql = 'insert into bet_match_resolutions values(:bet_match_id, :bet_match_type_id, :block_index, :settled, :bull_credit, :bear_credit, :winner, :escrow_less_fee, :fee)' cursor.execute(sql, bindings) # Equal[/NotEqual] bet. elif bet_match_type_id == equal_type_id and timestamp >= bet_match[ 'deadline']: # Recognise tx0, tx1 as the bull, bear (in the right direction). if bet_match['tx0_bet_type'] < bet_match['tx1_bet_type']: equal_address = bet_match['tx0_address'] notequal_address = bet_match['tx1_address'] else: equal_address = bet_match['tx1_address'] notequal_address = bet_match['tx0_address'] # Decide who won, and credit appropriately. if value == bet_match['target_value']: winner = 'Equal' bet_match_status = 'settled: for equal' util.credit(db, equal_address, config.XCP, escrow_less_fee, action='bet {}'.format(bet_match_status), event=tx['tx_hash']) else: winner = 'NotEqual' bet_match_status = 'settled: for notequal' util.credit(db, notequal_address, config.XCP, escrow_less_fee, action='bet {}'.format(bet_match_status), event=tx['tx_hash']) # Pay fee to feed. util.credit(db, bet_match['feed_address'], config.XCP, fee, action='feed fee', event=tx['tx_hash']) # For logging purposes. bindings = { 'bet_match_id': bet_match_id, 'bet_match_type_id': bet_match_type_id, 'block_index': tx['block_index'], 'settled': None, 'bull_credit': None, 'bear_credit': None, 'winner': winner, 'escrow_less_fee': escrow_less_fee, 'fee': fee } sql = 'insert into bet_match_resolutions values(:bet_match_id, :bet_match_type_id, :block_index, :settled, :bull_credit, :bear_credit, :winner, :escrow_less_fee, :fee)' cursor.execute(sql, bindings) # Update the bet match’s status. if bet_match_status: bindings = { 'status': bet_match_status, 'bet_match_id': util.make_id(bet_match['tx0_hash'], bet_match['tx1_hash']) } sql = 'update bet_matches set status = :status where id = :bet_match_id' cursor.execute(sql, bindings) log.message(db, tx['block_index'], 'update', 'bet_matches', bindings) broadcast_bet_match_cursor.close() cursor.close()
def parse(db, tx, message): cursor = db.cursor() # Unpack message. try: if util.enabled("broadcast_pack_text"): timestamp, value, fee_fraction_int, rawtext = struct.unpack( FORMAT + "{}s".format(len(message) - LENGTH), message ) textlen = VarIntSerializer.deserialize(rawtext) text = rawtext[-textlen:] assert len(text) == textlen else: if len(message) - LENGTH <= 52: curr_format = FORMAT + "{}p".format(len(message) - LENGTH) else: curr_format = FORMAT + "{}s".format(len(message) - LENGTH) timestamp, value, fee_fraction_int, text = struct.unpack(curr_format, message) try: text = text.decode("utf-8") except UnicodeDecodeError: text = "" status = "valid" except (struct.error) as e: timestamp, value, fee_fraction_int, text = 0, None, 0, None status = "invalid: could not unpack" if status == "valid": # For SQLite3 timestamp = min(timestamp, config.MAX_INT) value = min(value, config.MAX_INT) problems = validate(db, tx["source"], timestamp, value, fee_fraction_int, text, tx["block_index"]) if problems: status = "invalid: " + "; ".join(problems) # Lock? lock = False if text and text.lower() == "lock": lock = True timestamp, value, fee_fraction_int, text = 0, None, None, None else: lock = 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"], "timestamp": timestamp, "value": value, "fee_fraction_int": fee_fraction_int, "text": text, "locked": lock, "status": status, } if "integer overflow" not in status: sql = "insert into broadcasts values(:tx_index, :tx_hash, :block_index, :source, :timestamp, :value, :fee_fraction_int, :text, :locked, :status)" cursor.execute(sql, bindings) else: logger.warn("Not storing [broadcast] tx [%s]: %s" % (tx["tx_hash"], status)) logger.debug("Bindings: %s" % (json.dumps(bindings),)) # stop processing if broadcast is invalid for any reason if util.enabled("broadcast_invalid_check") and status != "valid": return # Negative values (default to ignore). if value is None or value < 0: # Cancel Open Bets? if value == -2: cursor.execute( """SELECT * FROM bets \ WHERE (status = ? AND feed_address = ?)""", ("open", tx["source"]), ) for i in list(cursor): bet.cancel_bet(db, i, "dropped", tx["block_index"]) # Cancel Pending Bet Matches? if value == -3: cursor.execute( """SELECT * FROM bet_matches \ WHERE (status = ? AND feed_address = ?)""", ("pending", tx["source"]), ) for bet_match in list(cursor): bet.cancel_bet_match(db, bet_match, "dropped", tx["block_index"]) cursor.close() return # stop processing if broadcast is invalid for any reason # @TODO: remove this check once broadcast_invalid_check has been activated if util.enabled("max_fee_fraction") and status != "valid": return # Handle bet matches that use this feed. cursor.execute( """SELECT * FROM bet_matches \ WHERE (status=? AND feed_address=?) ORDER BY tx1_index ASC, tx0_index ASC""", ("pending", tx["source"]), ) for bet_match in cursor.fetchall(): broadcast_bet_match_cursor = db.cursor() bet_match_id = util.make_id(bet_match["tx0_hash"], bet_match["tx1_hash"]) bet_match_status = None # Calculate total funds held in escrow and total fee to be paid if # the bet match is settled. Escrow less fee is amount to be paid back # to betters. total_escrow = bet_match["forward_quantity"] + bet_match["backward_quantity"] fee_fraction = fee_fraction_int / config.UNIT fee = int(fee_fraction * total_escrow) # Truncate. escrow_less_fee = total_escrow - fee # Get known bet match type IDs. 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"] # Get the bet match type ID of this bet match. bet_match_type_id = bet_match["tx0_bet_type"] + bet_match["tx1_bet_type"] # Contract for difference, with determinate settlement date. if bet_match_type_id == cfd_type_id: # Recognise tx0, tx1 as the bull, bear (in the right direction). if bet_match["tx0_bet_type"] < bet_match["tx1_bet_type"]: bull_address = bet_match["tx0_address"] bear_address = bet_match["tx1_address"] bull_escrow = bet_match["forward_quantity"] bear_escrow = bet_match["backward_quantity"] else: bull_address = bet_match["tx1_address"] bear_address = bet_match["tx0_address"] bull_escrow = bet_match["backward_quantity"] bear_escrow = bet_match["forward_quantity"] leverage = Fraction(bet_match["leverage"], 5040) initial_value = bet_match["initial_value"] bear_credit = bear_escrow - (value - initial_value) * leverage * config.UNIT bull_credit = escrow_less_fee - bear_credit bear_credit = round(bear_credit) bull_credit = round(bull_credit) # Liquidate, as necessary. if bull_credit >= escrow_less_fee or bull_credit <= 0: if bull_credit >= escrow_less_fee: bull_credit = escrow_less_fee bear_credit = 0 bet_match_status = "settled: liquidated for bull" util.credit( db, bull_address, config.XCP, bull_credit, action="bet {}".format(bet_match_status), event=tx["tx_hash"], ) elif bull_credit <= 0: bull_credit = 0 bear_credit = escrow_less_fee bet_match_status = "settled: liquidated for bear" util.credit( db, bear_address, config.XCP, bear_credit, action="bet {}".format(bet_match_status), event=tx["tx_hash"], ) # Pay fee to feed. util.credit(db, bet_match["feed_address"], config.XCP, fee, action="feed fee", event=tx["tx_hash"]) # For logging purposes. bindings = { "bet_match_id": bet_match_id, "bet_match_type_id": bet_match_type_id, "block_index": tx["block_index"], "settled": False, "bull_credit": bull_credit, "bear_credit": bear_credit, "winner": None, "escrow_less_fee": None, "fee": fee, } sql = "insert into bet_match_resolutions values(:bet_match_id, :bet_match_type_id, :block_index, :settled, :bull_credit, :bear_credit, :winner, :escrow_less_fee, :fee)" cursor.execute(sql, bindings) # Settle (if not liquidated). elif timestamp >= bet_match["deadline"]: bet_match_status = "settled" util.credit( db, bull_address, config.XCP, bull_credit, action="bet {}".format(bet_match_status), event=tx["tx_hash"], ) util.credit( db, bear_address, config.XCP, bear_credit, action="bet {}".format(bet_match_status), event=tx["tx_hash"], ) # Pay fee to feed. util.credit(db, bet_match["feed_address"], config.XCP, fee, action="feed fee", event=tx["tx_hash"]) # For logging purposes. bindings = { "bet_match_id": bet_match_id, "bet_match_type_id": bet_match_type_id, "block_index": tx["block_index"], "settled": True, "bull_credit": bull_credit, "bear_credit": bear_credit, "winner": None, "escrow_less_fee": None, "fee": fee, } sql = "insert into bet_match_resolutions values(:bet_match_id, :bet_match_type_id, :block_index, :settled, :bull_credit, :bear_credit, :winner, :escrow_less_fee, :fee)" cursor.execute(sql, bindings) # Equal[/NotEqual] bet. elif bet_match_type_id == equal_type_id and timestamp >= bet_match["deadline"]: # Recognise tx0, tx1 as the bull, bear (in the right direction). if bet_match["tx0_bet_type"] < bet_match["tx1_bet_type"]: equal_address = bet_match["tx0_address"] notequal_address = bet_match["tx1_address"] else: equal_address = bet_match["tx1_address"] notequal_address = bet_match["tx0_address"] # Decide who won, and credit appropriately. if value == bet_match["target_value"]: winner = "Equal" bet_match_status = "settled: for equal" util.credit( db, equal_address, config.XCP, escrow_less_fee, action="bet {}".format(bet_match_status), event=tx["tx_hash"], ) else: winner = "NotEqual" bet_match_status = "settled: for notequal" util.credit( db, notequal_address, config.XCP, escrow_less_fee, action="bet {}".format(bet_match_status), event=tx["tx_hash"], ) # Pay fee to feed. util.credit(db, bet_match["feed_address"], config.XCP, fee, action="feed fee", event=tx["tx_hash"]) # For logging purposes. bindings = { "bet_match_id": bet_match_id, "bet_match_type_id": bet_match_type_id, "block_index": tx["block_index"], "settled": None, "bull_credit": None, "bear_credit": None, "winner": winner, "escrow_less_fee": escrow_less_fee, "fee": fee, } sql = "insert into bet_match_resolutions values(:bet_match_id, :bet_match_type_id, :block_index, :settled, :bull_credit, :bear_credit, :winner, :escrow_less_fee, :fee)" cursor.execute(sql, bindings) # Update the bet match’s status. if bet_match_status: bindings = { "status": bet_match_status, "bet_match_id": util.make_id(bet_match["tx0_hash"], bet_match["tx1_hash"]), } sql = "update bet_matches set status = :status where id = :bet_match_id" cursor.execute(sql, bindings) log.message(db, tx["block_index"], "update", "bet_matches", bindings) broadcast_bet_match_cursor.close() cursor.close()
def parse(db, tx, message): cursor = db.cursor() # Unpack message. try: if len(message) != LENGTH: raise exceptions.UnpackError tx0_hash_bytes, tx1_hash_bytes = struct.unpack(FORMAT, message) tx0_hash, tx1_hash = binascii.hexlify(tx0_hash_bytes).decode( 'utf-8'), binascii.hexlify(tx1_hash_bytes).decode('utf-8') order_match_id = util.make_id(tx0_hash, tx1_hash) status = 'valid' except (exceptions.UnpackError, struct.error): tx0_hash, tx1_hash, order_match_id = None, None, None status = 'invalid: could not unpack' if status == 'valid': _, btc_quantity, escrowed_asset, escrowed_quantity, _, problems = validate( db, tx['source'], order_match_id, tx['block_index']) if problems: status = 'invalid: ' + '; '.join(problems) if status == 'valid': # BTC must be paid all at once. if tx['btc_amount'] >= btc_quantity: # Credit source address for the currency that he bought with the bitcoins. util.credit(db, tx['source'], escrowed_asset, escrowed_quantity, action='btcpay', event=tx['tx_hash']) status = 'valid' # Update order match. bindings = { 'status': 'completed', '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, tx['block_index'], 'update', 'order_matches', bindings) # Update give and get order status as filled if order_match is completed if util.enabled('btc_order_filled'): bindings = { 'status': 'pending', 'tx0_hash': tx0_hash, 'tx1_hash': 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 = cursor.fetchall() if len(order_matches) == 0: # mark both btc get and give orders as filled when order_match is completed and give or get remaining = 0 bindings = { 'status': 'filled', 'tx0_hash': tx0_hash, 'tx1_hash': tx1_hash } sql = 'update orders set status = :status where ((tx_hash in (:tx0_hash, :tx1_hash)) and ((give_remaining = 0) or (get_remaining = 0)))' cursor.execute(sql, bindings) else: # always mark btc get order as filled when order_match is completed and give or get remaining = 0 bindings = { 'status': 'filled', 'source': tx['destination'], 'tx0_hash': tx0_hash, 'tx1_hash': tx1_hash } sql = 'update orders set status = :status where ((tx_hash in (:tx0_hash, :tx1_hash)) and ((give_remaining = 0) or (get_remaining = 0)) and (source = :source))' cursor.execute(sql, bindings) # 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'], 'destination': tx['destination'], 'btc_amount': tx['btc_amount'], 'order_match_id': order_match_id, 'status': status, } if "integer overflow" not in status: sql = 'insert into btcpays values(:tx_index, :tx_hash, :block_index, :source, :destination, :btc_amount, :order_match_id, :status)' cursor.execute(sql, bindings) else: logger.warn("Not storing [btcpay] tx [%s]: %s" % (tx['tx_hash'], status)) logger.debug("Bindings: %s" % (json.dumps(bindings), ))
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): # 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): # 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): # 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: # 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: # Protocol change. if not (block_index >= 315000 or config.TESTNET): # 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 parse (db, tx, message): dividend_parse_cursor = db.cursor() # Unpack message. try: if (tx['block_index'] > 288150 or config.TESTNET or config.REGTEST) and len(message) == LENGTH_2: quantity_per_unit, asset_id, dividend_asset_id = struct.unpack(FORMAT_2, message) asset = util.get_asset_name(db, asset_id, tx['block_index']) dividend_asset = util.get_asset_name(db, dividend_asset_id, tx['block_index']) status = 'valid' elif len(message) == LENGTH_1: quantity_per_unit, asset_id = struct.unpack(FORMAT_1, message) asset = util.get_asset_name(db, asset_id, tx['block_index']) dividend_asset = config.XCP status = 'valid' else: raise exceptions.UnpackError except (exceptions.UnpackError, exceptions.AssetNameError, struct.error) as e: dividend_asset, quantity_per_unit, asset = None, None, None status = 'invalid: could not unpack' if dividend_asset == config.BTC: status = 'invalid: cannot pay {} dividends within protocol'.format(config.BTC) if status == 'valid': # For SQLite3 quantity_per_unit = min(quantity_per_unit, config.MAX_INT) dividend_total, outputs, problems, fee = validate(db, tx['source'], quantity_per_unit, asset, dividend_asset, block_index=tx['block_index']) if problems: status = 'invalid: ' + '; '.join(problems) if status == 'valid': # Debit. util.debit(db, tx['source'], dividend_asset, dividend_total, action='dividend', event=tx['tx_hash']) if tx['block_index'] >= 330000 or config.TESTNET or config.REGTEST: # Protocol change. util.debit(db, tx['source'], config.XCP, fee, action='dividend fee', event=tx['tx_hash']) # Credit. for output in outputs: if not util.enabled('dont_credit_zero_dividend') or output['dividend_quantity'] > 0: util.credit(db, output['address'], dividend_asset, output['dividend_quantity'], action='dividend', 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'], 'asset': asset, 'dividend_asset': dividend_asset, 'quantity_per_unit': quantity_per_unit, 'fee_paid': fee, 'status': status, } if "integer overflow" not in status: sql = 'insert into dividends values(:tx_index, :tx_hash, :block_index, :source, :asset, :dividend_asset, :quantity_per_unit, :fee_paid, :status)' dividend_parse_cursor.execute(sql, bindings) else: logger.warn("Not storing [dividend] tx [%s]: %s" % (tx['tx_hash'], status)) logger.debug("Bindings: %s" % (json.dumps(bindings), )) dividend_parse_cursor.close()
def parse (db, tx, message): cursor = db.cursor() # Unpack message. try: if len(message) - LENGTH <= 52: curr_format = FORMAT + '{}p'.format(len(message) - LENGTH) else: curr_format = FORMAT + '{}s'.format(len(message) - LENGTH) timestamp, value, fee_fraction_int, text = struct.unpack(curr_format, message) try: text = text.decode('utf-8') except UnicodeDecodeError: text = '' status = 'valid' except (struct.error) as e: timestamp, value, fee_fraction_int, text = 0, None, 0, None status = 'invalid: could not unpack' if status == 'valid': # For SQLite3 timestamp = min(timestamp, config.MAX_INT) value = min(value, config.MAX_INT) problems = validate(db, tx['source'], timestamp, value, fee_fraction_int, text, tx['block_index']) if problems: status = 'invalid: ' + '; '.join(problems) # Lock? lock = False if text and text.lower() == 'lock': lock = True timestamp, value, fee_fraction_int, text = 0, None, None, None else: lock = 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'], 'timestamp': timestamp, 'value': value, 'fee_fraction_int': fee_fraction_int, 'text': text, 'locked': lock, 'status': status, } sql='insert into broadcasts values(:tx_index, :tx_hash, :block_index, :source, :timestamp, :value, :fee_fraction_int, :text, :locked, :status)' cursor.execute(sql, bindings) # Negative values (default to ignore). if value == None or value < 0: # Cancel Open Bets? if value == -2: cursor.execute('''SELECT * FROM bets \ WHERE (status = ? AND feed_address = ?)''', ('open', tx['source'])) for i in list(cursor): bet.cancel_bet(db, i, 'dropped', tx['block_index']) # Cancel Pending Bet Matches? if value == -3: cursor.execute('''SELECT * FROM bet_matches \ WHERE (status = ? AND feed_address = ?)''', ('pending', tx['source'])) for bet_match in list(cursor): bet.cancel_bet_match(db, bet_match, 'dropped', tx['block_index']) cursor.close() return # Handle bet matches that use this feed. cursor.execute('''SELECT * FROM bet_matches \ WHERE (status=? AND feed_address=?) ORDER BY tx1_index ASC, tx0_index ASC''', ('pending', tx['source'])) for bet_match in cursor.fetchall(): if util.enabled('max_fee_fraction'): if status != 'valid': break broadcast_bet_match_cursor = db.cursor() bet_match_id = util.make_id(bet_match['tx0_hash'], bet_match['tx1_hash']) bet_match_status = None # Calculate total funds held in escrow and total fee to be paid if # the bet match is settled. Escrow less fee is amount to be paid back # to betters. total_escrow = bet_match['forward_quantity'] + bet_match['backward_quantity'] fee_fraction = fee_fraction_int / config.UNIT fee = int(fee_fraction * total_escrow) # Truncate. escrow_less_fee = total_escrow - fee # Get known bet match type IDs. 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'] # Get the bet match type ID of this bet match. bet_match_type_id = bet_match['tx0_bet_type'] + bet_match['tx1_bet_type'] # Contract for difference, with determinate settlement date. if bet_match_type_id == cfd_type_id: # Recognise tx0, tx1 as the bull, bear (in the right direction). if bet_match['tx0_bet_type'] < bet_match['tx1_bet_type']: bull_address = bet_match['tx0_address'] bear_address = bet_match['tx1_address'] bull_escrow = bet_match['forward_quantity'] bear_escrow = bet_match['backward_quantity'] else: bull_address = bet_match['tx1_address'] bear_address = bet_match['tx0_address'] bull_escrow = bet_match['backward_quantity'] bear_escrow = bet_match['forward_quantity'] leverage = Fraction(bet_match['leverage'], 5040) initial_value = bet_match['initial_value'] bear_credit = bear_escrow - (value - initial_value) * leverage * config.UNIT bull_credit = escrow_less_fee - bear_credit bear_credit = round(bear_credit) bull_credit = round(bull_credit) # Liquidate, as necessary. if bull_credit >= escrow_less_fee or bull_credit <= 0: if bull_credit >= escrow_less_fee: bull_credit = escrow_less_fee bear_credit = 0 bet_match_status = 'settled: liquidated for bull' util.credit(db, bull_address, config.XCP, bull_credit, action='bet {}'.format(bet_match_status), event=tx['tx_hash']) elif bull_credit <= 0: bull_credit = 0 bear_credit = escrow_less_fee bet_match_status = 'settled: liquidated for bear' util.credit(db, bear_address, config.XCP, bear_credit, action='bet {}'.format(bet_match_status), event=tx['tx_hash']) # Pay fee to feed. util.credit(db, bet_match['feed_address'], config.XCP, fee, action='feed fee', event=tx['tx_hash']) # For logging purposes. bindings = { 'bet_match_id': bet_match_id, 'bet_match_type_id': bet_match_type_id, 'block_index': tx['block_index'], 'settled': False, 'bull_credit': bull_credit, 'bear_credit': bear_credit, 'winner': None, 'escrow_less_fee': None, 'fee': fee } sql='insert into bet_match_resolutions values(:bet_match_id, :bet_match_type_id, :block_index, :settled, :bull_credit, :bear_credit, :winner, :escrow_less_fee, :fee)' cursor.execute(sql, bindings) # Settle (if not liquidated). elif timestamp >= bet_match['deadline']: bet_match_status = 'settled' util.credit(db, bull_address, config.XCP, bull_credit, action='bet {}'.format(bet_match_status), event=tx['tx_hash']) util.credit(db, bear_address, config.XCP, bear_credit, action='bet {}'.format(bet_match_status), event=tx['tx_hash']) # Pay fee to feed. util.credit(db, bet_match['feed_address'], config.XCP, fee, action='feed fee', event=tx['tx_hash']) # For logging purposes. bindings = { 'bet_match_id': bet_match_id, 'bet_match_type_id': bet_match_type_id, 'block_index': tx['block_index'], 'settled': True, 'bull_credit': bull_credit, 'bear_credit': bear_credit, 'winner': None, 'escrow_less_fee': None, 'fee': fee } sql='insert into bet_match_resolutions values(:bet_match_id, :bet_match_type_id, :block_index, :settled, :bull_credit, :bear_credit, :winner, :escrow_less_fee, :fee)' cursor.execute(sql, bindings) # Equal[/NotEqual] bet. elif bet_match_type_id == equal_type_id and timestamp >= bet_match['deadline']: # Recognise tx0, tx1 as the bull, bear (in the right direction). if bet_match['tx0_bet_type'] < bet_match['tx1_bet_type']: equal_address = bet_match['tx0_address'] notequal_address = bet_match['tx1_address'] else: equal_address = bet_match['tx1_address'] notequal_address = bet_match['tx0_address'] # Decide who won, and credit appropriately. if value == bet_match['target_value']: winner = 'Equal' bet_match_status = 'settled: for equal' util.credit(db, equal_address, config.XCP, escrow_less_fee, action='bet {}'.format(bet_match_status), event=tx['tx_hash']) else: winner = 'NotEqual' bet_match_status = 'settled: for notequal' util.credit(db, notequal_address, config.XCP, escrow_less_fee, action='bet {}'.format(bet_match_status), event=tx['tx_hash']) # Pay fee to feed. util.credit(db, bet_match['feed_address'], config.XCP, fee, action='feed fee', event=tx['tx_hash']) # For logging purposes. bindings = { 'bet_match_id': bet_match_id, 'bet_match_type_id': bet_match_type_id, 'block_index': tx['block_index'], 'settled': None, 'bull_credit': None, 'bear_credit': None, 'winner': winner, 'escrow_less_fee': escrow_less_fee, 'fee': fee } sql='insert into bet_match_resolutions values(:bet_match_id, :bet_match_type_id, :block_index, :settled, :bull_credit, :bear_credit, :winner, :escrow_less_fee, :fee)' cursor.execute(sql, bindings) # Update the bet match’s status. if bet_match_status: bindings = { 'status': bet_match_status, 'bet_match_id': util.make_id(bet_match['tx0_hash'], bet_match['tx1_hash']) } sql='update bet_matches set status = :status where id = :bet_match_id' cursor.execute(sql, bindings) log.message(db, tx['block_index'], 'update', 'bet_matches', bindings) broadcast_bet_match_cursor.close() cursor.close()
def parse(db, tx, MAINNET_BURNS, message=None): burn_parse_cursor = db.cursor() if config.TESTNET or config.REGTEST: problems = [] status = 'valid' if status == 'valid': problems = validate(db, tx['source'], tx['destination'], tx['btc_amount'], tx['block_index'], overburn=False) if problems: status = 'invalid: ' + '; '.join(problems) if tx['btc_amount'] != None: sent = tx['btc_amount'] else: sent = 0 if status == 'valid': # Calculate quantity of XCP earned. (Maximum 1 BTC in total, ever.) cursor = db.cursor() cursor.execute( '''SELECT * FROM burns WHERE (status = ? AND source = ?)''', ('valid', tx['source'])) burns = cursor.fetchall() already_burned = sum([burn['burned'] for burn in burns]) ONE = 1 * config.UNIT max_burn = ONE - already_burned if sent > max_burn: burned = max_burn # Exceeded maximum burn; earn what you can. else: burned = sent total_time = config.BURN_END - config.BURN_START partial_time = config.BURN_END - tx['block_index'] multiplier = (1000 + (500 * Fraction(partial_time, total_time))) earned = round(burned * multiplier) # Credit source address with earned XCP. util.credit(db, tx['source'], config.XCP, earned, action='burn', event=tx['tx_hash']) else: burned = 0 earned = 0 tx_index = tx['tx_index'] tx_hash = tx['tx_hash'] block_index = tx['block_index'] source = tx['source'] else: # Mainnet burns are hard‐coded. try: line = MAINNET_BURNS[tx['tx_hash']] except KeyError: return util.credit(db, line['source'], config.XCP, int(line['earned']), action='burn', event=line['tx_hash']) tx_index = tx['tx_index'] tx_hash = line['tx_hash'] block_index = line['block_index'] source = line['source'] burned = line['burned'] earned = line['earned'] status = 'valid' # Add parsed transaction to message-type–specific table. # TODO: store sent in table bindings = { 'tx_index': tx_index, 'tx_hash': tx_hash, 'block_index': block_index, 'source': source, 'burned': burned, 'earned': earned, 'status': status, } if "integer overflow" not in status: sql = 'insert into burns values(:tx_index, :tx_hash, :block_index, :source, :burned, :earned, :status)' burn_parse_cursor.execute(sql, bindings) else: logger.warn("Not storing [burn] tx [%s]: %s" % (tx['tx_hash'], status)) logger.debug("Bindings: %s" % (json.dumps(bindings), )) burn_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 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'])) # 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: # 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: # 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