ui: Allow selecting automation strategy when creating offer.

Add alternate pgp key urls.
This commit is contained in:
tecnovert 2022-06-01 00:38:50 +02:00
parent 08c10bc69e
commit 6153b76ec0
No known key found for this signature in database
GPG key ID: 8ED6D8750C4E3F93
10 changed files with 525 additions and 445 deletions

View file

@ -1083,13 +1083,16 @@ class BasicSwap(BaseApp):
xmr_offer.offer_id = offer_id
session.add(xmr_offer)
if auto_accept_bids:
automation_id = extra_options.get('automation_id', -1)
if automation_id == -1 and auto_accept_bids:
# Use default strategy
automation_id = 1
if automation_id != -1:
auto_link = AutomationLink(
active_ind=1,
linked_type=TableTypes.OFFER,
linked_id=offer_id,
strategy_id=1,
strategy_id=automation_id,
created_at=offer_created_at,
repeat_limit=1,
repeat_count=0)
@ -5470,6 +5473,10 @@ class BasicSwap(BaseApp):
query_str = 'SELECT strats.record_id, strats.label, strats.type_ind FROM automationstrategies AS strats'
query_str += ' WHERE strats.active_ind = 1 '
type_ind = filters.get('type_ind', None)
if type_ind is not None:
query_str += f' AND strats.type_ind = {type_ind} '
sort_dir = filters.get('sort_dir', 'DESC').upper()
sort_by = filters.get('sort_by', 'created_at')
query_str += f' ORDER BY strats.{sort_by} {sort_dir}'
@ -5501,6 +5508,21 @@ class BasicSwap(BaseApp):
session.remove()
self.mxDB.release()
def getLinkedStrategy(self, linked_type, linked_id):
self.mxDB.acquire()
try:
rv = []
session = scoped_session(self.session_factory)
query_str = 'SELECT links.strategy_id, strats.label FROM automationlinks links' + \
' LEFT JOIN automationstrategies strats ON strats.record_id = links.strategy_id' + \
' WHERE links.linked_type = {} AND links.linked_id = x\'{}\' AND links.active_ind = 1'.format(int(linked_type), linked_id.hex())
q = session.execute(query_str).first()
return q
finally:
session.close()
session.remove()
self.mxDB.release()
def newSMSGAddress(self, use_type=AddressTypes.RECV_OFFER, addressnote=None, session=None):
now = int(time.time())
use_session = None

View file

