Host-customized fork of https://github.com/tecnovert/basicswap/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

633 lines
25 KiB

# -*- coding: utf-8 -*-
# Copyright (c) 2022 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import traceback
from urllib import parse
from .util import (
PAGE_LIMIT,
getCoinType,
inputAmount,
setCoinFilter,
get_data_entry,
have_data_entry,
get_data_entry_or,
listAvailableCoins,
set_pagination_filters,
)
from basicswap.db import (
Concepts,
)
from basicswap.util import (
ensure,
format_amount,
format_timestamp,
)
from basicswap.basicswap_util import (
SwapTypes,
DebugTypes,
getLockName,
strBidState,
TxLockTypes,
strOfferState,
)
from basicswap.chainparams import (
Coins,
)
def value_or_none(v):
if v == -1 or v == '-1':
return None
return v
def decode_offer_id(v):
try:
offer_id = bytes.fromhex(v)
ensure(len(offer_id) == 28, 'Bad offer ID')
return offer_id
except Exception:
raise ValueError('Bad offer ID')
def parseOfferFormData(swap_client, form_data, page_data, options={}):
errors = []
parsed_data = {}
if have_data_entry(form_data, 'addr_to'):
page_data['addr_to'] = get_data_entry(form_data, 'addr_to')
addr_to = value_or_none(page_data['addr_to'])
if addr_to is not None:
parsed_data['addr_to'] = addr_to
if have_data_entry(form_data, 'addr_from'):
page_data['addr_from'] = get_data_entry(form_data, 'addr_from')
parsed_data['addr_from'] = value_or_none(page_data['addr_from'])
else:
parsed_data['addr_from'] = None
try:
page_data['coin_from'] = getCoinType(get_data_entry(form_data, 'coin_from'))
coin_from = Coins(page_data['coin_from'])
ci_from = swap_client.ci(coin_from)
if coin_from != Coins.XMR:
page_data['fee_from_conf'] = ci_from._conf_target # Set default value
parsed_data['coin_from'] = coin_from
except Exception:
errors.append('Unknown Coin From')
try:
page_data['coin_to'] = getCoinType(get_data_entry(form_data, 'coin_to'))
coin_to = Coins(page_data['coin_to'])
ci_to = swap_client.ci(coin_to)
if coin_to != Coins.XMR:
page_data['fee_to_conf'] = ci_to._conf_target # Set default value
parsed_data['coin_to'] = coin_to
except Exception:
errors.append('Unknown Coin To')
if parsed_data['coin_to'] in (Coins.XMR, Coins.PART_ANON):
page_data['swap_style'] = 'xmr'
else:
page_data['swap_style'] = 'atomic'
try:
page_data['amt_from'] = get_data_entry(form_data, 'amt_from')
parsed_data['amt_from'] = inputAmount(page_data['amt_from'], ci_from)
except Exception:
errors.append('Amount From')
try:
if 'amt_bid_min' not in page_data:
if options.get('add_min_bid_amt', False) is True:
parsed_data['amt_bid_min'] = ci_from.chainparams_network()['min_amount']
else:
raise ValueError('missing')
else:
page_data['amt_bid_min'] = get_data_entry(form_data, 'amt_bid_min')
parsed_data['amt_bid_min'] = inputAmount(page_data['amt_bid_min'], ci_from)
if parsed_data['amt_bid_min'] < 0 or parsed_data['amt_bid_min'] > parsed_data['amt_from']:
errors.append('Minimum Bid Amount out of range')
except Exception:
errors.append('Minimum Bid Amount')
try:
page_data['amt_to'] = get_data_entry(form_data, 'amt_to')
parsed_data['amt_to'] = inputAmount(page_data['amt_to'], ci_to)
except Exception:
errors.append('Amount To')
if 'amt_to' in parsed_data and 'amt_from' in parsed_data:
parsed_data['rate'] = ci_from.make_int(parsed_data['amt_to'] / parsed_data['amt_from'], r=1)
page_data['rate'] = ci_to.format_amount(parsed_data['rate'])
page_data['amt_var'] = True if have_data_entry(form_data, 'amt_var') else False
parsed_data['amt_var'] = page_data['amt_var']
page_data['rate_var'] = True if have_data_entry(form_data, 'rate_var') else False
parsed_data['rate_var'] = page_data['rate_var']
if have_data_entry(form_data, 'step1'):
if len(errors) == 0 and have_data_entry(form_data, 'continue'):
page_data['step2'] = True
return parsed_data, errors
page_data['step2'] = True
if have_data_entry(form_data, 'fee_from_conf'):
page_data['fee_from_conf'] = int(get_data_entry(form_data, 'fee_from_conf'))
parsed_data['fee_from_conf'] = page_data['fee_from_conf']
if have_data_entry(form_data, 'fee_from_extra'):
page_data['fee_from_extra'] = int(get_data_entry(form_data, 'fee_from_extra'))
parsed_data['fee_from_extra'] = page_data['fee_from_extra']
if have_data_entry(form_data, 'fee_to_conf'):
page_data['fee_to_conf'] = int(get_data_entry(form_data, 'fee_to_conf'))
parsed_data['fee_to_conf'] = page_data['fee_to_conf']
if have_data_entry(form_data, 'fee_to_extra'):
page_data['fee_to_extra'] = int(get_data_entry(form_data, 'fee_to_extra'))
parsed_data['fee_to_extra'] = page_data['fee_to_extra']
if have_data_entry(form_data, 'check_offer'):
page_data['check_offer'] = True
if have_data_entry(form_data, 'submit_offer'):
page_data['submit_offer'] = True
if have_data_entry(form_data, 'lockhrs'):
page_data['lockhrs'] = int(get_data_entry(form_data, 'lockhrs'))
parsed_data['lock_seconds'] = page_data['lockhrs'] * 60 * 60
elif have_data_entry(form_data, 'lockmins'):
page_data['lockmins'] = int(get_data_entry(form_data, 'lockmins'))
parsed_data['lock_seconds'] = page_data['lockmins'] * 60
elif have_data_entry(form_data, 'lockseconds'):
parsed_data['lock_seconds'] = int(get_data_entry(form_data, 'lockseconds'))
if have_data_entry(form_data, 'validhrs'):
page_data['validhrs'] = int(get_data_entry(form_data, 'validhrs'))
parsed_data['valid_for_seconds'] = page_data['validhrs'] * 60 * 60
elif have_data_entry(form_data, 'valid_for_seconds'):
parsed_data['valid_for_seconds'] = int(get_data_entry(form_data, 'valid_for_seconds'))
page_data['automation_strat_id'] = int(get_data_entry_or(form_data, 'automation_strat_id', -1))
parsed_data['automation_strat_id'] = page_data['automation_strat_id']
try:
if len(errors) == 0 and page_data['swap_style'] == 'xmr':
if have_data_entry(form_data, 'fee_rate_from'):
page_data['from_fee_override'] = get_data_entry(form_data, 'fee_rate_from')
parsed_data['from_fee_override'] = page_data['from_fee_override']
else:
from_fee_override, page_data['from_fee_src'] = swap_client.getFeeRateForCoin(parsed_data['coin_from'], page_data['fee_from_conf'])
if page_data['fee_from_extra'] > 0:
from_fee_override += from_fee_override * (float(page_data['fee_from_extra']) / 100.0)
page_data['from_fee_override'] = ci_from.format_amount(ci_from.make_int(from_fee_override, r=1))
parsed_data['from_fee_override'] = page_data['from_fee_override']
lock_spend_tx_vsize = ci_from.xmr_swap_alock_spend_tx_vsize()
lock_spend_tx_fee = ci_from.make_int(ci_from.make_int(from_fee_override, r=1) * lock_spend_tx_vsize / 1000, r=1)
page_data['amt_from_lock_spend_tx_fee'] = ci_from.format_amount(lock_spend_tx_fee // ci_from.COIN())
page_data['tla_from'] = ci_from.ticker()
if coin_to == Coins.XMR:
if have_data_entry(form_data, 'fee_rate_to'):
page_data['to_fee_override'] = get_data_entry(form_data, 'fee_rate_to')
parsed_data['to_fee_override'] = page_data['to_fee_override']
else:
to_fee_override, page_data['to_fee_src'] = swap_client.getFeeRateForCoin(parsed_data['coin_to'], page_data['fee_to_conf'])
if page_data['fee_to_extra'] > 0:
to_fee_override += to_fee_override * (float(page_data['fee_to_extra']) / 100.0)
page_data['to_fee_override'] = ci_to.format_amount(ci_to.make_int(to_fee_override, r=1))
parsed_data['to_fee_override'] = page_data['to_fee_override']
except Exception as e:
print('Error setting fee', str(e)) # Expected if missing fields
return parsed_data, errors
def postNewOfferFromParsed(swap_client, parsed_data):
swap_type = SwapTypes.SELLER_FIRST
if parsed_data['coin_to'] in (Coins.XMR, Coins.PART_ANON):
swap_type = SwapTypes.XMR_SWAP
if swap_client.coin_clients[parsed_data['coin_from']]['use_csv'] and swap_client.coin_clients[parsed_data['coin_to']]['use_csv']:
lock_type = TxLockTypes.SEQUENCE_LOCK_TIME
else:
lock_type = TxLockTypes.ABS_LOCK_TIME
extra_options = {}
if 'fee_from_conf' in parsed_data:
extra_options['from_fee_conf_target'] = parsed_data['fee_from_conf']
if 'from_fee_multiplier_percent' in parsed_data:
extra_options['from_fee_multiplier_percent'] = parsed_data['fee_from_extra']
if 'from_fee_override' in parsed_data:
extra_options['from_fee_override'] = parsed_data['from_fee_override']
if 'fee_to_conf' in parsed_data:
extra_options['to_fee_conf_target'] = parsed_data['fee_to_conf']
if 'to_fee_multiplier_percent' in parsed_data:
extra_options['to_fee_multiplier_percent'] = parsed_data['fee_to_extra']
if 'to_fee_override' in parsed_data:
extra_options['to_fee_override'] = parsed_data['to_fee_override']
if 'valid_for_seconds' in parsed_data:
extra_options['valid_for_seconds'] = parsed_data['valid_for_seconds']
if 'addr_to' in parsed_data:
extra_options['addr_send_to'] = parsed_data['addr_to']
if parsed_data.get('amt_var', False):
extra_options['amount_negotiable'] = parsed_data['amt_var']
if parsed_data.get('rate_var', False):
extra_options['rate_negotiable'] = parsed_data['rate_var']
if parsed_data.get('rate_var', None) is not None:
extra_options['rate_negotiable'] = parsed_data['rate_var']
if parsed_data.get('automation_strat_id', None) is not None:
extra_options['automation_id'] = parsed_data['automation_strat_id']
offer_id = swap_client.postOffer(
parsed_data['coin_from'],
parsed_data['coin_to'],
parsed_data['amt_from'],
parsed_data['rate'],
parsed_data['amt_bid_min'],
swap_type,
lock_type=lock_type,
lock_value=parsed_data['lock_seconds'],
addr_send_from=parsed_data['addr_from'],
extra_options=extra_options)
return offer_id
def postNewOffer(swap_client, form_data):
page_data = {}
parsed_data, errors = parseOfferFormData(swap_client, form_data, page_data, options={'add_min_bid_amt': True})
if len(errors) > 0:
raise ValueError('Parse errors: ' + ' '.join(errors))
return postNewOfferFromParsed(swap_client, parsed_data)
def offer_to_post_string(self, swap_client, offer_id):
offer, xmr_offer = swap_client.getXmrOffer(offer_id)
ensure(offer, 'Unknown offer ID')
ci_from = swap_client.ci(offer.coin_from)
ci_to = swap_client.ci(offer.coin_to)
offer_data = {
'formid': self.generate_form_id(),
'addr_to': offer.addr_to,
'addr_from': offer.addr_from,
'coin_from': offer.coin_from,
'coin_to': offer.coin_to,
# TODO store fee conf, or pass directly
# 'fee_from_conf'
# 'fee_to_conf'
'amt_from': ci_from.format_amount(offer.amount_from),
'amt_bid_min': ci_from.format_amount(offer.min_bid_amount),
'rate': ci_to.format_amount(offer.rate),
'amt_to': ci_to.format_amount((offer.amount_from * offer.rate) // ci_from.COIN()),
'validhrs': offer.time_valid // (60 * 60),
}
if offer.amount_negotiable:
offer_data['amt_var'] = True
if offer.rate_negotiable:
offer_data['rate_var'] = True
if offer.lock_type == TxLockTypes.SEQUENCE_LOCK_TIME or offer.lock_type == TxLockTypes.ABS_LOCK_TIME:
if offer.lock_value > 60 * 60:
offer_data['lockhrs'] = offer.lock_value // (60 * 60)
else:
offer_data['lockhrs'] = offer.lock_value // 60
try:
strategy = swap_client.getLinkedStrategy(Concepts.OFFER, offer.offer_id)
offer_data['automation_strat_id'] = strategy[0]
except Exception:
pass # None found
return parse.urlencode(offer_data).encode()
def page_newoffer(self, url_split, post_string):
server = self.server
swap_client = server.swap_client
summary = swap_client.getSummary()
messages = []
err_messages = []
page_data = {
# Set defaults
'addr_to': -1,
'fee_from_conf': 2,
'fee_to_conf': 2,
'validhrs': 1,
'lockhrs': 32,
'lockmins': 30, # used in debug mode
'debug_ui': swap_client.debug_ui,
'automation_strat_id': -1,
'amt_bid_min': format_amount(1000, 8),
}
post_data = parse.parse_qs(post_string)
if 'offer_from' in post_data:
offer_from_id_hex = post_data['offer_from'][0]
offer_from_id = decode_offer_id(offer_from_id_hex)
post_string = offer_to_post_string(self, swap_client, offer_from_id)
form_data = self.checkForm(post_string, 'newoffer', err_messages)
if form_data:
try:
parsed_data, errors = parseOfferFormData(swap_client, form_data, page_data)
for e in errors:
err_messages.append(str(e))
except Exception as e:
if swap_client.debug is True:
swap_client.log.error(traceback.format_exc())
err_messages.append(str(e))
if len(err_messages) == 0 and 'submit_offer' in page_data:
try:
offer_id = postNewOfferFromParsed(swap_client, parsed_data)
messages.append('<a href="/offer/' + offer_id.hex() + '">Sent Offer {}</a>'.format(offer_id.hex()))
page_data = {}
except Exception as e:
if swap_client.debug is True:
swap_client.log.error(traceback.format_exc())
err_messages.append(str(e))
if len(err_messages) == 0 and 'check_offer' in page_data:
template = server.env.get_template('offer_confirm.html')
elif 'step2' in page_data:
template = server.env.get_template('offer_new_2.html')
else:
template = server.env.get_template('offer_new_1.html')
if swap_client.debug_ui:
messages.append('Debug mode active.')
coins_from, coins_to = listAvailableCoins(swap_client, split_from=True)
automation_filters = {}
automation_filters['sort_by'] = 'label'
automation_filters['type_ind'] = Concepts.OFFER
automation_strategies = swap_client.listAutomationStrategies(automation_filters)
return self.render_template(template, {
'messages': messages,
'err_messages': err_messages,
'coins_from': coins_from,
'coins': coins_to,
'addrs': swap_client.listSmsgAddresses('offer_send_from'),
'addrs_to': swap_client.listSmsgAddresses('offer_send_to'),
'data': page_data,
'automation_strategies': automation_strategies,
'summary': summary,
})
def page_offer(self, url_split, post_string):
ensure(len(url_split) > 2, 'Offer ID not specified')
offer_id = decode_offer_id(url_split[2])
server = self.server
swap_client = server.swap_client
summary = swap_client.getSummary()
offer, xmr_offer = swap_client.getXmrOffer(offer_id)
ensure(offer, 'Unknown offer ID')
extend_data = { # Defaults
'nb_validmins': 10,
}
messages = []
err_messages = []
if swap_client.debug_ui:
messages.append('Debug mode active.')
sent_bid_id = None
show_bid_form = None
form_data = self.checkForm(post_string, 'offer', err_messages)
ci_from = swap_client.ci(Coins(offer.coin_from))
ci_to = swap_client.ci(Coins(offer.coin_to))
debugind = -1
# Set defaults
bid_amount = ci_from.format_amount(offer.amount_from)
bid_rate = ci_to.format_amount(offer.rate)
if form_data:
if b'revoke_offer' in form_data:
try:
swap_client.revokeOffer(offer_id)
messages.append('Offer revoked')
except Exception as ex:
err_messages.append('Revoke offer failed: ' + str(ex))
elif b'repeat_offer' in form_data:
# Can't set the post data here as browsers will always resend the original post data when responding to redirects
self.send_response(302)
self.send_header('Location', '/newoffer?offer_from=' + offer_id.hex())
self.end_headers()
return bytes()
elif b'newbid' in form_data:
show_bid_form = True
elif b'sendbid' in form_data:
try:
addr_from = form_data[b'addr_from'][0].decode('utf-8')
extend_data['nb_addr_from'] = addr_from
if addr_from == '-1':
addr_from = None
minutes_valid = int(form_data[b'validmins'][0].decode('utf-8'))
extend_data['nb_validmins'] = minutes_valid
extra_options = {
'valid_for_seconds': minutes_valid * 60,
}
if have_data_entry(form_data, 'bid_rate'):
bid_rate = get_data_entry(form_data, 'bid_rate')
extra_options['bid_rate'] = ci_to.make_int(bid_rate, r=1)
if have_data_entry(form_data, 'bid_amount'):
bid_amount = get_data_entry(form_data, 'bid_amount')
amount_from = inputAmount(bid_amount, ci_from)
else:
amount_from = offer.amount_from
debugind = int(get_data_entry_or(form_data, 'debugind', -1))
sent_bid_id = swap_client.postBid(offer_id, amount_from, addr_send_from=addr_from, extra_options=extra_options).hex()
if debugind > -1:
swap_client.setBidDebugInd(bytes.fromhex(sent_bid_id), debugind)
except Exception as ex:
if self.server.swap_client.debug is True:
self.server.swap_client.log.error(traceback.format_exc())
err_messages.append('Send bid failed: ' + str(ex))
show_bid_form = True
data = {
'tla_from': ci_from.ticker(),
'tla_to': ci_to.ticker(),
'state': strOfferState(offer.state),
'coin_from': ci_from.coin_name(),
'coin_to': ci_to.coin_name(),
'coin_from_ind': int(ci_from.coin_type()),
'coin_to_ind': int(ci_to.coin_type()),
'amt_from': ci_from.format_amount(offer.amount_from),
'amt_to': ci_to.format_amount((offer.amount_from * offer.rate) // ci_from.COIN()),
'amt_bid_min': ci_from.format_amount(offer.min_bid_amount),
'rate': ci_to.format_amount(offer.rate),
'lock_type': getLockName(offer.lock_type),
'lock_value': offer.lock_value,
'addr_from': offer.addr_from,
'addr_to': 'Public' if offer.addr_to == swap_client.network_addr else offer.addr_to,
'created_at': offer.created_at,
'expired_at': offer.expire_at,
'sent': 'True' if offer.was_sent else 'False',
'was_revoked': 'True' if offer.active_ind == 2 else 'False',
'show_bid_form': show_bid_form,
'amount_negotiable': offer.amount_negotiable,
'rate_negotiable': offer.rate_negotiable,
'bid_amount': bid_amount,
'bid_rate': bid_rate,
'debug_ui': swap_client.debug_ui,
'automation_strat_id': -1,
}
data.update(extend_data)
if offer.lock_type == TxLockTypes.SEQUENCE_LOCK_TIME or offer.lock_type == TxLockTypes.ABS_LOCK_TIME:
if offer.lock_value > 60 * 60:
data['lock_value_hr'] = ' ({} hours)'.format(offer.lock_value / (60 * 60))
else:
data['lock_value_hr'] = ' ({} minutes)'.format(offer.lock_value / 60)
addr_from_label, addr_to_label = swap_client.getAddressLabel([offer.addr_from, offer.addr_to])
if len(addr_from_label) > 0:
data['addr_from_label'] = '(' + addr_from_label + ')'
if len(addr_to_label) > 0:
data['addr_to_label'] = '(' + addr_to_label + ')'
if swap_client.debug_ui:
data['debug_ind'] = debugind
data['debug_options'] = [(int(t), t.name) for t in DebugTypes]
if xmr_offer:
int_fee_rate_now, fee_source = ci_from.get_fee_rate()
data['xmr_type'] = True
data['a_fee_rate'] = ci_from.format_amount(xmr_offer.a_fee_rate)
data['a_fee_rate_verify'] = ci_from.format_amount(int_fee_rate_now, conv_int=True)
data['a_fee_rate_verify_src'] = fee_source
data['a_fee_warn'] = xmr_offer.a_fee_rate < int_fee_rate_now
lock_spend_tx_vsize = ci_from.xmr_swap_alock_spend_tx_vsize()
lock_spend_tx_fee = ci_from.make_int(xmr_offer.a_fee_rate * lock_spend_tx_vsize / 1000, r=1)
data['amt_from_lock_spend_tx_fee'] = ci_from.format_amount(lock_spend_tx_fee // ci_from.COIN())
if offer.was_sent:
try:
strategy = swap_client.getLinkedStrategy(Concepts.OFFER, offer_id)
data['automation_strat_id'] = strategy[0]
data['automation_strat_label'] = strategy[1]
except Exception:
pass # None found
bids = swap_client.listBids(offer_id=offer_id)
formatted_bids = []
amt_swapped = 0
for b in bids:
amt_swapped += b[4]
formatted_bids.append((b[2].hex(), ci_from.format_amount(b[4]), strBidState(b[5]), ci_to.format_amount(b[10]), b[11]))
data['amt_swapped'] = ci_from.format_amount(amt_swapped)
template = server.env.get_template('offer.html')
return self.render_template(template, {
'offer_id': offer_id.hex(),
'sent_bid_id': sent_bid_id,
'messages': messages,
'err_messages': err_messages,
'data': data,
'bids': formatted_bids,
'addrs': None if show_bid_form is None else swap_client.listSmsgAddresses('bid'),
'summary': summary,
})
def page_offers(self, url_split, post_string, sent=False):
server = self.server
swap_client = server.swap_client
summary = swap_client.getSummary()
filters = {
'coin_from': -1,
'coin_to': -1,
'page_no': 1,
'limit': PAGE_LIMIT,
'sort_by': 'created_at',
'sort_dir': 'desc',
'sent_from': 'any' if sent is False else 'only',
}
messages = []
form_data = self.checkForm(post_string, 'offers', messages)
if form_data and have_data_entry(form_data, 'applyfilters'):
filters['coin_from'] = setCoinFilter(form_data, 'coin_from')
filters['coin_to'] = setCoinFilter(form_data, 'coin_to')
if have_data_entry(form_data, 'sort_by'):
sort_by = get_data_entry(form_data, 'sort_by')
ensure(sort_by in ['created_at', 'rate'], 'Invalid sort by')
filters['sort_by'] = sort_by
if have_data_entry(form_data, 'sort_dir'):
sort_dir = get_data_entry(form_data, 'sort_dir')
ensure(sort_dir in ['asc', 'desc'], 'Invalid sort dir')
filters['sort_dir'] = sort_dir
if have_data_entry(form_data, 'sent_from'):
sent_from = get_data_entry(form_data, 'sent_from')
ensure(sent_from in ['any', 'only'], 'Invalid sent filter')
filters['sent_from'] = sent_from
set_pagination_filters(form_data, filters)
if filters['sent_from'] == 'only':
sent = True
else:
sent = False
offers = swap_client.listOffers(sent, filters, with_bid_info=True)
formatted_offers = []
for row in offers:
o, completed_amount = row
ci_from = swap_client.ci(Coins(o.coin_from))
ci_to = swap_client.ci(Coins(o.coin_to))
formatted_offers.append((
format_timestamp(o.created_at),
o.offer_id.hex(),
ci_from.coin_name(), ci_to.coin_name(),
ci_from.format_amount(o.amount_from),
ci_to.format_amount((o.amount_from * o.rate) // ci_from.COIN()),
ci_to.format_amount(o.rate),
'Public' if o.addr_to == swap_client.network_addr else o.addr_to,
o.addr_from,
o.was_sent,
ci_from.format_amount(completed_amount)))
coins_from, coins_to = listAvailableCoins(swap_client, split_from=True)
template = server.env.get_template('offers.html')
return self.render_template(template, {
'page_type': 'Your Offers' if sent else 'Network Order Book',
'page_type_description': '' if sent else '',
'messages': messages,
'chart': 'hidden' if sent else '',
'coins_from': coins_from,
'coins': coins_to,
'messages': messages,
'filters': filters,
'offers': formatted_offers,
'summary': summary,
})