# Merged Python Files
#### bet.py
#! /usr/bin/python3
"""
Datastreams are identified by the address that publishes them, and referenced
in transaction outputs.
For CFD leverage, 1x = 5040, 2x = 10080, etc.: 5040 is a superior highly
composite number and a colossally abundant number, and has 1-10, 12 as factors.
All wagers are in XCP.
Expiring a bet match doesn’t re‐open the constituent bets. (So all bets may be ‘filled’.)
"""
import struct
import decimal
import json
D = decimal.Decimal
import time
import logging
logger = logging.getLogger(__name__)
from counterpartylib.lib import config
from counterpartylib.lib import exceptions
from counterpartylib.lib import util
from counterpartylib.lib import log
from counterpartylib.lib import message_type
FORMAT = '>HIQQdII'
LENGTH = 2 + 4 + 8 + 8 + 8 + 4 + 4
ID = 40
def initialise (db):
cursor = db.cursor()
# Bets.
cursor.execute('''CREATE TABLE IF NOT EXISTS bets(
tx_index INTEGER UNIQUE,
tx_hash TEXT UNIQUE,
block_index INTEGER,
source TEXT,
feed_address TEXT,
bet_type INTEGER,
deadline INTEGER,
wager_quantity INTEGER,
wager_remaining INTEGER,
counterwager_quantity INTEGER,
counterwager_remaining INTEGER,
target_value REAL,
leverage INTEGER,
expiration INTEGER,
expire_index INTEGER,
fee_fraction_int INTEGER,
status TEXT,
FOREIGN KEY (tx_index, tx_hash, block_index) REFERENCES transactions(tx_index, tx_hash, block_index),
PRIMARY KEY (tx_index, tx_hash))
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
block_index_idx ON bets (block_index)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
index_hash_idx ON bets (tx_index, tx_hash)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
expire_idx ON bets (status, expire_index)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
feed_valid_bettype_idx ON bets (feed_address, status, bet_type)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
source_idx ON bets (source)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
status_idx ON bets (status)
''')
# Bet Matches
cursor.execute('''CREATE TABLE IF NOT EXISTS bet_matches(
id TEXT PRIMARY KEY,
tx0_index INTEGER,
tx0_hash TEXT,
tx0_address TEXT,
tx1_index INTEGER,
tx1_hash TEXT,
tx1_address TEXT,
tx0_bet_type INTEGER,
tx1_bet_type INTEGER,
feed_address TEXT,
initial_value INTEGER,
deadline INTEGER,
target_value REAL,
leverage INTEGER,
forward_quantity INTEGER,
backward_quantity INTEGER,
tx0_block_index INTEGER,
tx1_block_index INTEGER,
block_index INTEGER,
tx0_expiration INTEGER,
tx1_expiration INTEGER,
match_expire_index INTEGER,
fee_fraction_int INTEGER,
status TEXT,
FOREIGN KEY (tx0_index, tx0_hash, tx0_block_index) REFERENCES transactions(tx_index, tx_hash, block_index),
FOREIGN KEY (tx1_index, tx1_hash, tx1_block_index) REFERENCES transactions(tx_index, tx_hash, block_index))
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
match_expire_idx ON bet_matches (status, match_expire_index)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
valid_feed_idx ON bet_matches (feed_address, status)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
id_idx ON bet_matches (id)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
tx0_address_idx ON bet_matches (tx0_address)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
tx1_address_idx ON bet_matches (tx1_address)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
status_idx ON bet_matches (status)
''')
# Bet Expirations
cursor.execute('''CREATE TABLE IF NOT EXISTS bet_expirations(
bet_index INTEGER PRIMARY KEY,
bet_hash TEXT UNIQUE,
source TEXT,
block_index INTEGER,
FOREIGN KEY (block_index) REFERENCES blocks(block_index),
FOREIGN KEY (bet_index, bet_hash) REFERENCES bets(tx_index, tx_hash))
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
block_index_idx ON bet_expirations (block_index)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
source_idx ON bet_expirations (source)
''')
# Bet Match Expirations
cursor.execute('''CREATE TABLE IF NOT EXISTS bet_match_expirations(
bet_match_id TEXT PRIMARY KEY,
tx0_address TEXT,
tx1_address TEXT,
block_index INTEGER,
FOREIGN KEY (bet_match_id) REFERENCES bet_matches(id),
FOREIGN KEY (block_index) REFERENCES blocks(block_index))
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
block_index_idx ON bet_match_expirations (block_index)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
tx0_address_idx ON bet_match_expirations (tx0_address)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
tx1_address_idx ON bet_match_expirations (tx1_address)
''')
# Bet Match Resolutions
cursor.execute('''CREATE TABLE IF NOT EXISTS bet_match_resolutions(
bet_match_id TEXT PRIMARY KEY,
bet_match_type_id INTEGER,
block_index INTEGER,
winner TEXT,
settled BOOL,
bull_credit INTEGER,
bear_credit INTEGER,
escrow_less_fee INTEGER,
fee INTEGER,
FOREIGN KEY (bet_match_id) REFERENCES bet_matches(id),
FOREIGN KEY (block_index) REFERENCES blocks(block_index))
''')
def cancel_bet (db, bet, status, block_index):
cursor = db.cursor()
# Update status of bet.
bindings = {
'status': status,
'tx_hash': bet['tx_hash']
}
sql='update bets set status = :status where tx_hash = :tx_hash'
cursor.execute(sql, bindings)
log.message(db, block_index, 'update', 'bets', bindings)
util.credit(db, bet['source'], config.XCP, bet['wager_remaining'], action='recredit wager remaining', event=bet['tx_hash'])
cursor = db.cursor()
def cancel_bet_match (db, bet_match, status, block_index):
# Does not re‐open, re‐fill, etc. constituent bets.
cursor = db.cursor()
# Recredit tx0 address.
util.credit(db, bet_match['tx0_address'], config.XCP,
bet_match['forward_quantity'], action='recredit forward quantity', event=bet_match['id'])
# Recredit tx1 address.
util.credit(db, bet_match['tx1_address'], config.XCP,
bet_match['backward_quantity'], action='recredit backward quantity', event=bet_match['id'])
# Update status of bet match.
bindings = {
'status': status,
'bet_match_id': bet_match['id']
}
sql='update bet_matches set status = :status where id = :bet_match_id'
cursor.execute(sql, bindings)
log.message(db, block_index, 'update', 'bet_matches', bindings)
cursor.close()
def get_fee_fraction (db, feed_address):
'''Get fee fraction from last broadcast from the feed_address address.
'''
cursor = db.cursor()
broadcasts = list(cursor.execute('''SELECT * FROM broadcasts WHERE (status = ? AND source = ?) ORDER BY tx_index ASC''', ('valid', feed_address)))
cursor.close()
if broadcasts:
last_broadcast = broadcasts[-1]
fee_fraction_int = last_broadcast['fee_fraction_int']
if fee_fraction_int: return fee_fraction_int / 1e8
else: return 0
else:
return 0
def validate (db, source, feed_address, bet_type, deadline, wager_quantity,
counterwager_quantity, target_value, leverage, expiration, block_index):
problems = []
if leverage is None: leverage = 5040
# For SQLite3
if wager_quantity > config.MAX_INT or counterwager_quantity > config.MAX_INT or bet_type > config.MAX_INT \
or deadline > config.MAX_INT or leverage > config.MAX_INT or block_index + expiration > config.MAX_INT:
problems.append('integer overflow')
# Look at feed to be bet on.
cursor = db.cursor()
broadcasts = list(cursor.execute('''SELECT * FROM broadcasts WHERE (status = ? AND source = ?) ORDER BY tx_index ASC''', ('valid', feed_address)))
cursor.close()
if not broadcasts:
problems.append('feed doesn’t exist')
elif not broadcasts[-1]['text']:
problems.append('feed is locked')
elif broadcasts[-1]['timestamp'] >= deadline:
problems.append('deadline in that feed’s past')
if not bet_type in (0, 1, 2, 3):
problems.append('unknown bet type')
# Valid leverage level?
if leverage != 5040 and bet_type in (2,3): # Equal, NotEqual
problems.append('leverage used with Equal or NotEqual')
if leverage < 5040 and not bet_type in (0,1): # BullCFD, BearCFD (fractional leverage makes sense precisely with CFDs)
problems.append('leverage level too low')
if bet_type in (0,1): # BullCFD, BearCFD
if block_index >= 312350: # Protocol change.
problems.append('CFDs temporarily disabled')
if not isinstance(wager_quantity, int):
problems.append('wager_quantity must be in satoshis')
return problems, leverage
if not isinstance(counterwager_quantity, int):
problems.append('counterwager_quantity must be in satoshis')
return problems, leverage
if not isinstance(expiration, int):
problems.append('expiration must be expressed as an integer block delta')
return problems, leverage
if wager_quantity <= 0: problems.append('non‐positive wager')
if counterwager_quantity <= 0: problems.append('non‐positive counterwager')
if deadline < 0: problems.append('negative deadline')
if expiration < 0: problems.append('negative expiration')
if expiration == 0 and not (block_index >= 317500 or config.TESTNET or config.REGTEST): # Protocol change.
problems.append('zero expiration')
if target_value:
if bet_type in (0,1): # BullCFD, BearCFD
problems.append('CFDs have no target value')
if target_value < 0:
problems.append('negative target value')
if expiration > config.MAX_EXPIRATION:
problems.append('expiration overflow')
return problems, leverage
def compose (db, source, feed_address, bet_type, deadline, wager_quantity,
counterwager_quantity, target_value, leverage, expiration):
if util.get_balance(db, source, config.XCP) < wager_quantity:
raise exceptions.ComposeError('insufficient funds')
problems, leverage = validate(db, source, feed_address, bet_type, deadline, wager_quantity,
counterwager_quantity, target_value, leverage, expiration, util.CURRENT_BLOCK_INDEX)
if util.date_passed(deadline):
problems.append('deadline passed')
if problems: raise exceptions.ComposeError(problems)
data = message_type.pack(ID)
data += struct.pack(FORMAT, bet_type, deadline,
wager_quantity, counterwager_quantity, target_value,
leverage, expiration)
return (source, [(feed_address, None)], data)
def parse (db, tx, message):
bet_parse_cursor = db.cursor()
# Unpack message.
try:
if len(message) != LENGTH:
raise exceptions.UnpackError
(bet_type, deadline, wager_quantity,
counterwager_quantity, target_value, leverage,
expiration) = struct.unpack(FORMAT, message)
status = 'open'
except (exceptions.UnpackError, struct.error):
(bet_type, deadline, wager_quantity,
counterwager_quantity, target_value, leverage,
expiration, fee_fraction_int) = 0, 0, 0, 0, 0, 0, 0, 0
status = 'invalid: could not unpack'
odds, fee_fraction = 0, 0
feed_address = tx['destination']
if status == 'open':
try:
odds = util.price(wager_quantity, counterwager_quantity)
except ZeroDivisionError:
odds = 0
fee_fraction = get_fee_fraction(db, feed_address)
# Overbet
bet_parse_cursor.execute('''SELECT * FROM balances \
WHERE (address = ? AND asset = ?)''', (tx['source'], config.XCP))
balances = list(bet_parse_cursor)
if not balances:
wager_quantity = 0
else:
balance = balances[0]['quantity']
if balance < wager_quantity:
wager_quantity = balance
counterwager_quantity = int(util.price(wager_quantity, odds))
problems, leverage = validate(db, tx['source'], feed_address, bet_type, deadline, wager_quantity,
counterwager_quantity, target_value, leverage, expiration, tx['block_index'])
if problems: status = 'invalid: ' + '; '.join(problems)
# Debit quantity wagered. (Escrow.)
if status == 'open':
util.debit(db, tx['source'], config.XCP, wager_quantity, action='bet', event=tx['tx_hash'])
# Add parsed transaction to message-type–specific table.
bindings = {
'tx_index': tx['tx_index'],
'tx_hash': tx['tx_hash'],
'block_index': tx['block_index'],
'source': tx['source'],
'feed_address': feed_address,
'bet_type': bet_type,
'deadline': deadline,
'wager_quantity': wager_quantity,
'wager_remaining': wager_quantity,
'counterwager_quantity': counterwager_quantity,
'counterwager_remaining': counterwager_quantity,
'target_value': target_value,
'leverage': leverage,
'expiration': expiration,
'expire_index': tx['block_index'] + expiration,
'fee_fraction_int': fee_fraction * 1e8,
'status': status,
}
if "integer overflow" not in status:
sql = 'insert into bets values(:tx_index, :tx_hash, :block_index, :source, :feed_address, :bet_type, :deadline, :wager_quantity, :wager_remaining, :counterwager_quantity, :counterwager_remaining, :target_value, :leverage, :expiration, :expire_index, :fee_fraction_int, :status)'
bet_parse_cursor.execute(sql, bindings)
else:
logger.warn("Not storing [bet] tx [%s]: %s" % (tx['tx_hash'], status))
logger.debug("Bindings: %s" % (json.dumps(bindings), ))
# Match.
if status == 'open' and tx['block_index'] != config.MEMPOOL_BLOCK_INDEX:
match(db, tx)
bet_parse_cursor.close()
def match (db, tx):
cursor = db.cursor()
# Get bet in question.
bets = list(cursor.execute('''SELECT * FROM bets\
WHERE (tx_index = ? AND status = ?)''', (tx['tx_index'], 'open')))
if not bets:
cursor.close()
return
else:
assert len(bets) == 1
tx1 = bets[0]
# Get counterbet_type.
if tx1['bet_type'] % 2: counterbet_type = tx1['bet_type'] - 1
else: counterbet_type = tx1['bet_type'] + 1
feed_address = tx1['feed_address']
cursor.execute('''SELECT * FROM bets\
WHERE (feed_address=? AND status=? AND bet_type=?)''',
(tx1['feed_address'], 'open', counterbet_type))
tx1_wager_remaining = tx1['wager_remaining']
tx1_counterwager_remaining = tx1['counterwager_remaining']
bet_matches = cursor.fetchall()
if tx['block_index'] > 284500 or config.TESTNET or config.REGTEST: # Protocol change.
sorted(bet_matches, key=lambda x: x['tx_index']) # Sort by tx index second.
sorted(bet_matches, key=lambda x: util.price(x['wager_quantity'], x['counterwager_quantity'])) # Sort by price first.
tx1_status = tx1['status']
for tx0 in bet_matches:
if tx1_status != 'open': break
logger.debug('Considering: ' + tx0['tx_hash'])
tx0_wager_remaining = tx0['wager_remaining']
tx0_counterwager_remaining = tx0['counterwager_remaining']
# Bet types must be opposite.
if counterbet_type != tx0['bet_type']:
logger.debug('Skipping: bet types disagree.')
continue
# Leverages must agree exactly
if tx0['leverage'] != tx1['leverage']:
logger.debug('Skipping: leverages disagree.')
continue
# Target values must agree exactly.
if tx0['target_value'] != tx1['target_value']:
logger.debug('Skipping: target values disagree.')
continue
# Fee fractions must agree exactly.
if tx0['fee_fraction_int'] != tx1['fee_fraction_int']:
logger.debug('Skipping: fee fractions disagree.')
continue
# Deadlines must agree exactly.
if tx0['deadline'] != tx1['deadline']:
logger.debug('Skipping: deadlines disagree.')
continue
# If the odds agree, make the trade. The found order sets the odds,
# and they trade as much as they can.
tx0_odds = util.price(tx0['wager_quantity'], tx0['counterwager_quantity'])
tx0_inverse_odds = util.price(tx0['counterwager_quantity'], tx0['wager_quantity'])
tx1_odds = util.price(tx1['wager_quantity'], tx1['counterwager_quantity'])
if tx['block_index'] < 286000: tx0_inverse_odds = util.price(1, tx0_odds) # Protocol change.
logger.debug('Tx0 Inverse Odds: {}; Tx1 Odds: {}'.format(float(tx0_inverse_odds), float(tx1_odds)))
if tx0_inverse_odds > tx1_odds:
logger.debug('Skipping: price mismatch.')
else:
logger.debug('Potential forward quantities: {}, {}'.format(tx0_wager_remaining, int(util.price(tx1_wager_remaining, tx1_odds))))
forward_quantity = int(min(tx0_wager_remaining, int(util.price(tx1_wager_remaining, tx1_odds))))
logger.debug('Forward Quantity: {}'.format(forward_quantity))
backward_quantity = round(forward_quantity / tx0_odds)
logger.debug('Backward Quantity: {}'.format(backward_quantity))
if not forward_quantity:
logger.debug('Skipping: zero forward quantity.')
continue
if tx1['block_index'] >= 286500 or config.TESTNET or config.REGTEST: # Protocol change.
if not backward_quantity:
logger.debug('Skipping: zero backward quantity.')
continue
bet_match_id = util.make_id(tx0['tx_hash'], tx1['tx_hash'])
# Debit the order.
# Counterwager remainings may be negative.
tx0_wager_remaining = tx0_wager_remaining - forward_quantity
tx0_counterwager_remaining = tx0_counterwager_remaining - backward_quantity
tx1_wager_remaining = tx1_wager_remaining - backward_quantity
tx1_counterwager_remaining = tx1_counterwager_remaining - forward_quantity
# tx0
tx0_status = 'open'
if tx0_wager_remaining <= 0 or tx0_counterwager_remaining <= 0:
# Fill order, and recredit give_remaining.
tx0_status = 'filled'
util.credit(db, tx0['source'], config.XCP, tx0_wager_remaining, event=tx1['tx_hash'], action='filled')
bindings = {
'wager_remaining': tx0_wager_remaining,
'counterwager_remaining': tx0_counterwager_remaining,
'status': tx0_status,
'tx_hash': tx0['tx_hash']
}
sql='update bets set wager_remaining = :wager_remaining, counterwager_remaining = :counterwager_remaining, status = :status where tx_hash = :tx_hash'
cursor.execute(sql, bindings)
log.message(db, tx['block_index'], 'update', 'bets', bindings)
if tx1['block_index'] >= 292000 or config.TESTNET or config.REGTEST: # Protocol change
if tx1_wager_remaining <= 0 or tx1_counterwager_remaining <= 0:
# Fill order, and recredit give_remaining.
tx1_status = 'filled'
util.credit(db, tx1['source'], config.XCP, tx1_wager_remaining, event=tx1['tx_hash'], action='filled')
# tx1
bindings = {
'wager_remaining': tx1_wager_remaining,
'counterwager_remaining': tx1_counterwager_remaining,
'status': tx1_status,
'tx_hash': tx1['tx_hash']
}
sql='update bets set wager_remaining = :wager_remaining, counterwager_remaining = :counterwager_remaining, status = :status where tx_hash = :tx_hash'
cursor.execute(sql, bindings)
log.message(db, tx['block_index'], 'update', 'bets', bindings)
# Get last value of feed.
broadcasts = list(cursor.execute('''SELECT * FROM broadcasts WHERE (status = ? AND source = ?) ORDER BY tx_index ASC''', ('valid', feed_address)))
initial_value = broadcasts[-1]['value']
# Record bet fulfillment.
bindings = {
'id': util.make_id(tx0['tx_hash'], tx['tx_hash']),
'tx0_index': tx0['tx_index'],
'tx0_hash': tx0['tx_hash'],
'tx0_address': tx0['source'],
'tx1_index': tx1['tx_index'],
'tx1_hash': tx1['tx_hash'],
'tx1_address': tx1['source'],
'tx0_bet_type': tx0['bet_type'],
'tx1_bet_type': tx1['bet_type'],
'feed_address': tx1['feed_address'],
'initial_value': initial_value,
'deadline': tx1['deadline'],
'target_value': tx1['target_value'],
'leverage': tx1['leverage'],
'forward_quantity': forward_quantity,
'backward_quantity': backward_quantity,
'tx0_block_index': tx0['block_index'],
'tx1_block_index': tx1['block_index'],
'block_index': max(tx0['block_index'], tx1['block_index']),
'tx0_expiration': tx0['expiration'],
'tx1_expiration': tx1['expiration'],
'match_expire_index': min(tx0['expire_index'], tx1['expire_index']),
'fee_fraction_int': tx1['fee_fraction_int'],
'status': 'pending',
}
sql='insert into bet_matches values(:id, :tx0_index, :tx0_hash, :tx0_address, :tx1_index, :tx1_hash, :tx1_address, :tx0_bet_type, :tx1_bet_type, :feed_address, :initial_value, :deadline, :target_value, :leverage, :forward_quantity, :backward_quantity, :tx0_block_index, :tx1_block_index, :block_index, :tx0_expiration, :tx1_expiration, :match_expire_index, :fee_fraction_int, :status)'
cursor.execute(sql, bindings)
cursor.close()
return
def expire (db, block_index, block_time):
cursor = db.cursor()
# Expire bets and give refunds for the quantity wager_remaining.
cursor.execute('''SELECT * FROM bets \
WHERE (status = ? AND expire_index < ?)''', ('open', block_index))
for bet in cursor.fetchall():
cancel_bet(db, bet, 'expired', block_index)
# Record bet expiration.
bindings = {
'bet_index': bet['tx_index'],
'bet_hash': bet['tx_hash'],
'source': bet['source'],
'block_index': block_index
}
sql='insert into bet_expirations values(:bet_index, :bet_hash, :source, :block_index)'
cursor.execute(sql, bindings)
# Expire bet matches whose deadline is more than two weeks before the current block time.
cursor.execute('''SELECT * FROM bet_matches \
WHERE (status = ? AND deadline < ?)''', ('pending', block_time - config.TWO_WEEKS))
for bet_match in cursor.fetchall():
cancel_bet_match(db, bet_match, 'expired', block_index)
# Record bet match expiration.
bindings = {
'bet_match_id': bet_match['id'],
'tx0_address': bet_match['tx0_address'],
'tx1_address': bet_match['tx1_address'],
'block_index': block_index
}
sql='insert into bet_match_expirations values(:bet_match_id, :tx0_address, :tx1_address, :block_index)'
cursor.execute(sql, bindings)
cursor.close()
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
#### broadcast.py
#! /usr/bin/python3
"""
Broadcast a message, with or without a price.
Multiple messages per block are allowed. Bets are be made on the 'timestamp'
field, and not the block index.
An address is a feed of broadcasts. Feeds may be locked with a broadcast whose
text field is identical to ‘lock’ (case insensitive). Bets on a feed reference
the address that is the source of the feed in an output which includes the
(latest) required fee.
Broadcasts without a price may not be used for betting. Broadcasts about events
with a small number of possible outcomes (e.g. sports games), should be
written, for example, such that a price of 1 XCP means one outcome, 2 XCP means
another, etc., which schema should be described in the 'text' field.
fee_fraction: .05 XCP means 5%. It may be greater than 1, however; but
because it is stored as a four‐byte integer, it may not be greater than about
42.
"""
import struct
import decimal
D = decimal.Decimal
from fractions import Fraction
import json
import logging
logger = logging.getLogger(__name__)
from bitcoin.core import VarIntSerializer
from counterpartylib.lib import exceptions
from counterpartylib.lib import config
from counterpartylib.lib import util
from counterpartylib.lib import log
from counterpartylib.lib import message_type
from . import (bet)
FORMAT = '>IdI'
LENGTH = 4 + 8 + 4
ID = 30
# NOTE: Pascal strings are used for storing texts for backwards‐compatibility.
def initialise(db):
cursor = db.cursor()
cursor.execute('''CREATE TABLE IF NOT EXISTS broadcasts(
tx_index INTEGER PRIMARY KEY,
tx_hash TEXT UNIQUE,
block_index INTEGER,
source TEXT,
timestamp INTEGER,
value REAL,
fee_fraction_int INTEGER,
text TEXT,
locked BOOL,
status TEXT,
FOREIGN KEY (tx_index, tx_hash, block_index) REFERENCES transactions(tx_index, tx_hash, block_index))
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
block_index_idx ON broadcasts (block_index)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
status_source_idx ON broadcasts (status, source)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
status_source_index_idx ON broadcasts (status, source, tx_index)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
timestamp_idx ON broadcasts (timestamp)
''')
def validate (db, source, timestamp, value, fee_fraction_int, text, block_index):
problems = []
# For SQLite3
if timestamp > config.MAX_INT or value > config.MAX_INT or fee_fraction_int > config.MAX_INT:
problems.append('integer overflow')
if util.enabled('max_fee_fraction'):
if fee_fraction_int >= config.UNIT:
problems.append('fee fraction greater than or equal to 1')
else:
if fee_fraction_int > 4294967295:
problems.append('fee fraction greater than 42.94967295')
if timestamp < 0: problems.append('negative timestamp')
if not source:
problems.append('null source address')
# Check previous broadcast in this feed.
cursor = db.cursor()
broadcasts = list(cursor.execute('''SELECT * FROM broadcasts WHERE (status = ? AND source = ?) ORDER BY tx_index ASC''', ('valid', source)))
cursor.close()
if broadcasts:
last_broadcast = broadcasts[-1]
if last_broadcast['locked']:
problems.append('locked feed')
elif timestamp <= last_broadcast['timestamp']:
problems.append('feed timestamps not monotonically increasing')
if not (block_index >= 317500 or config.TESTNET or config.REGTEST): # Protocol change.
if len(text) > 52:
problems.append('text too long')
if util.enabled('options_require_memo') and text and text.lower().startswith('options'):
try:
# Check for options and if they are valid.
options = util.parse_options_from_string(text)
if options is not False:
util.validate_address_options(options)
except exceptions.OptionsError as e:
problems.append(str(e))
return problems
def compose (db, source, timestamp, value, fee_fraction, text):
# Store the fee fraction as an integer.
fee_fraction_int = int(fee_fraction * 1e8)
problems = validate(db, source, timestamp, value, fee_fraction_int, text, util.CURRENT_BLOCK_INDEX)
if problems: raise exceptions.ComposeError(problems)
data = message_type.pack(ID)
# always use custom length byte instead of problematic usage of 52p format and make sure to encode('utf-8') for length
if util.enabled('broadcast_pack_text'):
data += struct.pack(FORMAT, timestamp, value, fee_fraction_int)
data += VarIntSerializer.serialize(len(text.encode('utf-8')))
data += text.encode('utf-8')
else:
if len(text) <= 52:
curr_format = FORMAT + '{}p'.format(len(text) + 1)
else:
curr_format = FORMAT + '{}s'.format(len(text))
data += struct.pack(curr_format, timestamp, value, fee_fraction_int, text.encode('utf-8'))
return (source, [], data)
def parse (db, tx, message):
cursor = db.cursor()
# Unpack message.
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:]
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
# 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'])
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']
if util.enabled('inmutable_fee_fraction'):
fee_fraction = bet_match['fee_fraction_int'] / config.UNIT
else:
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()
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
#### btcpay.py
#! /usr/bin/python3
import binascii
import json
import pprint
import struct
import logging
logger = logging.getLogger(__name__)
from counterpartylib.lib import config
from counterpartylib.lib import exceptions
from counterpartylib.lib import util
from counterpartylib.lib import log
from counterpartylib.lib import message_type
FORMAT = '>32s32s'
LENGTH = 32 + 32
ID = 11
def initialise(db):
cursor = db.cursor()
cursor.execute('''CREATE TABLE IF NOT EXISTS btcpays(
tx_index INTEGER PRIMARY KEY,
tx_hash TEXT UNIQUE,
block_index INTEGER,
source TEXT,
destination TEXT,
btc_amount INTEGER,
order_match_id TEXT,
status TEXT,
FOREIGN KEY (tx_index, tx_hash, block_index) REFERENCES transactions(tx_index, tx_hash, block_index))
''')
# Disallows invalids: FOREIGN KEY (order_match_id) REFERENCES order_matches(id))
cursor.execute('''CREATE INDEX IF NOT EXISTS
block_index_idx ON btcpays (block_index)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
source_idx ON btcpays (source)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
destination_idx ON btcpays (destination)
''')
def validate (db, source, order_match_id, block_index):
problems = []
order_match = None
cursor = db.cursor()
cursor.execute('''SELECT * FROM order_matches \
WHERE id = ?''', (order_match_id,))
order_matches = cursor.fetchall()
cursor.close()
if len(order_matches) == 0:
problems.append('no such order match %s' % order_match_id)
return None, None, None, None, order_match, problems
elif len(order_matches) > 1:
assert False
else:
order_match = order_matches[0]
if order_match['status'] == 'expired':
problems.append('order match expired')
elif order_match['status'] == 'completed':
problems.append('order match completed')
elif order_match['status'].startswith('invalid'):
problems.append('order match invalid')
elif order_match['status'] != 'pending':
raise exceptions.OrderError('unrecognised order match status')
# Figure out to which address the BTC are being paid.
# Check that source address is correct.
if order_match['backward_asset'] == config.BTC:
if source != order_match['tx1_address'] and not (block_index >= 313900 or config.TESTNET or config.REGTEST): # Protocol change.
problems.append('incorrect source address')
destination = order_match['tx0_address']
btc_quantity = order_match['backward_quantity']
escrowed_asset = order_match['forward_asset']
escrowed_quantity = order_match['forward_quantity']
elif order_match['forward_asset'] == config.BTC:
if source != order_match['tx0_address'] and not (block_index >= 313900 or config.TESTNET or config.REGTEST): # Protocol change.
problems.append('incorrect source address')
destination = order_match['tx1_address']
btc_quantity = order_match['forward_quantity']
escrowed_asset = order_match['backward_asset']
escrowed_quantity = order_match['backward_quantity']
else:
assert False
return destination, btc_quantity, escrowed_asset, escrowed_quantity, order_match, problems
def compose (db, source, order_match_id):
tx0_hash, tx1_hash = util.parse_id(order_match_id)
destination, btc_quantity, escrowed_asset, escrowed_quantity, order_match, problems = validate(db, source, order_match_id, util.CURRENT_BLOCK_INDEX)
if problems: raise exceptions.ComposeError(problems)
# Warn if down to the wire.
time_left = order_match['match_expire_index'] - util.CURRENT_BLOCK_INDEX
if time_left < 4:
logger.warning('Only {} blocks until that order match expires. The payment might not make into the blockchain in time.'.format(time_left))
if 10 - time_left < 4:
logger.warning('Order match has only {} confirmation(s).'.format(10 - time_left))
tx0_hash_bytes, tx1_hash_bytes = binascii.unhexlify(bytes(tx0_hash, 'utf-8')), binascii.unhexlify(bytes(tx1_hash, 'utf-8'))
data = message_type.pack(ID)
data += struct.pack(FORMAT, tx0_hash_bytes, tx1_hash_bytes)
return (source, [(destination, btc_quantity)], data)
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)
# 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), ))
cursor.close()
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
#### burn.py
#! /usr/bin/python3
import json
import struct
import decimal
import logging
logger = logging.getLogger(__name__)
D = decimal.Decimal
from fractions import Fraction
from counterpartylib.lib import (config, exceptions, util)
"""Burn {} to earn {} during a special period of time.""".format(config.BTC, config.XCP)
ID = 60
def initialise (db):
cursor = db.cursor()
cursor.execute('''CREATE TABLE IF NOT EXISTS burns(
tx_index INTEGER PRIMARY KEY,
tx_hash TEXT UNIQUE,
block_index INTEGER,
source TEXT,
burned INTEGER,
earned INTEGER,
status TEXT,
FOREIGN KEY (tx_index, tx_hash, block_index) REFERENCES transactions(tx_index, tx_hash, block_index))
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
status_idx ON burns (status)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
address_idx ON burns (source)
''')
def validate (db, source, destination, quantity, block_index, overburn=False):
problems = []
# Check destination address.
if destination != config.UNSPENDABLE:
problems.append('wrong destination address')
if not isinstance(quantity, int):
problems.append('quantity must be in satoshis')
return problems
if quantity < 0: problems.append('negative quantity')
# Try to make sure that the burned funds won't go to waste.
if block_index < config.BURN_START - 1:
problems.append('too early')
elif block_index > config.BURN_END:
problems.append('too late')
return problems
def compose (db, source, quantity, overburn=False):
cursor = db.cursor()
destination = config.UNSPENDABLE
problems = validate(db, source, destination, quantity, util.CURRENT_BLOCK_INDEX, overburn=overburn)
if problems: raise exceptions.ComposeError(problems)
# Check that a maximum of 1 BTC total is burned per address.
burns = list(cursor.execute('''SELECT * FROM burns WHERE (status = ? AND source = ?)''', ('valid', source)))
already_burned = sum([burn['burned'] for burn in burns])
if quantity > (1 * config.UNIT - already_burned) and not overburn:
raise exceptions.ComposeError('1 {} may be burned per address'.format(config.BTC))
cursor.close()
return (source, [(destination, quantity)], None)
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()
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
#### cancel.py
#! /usr/bin/python3
"""
offer_hash is the hash of either a bet or an order.
"""
import binascii
import struct
import json
import logging
logger = logging.getLogger(__name__)
from counterpartylib.lib import (config, exceptions, util, message_type)
from . import (order, bet, rps)
FORMAT = '>32s'
LENGTH = 32
ID = 70
def initialise (db):
cursor = db.cursor()
cursor.execute('''CREATE TABLE IF NOT EXISTS cancels(
tx_index INTEGER PRIMARY KEY,
tx_hash TEXT UNIQUE,
block_index INTEGER,
source TEXT,
offer_hash TEXT,
status TEXT,
FOREIGN KEY (tx_index, tx_hash, block_index) REFERENCES transactions(tx_index, tx_hash, block_index))
''')
# Offer hash is not a foreign key. (And it cannot be, because of some invalid cancels.)
cursor.execute('''CREATE INDEX IF NOT EXISTS
cancels_block_index_idx ON cancels (block_index)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
source_idx ON cancels (source)
''')
def validate (db, source, offer_hash):
problems = []
# TODO: make query only if necessary
cursor = db.cursor()
cursor.execute('''SELECT * from orders WHERE tx_hash = ?''', (offer_hash,))
orders = list(cursor)
cursor.execute('''SELECT * from bets WHERE tx_hash = ?''', (offer_hash,))
bets = list(cursor)
cursor.execute('''SELECT * from rps WHERE tx_hash = ?''', (offer_hash,))
rps = list(cursor)
cursor.close()
offer_type = None
if orders: offer_type = 'order'
elif bets: offer_type = 'bet'
elif rps: offer_type = 'rps'
else: problems = ['no open offer with that hash']
offer = None
if offer_type:
offers = orders + bets + rps
offer = offers[0]
if offer['source'] != source:
problems.append('incorrect source address')
if offer['status'] != 'open':
problems.append('offer not open')
return offer, offer_type, problems
def compose (db, source, offer_hash):
# Check that offer exists.
offer, offer_type, problems = validate(db, source, offer_hash)
if problems: raise exceptions.ComposeError(problems)
offer_hash_bytes = binascii.unhexlify(bytes(offer_hash, 'utf-8'))
data = message_type.pack(ID)
data += struct.pack(FORMAT, offer_hash_bytes)
return (source, [], data)
def parse (db, tx, message):
cursor = db.cursor()
# Unpack message.
try:
if len(message) != LENGTH:
raise exceptions.UnpackError
offer_hash_bytes = struct.unpack(FORMAT, message)[0]
offer_hash = binascii.hexlify(offer_hash_bytes).decode('utf-8')
status = 'valid'
except (exceptions.UnpackError, struct.error) as e:
offer_hash = None
status = 'invalid: could not unpack'
if status == 'valid':
offer, offer_type, problems = validate(db, tx['source'], offer_hash)
if problems:
status = 'invalid: ' + '; '.join(problems)
if status == 'valid':
# Cancel if order.
if offer_type == 'order':
order.cancel_order(db, offer, 'cancelled', tx['block_index'])
# Cancel if bet.
elif offer_type == 'bet':
bet.cancel_bet(db, offer, 'cancelled', tx['block_index'])
# Cancel if rps.
elif offer_type == 'rps':
rps.cancel_rps(db, offer, 'cancelled', tx['block_index'])
# If neither order or bet, mark as invalid.
else:
assert False
# Add parsed transaction to message-type–specific table.
bindings = {
'tx_index': tx['tx_index'],
'tx_hash': tx['tx_hash'],
'block_index': tx['block_index'],
'source': tx['source'],
'offer_hash': offer_hash,
'status': status,
}
if "integer overflow" not in status:
sql='INSERT INTO cancels VALUES (:tx_index, :tx_hash, :block_index, :source, :offer_hash, :status)'
cursor.execute(sql, bindings)
else:
logger.warn("Not storing [cancel] tx [%s]: %s" % (tx['tx_hash'], status))
logger.debug("Bindings: %s" % (json.dumps(bindings), ))
cursor.close()
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
#### destroy.py
#! /usr/bin/python3
"""Destroy a quantity of an asset."""
import struct
import json
import logging
logger = logging.getLogger(__name__)
from counterpartylib.lib import util
from counterpartylib.lib import config
from counterpartylib.lib import script
from counterpartylib.lib import message_type
from counterpartylib.lib.script import AddressError
from counterpartylib.lib.exceptions import *
FORMAT = '>QQ'
LENGTH = 8 + 8
MAX_TAG_LENGTH = 34
ID = 110
def initialise(db):
cursor = db.cursor()
cursor.execute('''CREATE TABLE IF NOT EXISTS destructions(
tx_index INTEGER PRIMARY KEY,
tx_hash TEXT UNIQUE,
block_index INTEGER,
source TEXT,
asset INTEGER,
quantity INTEGER,
tag TEXT,
status TEXT,
FOREIGN KEY (tx_index, tx_hash, block_index) REFERENCES transactions(tx_index, tx_hash, block_index))
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
status_idx ON destructions (status)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
address_idx ON destructions (source)
''')
def pack(db, asset, quantity, tag):
data = message_type.pack(ID)
if isinstance(tag, str):
tag = bytes(tag.encode('utf8'))[0:MAX_TAG_LENGTH]
elif isinstance(tag, bytes):
tag = tag[0:MAX_TAG_LENGTH]
else:
tag = b''
data += struct.pack(FORMAT, util.get_asset_id(db, asset, util.CURRENT_BLOCK_INDEX), quantity)
data += tag
return data
def unpack(db, message):
try:
asset_id, quantity = struct.unpack(FORMAT, message[0:16])
tag = message[16:]
asset = util.get_asset_name(db, asset_id, util.CURRENT_BLOCK_INDEX)
except struct.error:
raise UnpackError('could not unpack')
except AssetIDError:
raise UnpackError('asset id invalid')
return asset, quantity, tag
def validate (db, source, destination, asset, quantity):
try:
util.get_asset_id(db, asset, util.CURRENT_BLOCK_INDEX)
except AssetError:
raise ValidateError('asset invalid')
try:
script.validate(source)
except AddressError:
raise ValidateError('source address invalid')
if destination:
raise ValidateError('destination exists')
if asset == config.BTC:
raise ValidateError('cannot destroy {}'.format(config.BTC))
if type(quantity) != int:
raise ValidateError('quantity not integer')
if quantity > config.MAX_INT:
raise ValidateError('integer overflow, quantity too large')
if quantity < 0:
raise ValidateError('quantity negative')
if util.get_balance(db, source, asset) < quantity:
raise BalanceError('balance insufficient')
def compose (db, source, asset, quantity, tag):
# resolve subassets
asset = util.resolve_subasset_longname(db, asset)
validate(db, source, None, asset, quantity)
data = pack(db, asset, quantity, tag)
return (source, [], data)
def parse (db, tx, message):
status = 'valid'
asset, quantity, tag = None, None, None
try:
asset, quantity, tag = unpack(db, message)
validate(db, tx['source'], tx['destination'], asset, quantity)
util.debit(db, tx['source'], asset, quantity, 'destroy', tx['tx_hash'])
except UnpackError as e:
status = 'invalid: ' + ''.join(e.args)
except (ValidateError, BalanceError) as e:
status = 'invalid: ' + ''.join(e.args)
bindings = {
'tx_index': tx['tx_index'],
'tx_hash': tx['tx_hash'],
'block_index': tx['block_index'],
'source': tx['source'],
'asset': asset,
'quantity': quantity,
'tag': tag,
'status': status,
}
if "integer overflow" not in status:
sql = 'insert into destructions values(:tx_index, :tx_hash, :block_index, :source, :asset, :quantity, :tag, :status)'
cursor = db.cursor()
cursor.execute(sql, bindings)
else:
logger.warn("Not storing [destroy] tx [%s]: %s" % (tx['tx_hash'], status))
logger.debug("Bindings: %s" % (json.dumps(bindings), ))
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
#### dispenser.py
#! /usr/bin/python3
#
# What is a dispenser?
#
# A dispenser is a type of order where the holder address gives out a given amount
# of units of an asset for a given amount of BTC satoshis received.
# It's a very simple but powerful semantic to allow swaps to operate on-chain.
#
import binascii
import json
import pprint
import struct
import logging
from math import floor
logger = logging.getLogger(__name__)
from counterpartylib.lib import config
from counterpartylib.lib import exceptions
from counterpartylib.lib import util
from counterpartylib.lib import log
from counterpartylib.lib import message_type
from counterpartylib.lib import address
FORMAT = '>QQQQB'
LENGTH = 33
ID = 12
DISPENSE_ID = 13
STATUS_OPEN = 0
STATUS_OPEN_EMPTY_ADDRESS = 1
#STATUS_OPEN_ORACLE_PRICE = 20
#STATUS_OPEN_ORACLE_PRICE_EMPTY_ADDRESS = 21
STATUS_CLOSED = 10
def initialise(db):
cursor = db.cursor()
cursor.execute('''CREATE TABLE IF NOT EXISTS dispensers(
tx_index INTEGER PRIMARY KEY,
tx_hash TEXT UNIQUE,
block_index INTEGER,
source TEXT,
asset TEXT,
give_quantity INTEGER,
escrow_quantity INTEGER,
satoshirate INTEGER,
status INTEGER,
give_remaining INTEGER,
FOREIGN KEY (tx_index, tx_hash, block_index) REFERENCES transactions(tx_index, tx_hash, block_index))
''')
# Disallows invalids: FOREIGN KEY (order_match_id) REFERENCES order_matches(id))
cursor.execute('''CREATE INDEX IF NOT EXISTS
dispensers_source_idx ON dispensers (source)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
dispensers_asset_idx ON dispensers (asset)
''')
cursor.execute('''CREATE TABLE IF NOT EXISTS dispenses(
tx_index INTEGER,
dispense_index INTEGER,
tx_hash TEXT,
block_index INTEGER,
source TEXT,
destination TEXT,
asset TEXT,
dispense_quantity INTEGER,
dispenser_tx_hash TEXT,
PRIMARY KEY (tx_index, dispense_index, source, destination),
FOREIGN KEY (tx_index, tx_hash, block_index) REFERENCES transactions(tx_index, tx_hash, block_index))
''')
columns = [column['name'] for column in cursor.execute('''PRAGMA table_info(dispenses)''')]
if 'dispenser_tx_hash' not in columns:
cursor.execute('ALTER TABLE dispenses ADD COLUMN dispenser_tx_hash TEXT')
columns = [column['name'] for column in cursor.execute('''PRAGMA table_info(dispensers)''')]
if 'oracle_address' not in columns:
cursor.execute('ALTER TABLE dispensers ADD COLUMN oracle_address TEXT')
def validate (db, source, asset, give_quantity, escrow_quantity, mainchainrate, status, open_address, block_index, oracle_address):
problems = []
order_match = None
asset_id = None
if asset == config.BTC:
problems.append('cannot dispense %s' % config.BTC)
return None, problems
# resolve subassets
asset = util.resolve_subasset_longname(db, asset)
if status == STATUS_OPEN or status == STATUS_OPEN_EMPTY_ADDRESS:
if give_quantity <= 0:
problems.append('give_quantity must be positive')
if mainchainrate <= 0:
problems.append('mainchainrate must be positive')
if escrow_quantity < give_quantity:
problems.append('escrow_quantity must be greater or equal than give_quantity')
elif not(status == STATUS_CLOSED):
problems.append('invalid status %i' % status)
cursor = db.cursor()
cursor.execute('''SELECT quantity FROM balances \
WHERE address = ? and asset = ?''', (source,asset,))
available = cursor.fetchall()
if len(available) == 0:
problems.append('address doesn\'t has the asset %s' % asset)
elif len(available) >= 1 and available[0]['quantity'] < escrow_quantity:
problems.append('address doesn\'t has enough balance of %s (%i < %i)' % (asset, available[0]['quantity'], escrow_quantity))
else:
if status == STATUS_OPEN_EMPTY_ADDRESS and not(open_address):
open_address = source
status = STATUS_OPEN
query_address = open_address if status == STATUS_OPEN_EMPTY_ADDRESS else source
cursor.execute('''SELECT * FROM dispensers WHERE source = ? AND asset = ? AND status=?''', (query_address, asset, STATUS_OPEN))
open_dispensers = cursor.fetchall()
if status == STATUS_OPEN or status == STATUS_OPEN_EMPTY_ADDRESS:
if len(open_dispensers) > 0 and open_dispensers[0]['satoshirate'] != mainchainrate:
problems.append('address has a dispenser already opened for asset %s with a different mainchainrate' % asset)
if len(open_dispensers) > 0 and open_dispensers[0]['give_quantity'] != give_quantity:
problems.append('address has a dispenser already opened for asset %s with a different give_quantity' % asset)
elif status == STATUS_CLOSED:
if len(open_dispensers) == 0:
problems.append('address doesnt has an open dispenser for asset %s' % asset)
if status == STATUS_OPEN_EMPTY_ADDRESS:
cursor.execute('''SELECT count(*) cnt FROM balances WHERE address = ?''', (query_address,))
existing_balances = cursor.fetchall()
if existing_balances[0]['cnt'] > 0:
problems.append('cannot open on another address if it has any balance history')
if len(problems) == 0:
asset_id = util.generate_asset_id(asset, block_index)
if asset_id == 0:
problems.append('cannot dispense %s' % asset) # How can we test this on a test vector?
cursor.close()
if oracle_address is not None and util.enabled('oracle_dispensers', block_index):
last_price, last_fee, last_label, last_updated = util.get_oracle_last_price(db, oracle_address, block_index)
if last_price is None:
problems.append('The oracle address %s has not broadcasted any price yet' % oracle_address)
if len(problems) > 0:
return None, problems
else:
return asset_id, None
def compose (db, source, asset, give_quantity, escrow_quantity, mainchainrate, status, open_address=None, oracle_address=None):
assetid, problems = validate(db, source, asset, give_quantity, escrow_quantity, mainchainrate, status, open_address, util.CURRENT_BLOCK_INDEX, oracle_address)
if problems: raise exceptions.ComposeError(problems)
destination = []
data = message_type.pack(ID)
data += struct.pack(FORMAT, assetid, give_quantity, escrow_quantity, mainchainrate, status)
if status == STATUS_OPEN_EMPTY_ADDRESS and open_address:
data += address.pack(open_address)
if oracle_address is not None and util.enabled('oracle_dispensers'):
oracle_fee = calculate_oracle_fee(db, escrow_quantity, give_quantity, mainchainrate, oracle_address, util.CURRENT_BLOCK_INDEX)
if oracle_fee >= config.DEFAULT_REGULAR_DUST_SIZE:
destination.append((oracle_address,oracle_fee))
data += address.pack(oracle_address)
return (source, destination, data)
def calculate_oracle_fee(db, escrow_quantity, give_quantity, mainchainrate, oracle_address, block_index):
last_price, last_fee, last_fiat_label, last_updated = util.get_oracle_last_price(db, oracle_address, block_index)
last_fee_multiplier = (last_fee / config.UNIT)
#Format mainchainrate to ######.##
oracle_mainchainrate = util.satoshirate_to_fiat(mainchainrate)
oracle_mainchainrate_btc = oracle_mainchainrate/last_price
#Calculate the total amount earned for dispenser and the fee
remaining = int(floor(escrow_quantity / give_quantity))
total_quantity_btc = oracle_mainchainrate_btc * remaining
oracle_fee_btc = int(total_quantity_btc * last_fee_multiplier *config.UNIT)
return oracle_fee_btc
def parse (db, tx, message):
cursor = db.cursor()
# Unpack message.
try:
action_address = tx['source']
oracle_address = None
assetid, give_quantity, escrow_quantity, mainchainrate, dispenser_status = struct.unpack(FORMAT, message[0:LENGTH])
read = LENGTH
if dispenser_status == STATUS_OPEN_EMPTY_ADDRESS:
action_address = address.unpack(message[LENGTH:LENGTH+21])
read = LENGTH + 21
if len(message) > read:
oracle_address = address.unpack(message[read:read+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 util.enabled("dispenser_parsing_validation", util.CURRENT_BLOCK_INDEX):
asset_id, problems = validate(db, tx['source'], asset, give_quantity, escrow_quantity, mainchainrate, dispenser_status, action_address if dispenser_status == STATUS_OPEN_EMPTY_ADDRESS else None, tx['block_index'], oracle_address)
else:
problems = None
if problems:
status = 'invalid: ' + '; '.join(problems)
else:
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:
if (oracle_address != None) and util.enabled('oracle_dispensers', tx['block_index']):
oracle_fee = calculate_oracle_fee(db, escrow_quantity, give_quantity, mainchainrate, oracle_address, tx['block_index'])
if oracle_fee >= config.DEFAULT_REGULAR_DUST_SIZE:
if tx["destination"] != oracle_address or tx["btc_amount"] < oracle_fee:
status = 'invalid: insufficient or non-existent oracle fee'
if status == 'valid':
# 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,
'oracle_address': oracle_address
}
sql = 'insert into dispensers values(:tx_index, :tx_hash, :block_index, :source, :asset, :give_quantity, :escrow_quantity, :satoshirate, :status, :give_remaining, :oracle_address)'
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:
if (oracle_address != None) and util.enabled('oracle_dispensers', tx['block_index']):
oracle_fee = calculate_oracle_fee(db, escrow_quantity, give_quantity, mainchainrate, oracle_address, tx['block_index'])
if oracle_fee >= config.DEFAULT_REGULAR_DUST_SIZE:
if tx["destination"] != oracle_address or tx["btc_amount"] < oracle_fee:
status = 'invalid: insufficient or non-existent oracle fee'
if status == 'valid':
# Refill the dispenser by the given amount
bindings = {
'source': tx['source'],
'asset': asset,
'prev_status': dispenser_status,
'give_remaining': existing[0]['give_remaining'] + escrow_quantity,
'status': STATUS_OPEN,
'block_index': tx['block_index'],
'action':'refill dispenser',
'escrow_quantity':escrow_quantity
}
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 tx_index, 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'],
'tx_index': existing[0]['tx_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 is_dispensable(db, address, amount):
cursor = db.cursor()
cursor.execute('SELECT * FROM dispensers WHERE source=:source AND status=:status', {
'source': address,
'status': STATUS_OPEN
})
dispensers = cursor.fetchall()
cursor.close()
for next_dispenser in dispensers:
if next_dispenser["oracle_address"] != None:
last_price, last_fee, last_fiat_label, last_updated = util.get_oracle_last_price(db, next_dispenser['oracle_address'], util.CURRENT_BLOCK_INDEX)
fiatrate = util.satoshirate_to_fiat(next_dispenser["satoshirate"])
if amount >= fiatrate/last_price:
return True
else:
if amount >= next_dispenser["satoshirate"]:
return True
return False
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:
if (dispenser['oracle_address'] != None) and util.enabled('oracle_dispensers', tx['block_index']):
last_price, last_fee, last_fiat_label, last_updated = util.get_oracle_last_price(db, dispenser['oracle_address'], tx['block_index'])
fiatrate = util.satoshirate_to_fiat(satoshirate)
must_give = int(floor(((tx['btc_amount'] / config.UNIT) * last_price)/fiatrate))
else:
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()
#### dividend.py
#! /usr/bin/python3
"""Pay out dividends."""
import json
import struct
import decimal
D = decimal.Decimal
import logging
logger = logging.getLogger(__name__)
from counterpartylib.lib import (config, exceptions, util, message_type)
FORMAT_1 = '>QQ'
LENGTH_1 = 8 + 8
FORMAT_2 = '>QQQ'
LENGTH_2 = 8 + 8 + 8
ID = 50
def initialise (db):
cursor = db.cursor()
cursor.execute('''CREATE TABLE IF NOT EXISTS dividends(
tx_index INTEGER PRIMARY KEY,
tx_hash TEXT UNIQUE,
block_index INTEGER,
source TEXT,
asset TEXT,
dividend_asset TEXT,
quantity_per_unit INTEGER,
fee_paid INTEGER,
status TEXT,
FOREIGN KEY (tx_index, tx_hash, block_index) REFERENCES transactions(tx_index, tx_hash, block_index))
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
block_index_idx ON dividends (block_index)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
source_idx ON dividends (source)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
asset_idx ON dividends (asset)
''')
def validate (db, source, quantity_per_unit, asset, dividend_asset, block_index):
cursor = db.cursor()
problems = []
if asset == config.BTC:
problems.append('cannot pay dividends to holders of {}'.format(config.BTC))
if asset == config.XCP:
if (not block_index >= 317500) or block_index >= 320000 or config.TESTNET or config.REGTEST: # Protocol change.
problems.append('cannot pay dividends to holders of {}'.format(config.XCP))
if quantity_per_unit <= 0:
problems.append('non‐positive quantity per unit')
# For SQLite3
if quantity_per_unit > config.MAX_INT:
problems.append('integer overflow')
# Examine asset.
issuances = list(cursor.execute('''SELECT * FROM issuances WHERE (status = ? AND asset = ?) ORDER BY tx_index ASC''', ('valid', asset)))
if not issuances:
problems.append('no such asset, {}.'.format(asset))
return None, None, problems, 0
divisible = issuances[0]['divisible']
# Only issuer can pay dividends.
if block_index >= 320000 or config.TESTNET or config.REGTEST: # Protocol change.
if issuances[-1]['issuer'] != source:
problems.append('only issuer can pay dividends')
# Examine dividend asset.
if dividend_asset in (config.BTC, config.XCP):
dividend_divisible = True
else:
issuances = list(cursor.execute('''SELECT * FROM issuances WHERE (status = ? AND asset = ?)''', ('valid', dividend_asset)))
if not issuances:
problems.append('no such dividend asset, {}.'.format(dividend_asset))
return None, None, problems, 0
dividend_divisible = issuances[0]['divisible']
# Calculate dividend quantities.
exclude_empty = False
if util.enabled('zero_quantity_value_adjustment_1'):
exclude_empty = True
holders = util.holders(db, asset, exclude_empty)
outputs = []
addresses = []
dividend_total = 0
for holder in holders:
if block_index < 294500 and not (config.TESTNET or config.REGTEST): # Protocol change.
if holder['escrow']: continue
address = holder['address']
address_quantity = holder['address_quantity']
if block_index >= 296000 or config.TESTNET or config.REGTEST: # Protocol change.
if address == source: continue
dividend_quantity = address_quantity * quantity_per_unit
if divisible: dividend_quantity /= config.UNIT
if not util.enabled('nondivisible_dividend_fix') and not dividend_divisible: dividend_quantity /= config.UNIT # Pre-fix behaviour
if dividend_asset == config.BTC and dividend_quantity < config.DEFAULT_MULTISIG_DUST_SIZE: continue # A bit hackish.
dividend_quantity = int(dividend_quantity)
outputs.append({'address': address, 'address_quantity': address_quantity, 'dividend_quantity': dividend_quantity})
addresses.append(address)
dividend_total += dividend_quantity
if not dividend_total: problems.append('zero dividend')
if dividend_asset != config.BTC:
dividend_balances = list(cursor.execute('''SELECT * FROM balances WHERE (address = ? AND asset = ?)''', (source, dividend_asset)))
if not dividend_balances or dividend_balances[0]['quantity'] < dividend_total:
problems.append('insufficient funds ({})'.format(dividend_asset))
fee = 0
if not problems and dividend_asset != config.BTC:
holder_count = len(set(addresses))
if block_index >= 330000 or config.TESTNET or config.REGTEST: # Protocol change.
fee = int(0.0002 * config.UNIT * holder_count)
if fee:
balances = list(cursor.execute('''SELECT * FROM balances WHERE (address = ? AND asset = ?)''', (source, config.XCP)))
if not balances or balances[0]['quantity'] < fee:
problems.append('insufficient funds ({})'.format(config.XCP))
if not problems and dividend_asset == config.XCP:
total_cost = dividend_total + fee
if not dividend_balances or dividend_balances[0]['quantity'] < total_cost:
problems.append('insufficient funds ({})'.format(dividend_asset))
# For SQLite3
if fee > config.MAX_INT or dividend_total > config.MAX_INT:
problems.append('integer overflow')
cursor.close()
return dividend_total, outputs, problems, fee
def compose (db, source, quantity_per_unit, asset, dividend_asset):
# resolve subassets
asset = util.resolve_subasset_longname(db, asset)
dividend_asset = util.resolve_subasset_longname(db, dividend_asset)
dividend_total, outputs, problems, fee = validate(db, source, quantity_per_unit, asset, dividend_asset, util.CURRENT_BLOCK_INDEX)
if problems: raise exceptions.ComposeError(problems)
logger.info('Total quantity to be distributed in dividends: {} {}'.format(util.value_out(db, dividend_total, dividend_asset), dividend_asset))
if dividend_asset == config.BTC:
return (source, [(output['address'], output['dividend_quantity']) for output in outputs], None)
asset_id = util.get_asset_id(db, asset, util.CURRENT_BLOCK_INDEX)
dividend_asset_id = util.get_asset_id(db, dividend_asset, util.CURRENT_BLOCK_INDEX)
data = message_type.pack(ID)
data += struct.pack(FORMAT_2, quantity_per_unit, asset_id, dividend_asset_id)
return (source, [], data)
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()
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
#### issuance.py
#! /usr/bin/python3
"""
Allow simultaneous lock and transfer.
"""
import struct
import decimal
import json
import logging
logger = logging.getLogger(__name__)
D = decimal.Decimal
from counterpartylib.lib import (config, util, exceptions, util, message_type)
FORMAT_1 = '>QQ?'
LENGTH_1 = 8 + 8 + 1
FORMAT_2 = '>QQ??If'
LENGTH_2 = 8 + 8 + 1 + 1 + 4 + 4
SUBASSET_FORMAT = '>QQ?B'
SUBASSET_FORMAT_LENGTH = 8 + 8 + 1 + 1
ID = 20
SUBASSET_ID = 21
# NOTE: Pascal strings are used for storing descriptions for backwards‐compatibility.
def initialise(db):
cursor = db.cursor()
cursor.execute('''CREATE TABLE IF NOT EXISTS issuances(
tx_index INTEGER PRIMARY KEY,
tx_hash TEXT UNIQUE,
block_index INTEGER,
asset TEXT,
quantity INTEGER,
divisible BOOL,
source TEXT,
issuer TEXT,
transfer BOOL,
callable BOOL,
call_date INTEGER,
call_price REAL,
description TEXT,
fee_paid INTEGER,
locked BOOL,
status TEXT,
asset_longname TEXT,
reset BOOL,
FOREIGN KEY (tx_index, tx_hash, block_index) REFERENCES transactions(tx_index, tx_hash, block_index))
''')
# Add asset_longname for sub-assets
# SQLite can’t do `ALTER TABLE IF COLUMN NOT EXISTS`.
columns = [column['name'] for column in cursor.execute('''PRAGMA table_info(issuances)''')]
if 'asset_longname' not in columns:
cursor.execute('''ALTER TABLE issuances ADD COLUMN asset_longname TEXT''')
if 'reset' not in columns:
cursor.execute('''ALTER TABLE issuances ADD COLUMN reset BOOL''')
# If sweep_hotfix activated, Create issuances copy, copy old data, drop old table, rename new table, recreate indexes
# SQLite can’t do `ALTER TABLE IF COLUMN NOT EXISTS` nor can drop UNIQUE constraints
if 'msg_index' not in columns:
cursor.execute('''CREATE TABLE IF NOT EXISTS new_issuances(
tx_index INTEGER,
tx_hash TEXT,
msg_index INTEGER DEFAULT 0,
block_index INTEGER,
asset TEXT,
quantity INTEGER,
divisible BOOL,
source TEXT,
issuer TEXT,
transfer BOOL,
callable BOOL,
call_date INTEGER,
call_price REAL,
description TEXT,
fee_paid INTEGER,
locked BOOL,
status TEXT,
asset_longname TEXT,
reset BOOL,
PRIMARY KEY (tx_index, msg_index),
FOREIGN KEY (tx_index, tx_hash, block_index) REFERENCES transactions(tx_index, tx_hash, block_index),
UNIQUE (tx_hash, msg_index))
''')
cursor.execute('''INSERT INTO new_issuances(tx_index, tx_hash, msg_index,
block_index, asset, quantity, divisible, source, issuer, transfer, callable,
call_date, call_price, description, fee_paid, locked, status, asset_longname, reset)
SELECT tx_index, tx_hash, 0, block_index, asset, quantity, divisible, source,
issuer, transfer, callable, call_date, call_price, description, fee_paid,
locked, status, asset_longname, reset FROM issuances''', {})
cursor.execute('DROP TABLE issuances')
cursor.execute('ALTER TABLE new_issuances RENAME TO issuances')
cursor.execute('''CREATE INDEX IF NOT EXISTS
block_index_idx ON issuances (block_index)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
valid_asset_idx ON issuances (asset, status)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
status_idx ON issuances (status)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
source_idx ON issuances (source)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
asset_longname_idx ON issuances (asset_longname)
''')
def validate (db, source, destination, asset, quantity, divisible, lock, reset, callable_, call_date, call_price, description, subasset_parent, subasset_longname, block_index):
problems = []
fee = 0
if asset in (config.BTC, config.XCP):
problems.append('cannot issue {} or {}'.format(config.BTC, config.XCP))
if call_date is None: call_date = 0
if call_price is None: call_price = 0.0
if description is None: description = ""
if divisible is None: divisible = True
if lock is None: lock = False
if reset is None: reset = False
if isinstance(call_price, int): call_price = float(call_price)
#^ helps especially with calls from JS‐based clients, where parseFloat(15) returns 15 (not 15.0), which json takes as an int
if not isinstance(quantity, int):
problems.append('quantity must be in satoshis')
return call_date, call_price, problems, fee, description, divisible, None, None
if call_date and not isinstance(call_date, int):
problems.append('call_date must be epoch integer')
return call_date, call_price, problems, fee, description, divisible, None, None
if call_price and not isinstance(call_price, float):
problems.append('call_price must be a float')
return call_date, call_price, problems, fee, description, divisible, None, None
if quantity < 0: problems.append('negative quantity')
if call_price < 0: problems.append('negative call price')
if call_date < 0: problems.append('negative call date')
# Callable, or not.
if not callable_:
if block_index >= 312500 or config.TESTNET or config.REGTEST: # Protocol change.
call_date = 0
call_price = 0.0
elif block_index >= 310000: # Protocol change.
if call_date:
problems.append('call date for non‐callable asset')
if call_price:
problems.append('call price for non‐callable asset')
# Valid re-issuance?
cursor = db.cursor()
cursor.execute('''SELECT * FROM issuances \
WHERE (status = ? AND asset = ?)
ORDER BY tx_index ASC''', ('valid', asset))
issuances = cursor.fetchall()
cursor.close()
reissued_asset_longname = None
if issuances:
reissuance = True
last_issuance = issuances[-1]
reissued_asset_longname = last_issuance['asset_longname']
issuance_locked = False
if util.enabled('issuance_lock_fix'):
for issuance in issuances:
if issuance['locked']:
issuance_locked = True
break
elif last_issuance['locked']:
# before the issuance_lock_fix, only the last issuance was checked
issuance_locked = True
if last_issuance['issuer'] != source:
problems.append('issued by another address')
if (bool(last_issuance['divisible']) != bool(divisible)) and ((not util.enabled("cip03", block_index)) or (not reset)):
problems.append('cannot change divisibility')
if bool(last_issuance['callable']) != bool(callable_):
problems.append('cannot change callability')
if last_issuance['call_date'] > call_date and (call_date != 0 or (block_index < 312500 and (not config.TESTNET or not config.REGTEST))):
problems.append('cannot advance call date')
if last_issuance['call_price'] > call_price:
problems.append('cannot reduce call price')
if issuance_locked and quantity:
problems.append('locked asset and non‐zero quantity')
if issuance_locked and reset:
problems.append('cannot reset a locked asset')
else:
reissuance = False
if description.lower() == 'lock':
problems.append('cannot lock a non‐existent asset')
#if destination:
# problems.append('cannot transfer a non‐existent asset')
if reset:
problems.append('cannot reset a non existent asset')
# validate parent ownership for subasset
if subasset_longname is not None and not reissuance:
cursor = db.cursor()
cursor.execute('''SELECT * FROM issuances \
WHERE (status = ? AND asset = ?)
ORDER BY tx_index ASC''', ('valid', subasset_parent))
parent_issuances = cursor.fetchall()
cursor.close()
if parent_issuances:
last_parent_issuance = parent_issuances[-1]
if last_parent_issuance['issuer'] != source:
problems.append('parent asset owned by another address')
else:
problems.append('parent asset not found')
# validate subasset issuance is not a duplicate
if subasset_longname is not None and not reissuance:
cursor = db.cursor()
cursor.execute('''SELECT * FROM assets \
WHERE (asset_longname = ?)''', (subasset_longname,))
assets = cursor.fetchall()
if len(assets) > 0:
problems.append('subasset already exists')
# validate that the actual asset is numeric
if asset[0] != 'A':
problems.append('a subasset must be a numeric asset')
# Check for existence of fee funds.
if quantity or (block_index >= 315000 or config.TESTNET or config.REGTEST): # Protocol change.
if not reissuance or (block_index < 310000 and not config.TESTNET and not config.REGTEST): # Pay fee only upon first issuance. (Protocol change.)
cursor = db.cursor()
cursor.execute('''SELECT * FROM balances \
WHERE (address = ? AND asset = ?)''', (source, config.XCP))
balances = cursor.fetchall()
cursor.close()
if util.enabled('numeric_asset_names'): # Protocol change.
if subasset_longname is not None and util.enabled('subassets'): # Protocol change.
# subasset issuance is 0.25
fee = int(0.25 * config.UNIT)
elif len(asset) >= 13:
fee = 0
else:
fee = int(0.5 * config.UNIT)
elif block_index >= 291700 or config.TESTNET or config.REGTEST: # Protocol change.
fee = int(0.5 * config.UNIT)
elif block_index >= 286000 or config.TESTNET or config.REGTEST: # Protocol change.
fee = 5 * config.UNIT
elif block_index > 281236 or config.TESTNET or config.REGTEST: # Protocol change.
fee = 5
if fee and (not balances or balances[0]['quantity'] < fee):
problems.append('insufficient funds')
if not (block_index >= 317500 or config.TESTNET or config.REGTEST): # Protocol change.
if len(description) > 42:
problems.append('description too long')
# For SQLite3
call_date = min(call_date, config.MAX_INT)
assert isinstance(quantity, int)
if reset and util.enabled("cip03", block_index):#reset will overwrite the quantity
if quantity > config.MAX_INT:
problems.append('total quantity overflow')
else:
total = sum([issuance['quantity'] for issuance in issuances])
if total + quantity > config.MAX_INT:
problems.append('total quantity overflow')
if util.enabled("cip03", block_index) and reset and issuances:
cursor = db.cursor()
#Checking that all supply are held by the owner of the asset
cursor.execute('''SELECT * FROM balances \
WHERE asset = ? AND quantity > 0''', (asset,))
balances = cursor.fetchall()
cursor.close()
if (len(balances) == 0):
if util.asset_supply(db, asset) > 0:
problems.append('Cannot reset an asset with no holder')
elif (len(balances) > 1):
problems.append('Cannot reset an asset with many holders')
elif (len(balances) == 1):
if (balances[0]['address'] != last_issuance["issuer"]):
problems.append('Cannot reset an asset held by a different address than the owner')
#if destination and quantity:
# problems.append('cannot issue and transfer simultaneously')
# For SQLite3
if util.enabled('integer_overflow_fix', block_index=block_index) and (fee > config.MAX_INT or quantity > config.MAX_INT):
problems.append('integer overflow')
return call_date, call_price, problems, fee, description, divisible, lock, reset, reissuance, reissued_asset_longname
def compose (db, source, transfer_destination, asset, quantity, divisible, lock, reset, description):
# Callability is deprecated, so for re‐issuances set relevant parameters
# to old values; for first issuances, make uncallable.
cursor = db.cursor()
cursor.execute('''SELECT * FROM issuances \
WHERE (status = ? AND asset = ?)
ORDER BY tx_index ASC''', ('valid', asset))
issuances = cursor.fetchall()
if issuances:
last_issuance = issuances[-1]
callable_ = last_issuance['callable']
call_date = last_issuance['call_date']
call_price = last_issuance['call_price']
else:
callable_ = False
call_date = 0
call_price = 0.0
cursor.close()
# check subasset
subasset_parent = None
subasset_longname = None
if util.enabled('subassets'): # Protocol change.
subasset_parent, subasset_longname = util.parse_subasset_from_asset_name(asset)
if subasset_longname is not None:
# try to find an existing subasset
sa_cursor = db.cursor()
sa_cursor.execute('''SELECT * FROM assets \
WHERE (asset_longname = ?)''', (subasset_longname,))
assets = sa_cursor.fetchall()
sa_cursor.close()
if len(assets) > 0:
# this is a reissuance
asset = assets[0]['asset_name']
else:
# this is a new issuance
# generate a random numeric asset id which will map to this subasset
asset = util.generate_random_asset()
call_date, call_price, problems, fee, description, divisible, lock, reset, reissuance, reissued_asset_longname = validate(db, source, transfer_destination, asset, quantity, divisible, lock, reset, callable_, call_date, call_price, description, subasset_parent, subasset_longname, util.CURRENT_BLOCK_INDEX)
if problems: raise exceptions.ComposeError(problems)
asset_id = util.generate_asset_id(asset, util.CURRENT_BLOCK_INDEX)
if subasset_longname is None or reissuance:
asset_format = util.get_value_by_block_index("issuance_asset_serialization_format")
asset_format_length = util.get_value_by_block_index("issuance_asset_serialization_length")
# Type 20 standard issuance FORMAT_2 >QQ??If
# used for standard issuances and all reissuances
data = message_type.pack(ID)
if (len(description) <= 42) and not util.enabled('pascal_string_removed'):
curr_format = FORMAT_2 + '{}p'.format(len(description) + 1)
else:
curr_format = asset_format + '{}s'.format(len(description))
if (asset_format_length <= 19):# callbacks parameters were removed
data += struct.pack(curr_format, asset_id, quantity, 1 if divisible else 0, 1 if lock else 0, 1 if reset else 0, description.encode('utf-8'))
elif (asset_format_length <= 26):
data += struct.pack(curr_format, asset_id, quantity, 1 if divisible else 0, 1 if callable_ else 0,
call_date or 0, call_price or 0.0, description.encode('utf-8'))
elif (asset_format_length <= 27):# param reset was inserted
data += struct.pack(curr_format, asset_id, quantity, 1 if divisible else 0, 1 if reset else 0, 1 if callable_ else 0,
call_date or 0, call_price or 0.0, description.encode('utf-8'))
elif (asset_format_length <= 28):# param lock was inserted
data += struct.pack(curr_format, asset_id, quantity, 1 if divisible else 0, 1 if lock else 0, 1 if reset else 0, 1 if callable_ else 0,
call_date or 0, call_price or 0.0, description.encode('utf-8'))
else:
subasset_format = util.get_value_by_block_index("issuance_subasset_serialization_format",util.CURRENT_BLOCK_INDEX)
subasset_format_length = util.get_value_by_block_index("issuance_subasset_serialization_length",util.CURRENT_BLOCK_INDEX)
# Type 21 subasset issuance SUBASSET_FORMAT >QQ?B
# Used only for initial subasset issuance
# compacts a subasset name to save space
compacted_subasset_longname = util.compact_subasset_longname(subasset_longname)
compacted_subasset_length = len(compacted_subasset_longname)
data = message_type.pack(SUBASSET_ID)
curr_format = subasset_format + '{}s'.format(compacted_subasset_length) + '{}s'.format(len(description))
if subasset_format_length <= 18:
data += struct.pack(curr_format, asset_id, quantity, 1 if divisible else 0, compacted_subasset_length, compacted_subasset_longname, description.encode('utf-8'))
elif subasset_format_length <= 19:# param reset was inserted
data += struct.pack(curr_format, asset_id, quantity, 1 if divisible else 0, 1 if reset else 0, compacted_subasset_length, compacted_subasset_longname, description.encode('utf-8'))
elif subasset_format_length <= 20:# param lock was inserted
data += struct.pack(curr_format, asset_id, quantity, 1 if divisible else 0, 1 if lock else 0, 1 if reset else 0, compacted_subasset_length, compacted_subasset_longname, description.encode('utf-8'))
if transfer_destination:
destination_outputs = [(transfer_destination, None)]
else:
destination_outputs = []
return (source, destination_outputs, data)
def parse (db, tx, message, message_type_id):
issuance_parse_cursor = db.cursor()
asset_format = util.get_value_by_block_index("issuance_asset_serialization_format",tx['block_index'])
asset_format_length = util.get_value_by_block_index("issuance_asset_serialization_length",tx['block_index'])
subasset_format = util.get_value_by_block_index("issuance_subasset_serialization_format",tx['block_index'])
subasset_format_length = util.get_value_by_block_index("issuance_subasset_serialization_length",tx['block_index'])
# 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
lock = None
reset = None
if subasset_format_length <= 18:
asset_id, quantity, divisible, compacted_subasset_length = struct.unpack(subasset_format, message[0:subasset_format_length])
elif subasset_format_length <= 19:# param reset was inserted
asset_id, quantity, divisible, reset, compacted_subasset_length = struct.unpack(subasset_format, message[0:subasset_format_length])
elif subasset_format_length <= 20:# param lock was inserted
asset_id, quantity, divisible, lock, reset, 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) >= asset_format_length: # Protocol change.
if (len(message) - asset_format_length <= 42) and not util.enabled('pascal_string_removed'):
curr_format = asset_format + '{}p'.format(len(message) - asset_format_length)
else:
curr_format = asset_format + '{}s'.format(len(message) - asset_format_length)
lock = None
reset = None
if (asset_format_length <= 19):# callbacks parameters were removed
asset_id, quantity, divisible, lock, reset, description = struct.unpack(curr_format, message)
callable_, call_date, call_price = False, 0, 0.0
elif (asset_format_length <= 26):#the reset param didn't even exist
asset_id, quantity, divisible, callable_, call_date, call_price, description = struct.unpack(curr_format, message)
elif (asset_format_length <= 27):# param reset was inserted
asset_id, quantity, divisible, reset, callable_, call_date, call_price, description = struct.unpack(curr_format, message)
elif (asset_format_length <= 28):# param lock was inserted
asset_id, quantity, divisible, lock, reset, 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)
lock, reset, callable_, call_date, call_price, description = False, False, False, 0, 0.0, ''
try:
asset = util.generate_asset_name(asset_id, tx['block_index'])
##This is for backwards compatibility with assets names longer than 12 characters
if asset.startswith('A'):
namedAsset = util.get_asset_name(db, asset_id, tx['block_index'])
if (namedAsset != 0):
asset = namedAsset
status = 'valid'
except exceptions.AssetIDError:
asset = None
status = 'invalid: bad asset name'
except exceptions.UnpackError as e:
asset, quantity, divisible, lock, reset, callable_, call_date, call_price, description = None, None, 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, lock, reset, reissuance, reissued_asset_longname = validate(db, tx['source'], tx['destination'], asset, quantity, divisible, lock, reset, 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
# Reset?
if (status == 'valid') and reset and util.enabled("cip03", tx['block_index']):
balances_cursor = issuance_parse_cursor.execute('''SELECT * FROM balances WHERE asset = ? AND quantity > 0''', (asset,))
balances_result = balances_cursor.fetchall()
if len(balances_result) <= 1:
if len(balances_result) == 0:
issuances_cursor = issuance_parse_cursor.execute('''SELECT * FROM issuances WHERE asset = ? ORDER BY tx_index DESC''', (asset,))
issuances_result = issuances_cursor.fetchall()
owner_balance = 0
owner_address = issuances_result[0]['issuer']
else:
owner_balance = balances_result[0]["quantity"]
owner_address = balances_result[0]["address"]
if owner_address == tx['source']:
if owner_balance > 0:
util.debit(db, tx['source'], asset, owner_balance, 'reset destroy', tx['tx_hash'])
bindings = {
'tx_index': tx['tx_index'],
'tx_hash': tx['tx_hash'],
'block_index': tx['block_index'],
'source': tx['source'],
'asset': asset,
'quantity': owner_balance,
'tag': "reset",
'status': "valid",
'reset': True,
}
sql = 'insert into destructions values(:tx_index, :tx_hash, :block_index, :source, :asset, :quantity, :tag, :status)'
issuance_parse_cursor.execute(sql, bindings)
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': tx['source'],
'transfer': False,
'callable': callable_,
'call_date': call_date,
'call_price': call_price,
'description': description,
'fee_paid': 0,
'locked': lock,
'status': status,
'reset': True,
'asset_longname': reissued_asset_longname,
}
sql='insert into issuances values(:tx_index, :tx_hash, 0, :block_index, :asset, :quantity, :divisible, :source, :issuer, :transfer, :callable, :call_date, :call_price, :description, :fee_paid, :locked, :status, :asset_longname, :reset)'
issuance_parse_cursor.execute(sql, bindings)
# Credit.
if quantity:
util.credit(db, tx['source'], asset, quantity, action="reset issuance", event=tx['tx_hash'])
else:
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?
if not isinstance(lock,bool):
lock = False
if status == 'valid':
if (description and description.lower() == 'lock') or 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()
if len(issuances) > 0:
description = issuances[-1]['description'] # Use last description
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,
'reset': reset,
'status': status,
'asset_longname': asset_longname,
}
if "integer overflow" not in status:
sql='insert into issuances values(:tx_index, :tx_hash, 0, :block_index, :asset, :quantity, :divisible, :source, :issuer, :transfer, :callable, :call_date, :call_price, :description, :fee_paid, :locked, :status, :asset_longname, :reset)'
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()
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
#### order.py
#! /usr/bin/python3
# Filled orders may not be re‐opened, so only orders not involving BTC (and so
# which cannot have expired order matches) may be filled.
import json
import struct
import decimal
D = decimal.Decimal
import logging
logger = logging.getLogger(__name__)
from counterpartylib.lib import config
from counterpartylib.lib import exceptions
from counterpartylib.lib import util
from counterpartylib.lib import backend
from counterpartylib.lib import log
from counterpartylib.lib import message_type
FORMAT = '>QQQQHQ'
LENGTH = 8 + 8 + 8 + 8 + 2 + 8
ID = 10
def initialise(db):
cursor = db.cursor()
# Orders
cursor.execute('''CREATE TABLE IF NOT EXISTS orders(
tx_index INTEGER UNIQUE,
tx_hash TEXT UNIQUE,
block_index INTEGER,
source TEXT,
give_asset TEXT,
give_quantity INTEGER,
give_remaining INTEGER,
get_asset TEXT,
get_quantity INTEGER,
get_remaining INTEGER,
expiration INTEGER,
expire_index INTEGER,
fee_required INTEGER,
fee_required_remaining INTEGER,
fee_provided INTEGER,
fee_provided_remaining INTEGER,
status TEXT,
FOREIGN KEY (tx_index, tx_hash, block_index) REFERENCES transactions(tx_index, tx_hash, block_index),
PRIMARY KEY (tx_index, tx_hash))
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
block_index_idx ON orders (block_index)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
index_hash_idx ON orders (tx_index, tx_hash)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
expire_idx ON orders (expire_index, status)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
give_status_idx ON orders (give_asset, status)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
source_give_status_idx ON orders (source, give_asset, status)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
give_get_status_idx ON orders (get_asset, give_asset, status)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
source_idx ON orders (source)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
give_asset_idx ON orders (give_asset)
''')
# Order Matches
cursor.execute('''CREATE TABLE IF NOT EXISTS order_matches(
id TEXT PRIMARY KEY,
tx0_index INTEGER,
tx0_hash TEXT,
tx0_address TEXT,
tx1_index INTEGER,
tx1_hash TEXT,
tx1_address TEXT,
forward_asset TEXT,
forward_quantity INTEGER,
backward_asset TEXT,
backward_quantity INTEGER,
tx0_block_index INTEGER,
tx1_block_index INTEGER,
block_index INTEGER,
tx0_expiration INTEGER,
tx1_expiration INTEGER,
match_expire_index INTEGER,
fee_paid INTEGER,
status TEXT,
FOREIGN KEY (tx0_index, tx0_hash, tx0_block_index) REFERENCES transactions(tx_index, tx_hash, block_index),
FOREIGN KEY (tx1_index, tx1_hash, tx1_block_index) REFERENCES transactions(tx_index, tx_hash, block_index))
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
match_expire_idx ON order_matches (status, match_expire_index)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
forward_status_idx ON order_matches (forward_asset, status)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
backward_status_idx ON order_matches (backward_asset, status)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
id_idx ON order_matches (id)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
tx0_address_idx ON order_matches (tx0_address)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
tx1_address_idx ON order_matches (tx1_address)
''')
# Order Expirations
cursor.execute('''CREATE TABLE IF NOT EXISTS order_expirations(
order_index INTEGER PRIMARY KEY,
order_hash TEXT UNIQUE,
source TEXT,
block_index INTEGER,
FOREIGN KEY (block_index) REFERENCES blocks(block_index),
FOREIGN KEY (order_index, order_hash) REFERENCES orders(tx_index, tx_hash))
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
block_index_idx ON order_expirations (block_index)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
source_idx ON order_expirations (source)
''')
# Order Match Expirations
cursor.execute('''CREATE TABLE IF NOT EXISTS order_match_expirations(
order_match_id TEXT PRIMARY KEY,
tx0_address TEXT,
tx1_address TEXT,
block_index INTEGER,
FOREIGN KEY (order_match_id) REFERENCES order_matches(id),
FOREIGN KEY (block_index) REFERENCES blocks(block_index))
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
block_index_idx ON order_match_expirations (block_index)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
tx0_address_idx ON order_match_expirations (tx0_address)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
tx1_address_idx ON order_match_expirations (tx1_address)
''')
def exact_penalty (db, address, block_index, order_match_id):
# Penalize addresses that don’t make BTC payments. If an address lets an
# order match expire, expire sell BTC orders from that address.
cursor = db.cursor()
# Orders.
bad_orders = list(cursor.execute('''SELECT * FROM orders \
WHERE (source = ? AND give_asset = ? AND status = ?)''',
(address, config.BTC, 'open')))
for bad_order in bad_orders:
cancel_order(db, bad_order, 'expired', block_index)
if not (block_index >= 314250 or config.TESTNET or config.REGTEST): # Protocol change.
# Order matches.
bad_order_matches = list(cursor.execute('''SELECT * FROM order_matches \
WHERE ((tx0_address = ? AND forward_asset = ?) OR (tx1_address = ? AND backward_asset = ?)) AND (status = ?)''',
(address, config.BTC, address, config.BTC, 'pending')))
for bad_order_match in bad_order_matches:
cancel_order_match(db, bad_order_match, 'expired', block_index)
cursor.close()
return
def cancel_order (db, order, status, block_index):
cursor = db.cursor()
# Update status of order.
bindings = {
'status': status,
'tx_hash': order['tx_hash']
}
sql='update orders set status = :status where tx_hash = :tx_hash'
cursor.execute(sql, bindings)
log.message(db, block_index, 'update', 'orders', bindings)
if order['give_asset'] != config.BTC: # Can’t credit BTC.
util.credit(db, order['source'], order['give_asset'], order['give_remaining'], action='cancel order', event=order['tx_hash'])
if status == 'expired':
# Record offer expiration.
bindings = {
'order_index': order['tx_index'],
'order_hash': order['tx_hash'],
'source': order['source'],
'block_index': block_index
}
sql='insert into order_expirations values(:order_index, :order_hash, :source, :block_index)'
cursor.execute(sql, bindings)
cursor.close()
def cancel_order_match (db, order_match, status, block_index):
'''The only cancelling is an expiration.
'''
cursor = db.cursor()
# Skip order matches just expired as a penalty. (Not very efficient.)
if not (block_index >= 314250 or config.TESTNET or config.REGTEST): # Protocol change.
order_matches = list(cursor.execute('''SELECT * FROM order_matches \
WHERE (id = ? AND status = ?)''',
(order_match['id'], 'expired')))
if order_matches:
cursor.close()
return
# Update status of order match.
bindings = {
'status': status,
'order_match_id': order_match['id']
}
sql='update order_matches set status = :status where id = :order_match_id'
cursor.execute(sql, bindings)
log.message(db, block_index, 'update', 'order_matches', bindings)
order_match_id = util.make_id(order_match['tx0_hash'], order_match['tx1_hash'])
# If tx0 is dead, credit address directly; if not, replenish give remaining, get remaining, and fee required remaining.
orders = list(cursor.execute('''SELECT * FROM orders \
WHERE tx_index = ?''',
(order_match['tx0_index'],)))
assert len(orders) == 1
tx0_order = orders[0]
if tx0_order['status'] in ('expired', 'cancelled'):
tx0_order_status = tx0_order['status']
if order_match['forward_asset'] != config.BTC:
util.credit(db, order_match['tx0_address'],
order_match['forward_asset'],
order_match['forward_quantity'], action='order {}'.format(tx0_order_status), event=order_match['id'])
else:
tx0_give_remaining = tx0_order['give_remaining'] + order_match['forward_quantity']
tx0_get_remaining = tx0_order['get_remaining'] + order_match['backward_quantity']
if tx0_order['get_asset'] == config.BTC and (block_index >= 297000 or config.TESTNET or config.REGTEST): # Protocol change.
tx0_fee_required_remaining = tx0_order['fee_required_remaining'] + order_match['fee_paid']
else:
tx0_fee_required_remaining = tx0_order['fee_required_remaining']
tx0_order_status = tx0_order['status']
if (tx0_order_status == 'filled' and util.enabled("reopen_order_when_btcpay_expires_fix", block_index)): #This case could happen if a BTCpay expires and before the expiration, the order was filled by a correct BTCpay
tx0_order_status = 'open' # So, we have to open the order again
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, status = :status 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']
if (tx1_order_status == 'filled' and util.enabled("reopen_order_when_btcpay_expires_fix", block_index)): #This case could happen if a BTCpay expires and before the expiration, the order was filled by a correct BTCpay
tx1_order_status = 'open' # So, we have to open the order again
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, status = :status where tx_hash = :tx_hash'
cursor.execute(sql, bindings)
log.message(db, block_index, 'update', 'orders', bindings)
if block_index < 286500: # Protocol change.
# Sanity check: one of the two must have expired.
tx0_order_time_left = tx0_order['expire_index'] - block_index
tx1_order_time_left = tx1_order['expire_index'] - block_index
assert tx0_order_time_left or tx1_order_time_left
# Penalize tardiness.
if block_index >= 313900 or config.TESTNET or config.REGTEST: # Protocol change.
if tx0_order['status'] == 'expired' and order_match['forward_asset'] == config.BTC:
exact_penalty(db, order_match['tx0_address'], block_index, order_match['id'])
if tx1_order['status'] == 'expired' and order_match['backward_asset'] == config.BTC:
exact_penalty(db, order_match['tx1_address'], block_index, order_match['id'])
# Re‐match.
if block_index >= 310000 or config.TESTNET or config.REGTEST: # Protocol change.
if not (block_index >= 315000 or config.TESTNET or config.REGTEST): # Protocol change.
cursor.execute('''SELECT * FROM transactions\
WHERE tx_hash = ?''', (tx0_order['tx_hash'],))
match(db, list(cursor)[0], block_index)
cursor.execute('''SELECT * FROM transactions\
WHERE tx_hash = ?''', (tx1_order['tx_hash'],))
match(db, list(cursor)[0], block_index)
if status == 'expired':
# Record order match expiration.
bindings = {
'order_match_id': order_match['id'],
'tx0_address': order_match['tx0_address'],
'tx1_address': order_match['tx1_address'],
'block_index': block_index
}
sql='insert into order_match_expirations values(:order_match_id, :tx0_address, :tx1_address, :block_index)'
cursor.execute(sql, bindings)
cursor.close()
def validate (db, source, give_asset, give_quantity, get_asset, get_quantity, expiration, fee_required, block_index):
problems = []
cursor = db.cursor()
# For SQLite3
if give_quantity > config.MAX_INT or get_quantity > config.MAX_INT or fee_required > config.MAX_INT or block_index + expiration > config.MAX_INT:
problems.append('integer overflow')
if give_asset == config.BTC and get_asset == config.BTC:
problems.append('cannot trade {} for itself'.format(config.BTC))
if not isinstance(give_quantity, int):
problems.append('give_quantity must be in satoshis')
return problems
if not isinstance(get_quantity, int):
problems.append('get_quantity must be in satoshis')
return problems
if not isinstance(fee_required, int):
problems.append('fee_required must be in satoshis')
return problems
if not isinstance(expiration, int):
problems.append('expiration must be expressed as an integer block delta')
return problems
if give_quantity <= 0: problems.append('non‐positive give quantity')
if get_quantity <= 0: problems.append('non‐positive get quantity')
if fee_required < 0: problems.append('negative fee_required')
if expiration < 0: problems.append('negative expiration')
if expiration == 0 and not (block_index >= 317500 or config.TESTNET or config.REGTEST): # Protocol change.
problems.append('zero expiration')
if not give_quantity or not get_quantity:
problems.append('zero give or zero get')
cursor.execute('select * from issuances where (status = ? and asset = ?)', ('valid', give_asset))
if give_asset not in (config.BTC, config.XCP) and not cursor.fetchall():
problems.append('no such asset to give ({})'.format(give_asset))
cursor.execute('select * from issuances where (status = ? and asset = ?)', ('valid', get_asset))
if get_asset not in (config.BTC, config.XCP) and not cursor.fetchall():
problems.append('no such asset to get ({})'.format(get_asset))
if expiration > config.MAX_EXPIRATION:
problems.append('expiration overflow')
cursor.close()
return problems
def compose (db, source, give_asset, give_quantity, get_asset, get_quantity, expiration, fee_required):
cursor = db.cursor()
# resolve subassets
give_asset = util.resolve_subasset_longname(db, give_asset)
get_asset = util.resolve_subasset_longname(db, get_asset)
# Check balance.
if give_asset != config.BTC:
balances = list(cursor.execute('''SELECT * FROM balances WHERE (address = ? AND asset = ?)''', (source, give_asset)))
if (not balances or balances[0]['quantity'] < give_quantity):
raise exceptions.ComposeError('insufficient funds')
problems = validate(db, source, give_asset, give_quantity, get_asset, get_quantity, expiration, fee_required, util.CURRENT_BLOCK_INDEX)
if problems: raise exceptions.ComposeError(problems)
give_id = util.get_asset_id(db, give_asset, util.CURRENT_BLOCK_INDEX)
get_id = util.get_asset_id(db, get_asset, util.CURRENT_BLOCK_INDEX)
data = message_type.pack(ID)
data += struct.pack(FORMAT, give_id, give_quantity, get_id, get_quantity,
expiration, fee_required)
cursor.close()
return (source, [], data)
def parse (db, tx, message):
order_parse_cursor = db.cursor()
# Unpack message.
try:
if len(message) != LENGTH:
raise exceptions.UnpackError
give_id, give_quantity, get_id, get_quantity, expiration, fee_required = struct.unpack(FORMAT, message)
give_asset = util.get_asset_name(db, give_id, tx['block_index'])
get_asset = util.get_asset_name(db, get_id, tx['block_index'])
status = 'open'
except (exceptions.UnpackError, exceptions.AssetNameError, struct.error) as e:
give_asset, give_quantity, get_asset, get_quantity, expiration, fee_required = 0, 0, 0, 0, 0, 0
status = 'invalid: could not unpack'
price = 0
if status == 'open':
try:
price = util.price(get_quantity, give_quantity)
except ZeroDivisionError:
price = 0
# Overorder
order_parse_cursor.execute('''SELECT * FROM balances \
WHERE (address = ? AND asset = ?)''', (tx['source'], give_asset))
balances = list(order_parse_cursor)
if give_asset != config.BTC:
if not balances:
give_quantity = 0
else:
balance = balances[0]['quantity']
if balance < give_quantity:
give_quantity = balance
get_quantity = int(price * give_quantity)
problems = validate(db, tx['source'], give_asset, give_quantity, get_asset, get_quantity, expiration, fee_required, tx['block_index'])
if problems: status = 'invalid: ' + '; '.join(problems)
if util.enabled('btc_order_minimum'):
min_btc_quantity = 0.001 * config.UNIT # 0.001 BTC
if util.enabled('btc_order_minimum_adjustment_1'):
min_btc_quantity = 0.00001 * config.UNIT # 0.00001 BTC
if (give_asset == config.BTC and give_quantity < min_btc_quantity) or (get_asset == config.BTC and get_quantity < min_btc_quantity):
if problems:
status += '; btc order below minimum'
else:
status = 'invalid: btc order below minimum'
# Debit give quantity. (Escrow.)
if status == 'open':
if give_asset != config.BTC: # No need (or way) to debit BTC.
util.debit(db, tx['source'], give_asset, give_quantity, action='open order', event=tx['tx_hash'])
# Add parsed transaction to message-type–specific table.
bindings = {
'tx_index': tx['tx_index'],
'tx_hash': tx['tx_hash'],
'block_index': tx['block_index'],
'source': tx['source'],
'give_asset': give_asset,
'give_quantity': give_quantity,
'give_remaining': give_quantity,
'get_asset': get_asset,
'get_quantity': get_quantity,
'get_remaining': get_quantity,
'expiration': expiration,
'expire_index': tx['block_index'] + expiration,
'fee_required': fee_required,
'fee_required_remaining': fee_required,
'fee_provided': tx['fee'],
'fee_provided_remaining': tx['fee'],
'status': status,
}
if "integer overflow" not in status:
sql = 'insert into orders values(:tx_index, :tx_hash, :block_index, :source, :give_asset, :give_quantity, :give_remaining, :get_asset, :get_quantity, :get_remaining, :expiration, :expire_index, :fee_required, :fee_required_remaining, :fee_provided, :fee_provided_remaining, :status)'
order_parse_cursor.execute(sql, bindings)
else:
logger.warn("Not storing [order] tx [%s]: %s" % (tx['tx_hash'], status))
logger.debug("Bindings: %s" % (json.dumps(bindings), ))
# Match.
if status == 'open' and tx['block_index'] != config.MEMPOOL_BLOCK_INDEX:
match(db, tx)
order_parse_cursor.close()
def match (db, tx, block_index=None):
cursor = db.cursor()
# Get order in question.
orders = list(cursor.execute('''SELECT * FROM orders\
WHERE (tx_index = ? AND status = ?)''', (tx['tx_index'], 'open')))
if not orders:
cursor.close()
return
else:
assert len(orders) == 1
tx1 = orders[0]
cursor.execute('''SELECT * FROM orders \
WHERE (give_asset=? AND get_asset=? AND status=? AND tx_hash != ?)''',
(tx1['get_asset'], tx1['give_asset'], 'open', tx1['tx_hash']))
tx1_give_remaining = tx1['give_remaining']
tx1_get_remaining = tx1['get_remaining']
order_matches = cursor.fetchall()
if tx['block_index'] > 284500 or config.TESTNET or config.REGTEST: # Protocol change.
order_matches = sorted(order_matches, key=lambda x: x['tx_index']) # Sort by tx index second.
order_matches = sorted(order_matches, key=lambda x: util.price(x['get_quantity'], x['give_quantity'])) # Sort by price first.
# Get fee remaining.
tx1_fee_required_remaining = tx1['fee_required_remaining']
tx1_fee_provided_remaining = tx1['fee_provided_remaining']
tx1_status = tx1['status']
for tx0 in order_matches:
order_match_id = util.make_id(tx0['tx_hash'], tx1['tx_hash'])
if not block_index:
block_index = max(tx0['block_index'], tx1['block_index'])
if tx1_status != 'open': break
logger.debug('Considering: ' + tx0['tx_hash'])
tx0_give_remaining = tx0['give_remaining']
tx0_get_remaining = tx0['get_remaining']
# Ignore previous matches. (Both directions, just to be sure.)
cursor.execute('''SELECT * FROM order_matches
WHERE id = ? ''', (util.make_id(tx0['tx_hash'], tx1['tx_hash']), ))
if list(cursor):
logger.debug('Skipping: previous match')
continue
cursor.execute('''SELECT * FROM order_matches
WHERE id = ? ''', (util.make_id(tx1['tx_hash'], tx0['tx_hash']), ))
if list(cursor):
logger.debug('Skipping: previous match')
continue
# Get fee provided remaining.
tx0_fee_required_remaining = tx0['fee_required_remaining']
tx0_fee_provided_remaining = tx0['fee_provided_remaining']
# Make sure that that both orders still have funds remaining (if order involves BTC, and so cannot be ‘filled’).
if tx0['give_asset'] == config.BTC or tx0['get_asset'] == config.BTC: # Gratuitous
if tx0_give_remaining <= 0 or tx1_give_remaining <= 0:
logger.debug('Skipping: negative give quantity remaining')
continue
if block_index >= 292000 and block_index <= 310500 and not config.TESTNET or config.REGTEST: # Protocol changes
if tx0_get_remaining <= 0 or tx1_get_remaining <= 0:
logger.debug('Skipping: negative get quantity remaining')
continue
if block_index >= 294000 or config.TESTNET or config.REGTEST: # Protocol change.
if tx0['fee_required_remaining'] < 0:
logger.debug('Skipping: negative tx0 fee required remaining')
continue
if tx0['fee_provided_remaining'] < 0:
logger.debug('Skipping: negative tx0 fee provided remaining')
continue
if tx1_fee_provided_remaining < 0:
logger.debug('Skipping: negative tx1 fee provided remaining')
continue
if tx1_fee_required_remaining < 0:
logger.debug('Skipping: negative tx1 fee required remaining')
continue
# If the prices agree, make the trade. The found order sets the price,
# and they trade as much as they can.
tx0_price = util.price(tx0['get_quantity'], tx0['give_quantity'])
tx1_price = util.price(tx1['get_quantity'], tx1['give_quantity'])
tx1_inverse_price = util.price(tx1['give_quantity'], tx1['get_quantity'])
# Protocol change.
if tx['block_index'] < 286000: tx1_inverse_price = util.price(1, tx1_price)
logger.debug('Tx0 Price: {}; Tx1 Inverse Price: {}'.format(float(tx0_price), float(tx1_inverse_price)))
if tx0_price > tx1_inverse_price:
logger.debug('Skipping: price mismatch.')
else:
logger.debug('Potential forward quantities: {}, {}'.format(tx0_give_remaining, int(util.price(tx1_give_remaining, tx0_price))))
forward_quantity = int(min(tx0_give_remaining, int(util.price(tx1_give_remaining, tx0_price))))
logger.debug('Forward Quantity: {}'.format(forward_quantity))
backward_quantity = round(forward_quantity * tx0_price)
logger.debug('Backward Quantity: {}'.format(backward_quantity))
if not forward_quantity:
logger.debug('Skipping: zero forward quantity.')
continue
if block_index >= 286500 or config.TESTNET or config.REGTEST: # Protocol change.
if not backward_quantity:
logger.debug('Skipping: zero backward quantity.')
continue
forward_asset, backward_asset = tx1['get_asset'], tx1['give_asset']
if block_index >= 313900 or config.TESTNET or config.REGTEST: # Protocol change.
min_btc_quantity = 0.001 * config.UNIT # 0.001 BTC
if (forward_asset == config.BTC and forward_quantity <= min_btc_quantity) or (backward_asset == config.BTC and backward_quantity <= min_btc_quantity):
logger.debug('Skipping: below minimum {} quantity'.format(config.BTC))
continue
# Check and update fee remainings.
fee = 0
if block_index >= 286500 or config.TESTNET or config.REGTEST: # Protocol change. Deduct fee_required from provided_remaining, etc., if possible (else don’t match).
if tx1['get_asset'] == config.BTC:
if block_index >= 310500 or config.TESTNET or config.REGTEST: # Protocol change.
fee = int(tx1['fee_required'] * util.price(backward_quantity, tx1['give_quantity']))
else:
fee = int(tx1['fee_required_remaining'] * util.price(forward_quantity, tx1_get_remaining))
logger.debug('Tx0 fee provided remaining: {}; required fee: {}'.format(tx0_fee_provided_remaining / config.UNIT, fee / config.UNIT))
if tx0_fee_provided_remaining < fee:
logger.debug('Skipping: tx0 fee provided remaining is too low.')
continue
else:
tx0_fee_provided_remaining -= fee
if block_index >= 287800 or config.TESTNET or config.REGTEST: # Protocol change.
tx1_fee_required_remaining -= fee
elif tx1['give_asset'] == config.BTC:
if block_index >= 310500 or config.TESTNET or config.REGTEST: # Protocol change.
fee = int(tx0['fee_required'] * util.price(backward_quantity, tx0['give_quantity']))
else:
fee = int(tx0['fee_required_remaining'] * util.price(backward_quantity, tx0_get_remaining))
logger.debug('Tx1 fee provided remaining: {}; required fee: {}'.format(tx1_fee_provided_remaining / config.UNIT, fee / config.UNIT))
if tx1_fee_provided_remaining < fee:
logger.debug('Skipping: tx1 fee provided remaining is too low.')
continue
else:
tx1_fee_provided_remaining -= fee
if block_index >= 287800 or config.TESTNET or config.REGTEST: # Protocol change.
tx0_fee_required_remaining -= fee
else: # Don’t deduct.
if tx1['get_asset'] == config.BTC:
if tx0_fee_provided_remaining < tx1['fee_required']: continue
elif tx1['give_asset'] == config.BTC:
if tx1_fee_provided_remaining < tx0['fee_required']: continue
if config.BTC in (tx1['give_asset'], tx1['get_asset']):
status = 'pending'
else:
status = 'completed'
# Credit.
util.credit(db, tx1['source'], tx1['get_asset'],
forward_quantity, action='order match', event=order_match_id)
util.credit(db, tx0['source'], tx0['get_asset'],
backward_quantity, action='order match', event=order_match_id)
# Debit the order, even if it involves giving bitcoins, and so one
# can't debit the sending account.
# Get remainings may be negative.
tx0_give_remaining -= forward_quantity
tx0_get_remaining -= backward_quantity
tx1_give_remaining -= backward_quantity
tx1_get_remaining -= forward_quantity
# Update give_remaining, get_remaining.
# tx0
tx0_status = 'open'
if tx0_give_remaining <= 0 or (tx0_get_remaining <= 0 and (block_index >= 292000 or config.TESTNET or config.REGTEST)): # Protocol change
if tx0['give_asset'] != config.BTC and tx0['get_asset'] != config.BTC:
# Fill order, and recredit give_remaining.
tx0_status = 'filled'
util.credit(db, tx0['source'], tx0['give_asset'], tx0_give_remaining, event=tx1['tx_hash'], action='filled')
bindings = {
'give_remaining': tx0_give_remaining,
'get_remaining': tx0_get_remaining,
'fee_required_remaining': tx0_fee_required_remaining,
'fee_provided_remaining': tx0_fee_provided_remaining,
'status': tx0_status,
'tx_hash': tx0['tx_hash']
}
sql='update orders set give_remaining = :give_remaining, get_remaining = :get_remaining, fee_required_remaining = :fee_required_remaining, fee_provided_remaining = :fee_provided_remaining, status = :status where tx_hash = :tx_hash'
cursor.execute(sql, bindings)
log.message(db, block_index, 'update', 'orders', bindings)
# tx1
if tx1_give_remaining <= 0 or (tx1_get_remaining <= 0 and (block_index >= 292000 or config.TESTNET or config.REGTEST)): # Protocol change
if tx1['give_asset'] != config.BTC and tx1['get_asset'] != config.BTC:
# Fill order, and recredit give_remaining.
tx1_status = 'filled'
util.credit(db, tx1['source'], tx1['give_asset'], tx1_give_remaining, event=tx0['tx_hash'], action='filled')
bindings = {
'give_remaining': tx1_give_remaining,
'get_remaining': tx1_get_remaining,
'fee_required_remaining': tx1_fee_required_remaining,
'fee_provided_remaining': tx1_fee_provided_remaining,
'status': tx1_status,
'tx_hash': tx1['tx_hash']
}
sql='update orders set give_remaining = :give_remaining, get_remaining = :get_remaining, fee_required_remaining = :fee_required_remaining, fee_provided_remaining = :fee_provided_remaining, status = :status where tx_hash = :tx_hash'
cursor.execute(sql, bindings)
log.message(db, block_index, 'update', 'orders', bindings)
# Calculate when the match will expire.
if block_index >= 308000 or config.TESTNET or config.REGTEST: # Protocol change.
match_expire_index = block_index + 20
elif block_index >= 286500 or config.TESTNET or config.REGTEST: # Protocol change.
match_expire_index = block_index + 10
else:
match_expire_index = min(tx0['expire_index'], tx1['expire_index'])
# Record order match.
bindings = {
'id': util.make_id(tx0['tx_hash'], tx['tx_hash']),
'tx0_index': tx0['tx_index'],
'tx0_hash': tx0['tx_hash'],
'tx0_address': tx0['source'],
'tx1_index': tx1['tx_index'],
'tx1_hash': tx1['tx_hash'],
'tx1_address': tx1['source'],
'forward_asset': forward_asset,
'forward_quantity': forward_quantity,
'backward_asset': backward_asset,
'backward_quantity': backward_quantity,
'tx0_block_index': tx0['block_index'],
'tx1_block_index': tx1['block_index'],
'block_index': block_index,
'tx0_expiration': tx0['expiration'],
'tx1_expiration': tx1['expiration'],
'match_expire_index': match_expire_index,
'fee_paid': fee,
'status': status,
}
sql='insert into order_matches values(:id, :tx0_index, :tx0_hash, :tx0_address, :tx1_index, :tx1_hash, :tx1_address, :forward_asset, :forward_quantity, :backward_asset, :backward_quantity, :tx0_block_index, :tx1_block_index, :block_index, :tx0_expiration, :tx1_expiration, :match_expire_index, :fee_paid, :status)'
cursor.execute(sql, bindings)
if tx1_status == 'filled':
break
cursor.close()
return
def expire (db, block_index):
cursor = db.cursor()
# Expire orders and give refunds for the quantity give_remaining (if non-zero; if not BTC).
cursor.execute('''SELECT * FROM orders \
WHERE (status = ? AND expire_index < ?)''', ('open', block_index))
orders = list(cursor)
for order in orders:
cancel_order(db, order, 'expired', block_index)
# Expire order_matches for BTC with no BTC.
cursor.execute('''SELECT * FROM order_matches \
WHERE (status = ? and match_expire_index < ?)''', ('pending', block_index))
order_matches = list(cursor)
for order_match in order_matches:
cancel_order_match(db, order_match, 'expired', block_index)
# Expire btc sell order if match expires
if util.enabled('btc_sell_expire_on_match_expire'):
# Check for other pending order matches involving either tx0_hash or tx1_hash
bindings = {
'status': 'pending',
'tx0_hash': order_match['tx0_hash'],
'tx1_hash': order_match['tx1_hash']
}
sql='select * from order_matches where status = :status and ((tx0_hash in (:tx0_hash, :tx1_hash)) or ((tx1_hash in (:tx0_hash, :tx1_hash))))'
cursor.execute(sql, bindings)
order_matches_pending = cursor.fetchall()
# Set BTC sell order status as expired only if there are no pending order matches
if len(order_matches_pending) == 0:
if order_match['backward_asset'] == "BTC" and order_match['status'] == "expired":
cursor.execute('''SELECT * FROM orders \
WHERE tx_hash = ?''', (order_match['tx1_hash'],))
cancel_order(db, list(cursor)[0], 'expired', block_index)
if order_match['forward_asset'] == "BTC" and order_match['status'] == "expired":
cursor.execute('''SELECT * FROM orders \
WHERE tx_hash = ?''', (order_match['tx0_hash'],))
cancel_order(db, list(cursor)[0], 'expired', block_index)
if block_index >= 315000 or config.TESTNET or config.REGTEST: # Protocol change.
# Re‐match.
for order_match in order_matches:
cursor.execute('''SELECT * FROM transactions\
WHERE tx_hash = ?''', (order_match['tx0_hash'],))
match(db, list(cursor)[0], block_index)
cursor.execute('''SELECT * FROM transactions\
WHERE tx_hash = ?''', (order_match['tx1_hash'],))
match(db, list(cursor)[0], block_index)
cursor.close()
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
#### rps.py
#! /usr/bin/python3
"""
Transaction 1: rps (Open the game)
source: address used to play the game
wager: amount to bet
move_random_hash: sha256(sha256(move + random)) (stored as bytes, 16 bytes random)
possible_moves: arbitrary odd number >= 3
expiration: how many blocks the game is valid
Matching conditions:
- tx0_possible_moves = tx1_possible_moves
- tx0_wager = tx1_wager
Transaction 2: rpsresolve (Resolve the game)
source: same address as first transaction
random: 16 bytes random
move: the move number
rps_match_id: matching id
"""
import struct
import decimal
D = decimal.Decimal
import time
import binascii
import string
from counterpartylib.lib import config
from counterpartylib.lib import exceptions
from counterpartylib.lib import util
from counterpartylib.lib import log
from counterpartylib.lib import message_type
# possible_moves wager move_random_hash expiration
FORMAT = '>HQ32sI'
LENGTH = 2 + 8 + 32 + 4
ID = 80
def initialise (db):
cursor = db.cursor()
# RPS (Rock-Paper-Scissors)
cursor.execute('''CREATE TABLE IF NOT EXISTS rps(
tx_index INTEGER UNIQUE,
tx_hash TEXT UNIQUE,
block_index INTEGER,
source TEXT,
possible_moves INTEGER,
wager INTEGER,
move_random_hash TEXT,
expiration INTEGER,
expire_index INTEGER,
status TEXT,
FOREIGN KEY (tx_index, tx_hash, block_index) REFERENCES transactions(tx_index, tx_hash, block_index),
PRIMARY KEY (tx_index, tx_hash))
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
source_idx ON rps (source)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
matching_idx ON rps (wager, possible_moves)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
status_idx ON rps (status)
''')
# RPS Matches
cursor.execute('''CREATE TABLE IF NOT EXISTS rps_matches(
id TEXT PRIMARY KEY,
tx0_index INTEGER,
tx0_hash TEXT,
tx0_address TEXT,
tx1_index INTEGER,
tx1_hash TEXT,
tx1_address TEXT,
tx0_move_random_hash TEXT,
tx1_move_random_hash TEXT,
wager INTEGER,
possible_moves INTEGER,
tx0_block_index INTEGER,
tx1_block_index INTEGER,
block_index INTEGER,
tx0_expiration INTEGER,
tx1_expiration INTEGER,
match_expire_index INTEGER,
status TEXT,
FOREIGN KEY (tx0_index, tx0_hash, tx0_block_index) REFERENCES transactions(tx_index, tx_hash, block_index),
FOREIGN KEY (tx1_index, tx1_hash, tx1_block_index) REFERENCES transactions(tx_index, tx_hash, block_index))
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
rps_match_expire_idx ON rps_matches (status, match_expire_index)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
rps_tx0_address_idx ON rps_matches (tx0_address)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
rps_tx1_address_idx ON rps_matches (tx1_address)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
status_idx ON rps_matches (status)
''')
# RPS Expirations
cursor.execute('''CREATE TABLE IF NOT EXISTS rps_expirations(
rps_index INTEGER PRIMARY KEY,
rps_hash TEXT UNIQUE,
source TEXT,
block_index INTEGER,
FOREIGN KEY (block_index) REFERENCES blocks(block_index),
FOREIGN KEY (rps_index, rps_hash) REFERENCES rps(tx_index, tx_hash))
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
block_index_idx ON rps_expirations (block_index)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
source_idx ON rps_expirations (source)
''')
# RPS Match Expirations
cursor.execute('''CREATE TABLE IF NOT EXISTS rps_match_expirations(
rps_match_id TEXT PRIMARY KEY,
tx0_address TEXT,
tx1_address TEXT,
block_index INTEGER,
FOREIGN KEY (rps_match_id) REFERENCES rps_matches(id),
FOREIGN KEY (block_index) REFERENCES blocks(block_index))
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
block_index_idx ON rps_match_expirations (block_index)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
tx0_address_idx ON rps_match_expirations (tx0_address)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
tx1_address_idx ON rps_match_expirations (tx1_address)
''')
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 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 validate (db, source, possible_moves, wager, move_random_hash, expiration, block_index):
problems = []
if util.enabled('disable_rps'):
problems.append('rps disabled')
if not isinstance(possible_moves, int):
problems.append('possible_moves must be a integer')
return problems
if not isinstance(wager, int):
problems.append('wager must be in satoshis')
return problems
if not isinstance(expiration, int):
problems.append('expiration must be expressed as an integer block delta')
return problems
if not all(c in string.hexdigits for c in move_random_hash):
problems.append('move_random_hash must be an hexadecimal string')
return problems
move_random_hash_bytes = binascii.unhexlify(move_random_hash)
if possible_moves < 3:
problems.append('possible moves must be at least 3')
if possible_moves % 2 == 0:
problems.append('possible moves must be odd')
if wager <= 0:
problems.append('non‐positive wager')
if expiration < 0: problems.append('negative expiration')
if expiration == 0 and not (block_index >= 317500 or config.TESTNET or config.REGTEST): # Protocol change.
problems.append('zero expiration')
if expiration > config.MAX_EXPIRATION:
problems.append('expiration overflow')
if len(move_random_hash_bytes) != 32:
problems.append('move_random_hash must be 32 bytes in hexadecimal format')
return problems
def compose(db, source, possible_moves, wager, move_random_hash, expiration):
problems = validate(db, source, possible_moves, wager, move_random_hash, expiration, util.CURRENT_BLOCK_INDEX)
if problems: raise exceptions.ComposeError(problems)
data = message_type.pack(ID)
data += struct.pack(FORMAT, possible_moves, wager, binascii.unhexlify(move_random_hash), expiration)
return (source, [], data)
def parse(db, tx, message):
rps_parse_cursor = db.cursor()
# Unpack message.
try:
if len(message) != LENGTH:
raise exceptions.UnpackError
(possible_moves, wager, move_random_hash, expiration) = struct.unpack(FORMAT, message)
status = 'open'
except (exceptions.UnpackError, struct.error):
(possible_moves, wager, move_random_hash, expiration) = 0, 0, '', 0
status = 'invalid: could not unpack'
if status == 'open':
move_random_hash = binascii.hexlify(move_random_hash).decode('utf8')
# Overbet
rps_parse_cursor.execute('''SELECT * FROM balances \
WHERE (address = ? AND asset = ?)''', (tx['source'], 'XCP'))
balances = list(rps_parse_cursor)
if not balances:
wager = 0
else:
balance = balances[0]['quantity']
if balance < wager:
wager = balance
problems = validate(db, tx['source'], possible_moves, wager, move_random_hash, expiration, tx['block_index'])
if problems: status = 'invalid: {}'.format(', '.join(problems))
# Debit quantity wagered. (Escrow.)
if status == 'open':
util.debit(db, tx['source'], 'XCP', wager, action="open RPS", 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'],
'possible_moves': possible_moves,
'wager': wager,
'move_random_hash': move_random_hash,
'expiration': expiration,
'expire_index': tx['block_index'] + expiration,
'status': status,
}
sql = '''INSERT INTO rps VALUES (:tx_index, :tx_hash, :block_index, :source, :possible_moves, :wager, :move_random_hash, :expiration, :expire_index, :status)'''
rps_parse_cursor.execute(sql, bindings)
# Match.
if status == 'open':
match(db, tx, tx['block_index'])
rps_parse_cursor.close()
def match (db, tx, block_index):
cursor = db.cursor()
# Get rps in question.
rps = list(cursor.execute('''SELECT * FROM rps WHERE tx_index = ? AND status = ?''', (tx['tx_index'], 'open')))
if not rps:
cursor.close()
return
else:
assert len(rps) == 1
tx1 = rps[0]
possible_moves = tx1['possible_moves']
wager = tx1['wager']
tx1_status = 'open'
# Get rps match
bindings = (possible_moves, 'open', wager, tx1['source'])
# dont match twice same RPS
already_matched = []
old_rps_matches = cursor.execute('''SELECT * FROM rps_matches WHERE tx0_hash = ? OR tx1_hash = ?''', (tx1['tx_hash'], tx1['tx_hash']))
for old_rps_match in old_rps_matches:
counter_tx_hash = old_rps_match['tx1_hash'] if tx1['tx_hash'] == old_rps_match['tx0_hash'] else old_rps_match['tx0_hash']
already_matched.append(counter_tx_hash)
already_matched_cond = ''
if already_matched:
already_matched_cond = '''AND tx_hash NOT IN ({})'''.format(','.join(['?' for e in range(0, len(already_matched))]))
bindings += tuple(already_matched)
sql = '''SELECT * FROM rps WHERE (possible_moves = ? AND status = ? AND wager = ? AND source != ? {}) ORDER BY tx_index LIMIT 1'''.format(already_matched_cond)
rps_matches = list(cursor.execute(sql, bindings))
if rps_matches:
tx0 = rps_matches[0]
# update status
for txn in [tx0, tx1]:
bindings = {
'status': 'matched',
'tx_index': txn['tx_index']
}
cursor.execute('''UPDATE rps SET status = :status WHERE tx_index = :tx_index''', bindings)
log.message(db, block_index, 'update', 'rps', bindings)
bindings = {
'id': util.make_id(tx0['tx_hash'], tx1['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_move_random_hash': tx0['move_random_hash'],
'tx1_move_random_hash': tx1['move_random_hash'],
'wager': wager,
'possible_moves': possible_moves,
'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': block_index + 20,
'status': 'pending'
}
sql = '''INSERT INTO rps_matches VALUES (:id, :tx0_index, :tx0_hash, :tx0_address, :tx1_index, :tx1_hash, :tx1_address,
:tx0_move_random_hash, :tx1_move_random_hash, :wager, :possible_moves,
:tx0_block_index, :tx1_block_index, :block_index, :tx0_expiration, :tx1_expiration,
:match_expire_index, :status)'''
cursor.execute(sql, bindings)
cursor.close()
def expire (db, block_index):
cursor = db.cursor()
# Expire rps and give refunds for the quantity wager.
cursor.execute('''SELECT * FROM rps WHERE (status = ? AND expire_index < ?)''', ('open', block_index))
for rps in cursor.fetchall():
cancel_rps(db, rps, 'expired', block_index)
# Record rps expiration.
bindings = {
'rps_index': rps['tx_index'],
'rps_hash': rps['tx_hash'],
'source': rps['source'],
'block_index': block_index
}
sql = '''INSERT INTO rps_expirations VALUES (:rps_index, :rps_hash, :source, :block_index)'''
cursor.execute(sql, bindings)
# Expire rps matches
expire_bindings = ('pending', 'pending and resolved', 'resolved and pending', block_index)
cursor.execute('''SELECT * FROM rps_matches WHERE (status IN (?, ?, ?) AND match_expire_index < ?)''', expire_bindings)
for rps_match in cursor.fetchall():
new_rps_match_status = 'expired'
# pending loses against resolved
if rps_match['status'] == 'pending and resolved':
new_rps_match_status = 'concluded: second player wins'
elif rps_match['status'] == 'resolved and pending':
new_rps_match_status = 'concluded: first player wins'
update_rps_match_status(db, rps_match, new_rps_match_status, block_index)
# Record rps match expiration.
bindings = {
'rps_match_id': rps_match['id'],
'tx0_address': rps_match['tx0_address'],
'tx1_address': rps_match['tx1_address'],
'block_index': block_index
}
sql = '''INSERT INTO rps_match_expirations VALUES (:rps_match_id, :tx0_address, :tx1_address, :block_index)'''
cursor.execute(sql, bindings)
# Rematch not expired and not resolved RPS
if new_rps_match_status == 'expired':
sql = '''SELECT * FROM rps WHERE tx_hash IN (?, ?) AND status = ? AND expire_index >= ?'''
bindings = (rps_match['tx0_hash'], rps_match['tx1_hash'], 'matched', block_index)
matched_rps = list(cursor.execute(sql, bindings))
for rps in matched_rps:
cursor.execute('''UPDATE rps SET status = ? WHERE tx_index = ?''', ('open', rps['tx_index']))
# Re-debit XCP refund by close_rps_match.
util.debit(db, rps['source'], 'XCP', rps['wager'], action='reopen RPS after matching expiration', event=rps_match['id'])
# Rematch
match(db, {'tx_index': rps['tx_index']}, block_index)
cursor.close()
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
#### rpsresolve.py
#! /usr/bin/python3
import binascii
import struct
import logging
logger = logging.getLogger(__name__)
import string
from counterpartylib.lib import (config, exceptions, util, message_type)
from . import rps
# move random rps_match_id
FORMAT = '>H16s32s32s'
LENGTH = 2 + 16 + 32 + 32
ID = 81
def initialise (db):
cursor = db.cursor()
cursor.execute('''CREATE TABLE IF NOT EXISTS rpsresolves(
tx_index INTEGER PRIMARY KEY,
tx_hash TEXT UNIQUE,
block_index INTEGER,
source TEXT,
move INTEGER,
random TEXT,
rps_match_id TEXT,
status TEXT,
FOREIGN KEY (tx_index, tx_hash, block_index) REFERENCES transactions(tx_index, tx_hash, block_index))
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
block_index_idx ON rpsresolves (block_index)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
source_idx ON rpsresolves (source)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
rps_match_id_idx ON rpsresolves (rps_match_id)
''')
def validate (db, source, move, random, rps_match_id):
problems = []
rps_match = None
if not isinstance(move, int):
problems.append('move must be a integer')
return None, None, problems
if not all(c in string.hexdigits for c in random):
problems.append('random must be an hexadecimal string')
return None, None, problems
random_bytes = binascii.unhexlify(random)
if len(random_bytes) != 16:
problems.append('random must be 16 bytes in hexadecimal format')
return None, None, problems
cursor = db.cursor()
rps_matches = list(cursor.execute('''SELECT * FROM rps_matches WHERE id = ?''', (rps_match_id,)))
cursor.close()
if len(rps_matches) == 0:
problems.append('no such rps match')
return None, rps_match, problems
elif len(rps_matches) > 1:
assert False
rps_match = rps_matches[0]
if move<1:
problems.append('move must be greater than 0')
elif move > rps_match['possible_moves']:
problems.append('move must be lower than {}'.format(rps_match['possible_moves']))
if source not in [rps_match['tx0_address'], rps_match['tx1_address']]:
problems.append('invalid source address')
return None, rps_match, problems
if rps_match['tx0_address'] == source:
txn = 0
rps_match_status = ['pending', 'pending and resolved']
else:
txn = 1
rps_match_status = ['pending', 'resolved and pending']
move_random_hash = util.dhash(random_bytes + int(move).to_bytes(2, byteorder='big'))
move_random_hash = binascii.hexlify(move_random_hash).decode('utf-8')
if rps_match['tx{}_move_random_hash'.format(txn)] != move_random_hash:
problems.append('invalid move or random value')
return txn, rps_match, problems
if rps_match['status'] == 'expired':
problems.append('rps match expired')
elif rps_match['status'].startswith('concluded'):
problems.append('rps match concluded')
elif rps_match['status'].startswith('invalid'):
problems.append('rps match invalid')
elif rps_match['status'] not in rps_match_status:
problems.append('rps already resolved')
return txn, rps_match, problems
def compose (db, source, move, random, rps_match_id):
tx0_hash, tx1_hash = util.parse_id(rps_match_id)
txn, rps_match, problems = validate(db, source, move, random, rps_match_id)
if problems: raise exceptions.ComposeError(problems)
# Warn if down to the wire.
time_left = rps_match['match_expire_index'] - util.CURRENT_BLOCK_INDEX
if time_left < 4:
logger.warning('Only {} blocks until that rps match expires. The conclusion might not make into the blockchain in time.'.format(time_left))
tx0_hash_bytes = binascii.unhexlify(bytes(tx0_hash, 'utf-8'))
tx1_hash_bytes = binascii.unhexlify(bytes(tx1_hash, 'utf-8'))
random_bytes = binascii.unhexlify(bytes(random, 'utf-8'))
data = message_type.pack(ID)
data += struct.pack(FORMAT, move, random_bytes, tx0_hash_bytes, tx1_hash_bytes)
return (source, [], data)
def parse (db, tx, message):
cursor = db.cursor()
# Unpack message.
try:
if len(message) != LENGTH:
raise exceptions.UnpackError
move, random, 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')
rps_match_id = util.make_id(tx0_hash, tx1_hash)
random = binascii.hexlify(random).decode('utf-8')
status = 'valid'
except (exceptions.UnpackError, struct.error) as e:
move, random, tx0_hash, tx1_hash, rps_match_id = None, None, None, None, None
status = 'invalid: could not unpack'
if status == 'valid':
txn, rps_match, problems = validate(db, tx['source'], move, random, rps_match_id)
if problems:
rps_match = None
status = 'invalid: ' + '; '.join(problems)
# Add parsed transaction to message-type–specific table.
rpsresolves_bindings = {
'tx_index': tx['tx_index'],
'tx_hash': tx['tx_hash'],
'block_index': tx['block_index'],
'source': tx['source'],
'move': move,
'random': random,
'rps_match_id': rps_match_id,
'status': status
}
if status == 'valid':
rps_match_status = 'concluded'
if rps_match['status'] == 'pending':
rps_match_status = 'resolved and pending' if txn==0 else 'pending and resolved'
if rps_match_status == 'concluded':
counter_txn = 0 if txn == 1 else 1
counter_source = rps_match['tx{}_address'.format(counter_txn)]
sql = '''SELECT * FROM rpsresolves WHERE rps_match_id = ? AND source = ? AND status = ?'''
counter_games = list(cursor.execute(sql, (rps_match_id, counter_source, 'valid')))
assert len(counter_games) == 1
counter_game = counter_games[0]
winner = resolve_game(db, rpsresolves_bindings, counter_game)
if winner == 0:
rps_match_status = 'concluded: tie'
elif winner == counter_game['tx_index']:
rps_match_status = 'concluded: {} player wins'.format('first' if counter_txn == 0 else 'second')
else:
rps_match_status = 'concluded: {} player wins'.format('first' if txn == 0 else 'second')
rps.update_rps_match_status(db, rps_match, rps_match_status, tx['block_index'])
sql = '''INSERT INTO rpsresolves VALUES (:tx_index, :tx_hash, :block_index, :source, :move, :random, :rps_match_id, :status)'''
cursor.execute(sql, rpsresolves_bindings)
cursor.close()
# https://en.wikipedia.org/wiki/Rock-paper-scissors#Additional_weapons:
def resolve_game(db, resovlerps1, resovlerps2):
move1 = resovlerps1['move']
move2 = resovlerps2['move']
same_parity = (move1 % 2) == (move2 % 2)
if (same_parity and move1 < move2) or (not same_parity and move1 > move2):
return resovlerps1['tx_index']
elif (same_parity and move1 > move2) or (not same_parity and move1 < move2):
return resovlerps2['tx_index']
else:
return 0
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
#### send.py
#! /usr/bin/python3
from counterpartylib.lib.messages.versions import send1
from counterpartylib.lib.messages.versions import enhanced_send
from counterpartylib.lib.messages.versions import mpma
from counterpartylib.lib import util
from counterpartylib.lib import exceptions
from counterpartylib.lib import config
ID = send1.ID
def initialise (db):
cursor = db.cursor()
cursor.execute('''CREATE TABLE IF NOT EXISTS sends(
tx_index INTEGER PRIMARY KEY,
tx_hash TEXT UNIQUE,
block_index INTEGER,
source TEXT,
destination TEXT,
asset TEXT,
quantity INTEGER,
status TEXT,
FOREIGN KEY (tx_index, tx_hash, block_index) REFERENCES transactions(tx_index, tx_hash, block_index))
''')
columns = [column['name'] for column in cursor.execute('''PRAGMA table_info(sends)''')]
# If CIP10 activated, Create Sends copy, copy old data, drop old table, rename new table, recreate indexes
# SQLite can’t do `ALTER TABLE IF COLUMN NOT EXISTS` nor can drop UNIQUE constraints
if 'msg_index' not in columns:
if 'memo' not in columns:
cursor.execute('''CREATE TABLE IF NOT EXISTS new_sends(
tx_index INTEGER,
tx_hash TEXT,
block_index INTEGER,
source TEXT,
destination TEXT,
asset TEXT,
quantity INTEGER,
status TEXT,
msg_index INTEGER DEFAULT 0,
PRIMARY KEY (tx_index, msg_index),
FOREIGN KEY (tx_index, tx_hash, block_index) REFERENCES transactions(tx_index, tx_hash, block_index),
UNIQUE (tx_hash, msg_index) ON CONFLICT FAIL)
''')
cursor.execute('''INSERT INTO new_sends(tx_index, tx_hash, block_index, source, destination, asset, quantity, status)
SELECT tx_index, tx_hash, block_index, source, destination, asset, quantity, status
FROM sends''', {})
else:
cursor.execute('''CREATE TABLE IF NOT EXISTS new_sends(
tx_index INTEGER,
tx_hash TEXT,
block_index INTEGER,
source TEXT,
destination TEXT,
asset TEXT,
quantity INTEGER,
status TEXT,
memo BLOB,
msg_index INTEGER DEFAULT 0,
PRIMARY KEY (tx_index, msg_index),
FOREIGN KEY (tx_index, tx_hash, block_index) REFERENCES transactions(tx_index, tx_hash, block_index),
UNIQUE (tx_hash, msg_index) ON CONFLICT FAIL)
''')
cursor.execute('''INSERT INTO new_sends (tx_index, tx_hash, block_index, source, destination, asset, quantity, status, memo)
SELECT tx_index, tx_hash, block_index, source, destination, asset, quantity, status, memo
FROM sends''', {})
cursor.execute('DROP TABLE sends')
cursor.execute('ALTER TABLE new_sends RENAME TO sends')
cursor.execute('''CREATE INDEX IF NOT EXISTS
block_index_idx ON sends (block_index)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
source_idx ON sends (source)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
destination_idx ON sends (destination)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
asset_idx ON sends (asset)
''')
# Adds a memo to sends
# SQLite can’t do `ALTER TABLE IF COLUMN NOT EXISTS`.
if 'memo' not in columns:
cursor.execute('''ALTER TABLE sends ADD COLUMN memo BLOB''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
memo_idx ON sends (memo)
''')
def unpack(db, message, block_index):
return send1.unpack(db, message, block_index)
def validate (db, source, destination, asset, quantity, block_index):
return send1.validate(db, source, destination, asset, quantity, block_index)
def compose (db, source, destination, asset, quantity, memo=None, memo_is_hex=False, use_enhanced_send=None):
# special case - enhanced_send replaces send by default when it is enabled
# but it can be explicitly disabled with an API parameter
if util.enabled('enhanced_sends'):
# Another special case, if destination, asset and quantity are arrays, it's an MPMA send
if isinstance(destination, list) and isinstance(asset, list) and isinstance(quantity, list):
if util.enabled('mpma_sends'):
if len(destination) == len(asset) and len(asset) == len(quantity):
# Sending memos in a MPMA message can be done by several approaches:
# 1. Send a list of memos, there must be one for each send and they correspond to the sends by index
# - In this case memo_is_hex should be a list with the same cardinality
# 2. Send a dict with the message specific memos and the message wide memo (same for the hex specifier):
# - Each dict should have 2 members:
# + list: same as case (1). An array that specifies the memo for each send
# + msg_wide: the memo for the whole message. This memo will be used for sends that don't have a memo specified. Same as in (3)
# 3. Send one memo (either bytes or string) and True/False in memo_is_hex. This will be interpreted as a message wide memo.
if (len(destination) > config.MPMA_LIMIT):
raise exceptions.ComposeError('mpma sends have a maximum of '+str(config.MPMA_LIMIT)+' sends')
if isinstance(memo, list) and isinstance(memo_is_hex, list):
# (1) implemented here
if len(memo) != len(memo_is_hex):
raise exceptions.ComposeError('memo and memo_is_hex lists should have the same length')
elif len(memo) != len(destination):
raise exceptions.ComposeError('memo/memo_is_hex lists should have the same length as sends')
return mpma.compose(db, source, util.flat(zip(asset, destination, quantity, memo, memo_is_hex)), None, None)
elif isinstance(memo, dict) and isinstance(memo_is_hex, dict):
# (2) implemented here
if not('list' in memo and 'list' in memo_is_hex and 'msg_wide' in memo and 'msg_wide' in memo_is_hex):
raise exceptions.ComposeError('when specifying memo/memo_is_hex as a dict, they must contain keys "list" and "msg_wide"')
elif len(memo['list']) != len(memo_is_hex['list']):
raise exceptions.ComposeError('length of memo.list and memo_is_hex.list must be equal')
elif len(memo['list']) != len(destination):
raise exceptions.ComposeError('length of memo.list/memo_is_hex.list must be equal to the amount of sends')
return mpma.compose(db, source, util.flat(zip(asset, destination, quantity, memo['list'], memo_is_hex['list'])), memo['msg_wide'], memo_is_hex['msg_wide'])
else:
# (3) the default case
return mpma.compose(db, source, util.flat(zip(asset, destination, quantity)), memo, memo_is_hex)
else:
raise exceptions.ComposeError('destination, asset and quantity arrays must have the same amount of elements')
else:
raise exceptions.ComposeError('mpma sends are not enabled')
elif use_enhanced_send is None or use_enhanced_send == True:
return enhanced_send.compose(db, source, destination, asset, quantity, memo, memo_is_hex)
elif memo is not None or use_enhanced_send == True:
raise exceptions.ComposeError('enhanced sends are not enabled')
return send1.compose(db, source, destination, asset, quantity)
def parse (db, tx, message): # TODO: *args
return send1.parse(db, tx, message)
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
#### sweep.py
#! /usr/bin/python3
import struct
from counterpartylib.lib import exceptions
from counterpartylib.lib import config
from counterpartylib.lib import util
from counterpartylib.lib import log
from counterpartylib.lib import message_type
from counterpartylib.lib import address
from counterpartylib.lib.exceptions import *
FORMAT = '>21sB'
LENGTH = 22
MAX_MEMO_LENGTH = 34 # Could be higher, but we will keep it consistent with enhanced send
ID = 4
ANTISPAM_FEE_DECIMAL = 0.5
ANTISPAM_FEE = ANTISPAM_FEE_DECIMAL * config.UNIT
FLAG_BALANCES = 1
FLAG_OWNERSHIP = 2
FLAG_BINARY_MEMO = 4
FLAGS_ALL = FLAG_BINARY_MEMO | FLAG_BALANCES | FLAG_OWNERSHIP
def initialise(db):
cursor = db.cursor()
cursor.execute('''CREATE TABLE IF NOT EXISTS sweeps(
tx_index INTEGER PRIMARY KEY,
tx_hash TEXT UNIQUE,
block_index INTEGER,
source TEXT,
destination TEXT,
flags INTEGER,
status TEXT,
memo BLOB,
fee_paid INTEGER,
FOREIGN KEY (tx_index, tx_hash, block_index) REFERENCES transactions(tx_index, tx_hash, block_index))
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
block_index_idx ON sweeps (block_index)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
source_idx ON sweeps (source)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
destination_idx ON sweeps (destination)
''')
cursor.execute('''CREATE INDEX IF NOT EXISTS
memo_idx ON sweeps (memo)
''')
def validate (db, source, destination, flags, memo, block_index):
problems = []
if source == destination:
problems.append('destination cannot be the same as source')
cursor = db.cursor()
cursor.execute('''SELECT * FROM balances WHERE (address = ? AND asset = ?)''', (source, 'XCP'))
result = cursor.fetchall()
if len(result) == 0:
problems.append('insufficient XCP balance for sweep. Need %s XCP for antispam fee' % ANTISPAM_FEE_DECIMAL)
elif result[0]['quantity'] < ANTISPAM_FEE:
problems.append('insufficient XCP balance for sweep. Need %s XCP for antispam fee' % ANTISPAM_FEE_DECIMAL)
cursor.close()
if flags > FLAGS_ALL:
problems.append('invalid flags %i' % flags)
elif not(flags & (FLAG_BALANCES | FLAG_OWNERSHIP)):
problems.append('must specify which kind of transfer in flags')
if memo and len(memo) > MAX_MEMO_LENGTH:
problems.append('memo too long')
return problems
def compose (db, source, destination, flags, memo):
if memo is None:
memo = b''
elif flags & FLAG_BINARY_MEMO:
memo = bytes.fromhex(memo)
else:
memo = memo.encode('utf-8')
memo = struct.pack(">{}s".format(len(memo)), memo)
block_index = util.CURRENT_BLOCK_INDEX
problems = validate(db, source, destination, flags, memo, block_index)
if problems: raise exceptions.ComposeError(problems)
short_address_bytes = address.pack(destination)
data = message_type.pack(ID)
data += struct.pack(FORMAT, short_address_bytes, flags)
data += memo
return (source, [], data)
def unpack(db, message, block_index):
try:
memo_bytes_length = len(message) - LENGTH
if memo_bytes_length < 0:
raise exceptions.UnpackError('invalid message length')
if memo_bytes_length > MAX_MEMO_LENGTH:
raise exceptions.UnpackError('memo too long')
struct_format = FORMAT + ('{}s'.format(memo_bytes_length))
short_address_bytes, flags, memo_bytes = struct.unpack(struct_format, message)
if len(memo_bytes) == 0:
memo_bytes = None
elif not(flags & FLAG_BINARY_MEMO):
memo_bytes = memo_bytes.decode('utf-8')
# unpack address
full_address = address.unpack(short_address_bytes)
except (struct.error) as e:
logger.warning("sweep send unpack error: {}".format(e))
raise exceptions.UnpackError('could not unpack')
unpacked = {
'destination': full_address,
'flags': flags,
'memo': memo_bytes,
}
return unpacked
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)
if status == 'valid':
try:
util.debit(db, tx['source'], '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:
sweep_pos = 0
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'],
'msg_index': sweep_pos,
'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'],
'reset': False
}
sql='insert into issuances values(:tx_index, :tx_hash, :msg_index, :block_index, :asset, :quantity, :divisible, :source, :issuer, :transfer, :callable, :call_date, :call_price, :description, :fee_paid, :locked, :status, :asset_longname, :reset)'
cursor.execute(sql, bindings)
sweep_pos += 1
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()
#### __init__.py
# Each message gets identified by its first byte.
# The only exception is send.version1 which is 0 the first to fourth byte.
#
# Used IDs for messages:
#
# 0 send.version1
# 1 send.enhanced_send
# 10 order
# 11 btcpay
# 12 dispenser
# 20 issuance
# 21 issuance.subasset
# 30 broadcast
# 40 bet
# 50 dividend
# 60 burn
# 70 cancel
# 80 rps
# 81 rpsresolve
# 110 destroy
#
# Allocate each new type of message within the "logical" 10 number boundary
# Only allocate a 10 number boundary if it makes sense
#### enhanced_send.py
#! /usr/bin/python3
import struct
import json
import logging
logger = logging.getLogger(__name__)
from counterpartylib.lib import (config, util, exceptions, util, message_type, address)
FORMAT = '>QQ21s'
LENGTH = 8 + 8 + 21
MAX_MEMO_LENGTH = 34
ID = 2 # 0x02
def unpack(db, message, block_index):
try:
# account for memo bytes
memo_bytes_length = len(message) - LENGTH
if memo_bytes_length < 0:
raise exceptions.UnpackError('invalid message length')
if memo_bytes_length > MAX_MEMO_LENGTH:
raise exceptions.UnpackError('memo too long')
struct_format = FORMAT + ('{}s'.format(memo_bytes_length))
asset_id, quantity, short_address_bytes, memo_bytes = struct.unpack(struct_format, message)
if len(memo_bytes) == 0:
memo_bytes = None
# unpack address
full_address = address.unpack(short_address_bytes)
# asset id to name
asset = util.generate_asset_name(asset_id, block_index)
if asset == config.BTC:
raise exceptions.AssetNameError('{} not allowed'.format(config.BTC))
except (struct.error) as e:
logger.warning("enhanced send unpack error: {}".format(e))
raise exceptions.UnpackError('could not unpack')
except (exceptions.AssetNameError, exceptions.AssetIDError) as e:
logger.warning("enhanced send invalid asset id: {}".format(e))
raise exceptions.UnpackError('asset id invalid')
unpacked = {
'asset': asset,
'quantity': quantity,
'address': full_address,
'memo': memo_bytes,
}
return unpacked
def validate (db, source, destination, asset, quantity, memo_bytes, block_index):
problems = []
if asset == config.BTC: problems.append('cannot send {}'.format(config.BTC))
if not isinstance(quantity, int):
problems.append('quantity must be in satoshis')
return problems
if quantity < 0:
problems.append('negative quantity')
if quantity == 0:
problems.append('zero quantity')
# For SQLite3
if quantity > config.MAX_INT:
problems.append('integer overflow')
# destination is always required
if not destination:
problems.append('destination is required')
# check memo
if memo_bytes is not None and len(memo_bytes) > MAX_MEMO_LENGTH:
problems.append('memo is too long')
if util.enabled('options_require_memo'):
cursor = db.cursor()
try:
results = cursor.execute('SELECT options FROM addresses WHERE address=?', (destination,))
if results:
result = results.fetchone()
if result and util.active_options(result['options'], config.ADDRESS_OPTION_REQUIRE_MEMO):
if memo_bytes is None or (len(memo_bytes) == 0):
problems.append('destination requires memo')
finally:
cursor.close()
return problems
def compose (db, source, destination, asset, quantity, memo, memo_is_hex):
cursor = db.cursor()
# Just send BTC?
if asset == config.BTC:
return (source, [(destination, quantity)], None)
# resolve subassets
asset = util.resolve_subasset_longname(db, asset)
#quantity must be in int satoshi (not float, string, etc)
if not isinstance(quantity, int):
raise exceptions.ComposeError('quantity must be an int (in satoshi)')
# Only for outgoing (incoming will overburn).
balances = list(cursor.execute('''SELECT * FROM balances WHERE (address = ? AND asset = ?)''', (source, asset)))
if not balances or balances[0]['quantity'] < quantity:
raise exceptions.ComposeError('insufficient funds')
# convert memo to memo_bytes based on memo_is_hex setting
if memo is None:
memo_bytes = b''
elif memo_is_hex:
memo_bytes = bytes.fromhex(memo)
else:
memo = memo.encode('utf-8')
memo_bytes = struct.pack(">{}s".format(len(memo)), memo)
block_index = util.CURRENT_BLOCK_INDEX
problems = validate(db, source, destination, asset, quantity, memo_bytes, block_index)
if problems: raise exceptions.ComposeError(problems)
asset_id = util.get_asset_id(db, asset, block_index)
short_address_bytes = address.pack(destination)
data = message_type.pack(ID)
data += struct.pack(FORMAT, asset_id, quantity, short_address_bytes)
data += memo_bytes
cursor.close()
# return an empty array as the second argument because we don't need to send BTC dust to the recipient
return (source, [], data)
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 (tx_index, tx_hash, block_index, source, destination, asset, quantity, status, memo) 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()
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
#### mpma.py
#! /usr/bin/python3
import struct
import json
import logging
import binascii
import math
from bitcoin.core import key
from functools import reduce
from itertools import groupby
logger = logging.getLogger(__name__)
from bitstring import ReadError
from counterpartylib.lib import (config, util, exceptions, util, message_type, address)
from .mpma_util.internals import (_decode_mpmaSendDecode, _encode_mpmaSend)
ID = 3 # 0x03 is this specific message type
## expected functions for message version
def unpack(db, message, block_index):
try:
unpacked = _decode_mpmaSendDecode(message, block_index)
except (struct.error) as e:
raise exceptions.UnpackError('could not unpack')
except (exceptions.AssetNameError, exceptions.AssetIDError) as e:
raise exceptions.UnpackError('invalid asset in mpma send')
except (ReadError) as e:
raise exceptions.UnpackError('truncated data')
return unpacked
def validate (db, source, asset_dest_quant_list, block_index):
problems = []
if len(asset_dest_quant_list) == 0:
problems.append('send list cannot be empty')
if len(asset_dest_quant_list) == 1:
problems.append('send list cannot have only one element')
if len(asset_dest_quant_list) > 0:
# Need to manually unpack the tuple to avoid errors on scenarios where no memo is specified
grpd = groupby([(t[0], t[1]) for t in asset_dest_quant_list])
lengrps = [len(list(grpr)) for (group, grpr) in grpd]
cardinality = max(lengrps)
if cardinality > 1:
problems.append('cannot specify more than once a destination per asset')
cursor = db.cursor()
for t in asset_dest_quant_list:
# Need to manually unpack the tuple to avoid errors on scenarios where no memo is specified
asset = t[0]
destination = t[1]
quantity = t[2]
sendMemo = None
if len(t) > 3:
sendMemo = t[3]
if asset == config.BTC: problems.append('cannot send {} to {}'.format(config.BTC, destination))
if not isinstance(quantity, int):
problems.append('quantities must be an int (in satoshis) for {} to {}'.format(asset, destination))
if quantity < 0:
problems.append('negative quantity for {} to {}'.format(asset, destination))
if quantity == 0:
problems.append('zero quantity for {} to {}'.format(asset, destination))
# For SQLite3
if quantity > config.MAX_INT:
problems.append('integer overflow for {} to {}'.format(asset, destination))
# destination is always required
if not destination:
problems.append('destination is required for {}'.format(asset))
if util.enabled('options_require_memo'):
results = cursor.execute('SELECT options FROM addresses WHERE address=?', (destination,))
if results:
result = results.fetchone()
if result and result['options'] & config.ADDRESS_OPTION_REQUIRE_MEMO and (sendMemo is None):
problems.append('destination {} requires memo'.format(destination))
cursor.close()
return problems
def compose (db, source, asset_dest_quant_list, memo, memo_is_hex):
cursor = db.cursor()
out_balances = util.accumulate([(t[0], t[2]) for t in asset_dest_quant_list])
for (asset, quantity) in out_balances:
if util.enabled('mpma_subasset_support'):
# resolve subassets
asset = util.resolve_subasset_longname(db, asset)
if not isinstance(quantity, int):
raise exceptions.ComposeError('quantities must be an int (in satoshis) for {}'.format(asset))
balances = list(cursor.execute('''SELECT * FROM balances WHERE (address = ? AND asset = ?)''', (source, asset)))
if not balances or balances[0]['quantity'] < quantity:
raise exceptions.ComposeError('insufficient funds for {}'.format(asset))
block_index = util.CURRENT_BLOCK_INDEX
cursor.close()
problems = validate(db, source, asset_dest_quant_list, block_index)
if problems: raise exceptions.ComposeError(problems)
data = message_type.pack(ID)
data += _encode_mpmaSend(db, asset_dest_quant_list, block_index, memo=memo, memo_is_hex=memo_is_hex)
return (source, [], data)
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()
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
#### send1.py
#! /usr/bin/python3
"""Create and parse 'send'-type messages."""
import struct
import json
import logging
logger = logging.getLogger(__name__)
from ... import (config, exceptions, util, message_type)
FORMAT = '>QQ'
LENGTH = 8 + 8
ID = 0
def unpack(db, message, block_index):
# Only used for `unpack` API call at the moment.
try:
asset_id, quantity = struct.unpack(FORMAT, message)
asset = util.get_asset_name(db, asset_id, block_index)
except struct.error:
raise exceptions.UnpackError('could not unpack')
except AssetNameError:
raise exceptions.UnpackError('asset id invalid')
unpacked = {
'asset': asset,
'quantity': quantity
}
return unpacked
def validate (db, source, destination, asset, quantity, block_index):
problems = []
if asset == config.BTC: problems.append('cannot send bitcoins') # Only for parsing.
if not isinstance(quantity, int):
problems.append('quantity must be in satoshis')
return problems
if quantity < 0:
problems.append('negative quantity')
# For SQLite3
if quantity > config.MAX_INT:
problems.append('integer overflow')
if util.enabled('send_destination_required'): # Protocol change.
if not destination:
problems.append('destination is required')
if util.enabled('options_require_memo'):
# Check destination address options
cursor = db.cursor()
results = cursor.execute('SELECT options FROM addresses WHERE address=?', (destination,))
if results:
result = results.fetchone()
if result and util.active_options(result['options'], config.ADDRESS_OPTION_REQUIRE_MEMO):
problems.append('destination requires memo')
cursor.close()
return problems
def compose (db, source, destination, asset, quantity):
cursor = db.cursor()
# Just send BTC?
if asset == config.BTC:
return (source, [(destination, quantity)], None)
# resolve subassets
asset = util.resolve_subasset_longname(db, asset)
#quantity must be in int satoshi (not float, string, etc)
if not isinstance(quantity, int):
raise exceptions.ComposeError('quantity must be an int (in satoshi)')
# Only for outgoing (incoming will overburn).
balances = list(cursor.execute('''SELECT * FROM balances WHERE (address = ? AND asset = ?)''', (source, asset)))
if not balances or balances[0]['quantity'] < quantity:
raise exceptions.ComposeError('insufficient funds')
block_index = util.CURRENT_BLOCK_INDEX
problems = validate(db, source, destination, asset, quantity, block_index)
if problems: raise exceptions.ComposeError(problems)
asset_id = util.get_asset_id(db, asset, block_index)
data = message_type.pack(ID)
data += struct.pack(FORMAT, asset_id, quantity)
cursor.close()
return (source, [(destination, None)], data)
def parse (db, tx, message):
cursor = db.cursor()
# Unpack message.
try:
if len(message) != LENGTH:
raise exceptions.UnpackError
asset_id, quantity = struct.unpack(FORMAT, message)
asset = util.get_asset_name(db, asset_id, tx['block_index'])
status = 'valid'
except (exceptions.UnpackError, exceptions.AssetNameError, struct.error) as e:
asset, quantity = None, None
status = 'invalid: could not unpack'
if status == 'valid':
# Oversend
cursor.execute('''SELECT * FROM balances \
WHERE (address = ? AND asset = ?)''', (tx['source'], asset))
balances = cursor.fetchall()
if not balances:
status = 'invalid: insufficient funds'
elif balances[0]['quantity'] < quantity:
quantity = min(balances[0]['quantity'], quantity)
# For SQLite3
if quantity:
quantity = min(quantity, config.MAX_INT)
if status == 'valid':
problems = validate(db, tx['source'], tx['destination'], asset, quantity, tx['block_index'])
if problems: status = 'invalid: ' + '; '.join(problems)
if status == 'valid':
util.debit(db, tx['source'], asset, quantity, action='send', event=tx['tx_hash'])
util.credit(db, tx['destination'], asset, quantity, action='send', 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'],
'destination': tx['destination'],
'asset': asset,
'quantity': quantity,
'status': status,
}
if "integer overflow" not in status and "quantity must be in satoshis" not in status:
sql = 'insert into sends (tx_index, tx_hash, block_index, source, destination, asset, quantity, status, memo) values(:tx_index, :tx_hash, :block_index, :source, :destination, :asset, :quantity, :status, NULL)'
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()
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
#### __init__.py
#### internals.py
import struct
import json
import logging
import binascii
import math
from bitcoin.core import key
from functools import reduce
from itertools import groupby
from bitstring import BitArray, BitStream, ConstBitStream, ReadError
logger = logging.getLogger(__name__)
from counterpartylib.lib import (config, util, exceptions, util, message_type, address)
## encoding functions
def _encode_constructBaseLUT(snds):
# t is a tuple of the form (asset, addr, amnt [, memo, is_hex])
return sorted(list(set([t[1] for t in snds]))) # Sorted to make list determinist
def _encode_constructBaseAssets(sends):
# t is a tuple of the form (asset, addr, amnt [, memo, is_hex])
return sorted(list(set([t[0] for t in sends]))) # Sorted to make list determinist
def _encode_constructLUT(sends):
baseLUT = _encode_constructBaseLUT(sends)
# What's this? It calculates the minimal number of bits needed to represent an item index inside the baseLUT
lutNbits = math.ceil(math.log2(len(baseLUT)))
return {
"nbits": lutNbits,
"addrs": baseLUT
}
def _encode_compressLUT(lut):
return b''.join([struct.pack('>H', len(lut['addrs']))] +
[
address.pack(addr)
for addr in lut['addrs']
])
def _encode_memo(memo=None, is_hex=False):
'''Tightly pack a memo as a Bit array'''
if memo is not None:
# signal a 1 bit for existence of the memo
barr = BitArray('0b1')
if is_hex:
# signal a 1 bit for hex encoded memos
barr.append('0b1')
if type(memo) is str: # append the string as hex-string
barr.append('uint:6=%i' % (len(memo) >> 1))
memo = '0x%s' % memo
else:
barr.append('uint:6=%i' % len(memo))
barr.append(memo)
else:
# signal a 0 bit for a string encoded memo
barr.append('0b0')
barr.append('uint:6=%i' % len(memo))
barr.append(BitArray(memo.encode('utf-8')))
return barr
else:
# if the memo is None, return just a 0 bit
return BitArray('0b0')
def _safe_tuple_index(t, i):
'''Get an element from a tuple, returning None if it's out of bounds'''
if len(t) <= i:
return None
else:
return t[i]
def _encode_constructSendList(send_asset, lut, sends):
# t is a tuple of the form (asset, addr, amnt, memo, is_hex)
# if there's no memo specified, memo and is_hex are None
return [
(lut['addrs'].index(t[1]), t[2], _safe_tuple_index(t, 3), _safe_tuple_index(t, 4))
for t in sends
if t[0] == send_asset
]
def _solve_asset(db, assetName, block_index):
asset = util.resolve_subasset_longname(db, assetName)
return util.get_asset_id(db, asset, block_index)
def _encode_compressSendList(db, nbits, send, block_index):
r = BitArray()
r.append('uintbe:64=%i' % _solve_asset(db, send['assetName'], block_index))
r.append('uint:%i=%i' % (nbits, len(send['sendList'])-1))
for sendItem in send['sendList']:
idx = sendItem[0]
amnt = sendItem[1]
r.append('uint:%i=%i' % (nbits, idx))
r.append('uintbe:64=%i' % amnt)
try:
memoStr = _encode_memo(memo=sendItem[2], is_hex=sendItem[3])
except:
memoStr = BitArray('0b0')
r.append(memoStr)
return r
def _encode_constructSends(sends):
lut = _encode_constructLUT(sends)
assets = _encode_constructBaseAssets(sends)
sendLists = [
{
"assetName": asset,
"sendList": _encode_constructSendList(asset, lut, sends)
}
for asset in assets
]
return {
"lut": lut,
"sendLists": sendLists
}
def _encode_compressSends(db, mpmaSend, block_index, memo=None, memo_is_hex=False):
compressedLUT = _encode_compressLUT(mpmaSend['lut'])
memo_arr = _encode_memo(memo, memo_is_hex).bin
isends = '0b' + memo_arr + ''.join([
''.join(['1', _encode_compressSendList(db, mpmaSend['lut']['nbits'], sendList, block_index).bin])
for sendList in mpmaSend['sendLists']
])
bstr = ''.join([isends, '0'])
pad = '0' * ((8 - (len(bstr) - 2)) % 8) # That -2 is because the prefix 0b is there
barr = BitArray(bstr + pad)
return b''.join([
compressedLUT,
barr.bytes
])
def _encode_mpmaSend(db, sends, block_index, memo=None, memo_is_hex=False):
mpma = _encode_constructSends(sends)
send = _encode_compressSends(db, mpma, block_index, memo=memo, memo_is_hex=memo_is_hex)
return send
## decoding functions
def _decode_decodeLUT(data):
(numAddresses,) = struct.unpack('>H', data[0:2])
if numAddresses == 0:
raise exceptions.DecodeError('address list can\'t be empty')
p = 2
addressList = []
bytesPerAddress = 21
for i in range(0, numAddresses):
addr_raw = data[p:p+bytesPerAddress]
addressList.append(address.unpack(addr_raw))
p += bytesPerAddress
lutNbits = math.ceil(math.log2(numAddresses))
return addressList, lutNbits, data[p:]
def _decode_decodeSendList(stream, nbits, lut, block_index):
asset_id = stream.read('uintbe:64')
if nbits > 0:
numRecipients = stream.read('uint:%i' % nbits)
rangeLimit = numRecipients + 1
else:
numRecipients = 1
rangeLimit = numRecipients
sendList = []
asset = util.generate_asset_name(asset_id, block_index)
for i in range(0, rangeLimit):
if nbits > 0:
idx = stream.read('uint:%i' % nbits)
else:
idx = 0
addr = lut[idx]
amount = stream.read('uintbe:64')
memo, is_hex = _decode_memo(stream)
if memo is not None:
sendList.append((addr, amount, memo, is_hex))
else:
sendList.append((addr, amount))
return asset, sendList
def _decode_decodeSends(stream, nbits, lut, block_index):
#stream = ConstBitStream(data)
sends = {}
while stream.read('bool'):
asset, sendList = _decode_decodeSendList(stream, nbits, lut, block_index)
sends[asset] = sendList
return sends
def _decode_memo(stream):
if stream.read('bool'):
is_hex = stream.read('bool')
mlen = stream.read('uint:6')
data = stream.read('bytes:%i' % mlen)
if not(is_hex):
# is an utf8 string
data = data.decode('utf-8')
return data, is_hex
else:
return None, None
def _decode_mpmaSendDecode(data, block_index):
lut, nbits, remain = _decode_decodeLUT(data)
stream = ConstBitStream(remain)
memo, is_hex = _decode_memo(stream)
sends = _decode_decodeSends(stream, nbits, lut, block_index)
if memo is not None:
for asset in sends:
sendList = sends[asset]
for idx, send in enumerate(sendList):
if len(send) == 2:
sendList[idx] = (send[0], send[1], memo, is_hex)
return sends
#### __init__.py