@ -27,14 +27,14 @@ def upgradeDatabaseData(self, data_version):
session.add(AutomationStrategy(
active_ind=1,
label='Accept All',
type_ind=TableTypes.BID,
type_ind=TableTypes.OFFER,
data=json.dumps({'full_amount_only': True,
'max_bids': 1}).encode('utf-8'),
only_known_identities=False))
session.add(AutomationStrategy(
active_ind=1,
label='Accept Known',
type_ind=TableTypes.BID,
type_ind=TableTypes.OFFER,
data=json.dumps({'full_amount_only': True,
'max_bids': 1}).encode('utf-8'),
only_known_identities=True,

View file

@ -28,12 +28,9 @@ from .chainparams import (
from .basicswap_util import (
SwapTypes,
DebugTypes,
strOfferState,
strBidState,
strTxState,
strAddressType,
getLockName,
TxLockTypes,
)
from .js_server import (
js_error,
@ -51,10 +48,8 @@ from .js_server import (
)
from .ui.util import (
PAGE_LIMIT,
inputAmount,
describeBid,
getCoinName,
getCoinType,
get_data_entry,
have_data_entry,
get_data_entry_or,
@ -62,11 +57,11 @@ from .ui.util import (
set_pagination_filters,
)
from .ui.page_tor import page_tor
from .ui.page_offers import page_offers
from .ui.page_offers import page_offers, page_offer, page_newoffer
from .ui.page_automation import (
page_automation_strategies,
page_automation_strategy,
page_automation_strategy_new
page_automation_strategy_new,
)
@ -74,12 +69,6 @@ env = Environment(loader=PackageLoader('basicswap', 'templates'))
env.filters['formatts'] = format_timestamp
def value_or_none(v):
if v == -1 or v == '-1':
return None
return v
def validateTextInput(text, name, messages, max_length=None):
if max_length is not None and len(text) > max_length:
messages.append(f'Error: {name} is too long')
@ -636,419 +625,6 @@ class HttpHandler(BaseHTTPRequestHandler):
form_id=os.urandom(8).hex(),
), 'UTF-8')
def parseOfferFormData(self, form_data, page_data):
swap_client = self.server.swap_client
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)
# TODO: Add min_bid to the ui
parsed_data['min_bid'] = ci_from.chainparams_network()['min_amount']
except Exception:
errors.append('Amount From')
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']
# Change default autoaccept to false
if page_data['amt_var'] or page_data['rate_var']:
page_data['autoaccept'] = False
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['autoaccept'] = True if have_data_entry(form_data, 'autoaccept') else False
parsed_data['autoaccept'] = page_data['autoaccept']
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(self, parsed_data):
swap_client = self.server.swap_client
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']
offer_id = swap_client.postOffer(
parsed_data['coin_from'],
parsed_data['coin_to'],
parsed_data['amt_from'],
parsed_data['rate'],
parsed_data['min_bid'],
swap_type,
lock_type=lock_type,
lock_value=parsed_data['lock_seconds'],
auto_accept_bids=parsed_data['autoaccept'],
addr_send_from=parsed_data['addr_from'],
extra_options=extra_options)
return offer_id
def postNewOffer(self, form_data):
page_data = {}
parsed_data, errors = self.parseOfferFormData(form_data, page_data)
if len(errors) > 0:
raise ValueError('Parse errors: ' + ' '.join(errors))
return self.postNewOfferFromParsed(parsed_data)
def page_newoffer(self, url_split, post_string):
swap_client = self.server.swap_client
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
'autoaccept': True,
'debug_ui': swap_client.debug_ui,
}
form_data = self.checkForm(post_string, 'newoffer', messages)
if form_data:
try:
parsed_data, errors = self.parseOfferFormData(form_data, page_data)
for e in errors:
messages.append('Error: {}'.format(str(e)))
except Exception as e:
if swap_client.debug is True:
swap_client.log.error(traceback.format_exc())
messages.append('Error: {}'.format(str(e)))
if len(messages) == 0 and 'submit_offer' in page_data:
try:
offer_id = self.postNewOfferFromParsed(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())
messages.append('Error: {}'.format(str(e)))
if len(messages) == 0 and 'check_offer' in page_data:
template = env.get_template('offer_confirm.html')
elif 'step2' in page_data:
template = env.get_template('offer_new_2.html')
else:
template = 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)
return bytes(template.render(
title=self.server.title,
h2=self.server.title,
messages=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,
form_id=os.urandom(8).hex(),
), 'UTF-8')
def page_offer(self, url_split, post_string):
ensure(len(url_split) > 2, 'Offer ID not specified')
try:
offer_id = bytes.fromhex(url_split[2])
assert(len(offer_id) == 28)
except Exception:
raise ValueError('Bad offer ID')
swap_client = self.server.swap_client
offer, xmr_offer = swap_client.getXmrOffer(offer_id)
ensure(offer, 'Unknown offer ID')
extend_data = { # Defaults
'nb_validmins': 10,
}
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', 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:
messages.append('Revoke offer failed: ' + str(ex))
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())
messages.append('Error: 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()),
'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,
}
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:
data['auto_accept'] = 'True' if offer.auto_accept_bids else 'False'
bids = swap_client.listBids(offer_id=offer_id)
template = env.get_template('offer.html')
return bytes(template.render(
title=self.server.title,
h2=self.server.title,
offer_id=offer_id.hex(),
sent_bid_id=sent_bid_id,
messages=messages,
data=data,
bids=[(b[2].hex(), ci_from.format_amount(b[4]), strBidState(b[5]), ci_to.format_amount(b[10]), b[11]) for b in bids],
addrs=None if show_bid_form is None else swap_client.listSmsgAddresses('bid'),
form_id=os.urandom(8).hex(),
), 'UTF-8')
def page_bid(self, url_split, post_string):
ensure(len(url_split) > 2, 'Bid ID not specified')
try:
@ -1414,11 +990,11 @@ class HttpHandler(BaseHTTPRequestHandler):
if url_split[1] == 'explorers':
return self.page_explorers(url_split, post_string)
if url_split[1] == 'offer':
return self.page_offer(url_split, post_string)
return page_offer(self, url_split, post_string)
if url_split[1] == 'offers':
return page_offers(self, url_split, post_string)
if url_split[1] == 'newoffer':
return self.page_newoffer(url_split, post_string)
return page_newoffer(self, url_split, post_string)
if url_split[1] == 'sentoffers':
return page_offers(self, url_split, post_string, sent=True)
if url_split[1] == 'bid':

