diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index b17d7ea..55de02d 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -52,6 +52,7 @@ from .util import ( format_amount, format_timestamp, DeserialiseNum, + zeroIfNone, make_int, ensure, ) @@ -143,6 +144,8 @@ from .basicswap_util import ( getLastBidState, isActiveBidState, NotificationTypes as NT, + AutomationOverrideOptions, + VisibilityOverrideOptions, ) @@ -157,12 +160,6 @@ def validOfferStateToReceiveBid(offer_state): return False -def zeroIfNone(value): - if value is None: - return 0 - return value - - def threadPollXMRChainState(swap_client, coin_type): ci = swap_client.ci(coin_type) cc = swap_client.coin_clients[coin_type] @@ -1189,17 +1186,57 @@ class BasicSwap(BaseApp): try: now = int(time.time()) session = self.openSession() - q = session.execute(f'SELECT COUNT(*) FROM knownidentities WHERE address = "{address}"').first() + q = session.execute('SELECT COUNT(*) FROM knownidentities WHERE address = :address', {'address': address}).first() if q[0] < 1: - q = session.execute(f'INSERT INTO knownidentities (active_ind, address, created_at) VALUES (1, "{address}", {now})') + session.execute('INSERT INTO knownidentities (active_ind, address, created_at) VALUES (1, :address, :now)', {'address': address, 'now': now}) - values = [] - pattern = '' if 'label' in data: - pattern += (', ' if pattern != '' else '') - pattern += 'label = "{}"'.format(data['label']) - values.append(address) - q = session.execute(f'UPDATE knownidentities SET {pattern} WHERE address = "{address}"') + session.execute('UPDATE knownidentities SET label = :label WHERE address = :address', {'address': address, 'label': data['label']}) + + if 'automation_override' in data: + new_value: int = 0 + data_value = data['automation_override'] + if isinstance(data_value, int): + new_value = data_value + elif isinstance(data_value, str): + if data_value.isdigit(): + new_value = int(data_value) + elif data_value == 'default': + new_value = 0 + elif data_value == 'always_accept': + new_value = int(AutomationOverrideOptions.ALWAYS_ACCEPT) + elif data_value == 'never_accept': + new_value = int(AutomationOverrideOptions.NEVER_ACCEPT) + else: + raise ValueError('Unknown automation_override value') + else: + raise ValueError('Unknown automation_override type') + + session.execute('UPDATE knownidentities SET automation_override = :new_value WHERE address = :address', {'address': address, 'new_value': new_value}) + + if 'visibility_override' in data: + new_value: int = 0 + data_value = data['visibility_override'] + if isinstance(data_value, int): + new_value = data_value + elif isinstance(data_value, str): + if data_value.isdigit(): + new_value = int(data_value) + elif data_value == 'default': + new_value = 0 + elif data_value == 'hide': + new_value = int(VisibilityOverrideOptions.HIDE) + elif data_value == 'block': + new_value = int(VisibilityOverrideOptions.BLOCK) + else: + raise ValueError('Unknown visibility_override value') + else: + raise ValueError('Unknown visibility_override type') + + session.execute('UPDATE knownidentities SET visibility_override = :new_value WHERE address = :address', {'address': address, 'new_value': new_value}) + + if 'note' in data: + session.execute('UPDATE knownidentities SET note = :note WHERE address = :address', {'address': address, 'note': data['note']}) finally: self.closeSession(session) @@ -1209,7 +1246,8 @@ class BasicSwap(BaseApp): session = self.openSession() query_str = 'SELECT address, label, num_sent_bids_successful, num_recv_bids_successful, ' + \ - ' num_sent_bids_rejected, num_recv_bids_rejected, num_sent_bids_failed, num_recv_bids_failed ' + \ + ' num_sent_bids_rejected, num_recv_bids_rejected, num_sent_bids_failed, num_recv_bids_failed, ' + \ + ' automation_override, visibility_override, note ' + \ ' FROM knownidentities ' + \ ' WHERE active_ind = 1 ' @@ -1240,6 +1278,9 @@ class BasicSwap(BaseApp): 'num_recv_bids_rejected': zeroIfNone(row[5]), 'num_sent_bids_failed': zeroIfNone(row[6]), 'num_recv_bids_failed': zeroIfNone(row[7]), + 'automation_override': zeroIfNone(row[8]), + 'visibility_override': zeroIfNone(row[9]), + 'note': row[10], } rv.append(identity) return rv @@ -2186,22 +2227,6 @@ class BasicSwap(BaseApp): session.remove() self.mxDB.release() - def updateIdentity(self, address, label): - self.mxDB.acquire() - try: - session = scoped_session(self.session_factory) - identity = session.query(KnownIdentity).filter_by(address=address).first() - if identity is None: - identity = KnownIdentity(active_ind=1, address=address) - identity.label = label - identity.updated_at = int(time.time()) - session.add(identity) - session.commit() - finally: - session.close() - session.remove() - self.mxDB.release() - def list_bid_events(self, bid_id, session): query_str = 'SELECT created_at, event_type, event_msg FROM eventlog ' + \ 'WHERE active_ind = 1 AND linked_type = {} AND linked_id = x\'{}\' '.format(Concepts.BID, bid_id.hex()) @@ -4158,6 +4183,24 @@ class BasicSwap(BaseApp): total_value += amount return bids, total_value + def evaluateKnownIdentityForAutoAccept(self, strategy, identity_stats) -> bool: + if identity_stats: + if identity_stats.automation_override == AutomationOverrideOptions.NEVER_ACCEPT: + raise AutomationConstraint('From address is marked never accept') + if identity_stats.automation_override == AutomationOverrideOptions.ALWAYS_ACCEPT: + return True + + if strategy.only_known_identities: + if not identity_stats: + raise AutomationConstraint('Unknown bidder') + + # TODO: More options + if identity_stats.num_recv_bids_successful < 1: + raise AutomationConstraint('Bidder has too few successful swaps') + if identity_stats.num_recv_bids_successful <= identity_stats.num_recv_bids_failed: + raise AutomationConstraint('Bidder has too many failed swaps') + return True + def shouldAutoAcceptBid(self, offer, bid, session=None): try: use_session = self.openSession(session) @@ -4195,16 +4238,8 @@ class BasicSwap(BaseApp): if num_not_completed >= max_concurrent_bids: raise AutomationConstraint('Already have {} bids to complete'.format(num_not_completed)) - if strategy.only_known_identities: - identity_stats = use_session.query(KnownIdentity).filter_by(address=bid.bid_addr).first() - if not identity_stats: - raise AutomationConstraint('Unknown bidder') - - # TODO: More options - if identity_stats.num_recv_bids_successful < 1: - raise AutomationConstraint('Bidder has too few successful swaps') - if identity_stats.num_recv_bids_successful <= identity_stats.num_recv_bids_failed: - raise AutomationConstraint('Bidder has too many failed swaps') + identity_stats = use_session.query(KnownIdentity).filter_by(address=bid.bid_addr).first() + self.evaluateKnownIdentityForAutoAccept(strategy, identity_stats) self.logEvent(Concepts.BID, bid.bid_id, diff --git a/basicswap/basicswap_util.py b/basicswap/basicswap_util.py index f8cb7da..34a700c 100644 --- a/basicswap/basicswap_util.py +++ b/basicswap/basicswap_util.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2021-2022 tecnovert +# Copyright (c) 2021-2023 tecnovert # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. @@ -192,6 +192,45 @@ class DebugTypes(IntEnum): B_LOCK_TX_MISSED_SEND = auto() +class NotificationTypes(IntEnum): + NONE = 0 + OFFER_RECEIVED = auto() + BID_RECEIVED = auto() + BID_ACCEPTED = auto() + + +class AutomationOverrideOptions(IntEnum): + DEFAULT = 0 + ALWAYS_ACCEPT = 1 + NEVER_ACCEPT = auto() + + +def strAutomationOverrideOption(option): + if option == AutomationOverrideOptions.DEFAULT: + return 'Default' + if option == AutomationOverrideOptions.ALWAYS_ACCEPT: + return 'Always Accept' + if option == AutomationOverrideOptions.NEVER_ACCEPT: + return 'Never Accept' + return 'Unknown' + + +class VisibilityOverrideOptions(IntEnum): + DEFAULT = 0 + HIDE = 1 + BLOCK = auto() + + +def strVisibilityOverrideOption(option): + if option == VisibilityOverrideOptions.DEFAULT: + return 'Default' + if option == VisibilityOverrideOptions.HIDE: + return 'Hide' + if option == VisibilityOverrideOptions.BLOCK: + return 'Block' + return 'Unknown' + + def strOfferState(state): if state == OfferStates.OFFER_SENT: return 'Sent' @@ -202,13 +241,6 @@ def strOfferState(state): return 'Unknown' -class NotificationTypes(IntEnum): - NONE = 0 - OFFER_RECEIVED = auto() - BID_RECEIVED = auto() - BID_ACCEPTED = auto() - - def strBidState(state): if state == BidStates.BID_SENT: return 'Sent' diff --git a/basicswap/db.py b/basicswap/db.py index 4bb8198..b11d581 100644 --- a/basicswap/db.py +++ b/basicswap/db.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2019-2022 tecnovert +# Copyright (c) 2019-2023 tecnovert # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. @@ -12,7 +12,7 @@ from enum import IntEnum, auto from sqlalchemy.ext.declarative import declarative_base -CURRENT_DB_VERSION = 17 +CURRENT_DB_VERSION = 18 CURRENT_DB_DATA_VERSION = 2 Base = declarative_base() @@ -421,6 +421,8 @@ class KnownIdentity(Base): num_recv_bids_rejected = sa.Column(sa.Integer) num_sent_bids_failed = sa.Column(sa.Integer) num_recv_bids_failed = sa.Column(sa.Integer) + automation_override = sa.Column(sa.Integer) # AutomationOverrideOptions + visibility_override = sa.Column(sa.Integer) # VisibilityOverrideOptions note = sa.Column(sa.String) updated_at = sa.Column(sa.BigInteger) created_at = sa.Column(sa.BigInteger) diff --git a/basicswap/db_upgrades.py b/basicswap/db_upgrades.py index d44bf69..538feb7 100644 --- a/basicswap/db_upgrades.py +++ b/basicswap/db_upgrades.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2022 tecnovert +# Copyright (c) 2022-2023 tecnovert # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. @@ -238,6 +238,11 @@ def upgradeDatabase(self, db_version): tx_data BLOB, used_by BLOB, PRIMARY KEY (record_id))''') + elif current_version == 16: + db_version += 1 + session.execute('ALTER TABLE knownidentities ADD COLUMN automation_override INTEGER') + session.execute('ALTER TABLE knownidentities ADD COLUMN visibility_override INTEGER') + session.execute('UPDATE knownidentities SET active_ind = 1') if current_version != db_version: self.db_version = db_version diff --git a/basicswap/http_server.py b/basicswap/http_server.py index a06f3b4..aeb466d 100644 --- a/basicswap/http_server.py +++ b/basicswap/http_server.py @@ -17,6 +17,7 @@ from . import __version__ from .util import ( dumpj, ensure, + zeroIfNone, LockedCoinError, format_timestamp, ) @@ -25,9 +26,11 @@ from .chainparams import ( chainparams, ) from .basicswap_util import ( - strBidState, strTxState, + strBidState, strAddressType, + AutomationOverrideOptions, + strAutomationOverrideOption, ) from .js_server import ( @@ -37,6 +40,7 @@ from .js_server import ( from .ui.util import ( getCoinName, get_data_entry, + get_data_entry_or, have_data_entry, listAvailableCoins, ) @@ -447,10 +451,13 @@ class HttpHandler(BaseHTTPRequestHandler): if have_data_entry(form_data, 'edit'): page_data['show_edit_form'] = True if have_data_entry(form_data, 'apply'): - new_label = get_data_entry(form_data, 'label') - try: - swap_client.updateIdentity(identity_address, new_label) + data = { + 'label': get_data_entry_or(form_data, 'label', ''), + 'note': get_data_entry_or(form_data, 'note', ''), + 'automation_override': get_data_entry(form_data, 'automation_override'), + } + swap_client.setIdentityData({'address': identity_address}, data) messages.append('Updated') except Exception as e: err_messages.append(str(e)) @@ -466,14 +473,19 @@ class HttpHandler(BaseHTTPRequestHandler): page_data['num_recv_bids_rejected'] = identity.num_recv_bids_rejected page_data['num_sent_bids_failed'] = identity.num_sent_bids_failed page_data['num_recv_bids_failed'] = identity.num_recv_bids_failed + automation_override = zeroIfNone(identity.automation_override) + page_data['automation_override'] = automation_override + page_data['str_automation_override'] = strAutomationOverrideOption(automation_override) + page_data['note'] = identity.note except Exception as e: - messages.append(e) + err_messages.append(e) template = env.get_template('identity.html') return self.render_template(template, { 'messages': messages, 'err_messages': err_messages, 'data': page_data, + 'automation_override_options': [(int(opt), strAutomationOverrideOption(opt)) for opt in AutomationOverrideOptions if opt > 0], 'summary': summary, }) diff --git a/basicswap/js_server.py b/basicswap/js_server.py index 95cebda..99dcf57 100644 --- a/basicswap/js_server.py +++ b/basicswap/js_server.py @@ -517,6 +517,12 @@ def js_identities(self, url_split, post_string: str, is_json: bool) -> bytes: set_data = {} if have_data_entry(post_data, 'set_label'): set_data['label'] = get_data_entry(post_data, 'set_label') + if have_data_entry(post_data, 'set_automation_override'): + set_data['automation_override'] = get_data_entry(post_data, 'set_automation_override') + if have_data_entry(post_data, 'set_visibility_override'): + set_data['visibility_override'] = get_data_entry(post_data, 'set_visibility_override') + if have_data_entry(post_data, 'set_note'): + set_data['note'] = get_data_entry(post_data, 'set_note') if set_data: ensure('address' in filters, 'Must provide an address to modify data') @@ -631,7 +637,7 @@ pages = { 'rateslist': js_rates_list, 'generatenotification': js_generatenotification, 'notifications': js_notifications, - 'identities': js_identities, + 'identities': js_identities, 'vacuumdb': js_vacuumdb, 'getcoinseed': js_getcoinseed, 'setpassword': js_setpassword, diff --git a/basicswap/templates/identity.html b/basicswap/templates/identity.html index 8f46aa6..6b79eee 100644 --- a/basicswap/templates/identity.html +++ b/basicswap/templates/identity.html @@ -63,11 +63,38 @@ + + Automation Override + + + + + + Notes + + + + {% else %} Label {{ data.label }} + + Automation Override + {{ data.str_automation_override }} + + + Notes + + + + {% endif %} Successful Sent Bids @@ -123,4 +150,4 @@ {% include 'footer.html' %} - \ No newline at end of file + diff --git a/basicswap/util/__init__.py b/basicswap/util/__init__.py index d85938a..b998684 100644 --- a/basicswap/util/__init__.py +++ b/basicswap/util/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2018-2022 tecnovert +# Copyright (c) 2018-2023 tecnovert # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. @@ -177,7 +177,7 @@ def format_amount(i, display_scale, scale=None): return rv -def format_timestamp(value, with_seconds=False): +def format_timestamp(value: int, with_seconds=False) -> str: str_format = '%Y-%m-%d %H:%M' if with_seconds: str_format += ':%S' @@ -205,5 +205,11 @@ def h2b(h: str) -> bytes: return bytes.fromhex(h) -def i2h(x): +def i2h(x: int) -> str: return b2h(i2b(x)) + + +def zeroIfNone(value) -> int: + if value is None: + return 0 + return value diff --git a/scripts/createoffers.py b/scripts/createoffers.py index 5ed0109..8683e68 100755 --- a/scripts/createoffers.py +++ b/scripts/createoffers.py @@ -76,27 +76,27 @@ def readConfig(args, known_coins): with open(config_path) as fs: config = json.load(fs) - if not 'offers' in config: + if 'offers' not in config: config['offers'] = [] - if not 'bids' in config: + if 'bids' not in config: config['bids'] = [] - if not 'stealthex' in config: + if 'stealthex' not in config: config['stealthex'] = [] - if not 'min_seconds_between_offers' in config: + if 'min_seconds_between_offers' not in config: config['min_seconds_between_offers'] = 60 print('Set min_seconds_between_offers', config['min_seconds_between_offers']) num_changes += 1 - if not 'max_seconds_between_offers' in config: + if 'max_seconds_between_offers' not in config: config['max_seconds_between_offers'] = config['min_seconds_between_offers'] * 4 print('Set max_seconds_between_offers', config['max_seconds_between_offers']) num_changes += 1 - if not 'min_seconds_between_bids' in config: + if 'min_seconds_between_bids' not in config: config['min_seconds_between_bids'] = 60 print('Set min_seconds_between_bids', config['min_seconds_between_bids']) num_changes += 1 - if not 'max_seconds_between_bids' in config: + if 'max_seconds_between_bids' not in config: config['max_seconds_between_bids'] = config['min_seconds_between_bids'] * 4 print('Set max_seconds_between_bids', config['max_seconds_between_bids']) num_changes += 1 @@ -152,7 +152,7 @@ def readConfig(args, known_coins): bid_template['coin_to'] = findCoin(bid_template['coin_to'], known_coins) if bid_template['name'] in bid_templates_map: - print('renaming bid template', offer_templates_map_template['name']) + print('renaming bid template', bid_template['name']) original_name = bid_template['name'] offset = 2 while f'{original_name}_{offset}' in bid_templates_map: @@ -167,7 +167,6 @@ def readConfig(args, known_coins): for i, swap in enumerate(stealthex_swaps): num_enabled += 1 if swap.get('enabled', True) else 0 swap['coin_from'] = findCoin(swap['coin_from'], known_coins) - #bid_template['coin_to'] = findCoin(bid_template['coin_to'], known_coins) config['num_enabled_swaps'] = num_enabled if num_changes > 0: @@ -303,10 +302,10 @@ def main(): new_offer = read_json_api('offers/new', offer_data) print('New offer: {}'.format(new_offer['offer_id'])) num_state_changes += 1 - if not 'offers' in script_state: + if 'offers' not in script_state: script_state['offers'] = {} template_name = offer_template['name'] - if not template_name in script_state['offers']: + if template_name not in script_state['offers']: script_state['offers'][template_name] = [] script_state['offers'][template_name].append({'offer_id': new_offer['offer_id'], 'time': int(time.time())}) max_seconds_between_offers = config['max_seconds_between_offers'] @@ -328,10 +327,10 @@ def main(): # Check bids in progress max_concurrent = bid_template.get('max_concurrent', 1) - if not 'bids' in script_state: + if 'bids' not in script_state: script_state['bids'] = {} template_name = bid_template['name'] - if not template_name in script_state['bids']: + if template_name not in script_state['bids']: script_state['bids'][template_name] = [] previous_bids = script_state['bids'][template_name] @@ -405,12 +404,22 @@ def main(): offer_identity = read_json_api('identities/{}'.format(offer['addr_from'])) if len(offer_identity) > 0: - successful_sent_bids = offer_identity[0]['num_sent_bids_successful'] - failed_sent_bids = offer_identity[0]['num_sent_bids_failed'] - if failed_sent_bids > 3 and failed_sent_bids > successful_sent_bids: + id_offer_from = offer_identity[0] + automation_override = id_offer_from['automation_override'] + if automation_override == 2: if args.debug: - print(f'Not bidding on offer {offer_id}, too many failed bids ({failed_sent_bids}).') + print(f'Not bidding on offer {offer_id}, automation_override ({automation_override}).') continue + if automation_override == 1: + if args.debug: + print('Offer address from {}, set to always accept.'.format(offer['addr_from'])) + else: + successful_sent_bids = id_offer_from['num_sent_bids_successful'] + failed_sent_bids = id_offer_from['num_sent_bids_failed'] + if failed_sent_bids > 3 and failed_sent_bids > successful_sent_bids: + if args.debug: + print(f'Not bidding on offer {offer_id}, too many failed bids ({failed_sent_bids}).') + continue max_coin_from_balance = bid_template.get('max_coin_from_balance', -1) if max_coin_from_balance > 0: @@ -533,10 +542,10 @@ def main(): 'address_to': address_to, 'amount_from': swap_amount, 'fixed': True, - #'extra_id_to': - #'referral': + # 'extra_id_to': + # 'referral': 'refund_address': address_refund, - #'refund_extra_id': + # 'refund_extra_id': 'rate_id': estimate_response['rate_id'], } diff --git a/tests/basicswap/extended/test_scripts.py b/tests/basicswap/extended/test_scripts.py index 12ca7b3..a7f5218 100644 --- a/tests/basicswap/extended/test_scripts.py +++ b/tests/basicswap/extended/test_scripts.py @@ -197,7 +197,6 @@ class Test(unittest.TestCase): cls.node1_statefile = os.path.join(datadir, 'node1_state.json') cls.node1_args = [script_path, '--port', str(UI_PORT + 1), '--configfile', cls.node1_configfile, '--statefile', cls.node1_statefile, '--oneshot', '--debug'] - @classmethod def tearDownClass(cls): logging.info('Stopping test') @@ -333,7 +332,7 @@ class Test(unittest.TestCase): assert (count_lines_with(rv_stdout, 'Bid rate too low for offer') == 3) assert (count_lines_with(rv_stdout, 'Already bidding on offer') == 1) - logging.info(f'Modifying node1 config') + logging.info('Modifying node1 config') node1_test1_config['bids'][0]['maxrate'] = 0.07 node1_test1_config['bids'][0]['max_coin_from_balance'] = 100 node1_test1_config['bids'][0]['min_coin_to_balance'] = 100 @@ -418,7 +417,7 @@ class Test(unittest.TestCase): assert (len(possible_bids) == 1) assert (float(possible_bids[0]['amount_from'] < 20.0)) - logging.info(f'Adding mock data to node1 db for tests') + logging.info('Adding mock data to node1 db for tests') rows = [] offers = read_json_api(UI_PORT, 'offers') @@ -523,7 +522,6 @@ class Test(unittest.TestCase): offer_ids = [] logging.info('Create three offers') - for i in range(3): if i > 0: with open(self.node0_statefile) as fs: diff --git a/tests/basicswap/test_run.py b/tests/basicswap/test_run.py index f2bf8e4..a6396b4 100644 --- a/tests/basicswap/test_run.py +++ b/tests/basicswap/test_run.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -# Copyright (c) 2019-2022 tecnovert +# Copyright (c) 2019-2023 tecnovert # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. @@ -145,6 +145,20 @@ class Test(BaseTest): rv = read_json_api(1800, 'identities/pPCsRro5po7Yu6kyu5XjSyr3A1PPdk9j1F', {'set_label': 'test 3'}) assert (rv['error'] == 'Invalid identity address') + rv = read_json_api(1800, 'identities/ppCsRro5po7Yu6kyu5XjSyr3A1PPdk9j1F', {'set_note': 'note 1'}) + assert (len(rv) == 1) + assert (rv[0]['address'] == 'ppCsRro5po7Yu6kyu5XjSyr3A1PPdk9j1F') + assert (rv[0]['label'] == 'test 2') + assert (rv[0]['note'] == 'note 1') + + rv = read_json_api(1800, 'identities/ppCsRro5po7Yu6kyu5XjSyr3A1PPdk9j1F', {'set_automation_override': 1}) + assert (len(rv) == 1) + assert (rv[0]['automation_override'] == 1) + + rv = read_json_api(1800, 'identities/ppCsRro5po7Yu6kyu5XjSyr3A1PPdk9j1F', {'set_visibility_override': 'hide'}) + assert (len(rv) == 1) + assert (rv[0]['visibility_override'] == 1) + def test_01_verifyrawtransaction(self): txn = '0200000001eb6e5c4ebba4efa32f40c7314cad456a64008e91ee30b2dd0235ab9bb67fbdbb01000000ee47304402200956933242dde94f6cf8f195a470f8d02aef21ec5c9b66c5d3871594bdb74c9d02201d7e1b440de8f4da672d689f9e37e98815fb63dbc1706353290887eb6e8f7235012103dc1b24feb32841bc2f4375da91fa97834e5983668c2a39a6b7eadb60e7033f9d205a803b28fe2f86c17db91fa99d7ed2598f79b5677ffe869de2e478c0d1c02cc7514c606382012088a8201fe90717abb84b481c2a59112414ae56ec8acc72273642ca26cc7a5812fdc8f68876a914225fbfa4cb725b75e511810ac4d6f74069bdded26703520140b27576a914207eb66b2fd6ed9924d6217efc7fa7b38dfabe666888acffffffff01e0167118020000001976a9140044e188928710cecba8311f1cf412135b98145c88ac00000000' prevout = {