async def cancel_queued_transaction(self, tx_hash, signature): if not validate_transaction_hash(tx_hash): raise JsonRPCInvalidParamsError(data={'id': 'invalid_transaction_hash', 'message': 'Invalid Transaction Hash'}) if not validate_signature(signature): raise JsonRPCInvalidParamsError(data={'id': 'invalid_signature', 'message': 'Invalid Signature'}) async with self.db: tx = await self.db.fetchrow("SELECT * FROM transactions WHERE hash = $1 AND (status != 'error' OR status = 'new')", tx_hash) if tx is None: raise JsonRPCError(None, -32000, "Transaction not found", {'id': 'not_found', 'message': 'Transaction not found'}) elif tx['status'] != 'queued' and tx['status'] != 'new': raise JsonRPCError(None, -32000, "Transaction already sent to node", {'id': 'invalid_transaction_status', 'message': 'Transaction already sent to node'}) message = "Cancel transaction " + tx_hash if not personal_ecrecover(message, signature, tx['from_address']): raise JsonRPCError(None, -32000, "Permission Denied", {'id': 'permission_denied', 'message': 'Permission Denied'}) log.info("Setting tx '{}' to error due to user cancelation".format(tx['hash'])) manager_dispatcher.update_transaction(tx['transaction_id'], 'error')
async def test_eth_nodes_out_of_sync_on_confirm(self, *, monitor, push_client): # shutdown the monitor so that the confirmed transaction status can be # manually triggered before the transaction is actually confirmed await monitor.shutdown() val = 761751855997712 body = {"registration_id": TEST_GCM_ID, "address": TEST_ID_ADDRESS} resp = await self.fetch_signed("/gcm/register", signing_key=TEST_ID_KEY, method="POST", body=body) self.assertResponseCodeEqual(resp, 204, resp.body) tx_hash = await self.send_tx(FAUCET_PRIVATE_KEY, TEST_ID_ADDRESS, val) tx_id = None while tx_id is None: async with self.pool.acquire() as con: tx_id = await con.fetchval( "SELECT transaction_id FROM transactions WHERE hash = $1", tx_hash) # force confirmed, even though it should take some time manager_dispatcher.update_transaction(tx_id, 'confirmed') _, pn = await push_client.get() message = parse_sofa_message(pn['message']) self.assertIsInstance(message, SofaPayment) self.assertEqual(message['txHash'], tx_hash) self.assertEqual(message['status'], 'unconfirmed') _, pn = await push_client.get() message = parse_sofa_message(pn['message']) self.assertIsInstance(message, SofaPayment) self.assertEqual(message['txHash'], tx_hash) self.assertEqual(message['status'], 'confirmed')
async def send_transaction(self, *, tx, signature=None): try: tx = decode_transaction(tx) except: raise JsonRPCInvalidParamsError(data={ 'id': 'invalid_transaction', 'message': 'Invalid Transaction' }) if is_transaction_signed(tx): tx_sig = data_encoder(signature_from_transaction(tx)) if signature: if tx_sig != signature: raise JsonRPCInvalidParamsError( data={ 'id': 'invalid_signature', 'message': 'Invalid Signature: Signature in payload and signature of transaction do not match' }) else: signature = tx_sig else: if signature is None: raise JsonRPCInvalidParamsError(data={ 'id': 'missing_signature', 'message': 'Missing Signature' }) if not validate_signature(signature): raise JsonRPCInvalidParamsError( data={ 'id': 'invalid_signature', 'message': 'Invalid Signature: {}'.format('Invalid length' if len( signature) != 132 else 'Invalid hex value') }) try: sig = data_decoder(signature) except Exception: log.exception( "Unexpected error decoding valid signature: {}".format( signature)) raise JsonRPCInvalidParamsError(data={ 'id': 'invalid_signature', 'message': 'Invalid Signature' }) add_signature_to_transaction(tx, sig) # validate network id, if it's not for "all networks" if tx.network_id is not None and self.network_id != tx.network_id: raise JsonRPCInvalidParamsError(data={ 'id': 'invalid_network_id', 'message': 'Invalid Network ID' }) from_address = data_encoder(tx.sender) to_address = data_encoder(tx.to) # prevent spamming of transactions with the same nonce from the same sender async with RedisLock("{}:{}".format(from_address, tx.nonce), raise_when_locked=partial( JsonRPCInvalidParamsError, data={ 'id': 'invalid_nonce', 'message': 'Nonce already used' }), ex=5): # check for transaction overwriting async with self.db: existing = await self.db.fetchrow( "SELECT * FROM transactions WHERE " "from_address = $1 AND nonce = $2 AND " "(status != 'error' or status is NULL)", from_address, tx.nonce) # disallow transaction overwriting when the gas is lower or the transaction is confirmed if existing and (parse_int(existing['gas_price']) >= tx.gasprice or existing['status'] == 'confirmed'): raise JsonRPCInvalidParamsError(data={ 'id': 'invalid_nonce', 'message': 'Nonce already used' }) # make sure the account has enough funds for the transaction network_balance, balance, _, _ = await self.get_balances( from_address) if existing: balance += parse_int(existing['value']) + parse_int( existing['gas']) * parse_int(existing['gas_price']) if balance < (tx.value + (tx.startgas * tx.gasprice)): raise JsonRPCInsufficientFundsError( data={ 'id': 'insufficient_funds', 'message': 'Insufficient Funds' }) # validate the nonce (only necessary if tx doesn't already exist) if not existing: c_nonce = await self.get_transaction_count(from_address) if tx.nonce < c_nonce: raise JsonRPCInvalidParamsError( data={ 'id': 'invalid_nonce', 'message': 'Provided nonce is too low' }) if tx.nonce > c_nonce: raise JsonRPCInvalidParamsError( data={ 'id': 'invalid_nonce', 'message': 'Provided nonce is too high' }) if tx.intrinsic_gas_used > tx.startgas: raise JsonRPCInvalidParamsError( data={ 'id': 'invalid_transaction', 'message': 'Transaction gas is too low. There is not enough gas to cover minimal cost of the transaction (minimal: {}, got: {}). Try increasing supplied gas.' .format(tx.intrinsic_gas_used, tx.startgas) }) # now this tx fits enough of the criteria to allow it # onto the transaction queue tx_hash = calculate_transaction_hash(tx) if existing: log.info( "Setting tx '{}' to error due to forced overwrite".format( existing['hash'])) manager_dispatcher.update_transaction( existing['transaction_id'], 'error') data = data_encoder(tx.data) if data and \ ((data.startswith("0xa9059cbb") and len(data) == 138) or \ (data.startswith("0x23b872dd") and len(data) == 202)): # check if the token is a known erc20 token async with self.db: erc20_token = await self.db.fetchrow( "SELECT * FROM tokens WHERE contract_address = $1", to_address) else: erc20_token = False # add tx to database async with self.db: db_tx = await self.db.fetchrow( "INSERT INTO transactions " "(hash, from_address, to_address, nonce, " "value, gas, gas_price, " "data, v, r, s, " "sender_toshi_id) " "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) " "RETURNING transaction_id", tx_hash, from_address, to_address, tx.nonce, hex(tx.value), hex(tx.startgas), hex(tx.gasprice), data_encoder(tx.data), hex(tx.v), hex(tx.r), hex(tx.s), self.user_toshi_id) if erc20_token: token_value = int(data[-64:], 16) if data.startswith("0x23b872dd"): erc20_from_address = "0x" + data[34:74] erc20_to_address = "0x" + data[98:138] else: erc20_from_address = from_address erc20_to_address = "0x" + data[34:74] await self.db.execute( "INSERT INTO token_transactions " "(transaction_id, transaction_log_index, contract_address, from_address, to_address, value) " "VALUES ($1, $2, $3, $4, $5, $6)", db_tx['transaction_id'], 0, erc20_token['contract_address'], erc20_from_address, erc20_to_address, hex(token_value)) await self.db.commit() # trigger processing the transaction queue manager_dispatcher.process_transaction_queue(from_address) # analytics # use notification registrations to try find toshi ids for users if self.user_toshi_id: sender_toshi_id = self.user_toshi_id else: async with self.db: sender_toshi_id = await self.db.fetchval( "SELECT toshi_id FROM notification_registrations WHERE " "eth_address = $1", from_address) async with self.db: receiver_toshi_id = await self.db.fetchval( "SELECT toshi_id FROM notification_registrations WHERE " "eth_address = $1", to_address) self.track(sender_toshi_id, "Sent transaction") # it doesn't make sense to add user agent here as we # don't know the receiver's user agent self.track(receiver_toshi_id, "Received transaction", add_user_agent=False) return tx_hash
async def update_transaction(self, transaction_id, status, retry_start_time=0): async with self.db: tx = await self.db.fetchrow( "SELECT * FROM transactions WHERE transaction_id = $1", transaction_id) if tx is None or tx['status'] == status: return token_txs = await self.db.fetch( "SELECT tok.symbol, tok.name, tok.decimals, tx.contract_address, tx.value, tx.from_address, tx.to_address, tx.transaction_log_index, tx.status " "FROM token_transactions tx " "JOIN tokens tok " "ON tok.contract_address = tx.contract_address " "WHERE tx.transaction_id = $1", transaction_id) # check if we're trying to update the state of a tx that is already confirmed, we have an issue if tx['status'] == 'confirmed': log.warning( "Trying to update status of tx {} to {}, but tx is already confirmed" .format(tx['hash'], status)) return # only log if the transaction is internal if tx['v'] is not None: log.info( "Updating status of tx {} to {} (previously: {})".format( tx['hash'], status, tx['status'])) if status == 'confirmed': try: bulk = self.eth.bulk() transaction = bulk.eth_getTransactionByHash(tx['hash']) tx_receipt = bulk.eth_getTransactionReceipt(tx['hash']) await bulk.execute() transaction = transaction.result() tx_receipt = tx_receipt.result() except: log.exception("Error getting transaction: {}".format( tx['hash'])) transaction = None tx_receipt = None if transaction and 'blockNumber' in transaction and transaction[ 'blockNumber'] is not None: if retry_start_time > 0: log.info( "successfully confirmed tx {} after {} seconds".format( tx['hash'], round(time.time() - retry_start_time, 2))) token_tx_updates = [] updated_token_txs = [] for token_tx in token_txs: from_address = token_tx['from_address'] to_address = token_tx['to_address'] # check transaction receipt to make sure the transfer was successful has_transfer_event = False token_tx_status = 'confirmed' if tx_receipt[ 'logs'] is not None: # should always be [], but checking just incase for _log in tx_receipt['logs']: if len(_log['topics']) > 0 and _log['topics'][ 0] == TRANSFER_TOPIC: if len(_log['topics']) == 3 and len(_log['data']) == 66 and \ decode_single_address(_log['topics'][1]) == from_address and \ decode_single_address(_log['topics'][2]) == to_address: has_transfer_event = True break elif len(_log['topics']) == 1 and len( _log['data']) == 194: erc20_from_address, erc20_to_address, erc20_value = decode_abi( ['address', 'address', 'uint256'], data_decoder(_log['data'])) if erc20_from_address == from_address and \ erc20_to_address == to_address: has_transfer_event = True break elif _log['address'] == WETH_CONTRACT_ADDRESS: if _log['topics'][ 0] == DEPOSIT_TOPIC and decode_single_address( _log['topics'][1]) == to_address: has_transfer_event = True break elif _log['topics'][ 0] == WITHDRAWAL_TOPIC and decode_single_address( _log['topics'][1]) == from_address: has_transfer_event = True break if not has_transfer_event: # there was no Transfer event matching this transaction, this means something went wrong token_tx_status = 'error' else: erc20_dispatcher.update_token_cache( token_tx['contract_address'], from_address, to_address, blocknumber=parse_int( transaction['blockNumber'])) else: log.error( "Unexpectedly got null for tx receipt logs for tx: {}" .format(tx['hash'])) token_tx_status = 'error' token_tx_updates.append( (token_tx_status, tx['transaction_id'], token_tx['transaction_log_index'])) token_tx = dict(token_tx) token_tx['status'] = token_tx_status updated_token_txs.append(token_tx) token_txs = updated_token_txs blocknumber = parse_int(transaction['blockNumber']) async with self.db: await self.db.execute( "UPDATE transactions SET status = $1, blocknumber = $2, updated = (now() AT TIME ZONE 'utc') " "WHERE transaction_id = $3", status, blocknumber, transaction_id) if token_tx_updates: await self.db.executemany( "UPDATE token_transactions SET status = $1 " "WHERE transaction_id = $2 AND transaction_log_index = $3", token_tx_updates) await self.db.commit() else: # this is probably because the node hasn't caught up with the latest block yet, retry in a "bit" (but only retry up to 60 seconds) if retry_start_time > 0 and time.time( ) - retry_start_time >= 60: if transaction is None: log.error( "requested transaction {}'s status to be set to confirmed, but cannot find the transaction" .format(tx['hash'])) else: log.error( "requested transaction {}'s status to be set to confirmed, but transaction is not confirmed on the node" .format(tx['hash'])) return await asyncio.sleep(random.random()) manager_dispatcher.update_transaction( transaction_id, status, retry_start_time=retry_start_time or time.time()) return else: async with self.db: await self.db.execute( "UPDATE transactions SET status = $1, updated = (now() AT TIME ZONE 'utc') WHERE transaction_id = $2", status, transaction_id) await self.db.commit() # render notification # don't send "queued" if status == 'queued': status = 'unconfirmed' elif status == 'unconfirmed' and tx['status'] == 'queued': # there's already been a tx for this so no need to send another return messages = [] # check if this is an erc20 transaction, if so use those values if token_txs: for token_tx in token_txs: token_tx_status = token_tx['status'] from_address = token_tx['from_address'] to_address = token_tx['to_address'] # TokenPayment PNs are not shown at the moment, so i'm removing # this for the time being until they're required # if token_tx_status == 'confirmed': # data = { # "txHash": tx['hash'], # "fromAddress": from_address, # "toAddress": to_address, # "status": token_tx_status, # "value": token_tx['value'], # "contractAddress": token_tx['contract_address'] # } # messages.append((from_address, to_address, token_tx_status, "SOFA::TokenPayment: " + json_encode(data))) # if a WETH deposit or withdrawal, we need to let the client know to # update their ETHER balance using a normal SOFA:Payment if token_tx['contract_address'] == WETH_CONTRACT_ADDRESS and ( from_address == "0x0000000000000000000000000000000000000000" or to_address == "0x0000000000000000000000000000000000000000"): payment = SofaPayment( value=parse_int(token_tx['value']), txHash=tx['hash'], status=status, fromAddress=from_address, toAddress=to_address, networkId=config['ethereum']['network_id']) messages.append( (from_address, to_address, status, payment.render())) else: from_address = tx['from_address'] to_address = tx['to_address'] payment = SofaPayment(value=parse_int(tx['value']), txHash=tx['hash'], status=status, fromAddress=from_address, toAddress=to_address, networkId=config['ethereum']['network_id']) messages.append( (from_address, to_address, status, payment.render())) # figure out what addresses need pns # from address always needs a pn for from_address, to_address, status, message in messages: manager_dispatcher.send_notification(from_address, message) # no need to check to_address for contract deployments if to_address == "0x": # TODO: update any notification registrations to be marked as a contract return # check if this is a brand new tx with no status if tx['status'] == 'new': # if an error has happened before any PNs have been sent # we only need to send the error to the sender, thus we # only add 'to' if the new status is not an error if status != 'error': manager_dispatcher.send_notification(to_address, message) else: manager_dispatcher.send_notification(to_address, message) # trigger a processing of the to_address's queue incase it has # things waiting on this transaction manager_dispatcher.process_transaction_queue(to_address)
async def update_transaction(self, transaction_id, status): async with self.db: tx = await self.db.fetchrow( "SELECT * FROM transactions WHERE transaction_id = $1", transaction_id) if tx is None or tx['status'] == status: return token_txs = await self.db.fetch( "SELECT tok.symbol, tok.name, tok.decimals, tx.contract_address, tx.value, tx.from_address, tx.to_address, tx.transaction_log_index, tx.status " "FROM token_transactions tx " "JOIN tokens tok " "ON tok.contract_address = tx.contract_address " "WHERE tx.transaction_id = $1", transaction_id) # check if we're trying to update the state of a tx that is already confirmed, we have an issue if tx['status'] == 'confirmed': log.warning( "Trying to update status of tx {} to {}, but tx is already confirmed" .format(tx['hash'], status)) return # only log if the transaction is internal if tx['v'] is not None: log.info( "Updating status of tx {} to {} (previously: {})".format( tx['hash'], status, tx['status'])) if status == 'confirmed': transaction = await self.eth.eth_getTransactionByHash(tx['hash']) if transaction and 'blockNumber' in transaction: blocknumber = parse_int(transaction['blockNumber']) async with self.db: await self.db.execute( "UPDATE transactions SET status = $1, blocknumber = $2, updated = (now() AT TIME ZONE 'utc') " "WHERE transaction_id = $3", status, blocknumber, transaction_id) await self.db.commit() else: log.error( "requested transaction '{}''s status to be set to confirmed, but cannot find the transaction" .format(tx['hash'])) else: async with self.db: await self.db.execute( "UPDATE transactions SET status = $1, updated = (now() AT TIME ZONE 'utc') WHERE transaction_id = $2", status, transaction_id) await self.db.commit() # render notification # don't send "queued" if status == 'queued': status = 'unconfirmed' elif status == 'unconfirmed' and tx['status'] == 'queued': # there's already been a tx for this so no need to send another return messages = [] # check if this is an erc20 transaction, if so use those values if token_txs: if status == 'confirmed': tx_receipt = await self.eth.eth_getTransactionReceipt( tx['hash']) if tx_receipt is None: log.error( "Failed to get transaction receipt for confirmed transaction: {}" .format(tx_receipt)) # requeue to try again manager_dispatcher.update_transaction( transaction_id, status) return for token_tx in token_txs: token_tx_status = status from_address = token_tx['from_address'] to_address = token_tx['to_address'] if status == 'confirmed': # check transaction receipt to make sure the transfer was successful has_transfer_event = False if tx_receipt[ 'logs'] is not None: # should always be [], but checking just incase for _log in tx_receipt['logs']: if len(_log['topics']) > 2: if _log['topics'][0] == TRANSFER_TOPIC and \ decode_single_address(_log['topics'][1]) == from_address and \ decode_single_address(_log['topics'][2]) == to_address: has_transfer_event = True break elif _log['address'] == WETH_CONTRACT_ADDRESS: if _log['topics'][ 0] == DEPOSIT_TOPIC and decode_single_address( _log['topics'][1]) == to_address: has_transfer_event = True break elif _log['topics'][ 0] == WITHDRAWAL_TOPIC and decode_single_address( _log['topics'][1]) == from_address: has_transfer_event = True break if not has_transfer_event: # there was no Transfer event matching this transaction token_tx_status = 'error' else: erc20_dispatcher.update_token_cache( token_tx['contract_address'], from_address, to_address, blocknumber=parse_int(transaction['blockNumber'])) if token_tx_status == 'confirmed': data = { "txHash": tx['hash'], "fromAddress": from_address, "toAddress": to_address, "status": token_tx_status, "value": token_tx['value'], "contractAddress": token_tx['contract_address'] } messages.append( (from_address, to_address, token_tx_status, "SOFA::TokenPayment: " + json_encode(data))) async with self.db: await self.db.execute( "UPDATE token_transactions SET status = $1 " "WHERE transaction_id = $2 AND transaction_log_index = $3", token_tx_status, tx['transaction_id'], token_tx['transaction_log_index']) await self.db.commit() # if a WETH deposit or withdrawal, we need to let the client know to # update their ETHER balance using a normal SOFA:Payment if token_tx['contract_address'] == WETH_CONTRACT_ADDRESS and ( from_address == "0x0000000000000000000000000000000000000000" or to_address == "0x0000000000000000000000000000000000000000"): payment = SofaPayment( value=parse_int(token_tx['value']), txHash=tx['hash'], status=status, fromAddress=from_address, toAddress=to_address, networkId=config['ethereum']['network_id']) messages.append( (from_address, to_address, status, payment.render())) else: from_address = tx['from_address'] to_address = tx['to_address'] payment = SofaPayment(value=parse_int(tx['value']), txHash=tx['hash'], status=status, fromAddress=from_address, toAddress=to_address, networkId=config['ethereum']['network_id']) messages.append( (from_address, to_address, status, payment.render())) # figure out what addresses need pns # from address always needs a pn for from_address, to_address, status, message in messages: manager_dispatcher.send_notification(from_address, message) # no need to check to_address for contract deployments if to_address == "0x": # TODO: update any notification registrations to be marked as a contract return # check if this is a brand new tx with no status if tx['status'] is None: # if an error has happened before any PNs have been sent # we only need to send the error to the sender, thus we # only add 'to' if the new status is not an error if status != 'error': manager_dispatcher.send_notification(to_address, message) else: manager_dispatcher.send_notification(to_address, message) # trigger a processing of the to_address's queue incase it has # things waiting on this transaction manager_dispatcher.process_transaction_queue(to_address)
async def process_transaction(self, transaction): to_address = transaction['to'] # make sure we use a valid encoding of "empty" for contract deployments if to_address is None: to_address = "0x" from_address = transaction['from'] async with self.pool.acquire() as con: # find if we have a record of this tx by checking the from address and nonce db_txs = await con.fetch("SELECT * FROM transactions WHERE " "from_address = $1 AND nonce = $2", from_address, parse_int(transaction['nonce'])) if len(db_txs) > 1: # see if one has the same hash db_tx = await con.fetchrow("SELECT * FROM transactions WHERE " "from_address = $1 AND nonce = $2 AND hash = $3 AND (status != $4 OR status IS NULL)", from_address, parse_int(transaction['nonce']), transaction['hash'], 'error') if db_tx is None: # find if there are any that aren't marked as error no_error = await con.fetch("SELECT * FROM transactions WHERE " "from_address = $1 AND nonce = $2 AND hash != $3 AND (status != $4 OR status IS NULL)", from_address, parse_int(transaction['nonce']), transaction['hash'], 'error') if len(no_error) == 1: db_tx = no_error[0] elif len(no_error) != 0: log.warning("Multiple transactions from '{}' exist with nonce '{}' in unknown state") elif len(db_txs) == 1: db_tx = db_txs[0] else: db_tx = None # if we have a previous transaction, do some checking to see what's going on # see if this is an overwritten transaction # if the status of the old tx was previously an error, we don't care about it # otherwise, we have to notify the interested parties of the overwrite if db_tx and db_tx['hash'] != transaction['hash'] and db_tx['status'] != 'error': if db_tx['v'] is not None: log.warning("found overwritten transaction!") log.warning("tx from: {}".format(from_address)) log.warning("nonce: {}".format(parse_int(transaction['nonce']))) log.warning("old tx hash: {}".format(db_tx['hash'])) log.warning("new tx hash: {}".format(transaction['hash'])) manager_dispatcher.update_transaction(db_tx['transaction_id'], 'error') db_tx = None # check for erc20 transfers erc20_transfers = [] if transaction['blockNumber'] is not None and \ 'logs' in transaction and \ len(transaction['logs']) > 0: # find any logs with erc20 token related topics for _log in transaction['logs']: if len(_log['topics']) > 0: # Transfer(address,address,uint256) if _log['topics'][0] == TRANSFER_TOPIC: # make sure the log address is for one we're interested in is_known_token = await con.fetchval("SELECT 1 FROM tokens WHERE contract_address = $1", _log['address']) if not is_known_token: continue if len(_log['topics']) < 3 or len(_log['data']) != 66: log.warning('Got invalid erc20 Transfer event in tx: {}'.format(transaction['hash'])) continue erc20_from_address = decode_single(('address', '', []), data_decoder(_log['topics'][1])) erc20_to_address = decode_single(('address', '', []), data_decoder(_log['topics'][2])) erc20_is_interesting = await con.fetchval( "SELECT 1 FROM token_registrations " "WHERE eth_address = $1 OR eth_address = $2", erc20_from_address, erc20_to_address) if erc20_is_interesting: erc20_value = decode_abi(['uint256'], data_decoder(_log['data']))[0] erc20_transfers.append((_log['address'], int(_log['transactionLogIndex'], 16), erc20_from_address, erc20_to_address, hex(erc20_value), 'confirmed')) # special checks for WETH, since it's rarely 'Transfer'ed, but we # still need to update it elif (_log['topics'][0] == DEPOSIT_TOPIC or _log['topics'][0] == WITHDRAWAL_TOPIC) and _log['address'] == WETH_CONTRACT_ADDRESS: eth_address = decode_single(('address', '', []), data_decoder(_log['topics'][1])) erc20_is_interesting = await con.fetchval( "SELECT 1 FROM token_registrations " "WHERE eth_address = $1", eth_address) if erc20_is_interesting: erc20_value = decode_abi(['uint256'], data_decoder(_log['data']))[0] if _log['topics'][0] == DEPOSIT_TOPIC: erc20_to_address = eth_address erc20_from_address = "0x0000000000000000000000000000000000000000" else: erc20_to_address = "0x0000000000000000000000000000000000000000" erc20_from_address = eth_address erc20_transfers.append((WETH_CONTRACT_ADDRESS, int(_log['transactionLogIndex'], 16), erc20_from_address, erc20_to_address, hex(erc20_value), 'confirmed')) elif transaction['blockNumber'] is None and db_tx is None: # transaction is pending, attempt to guess if this is a token # transaction based off it's input if transaction['input']: data = transaction['input'] if (data.startswith("0xa9059cbb") and len(data) == 138) or (data.startswith("0x23b872dd") and len(data) == 202): token_value = hex(int(data[-64:], 16)) if data.startswith("0x23b872dd"): erc20_from_address = "0x" + data[34:74] erc20_to_address = "0x" + data[98:138] else: erc20_from_address = from_address erc20_to_address = "0x" + data[34:74] erc20_transfers.append((to_address, 0, erc20_from_address, erc20_to_address, token_value, 'unconfirmed')) # special WETH handling elif data == '0xd0e30db0' and transaction['to'] == WETH_CONTRACT_ADDRESS: erc20_transfers.append((WETH_CONTRACT_ADDRESS, 0, "0x0000000000000000000000000000000000000000", transaction['from'], transaction['value'], 'unconfirmed')) elif data.startswith('0x2e1a7d4d') and len(data) == 74: token_value = hex(int(data[-64:], 16)) erc20_transfers.append((WETH_CONTRACT_ADDRESS, 0, transaction['from'], "0x0000000000000000000000000000000000000000", token_value, 'unconfirmed')) if db_tx: is_interesting = True else: # find out if there is anyone interested in this transaction is_interesting = await con.fetchval("SELECT 1 FROM notification_registrations " "WHERE eth_address = $1 OR eth_address = $2", to_address, from_address) if not is_interesting and len(erc20_transfers) > 0: for _, _, erc20_from_address, erc20_to_address, _, _ in erc20_transfers: is_interesting = await con.fetchval("SELECT 1 FROM notification_registrations " "WHERE eth_address = $1 OR eth_address = $2", erc20_to_address, erc20_from_address) if is_interesting: break is_interesting = await con.fetchval("SELECT 1 FROM token_registrations " "WHERE eth_address = $1 OR eth_address = $2", erc20_to_address, erc20_from_address) if is_interesting: break if not is_interesting: return if db_tx is None: # if so, add it to the database and trigger an update # add tx to database db_tx = await con.fetchrow( "INSERT INTO transactions " "(hash, from_address, to_address, nonce, " "value, gas, gas_price, " "data) " "VALUES ($1, $2, $3, $4, $5, $6, $7, $8) " "RETURNING transaction_id", transaction['hash'], from_address, to_address, parse_int(transaction['nonce']), hex(parse_int(transaction['value'])), hex(parse_int(transaction['gas'])), hex(parse_int(transaction['gasPrice'])), transaction['input']) for erc20_contract_address, transaction_log_index, erc20_from_address, erc20_to_address, erc20_value, erc20_status in erc20_transfers: is_interesting = await con.fetchval("SELECT 1 FROM notification_registrations " "WHERE eth_address = $1 OR eth_address = $2", erc20_to_address, erc20_from_address) if not is_interesting: is_interesting = await con.fetchrow("SELECT 1 FROM token_registrations " "WHERE eth_address = $1 OR eth_address = $2", erc20_to_address, erc20_from_address) if is_interesting: await con.execute( "INSERT INTO token_transactions " "(transaction_id, transaction_log_index, contract_address, from_address, to_address, value, status) " "VALUES ($1, $2, $3, $4, $5, $6, $7) " "ON CONFLICT (transaction_id, transaction_log_index) DO UPDATE " "SET from_address = EXCLUDED.from_address, to_address = EXCLUDED.to_address, value = EXCLUDED.value", db_tx['transaction_id'], transaction_log_index, erc20_contract_address, erc20_from_address, erc20_to_address, erc20_value, erc20_status) manager_dispatcher.update_transaction( db_tx['transaction_id'], 'confirmed' if transaction['blockNumber'] is not None else 'unconfirmed') return db_tx['transaction_id']