View file

@ -28,6 +28,7 @@ from .ui.util import (
have_data_entry,
tickerToCoinId,
)
from .ui.page_offers import postNewOffer
from .protocols.xmr_swap_1 import recoverNoScriptTxnWithKey, getChainBSplitKey
@ -74,6 +75,7 @@ def js_wallets(self, url_split, post_string, is_json):
def js_offers(self, url_split, post_string, is_json, sent=False):
swap_client = self.server.swap_client
offer_id = None
if len(url_split) > 3:
if url_split[3] == 'new':
@ -84,7 +86,7 @@ def js_offers(self, url_split, post_string, is_json, sent=False):
form_data['is_json'] = True
else:
form_data = urllib.parse.parse_qs(post_string)
offer_id = self.postNewOffer(form_data)
offer_id = postNewOffer(swap_client, form_data)
rv = {'offer_id': offer_id.hex()}
return bytes(json.dumps(rv), 'UTF-8')
offer_id = bytes.fromhex(url_split[3])

View file

@ -35,7 +35,14 @@
<tr><td>Sent</td><td>{{ data.sent }}</td></tr>
<tr><td>Revoked</td><td>{{ data.was_revoked }}</td></tr>
{% if data.sent == 'True' %}
<tr><td>Auto Accept Bids</td><td>{{ data.auto_accept }}</td></tr>
<tr><td>Auto Accept Strategy</td>
<td>
{% if data.automation_strat_id == -1 %}
None
{% else %}
<a class="monospace" href="/automationstrategy/{{ data.automation_strat_id }}">{{ data.automation_strat_label }}</a>
{% endif %}
</td></tr>
{% endif %}
{% if data.xmr_type == true %}

View file

@ -59,7 +59,7 @@
<option value="100"{% if data.fee_to_extra==100 %} selected{% endif %}>100%</option>
</select></td></tr>
{% endif %}
</td><td>Rate</td><td><input type="text" id="rate" name="rate" value="{{ data.rate }}" readonly></td></tr>
<tr><td>Rate</td><td><input type="text" id="rate" name="rate" value="{{ data.rate }}" readonly></td></tr>
<tr><td>Amount Variable</td><td colspan=3><input type="checkbox" id="amt_var" name="amt_var_" value="av" {% if data.amt_var==true %} checked="true"{% endif %} disabled></td></tr>
<tr><td>Rate Variable</td><td colspan=3><input type="checkbox" id="rate_var" name="rate_var_" value="rv" {% if data.rate_var==true %} checked="true"{% endif %} disabled></td></tr>
@ -69,7 +69,13 @@
{% else %}
<tr><td>Contract locked (hrs)</td><td><input type="number" name="lockhrs" min="1" max="64" value="{{ data.lockhrs }}" readonly></td>{% if data.swap_style != 'xmr' %}<td colspan=2>Participate txn will be locked for half the time.</td>{% endif %}</tr>
{% endif %}
<tr><td>Auto Accept Bids</td><td colspan=3><input type="checkbox" id="autoaccept" name="autoaccept_" value="aa" {% if data.autoaccept==true %} checked="true"{% endif %} disabled></td></tr>
<tr><td>Auto Accept Strategy</td><td colspan=3>
<select name="automation_strat_id_" disabled><option value="-1"{% if data.automation_strat_id==-1 %} selected{% endif %}>-- None --</option>
{% for a in automation_strategies %}
<option value="{{ a[0] }}"{% if data.automation_strat_id==a[0] %} selected{% endif %}>{{ a[1] }}</option>
{% endfor %}
</select>
</td></tr>
</table>
<input name="submit_offer" type="submit" value="Confirm Offer">
@ -81,8 +87,8 @@
<input type="hidden" name="fee_from_extra" value="{{ data.fee_from_extra }}">
<input type="hidden" name="coin_to" value="{{ data.coin_to }}">
<input type="hidden" name="fee_to_extra" value="{{ data.fee_to_extra }}">
{% if data.autoaccept==true %}
<input type="hidden" name="autoaccept" value="aa">
{% if data.automation_strat_id != -1 %}
<input type="hidden" name="automation_strat_id" value="{{ data.automation_strat_id }}">
{% endif %}
{% if data.amt_var==true %}
<input type="hidden" name="amt_var" value="av">

View file

@ -36,8 +36,8 @@
{% endfor %}
</select>
</td><td>Amount To</td><td><input type="text" id="amt_to" name="amt_to" value="{{ data.amt_to }}" onchange="set_rate('amt_to');"></td><td>The amount you will receive.</td></tr>
</td><td>Rate</td><td><input type="text" id="rate" name="rate" value="{{ data.rate }}" onchange="set_rate('rate');"></td><td>Lock Rate: <input type="checkbox" id="rate_lock" name="rate_lock" value="rl" checked="true"></td></tr>
<tr><td>Rate</td><td><input type="text" id="rate" name="rate" value="{{ data.rate }}" onchange="set_rate('rate');"></td><td>Lock Rate: <input type="checkbox" id="rate_lock" name="rate_lock" value="rl" checked="true"></td></tr>
<tr><td>Amount Variable</td><td><input type="checkbox" id="amt_var" name="amt_var" value="av" {% if data.amt_var==true %} checked="true"{% endif %}></td></tr>
<tr><td>Rate Variable</td><td><input type="checkbox" id="rate_var" name="rate_var" value="rv" {% if data.rate_var==true %} checked="true"{% endif %}></td></tr>
</table>

View file

@ -56,7 +56,7 @@
<option value="100"{% if data.fee_to_extra==100 %} selected{% endif %}>100%</option>
</select></td></tr>
{% endif %}
</td><td>Rate</td><td><input type="text" id="rate" name="rate" value="{{ data.rate }}" readonly></td></tr>
<tr><td>Rate</td><td><input type="text" id="rate" name="rate" value="{{ data.rate }}" readonly></td></tr>
<tr><td>Amount Variable</td><td colspan=3><input type="checkbox" id="amt_var" name="amt_var_" value="av" {% if data.amt_var==true %} checked="true"{% endif %} disabled></td></tr>
<tr><td>Rate Variable</td><td colspan=3><input type="checkbox" id="rate_var" name="rate_var_" value="rv" {% if data.rate_var==true %} checked="true"{% endif %} disabled></td></tr>
@ -67,7 +67,13 @@
{% else %}
<tr><td>Contract locked (hrs)</td><td><input type="number" name="lockhrs" min="1" max="96" value="{{ data.lockhrs }}"></td>{% if data.swap_style != 'xmr' %}<td colspan=2>Participate txn will be locked for half the time.</td>{% endif %}</tr>
{% endif %}
<tr><td>Auto Accept Bids</td><td colspan=3><input type="checkbox" id="autoaccept" name="autoaccept" value="aa" {% if data.autoaccept==true %} checked="true"{% endif %}></td></tr>
<tr><td>Auto Accept Strategy</td><td colspan=3>
<select name="automation_strat_id"><option value="-1"{% if data.automation_strat_id==-1 %} selected{% endif %}>-- None --</option>
{% for a in automation_strategies %}
<option value="{{ a[0] }}"{% if data.automation_strat_id==a[0] %} selected{% endif %}>{{ a[1] }}</option>
{% endfor %}
</select>
</td></tr>
</table>

View file

@ -5,24 +5,477 @@
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import os
import traceback
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 (
TableTypes,
)
from basicswap.util import (
ensure,
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 parseOfferFormData(swap_client, form_data, page_data):
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)
# TODO: Add min_bid to the ui
parsed_data['min_bid'] = ci_from.chainparams_network()['min_amount']
except Exception:
errors.append('Amount From')
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['min_bid'],
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)
if len(errors) > 0:
raise ValueError('Parse errors: ' + ' '.join(errors))
return postNewOfferFromParsed(swap_client, parsed_data)
def page_newoffer(self, url_split, post_string):
server = self.server
swap_client = server.swap_client
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,
}
form_data = self.checkForm(post_string, 'newoffer', messages)
if form_data:
try:
parsed_data, errors = parseOfferFormData(swap_client, form_data, page_data)
for e in errors:
messages.append('Error: {}'.format(str(e)))
except Exception as e:
if swap_client.debug is True:
swap_client.log.error(traceback.format_exc())
messages.append('Error: {}'.format(str(e)))
if len(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())
messages.append('Error: {}'.format(str(e)))
if len(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'] = TableTypes.OFFER
automation_strategies = swap_client.listAutomationStrategies(automation_filters)
return bytes(template.render(
title=server.title,
h2=server.title,
messages=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,
form_id=os.urandom(8).hex(),
), 'UTF-8')
def page_offer(self, url_split, post_string):
ensure(len(url_split) > 2, 'Offer ID not specified')
try:
offer_id = bytes.fromhex(url_split[2])
ensure(len(offer_id) == 28, 'Bad offer ID')
except Exception:
raise ValueError('Bad offer ID')
server = self.server
swap_client = server.swap_client
offer, xmr_offer = swap_client.getXmrOffer(offer_id)
ensure(offer, 'Unknown offer ID')
extend_data = { # Defaults
'nb_validmins': 10,
}
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', 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:
messages.append('Revoke offer failed: ' + str(ex))
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())
messages.append('Error: 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()),
'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(TableTypes.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)
template = server.env.get_template('offer.html')
return bytes(template.render(
title=server.title,
h2=server.title,
offer_id=offer_id.hex(),
sent_bid_id=sent_bid_id,
messages=messages,
data=data,
bids=[(b[2].hex(), ci_from.format_amount(b[4]), strBidState(b[5]), ci_to.format_amount(b[10]), b[11]) for b in bids],
addrs=None if show_bid_form is None else swap_client.listSmsgAddresses('bid'),
form_id=os.urandom(8).hex(),
), 'UTF-8')
def page_offers(self, url_split, post_string, sent=False):
server = self.server
swap_client = server.swap_client

View file

@ -367,10 +367,18 @@ def prepareCore(coin, version_data, settings, data_dir):
if verified.username is None:
logger.warning('Signature not verified.')
pubkeyurl = 'https://raw.githubusercontent.com/tecnovert/basicswap/master/gitianpubkeys/{}_{}.pgp'.format(coin, signing_key_name)
logger.info('Importing public key from url: ' + pubkeyurl)
rv = gpg.import_keys(downloadBytes(pubkeyurl))
filename = '{}_{}.pgp'.format(coin, signing_key_name)
pubkeyurls = (
'https://raw.githubusercontent.com/tecnovert/basicswap/master/gitianpubkeys/' + filename,
'https://gitlab.com/particl/basicswap/-/raw/master/gitianpubkeys/' + filename,
)
for url in pubkeyurls:
try:
logger.info('Importing public key from url: ' + url)
rv = gpg.import_keys(downloadBytes(url))
break
except Exception as e:
print('Import from url failed', e)
for key in rv.fingerprints:
gpg.trust_keys(key, 'TRUST_FULLY')