diff --git a/subscrape/scrapers/moonbeam_scraper.py b/subscrape/scrapers/moonbeam_scraper.py index 4be43b8..fd6e5d4 100644 --- a/subscrape/scrapers/moonbeam_scraper.py +++ b/subscrape/scrapers/moonbeam_scraper.py @@ -251,79 +251,80 @@ async def __process_transaction_on_account(transaction): self.transactions[account][timestamp]['output_b_token_symbol'] = '' self.transactions[account][timestamp]['output_b_quantity'] = '' - if 'input' in transaction and len(transaction['input']) >= 8: - # assume this was a call to a contract since input data was provided - contract_address = transaction['to'].lower() - - await self.retrieve_and_cache_contract_abi(contract_address) - - if contract_address in self.abis and self.abis[contract_address] is not None: - decoded_transaction = decode_tx(contract_address, transaction['input'], self.abis[contract_address]) - - if decoded_transaction[0] == 'decode error': - if contract_address not in self.contracts_with_known_decode_errors: - self.contracts_with_known_decode_errors.append(contract_address) - decode_traceback = decoded_transaction[1] - self.logger.warning(f'Unable to decode contract interaction with contract ' - f'{contract_address} in transaction:\r\n' - f'{transaction}\r\n\r\n' - f'{decode_traceback}\r\n' - f'---- Now continuing processing the rest of the transactions ----\r\n') - else: - # successfully decoded the input data to the contract interaction - contract_method_name = decoded_transaction[0] - self.transactions[account][timestamp]['contract_method_name'] = contract_method_name - decoded_func_params = json.loads(decoded_transaction[1]) - - if contract_method_name in {'swapExactTokensForTokens', 'swapTokensForExactTokens', - 'swapExactTokensForETH', 'swapTokensForExactETH', - 'swapExactTokensForTokensSupportingFeeOnTransferTokens', - 'swapExactTokensForETHSupportingFeeOnTransferTokens', - 'swapExactETHForTokens', 'swapETHForExactTokens', - 'swapExactNativeCurrencyForTokens', - 'addLiquiditySingleNativeCurrency', 'addLiquiditySingleToken'}: - await self.__decode_token_swap_transaction(account, transaction, contract_method_name, - decoded_func_params) - elif contract_method_name in {'addLiquidity', 'addLiquidityETH', 'addLiquidityNativeCurrency'}: - await self.__decode_add_liquidity_transaction(account, transaction, contract_method_name, - decoded_func_params) - elif contract_method_name in {'removeLiquidity', 'removeLiquidityETH', - 'removeLiquidityETHWithPermit', 'removeLiquidityNativeCurrency'}: - await self.__decode_remove_liquidity_transaction(account, transaction, contract_method_name, - decoded_func_params) - elif contract_method_name in {'deposit', 'depositWithPermit', 'depositEth', 'depositETH'}: - await self.__decode_deposit_transaction(account, transaction, contract_method_name, - decoded_func_params) - elif contract_method_name in {'withdraw', 'leave'}: - await self.__decode_withdraw_transaction(account, transaction, contract_method_name, - decoded_func_params) - elif contract_method_name in {'redeem'}: - await self.__decode_redeem_transaction(account, transaction, contract_method_name, - decoded_func_params) - else: - # todo: handle (and don't ignore) 'stake' contract methods - # 'claim' and 'collect' probably remain ignored for DPS. - # 'approve', 'nominate', 'revoke_nomination' permanently ignore because not financially related. - self.transactions[account][timestamp]['action'] = contract_method_name - if contract_method_name not in {'enter', 'leave', - 'increaseAmountWithPermit', - 'approve', 'claim', 'collect', - 'stake', 'unstake', 'delegate', - 'nominator_bond_more', 'nominator_bond_less', - 'execute_delegation_request', 'cancel_delegation_request', - 'schedule_delegator_bond_less', - 'schedule_revoke_delegation', - 'delegator_bond_more', 'delegator_bond_less', - 'nominate', 'revoke_nomination', 'createProxyWithNonce', - 'exchangeOldForCanonical', 'createProfile', - 'deactivateProfile', 'transfer', - 'claimFlagship', 'buyVoyage', 'repairFlagships', - 'lockVoyageItems', 'claimRewards', 'setApprovalForAll', - 'lockToClaimRewards', 'claimLockedRewards', 'increaseLock', - 'standard_vote', 'flipCoin', 'listToken', 'delistToken', - 'transferFrom', 'safeTransferFrom', 'createWithPermit'}: - self.logger.info(f'contract method {contract_method_name} not yet supported for ' - f'contract {contract_address}.') + if 'input' not in transaction or len(transaction['input']) < 8: + # not enough data was provided, so probably not a contract call. exit early. + return + + contract_address = transaction['to'].lower() + await self.retrieve_and_cache_contract_abi(contract_address) + if contract_address not in self.abis or self.abis[contract_address] is None: + # without an ABI to interpret the call data or events, we can't do anything else + return + + decoded_transaction = decode_tx(contract_address, transaction['input'], self.abis[contract_address]) + + if decoded_transaction[0] == 'decode error': + if contract_address not in self.contracts_with_known_decode_errors: + self.contracts_with_known_decode_errors.append(contract_address) + decode_traceback = decoded_transaction[1] + self.logger.warning(f'Unable to decode contract interaction with contract ' + f'{contract_address} in transaction:\r\n' + f'{transaction}\r\n\r\n' + f'{decode_traceback}\r\n' + f'---- Now continuing processing the rest of the transactions ----\r\n') + else: + # successfully decoded the input data to the contract interaction + contract_method_name = decoded_transaction[0] + self.transactions[account][timestamp]['contract_method_name'] = contract_method_name + decoded_func_params = json.loads(decoded_transaction[1]) + + if contract_method_name in {'swapExactTokensForTokens', 'swapTokensForExactTokens', + 'swapExactTokensForETH', 'swapTokensForExactETH', + 'swapExactTokensForTokensSupportingFeeOnTransferTokens', + 'swapExactTokensForETHSupportingFeeOnTransferTokens', + 'swapExactETHForTokens', 'swapETHForExactTokens', + 'swapExactNativeCurrencyForTokens', 'swapExactTokensForNativeCurrency', + 'swapNativeCurrencyForExactTokens', 'swapTokensForExactNativeCurrency'}: + await self.__decode_token_swap_tx(account, transaction, contract_method_name, decoded_func_params) + elif contract_method_name in {'addLiquidity', 'addLiquidityETH', 'addLiquidityNativeCurrency', + 'addLiquiditySingleToken', + 'addLiquiditySingleNativeCurrency'}: + await self.__decode_add_liquidity_tx(account, transaction, contract_method_name, + decoded_func_params) + elif contract_method_name in {'removeLiquidity', 'removeLiquidityETH', + 'removeLiquidityETHWithPermit', 'removeLiquidityNativeCurrency'}: + await self.__decode_remove_liquidity_tx(account, transaction, contract_method_name, + decoded_func_params) + elif contract_method_name in {'deposit', 'depositWithPermit', 'depositEth', 'depositETH'}: + await self.__decode_deposit_tx(account, transaction, contract_method_name, decoded_func_params) + elif contract_method_name in {'withdraw', 'leave'}: + await self.__decode_withdraw_tx(account, transaction, contract_method_name, decoded_func_params) + elif contract_method_name in {'redeem'}: + await self.__decode_redeem_tx(account, transaction, contract_method_name, decoded_func_params) + else: + # todo: handle (and don't ignore) 'stake' contract methods + # 'claim' and 'collect' probably remain ignored for DPS. + # 'approve', 'nominate', 'revoke_nomination' permanently ignore because not financially related. + self.transactions[account][timestamp]['action'] = contract_method_name + if contract_method_name not in {'enter', 'leave', + 'increaseAmountWithPermit', + 'approve', 'claim', 'collect', + 'stake', 'unstake', 'delegate', + 'nominator_bond_more', 'nominator_bond_less', + 'execute_delegation_request', 'cancel_delegation_request', + 'schedule_delegator_bond_less', + 'schedule_revoke_delegation', + 'delegator_bond_more', 'delegator_bond_less', + 'nominate', 'revoke_nomination', 'createProxyWithNonce', + 'exchangeOldForCanonical', 'createProfile', + 'deactivateProfile', 'transfer', + 'claimFlagship', 'buyVoyage', 'repairFlagships', + 'lockVoyageItems', 'claimRewards', 'setApprovalForAll', + 'lockToClaimRewards', 'claimLockedRewards', 'increaseLock', + 'standard_vote', 'flipCoin', 'listToken', 'delistToken', + 'transferFrom', 'safeTransferFrom', 'createWithPermit'}: + self.logger.info(f'contract method {contract_method_name} not yet supported for ' + f'contract {contract_address}.') # todo: handle staking rewards # todo: handle deposit/withdraw single-sided liquidity (like WMOVR pool on Solarbeam) @@ -381,7 +382,7 @@ async def decode_logs(self, transaction): decoded_logs.append((evt_name, decoded_event_data, schema, token_address)) return decoded_logs - async def __decode_token_swap_transaction(self, account, transaction, contract_method_name, decoded_func_params): + async def __decode_token_swap_tx(self, account, transaction, contract_method_name, decoded_func_params): """Decode transaction receipts/logs from a token swap contract interaction :param account: the 'owner' account that we're analyzing transactions for @@ -412,24 +413,23 @@ async def __decode_token_swap_transaction(self, account, transaction, contract_m self.transactions[account][timestamp]['output_a_token_symbol'] = output_token_info['symbol'] if contract_method_name in {'swapExactTokensForTokens', 'swapExactTokensForETH', 'swapExactTokensForTokensSupportingFeeOnTransferTokens', - 'swapExactTokensForETHSupportingFeeOnTransferTokens'}: + 'swapExactTokensForETHSupportingFeeOnTransferTokens', + 'swapExactTokensForNativeCurrency'}: amount_in = decoded_func_params['amountIn'] amount_out = decoded_func_params['amountOutMin'] - elif contract_method_name in {"swapTokensForExactTokens", "swapTokensForExactETH"}: + elif contract_method_name in {'swapTokensForExactTokens', 'swapTokensForExactETH', + 'swapTokensForExactNativeCurrency'}: amount_in = decoded_func_params['amountInMax'] amount_out = decoded_func_params['amountOut'] - elif contract_method_name in {"swapExactETHForTokens", "swapExactNativeCurrencyForTokens"}: + elif contract_method_name in {'swapExactETHForTokens', 'swapExactNativeCurrencyForTokens'}: amount_in = int(transaction['value']) amount_out = decoded_func_params['amountOutMin'] - elif contract_method_name == "swapETHForExactTokens": + elif contract_method_name in {'swapETHForExactTokens', 'swapNativeCurrencyForExactTokens'}: amount_in = int(transaction['value']) amount_out = decoded_func_params['amountOut'] elif contract_method_name == 'addLiquiditySingleNativeCurrency': amount_in = decoded_func_params['nativeCurrencySwapInMax'] amount_out = decoded_func_params['amountSwapOut'] - elif contract_method_name == 'addLiquiditySingleToken': - amount_in = decoded_func_params['amountSwapInMax'] - amount_out = decoded_func_params['amountSwapOut'] else: self.logger.error(f'contract method {contract_method_name} not recognized') return @@ -515,17 +515,17 @@ async def __decode_token_swap_transaction(self, account, transaction, contract_m or (exact_amount_in_float < requested_input_quantity_float - input_tolerance): self.logger.warning(f"For transaction {tx_hash} with contract {contract_address} method" f" {contract_method_name}, expected log decoded input quantity" - f" {exact_amount_in_float} to be within 20% of the tx input quantity" + f" {exact_amount_in_float} to be within 20% of the requested tx input quantity" f" {requested_input_quantity_float} but it's not.") output_tolerance = requested_output_quantity_float * 0.2 # 20% each side if (exact_amount_out_float > requested_output_quantity_float + output_tolerance) \ or (exact_amount_out_float < requested_output_quantity_float - output_tolerance): self.logger.warning(f"For transaction {tx_hash} with contract {contract_address} method" f" {contract_method_name}, expected log decoded output quantity" - f" {exact_amount_out_float} to be within 20% of the tx output quantity" + f" {exact_amount_out_float} to be within 20% of the requested tx output quantity" f" {requested_output_quantity_float} but it's not.") - async def __decode_add_liquidity_transaction(self, account, transaction, contract_method_name, decoded_func_params): + async def __decode_add_liquidity_tx(self, account, transaction, contract_method_name, decoded_func_params): """Decode transaction receipts/logs from a liquidity adding contract interaction :param account: the 'owner' account that we're analyzing transactions for @@ -572,6 +572,20 @@ async def __decode_add_liquidity_transaction(self, account, transaction, contrac input_token_b = self.__get_custom_token_info('WMOVR')['address'].lower() amount_in_a = decoded_func_params['amountTokenDesired'] amount_in_b = int(transaction['value']) + elif contract_method_name == 'addLiquiditySingleToken': + # This Zenlink method specifies overall input in `amountIn` but the other func params just specify the + # precursor token swap before minting LP. No LP expected output amount is specified in the contract call. + input_token_a = decoded_func_params['path'][0].lower() + input_token_b = decoded_func_params['path'][1].lower() + amount_in_a = decoded_func_params['amountIn'] + amount_in_b = 0 # decoded_func_params['amountSwapOut'] holds the intermediate amount + amount_out = None + elif contract_method_name == 'addLiquiditySingleNativeCurrency': + # This Zenlink method's params in `decoded_func_params` only specify the swap, not the LP creation. + input_token_a = decoded_func_params['path'][0].lower() + input_token_b = decoded_func_params['path'][1].lower() + amount_in_a = None + amount_in_b = None else: self.logger.error(f'contract method {contract_method_name} not recognized') @@ -579,14 +593,20 @@ async def __decode_add_liquidity_transaction(self, account, transaction, contrac requested_input_b_quantity_float = None if input_token_a is not None: input_token_a_info = await self.__retrieve_and_cache_token_info_from_contract(input_token_a) - input_token_b_info = await self.__retrieve_and_cache_token_info_from_contract(input_token_b) self.transactions[account][timestamp]['input_a_token_name'] = input_token_a_info['name'] self.transactions[account][timestamp]['input_a_token_symbol'] = input_token_a_info['symbol'] + if 'amount_in_a' in locals() and amount_in_a is not None: + requested_input_a_quantity_float = amount_in_a / (10 ** int(input_token_a_info['decimals'])) + else: + requested_input_a_quantity_float = None + if input_token_b is not None: + input_token_b_info = await self.__retrieve_and_cache_token_info_from_contract(input_token_b) self.transactions[account][timestamp]['input_b_token_name'] = input_token_b_info['name'] self.transactions[account][timestamp]['input_b_token_symbol'] = input_token_b_info['symbol'] - - requested_input_a_quantity_float = amount_in_a / (10 ** int(input_token_a_info['decimals'])) - requested_input_b_quantity_float = amount_in_b / (10 ** int(input_token_b_info['decimals'])) + if 'amount_in_b' in locals() and amount_in_b is not None: + requested_input_b_quantity_float = amount_in_b / (10 ** int(input_token_b_info['decimals'])) + else: + requested_input_b_quantity_float = None # We only have an estimate based on the inputs so far. Use the trace logs to find # the exact liquidity quantities @@ -604,7 +624,10 @@ async def __decode_add_liquidity_transaction(self, account, transaction, contrac exact_mint_input_b_quantity_int = 0 exact_transfer_input_a_quantity_int = 0 exact_transfer_input_b_quantity_int = 0 - exact_output_quantity_int = 0 + # for rare tx types ('addLiquiditySingleToken') there could be multiple transfers back to the + # originating account of intermediate tokens. But we won't know the token address of the output LP + # until the 'Mint' event occurs. Therefore just capture the transfer values per token. + exact_transfer_output_quantities_int = {} output_token_info = None for (evt_name, decoded_event_data, schema, token_address) in decoded_logs: decoded_event_params = json.loads(decoded_event_data) @@ -624,9 +647,9 @@ async def __decode_add_liquidity_transaction(self, account, transaction, contrac if decoded_event_source_address == account: # Depositing token A or B into the LP token contract if token_address == input_token_a: - exact_transfer_input_a_quantity_int = decoded_event_quantity_int + exact_transfer_input_a_quantity_int += decoded_event_quantity_int elif token_address == input_token_b: - exact_transfer_input_b_quantity_int = decoded_event_quantity_int + exact_transfer_input_b_quantity_int += decoded_event_quantity_int else: self.logger.warning(f"For transaction {tx_hash}, contract_method {contract_method_name}" f" transfer from decoded_event_source_address" @@ -634,18 +657,22 @@ async def __decode_add_liquidity_transaction(self, account, transaction, contrac f" doesn't match original input_token_a {input_token_a} or input_token_b" f" {input_token_b} from token path.") elif decoded_event_destination_address == account: - exact_output_quantity_int = decoded_event_quantity_int + if token_address in exact_transfer_output_quantities_int: + exact_transfer_output_quantities_int[token_address] += decoded_event_quantity_int + else: + exact_transfer_output_quantities_int[token_address] = decoded_event_quantity_int else: pass # ignore all the other Transfer events elif evt_name == 'Deposit': decoded_event_quantity_int = self.__extract_quantity_from_params(transaction, evt_name, decoded_event_params) - if contract_method_name == "addLiquidityNativeCurrency": + if contract_method_name in {'addLiquidityNativeCurrency', 'addLiquidityETH', + 'addLiquiditySingleNativeCurrency'}: # Depositing token A or B into the LP token contract if token_address == input_token_a: - exact_transfer_input_a_quantity_int = decoded_event_quantity_int + exact_transfer_input_a_quantity_int += decoded_event_quantity_int elif token_address == input_token_b: - exact_transfer_input_b_quantity_int = decoded_event_quantity_int + exact_transfer_input_b_quantity_int += decoded_event_quantity_int else: self.logger.warning(f"For transaction {tx_hash}, contract_method {contract_method_name}" f" transfer from decoded_event_source_address" @@ -657,33 +684,29 @@ async def __decode_add_liquidity_transaction(self, account, transaction, contrac else: continue - # There's at least one contract that reverses the order of the tokens when it uses them to mint LP tokens. - # Therefore, compare the Transfer log event quantity to the Mint event quantity to see if they're reversed. - # If A & B transferred are >1% different, but Mint B is within 1% of Transfer A AND Mint A is within 1% of - # Transfer B then they must be swapped. - # DEX fee usually 0.3%, so comparison tolerance of 1% is sufficiently wide for that. - input_tolerance_a_int = int(exact_transfer_input_a_quantity_int * 0.01) - input_tolerance_b_int = int(exact_transfer_input_b_quantity_int * 0.01) - if ((exact_transfer_input_a_quantity_int > exact_transfer_input_b_quantity_int + input_tolerance_b_int) - or (exact_transfer_input_a_quantity_int < exact_transfer_input_b_quantity_int - input_tolerance_b_int)) \ - and (exact_mint_input_b_quantity_int > exact_transfer_input_a_quantity_int - input_tolerance_a_int) \ - and (exact_mint_input_b_quantity_int < exact_transfer_input_a_quantity_int + input_tolerance_a_int) \ - and (exact_mint_input_a_quantity_int > exact_transfer_input_b_quantity_int - input_tolerance_b_int) \ - and (exact_mint_input_a_quantity_int < exact_transfer_input_b_quantity_int + input_tolerance_b_int): - # tokens A & B were flipped during the Mint event. - temp = exact_mint_input_a_quantity_int - exact_mint_input_a_quantity_int = exact_mint_input_b_quantity_int - exact_mint_input_b_quantity_int = temp - - exact_amount_in_a_float = exact_mint_input_a_quantity_int / (10 ** int(input_token_a_info['decimals'])) - exact_amount_in_b_float = exact_mint_input_b_quantity_int / (10 ** int(input_token_b_info['decimals'])) - exact_amount_out_float = exact_output_quantity_int / (10 ** int(output_token_info['decimals'])) + exact_amount_in_a_float = exact_transfer_input_a_quantity_int / (10 ** int(input_token_a_info['decimals'])) self.transactions[account][timestamp]['input_a_quantity'] = exact_amount_in_a_float + if contract_method_name in {'addLiquiditySingleToken', 'addLiquiditySingleNativeCurrency'}: + # if an intermediate token swap occurred and then originating account provides this token for LP, it will + # appear as if the intermediate token was an original input to this transaction. That's not true for a + # 'SingleToken' LP transaction, so ignore token amounts from events. + exact_amount_in_b_float = 0 + else: + exact_amount_in_b_float = exact_transfer_input_b_quantity_int / (10 ** int(input_token_b_info['decimals'])) self.transactions[account][timestamp]['input_b_quantity'] = exact_amount_in_b_float - self.transactions[account][timestamp]['output_a_quantity'] = exact_amount_out_float - if 'output_token_info' in locals(): # if 'output_token_info' has been defined/found - self.transactions[account][timestamp]['output_a_token_name'] = output_token_info['name'] - self.transactions[account][timestamp]['output_a_token_symbol'] = output_token_info['symbol'] + + if 'output_token_info' in locals() and output_token_info is not None: + # 'output_token_info' has been defined/found + if output_token in exact_transfer_output_quantities_int: + exact_amount_out_float = exact_transfer_output_quantities_int[output_token] \ + / (10 ** int(output_token_info['decimals'])) + self.transactions[account][timestamp]['output_a_quantity'] = exact_amount_out_float + self.transactions[account][timestamp]['output_a_token_name'] = output_token_info['name'] + self.transactions[account][timestamp]['output_a_token_symbol'] = output_token_info['symbol'] + else: + self.logger.warning(f"For transaction {tx_hash} with contract {contract_address}, method" + f" '{contract_method_name}', the 'Mint' event token output address {output_token}" + f" doesn't match any token transfer back to the originating account {account}.") if input_token_a == '0xf37626e2284742305858052615e94b380b23b3b7' \ or input_token_a_info['name'] == 'TreasureMaps': @@ -692,34 +715,36 @@ async def __decode_add_liquidity_transaction(self, account, transaction, contrac # validate that the exact amounts are somewhat similar to the contract input values # (to make sure we're matching up the right values). - input_tolerance = exact_amount_in_a_float * 0.2 # 20% each side - if (exact_amount_in_a_float > requested_input_a_quantity_float + input_tolerance) \ - or (exact_amount_in_a_float < requested_input_a_quantity_float - input_tolerance): - self.logger.warning(f"For transaction {tx_hash} with contract {contract_address} method" - f" '{contract_method_name}', expected log decoded LP input A quantity" - f" {exact_amount_in_a_float} to be within 20% of the tx input quantity" - f" {requested_input_a_quantity_float} but it's not.") - input_tolerance = exact_amount_in_b_float * 0.2 # 20% each side - if (exact_amount_in_b_float > requested_input_b_quantity_float + input_tolerance) \ - or (exact_amount_in_b_float < requested_input_b_quantity_float - input_tolerance): - self.logger.warning(f"For transaction {tx_hash} with contract {contract_address} method" - f" '{contract_method_name}', expected log decoded LP input B quantity" - f" {exact_amount_in_b_float} to be within 20% of the tx input quantity" - f" {requested_input_b_quantity_float} but it's not.") - if 'amount_out' in locals(): # if variable 'amount_out' has been defined + if 'requested_input_a_quantity_float' in locals() and requested_input_a_quantity_float is not None: + input_a_tolerance = exact_amount_in_a_float * 0.2 # 20% each side + if (exact_amount_in_a_float > requested_input_a_quantity_float + input_a_tolerance) \ + or (exact_amount_in_a_float < requested_input_a_quantity_float - input_a_tolerance): + self.logger.warning(f"For transaction {tx_hash} with contract {contract_address} method" + f" '{contract_method_name}', expected log decoded LP input A quantity" + f" {exact_amount_in_a_float} to be within 20% of the requested tx input quantity" + f" {requested_input_a_quantity_float} but it's not.") + if 'requested_input_b_quantity_float' in locals() and requested_input_b_quantity_float is not None: + input_b_tolerance = exact_amount_in_b_float * 0.2 # 20% each side + if (exact_amount_in_b_float > requested_input_b_quantity_float + input_b_tolerance) \ + or (exact_amount_in_b_float < requested_input_b_quantity_float - input_b_tolerance): + self.logger.warning(f"For transaction {tx_hash} with contract {contract_address} method" + f" '{contract_method_name}', expected log decoded LP input B quantity" + f" {exact_amount_in_b_float} to be within 20% of the requested tx input quantity" + f" {requested_input_b_quantity_float} but it's not.") + if 'amount_out' in locals() and amount_out is not None: # if variable 'amount_out' has been defined output_tolerance = exact_amount_out_float * 0.2 # 20% each side if (exact_amount_out_float > amount_out + output_tolerance) \ or (exact_amount_out_float < amount_out - output_tolerance): self.logger.warning(f"For transaction {tx_hash} with contract {contract_address} method" f" '{contract_method_name}', expected log decoded LP output quantity" - f" {exact_amount_out_float} to be within 20% of the tx output quantity" + f" {exact_amount_out_float} to be within 20% of the requested tx output quantity" f" {amount_out} but it's not.") else: # There was no output info in the original request. Therefore, nothing to compare to. pass - async def __decode_remove_liquidity_transaction(self, account, transaction, contract_method_name, - decoded_func_params): + async def __decode_remove_liquidity_tx(self, account, transaction, contract_method_name, + decoded_func_params): """Decode transaction receipts/logs from a liquidity removing contract interaction :param account: the 'owner' account that we're analyzing transactions for @@ -817,7 +842,7 @@ async def __decode_remove_liquidity_transaction(self, account, transaction, cont elif evt_name == 'Withdrawal': decoded_event_quantity_int = \ self.__extract_quantity_from_params(transaction, evt_name, decoded_event_params) - if contract_method_name == "removeLiquidityNativeCurrency": + if contract_method_name in {'removeLiquidityNativeCurrency', 'removeLiquidityETH'}: # Receiving token A or B from breaking up LP if token_address == output_token_a: exact_transfer_output_a_quantity_int = decoded_event_quantity_int @@ -869,24 +894,24 @@ async def __decode_remove_liquidity_transaction(self, account, transaction, cont or (exact_amount_in_float < requested_input_quantity_float - input_tolerance): self.logger.warning(f"For transaction {tx_hash} with contract {contract_address} method" f" '{contract_method_name}', expected log decoded LP input quantity" - f" {exact_amount_in_float} to be within 20% of the tx input quantity" + f" {exact_amount_in_float} to be within 20% of the requested tx input quantity" f" {requested_input_quantity_float} but it's not.") output_tolerance = exact_amount_out_a_float * 0.2 # 20% each side if (exact_amount_out_a_float > requested_output_a_quantity_float + output_tolerance) \ or (exact_amount_out_a_float < requested_output_a_quantity_float - output_tolerance): self.logger.warning(f"For transaction {tx_hash} with contract {contract_address} method" f" '{contract_method_name}', expected log decoded LP output A quantity" - f" {exact_amount_out_a_float} to be within 20% of the tx output quantity" + f" {exact_amount_out_a_float} to be within 20% of the requested tx output quantity" f" {requested_output_a_quantity_float} but it's not.") output_tolerance = exact_amount_out_b_float * 0.2 # 20% each side if (exact_amount_out_b_float > requested_output_b_quantity_float + output_tolerance) \ or (exact_amount_out_b_float < requested_output_b_quantity_float - output_tolerance): self.logger.warning(f"For transaction {tx_hash} with contract {contract_address} method" f" '{contract_method_name}', expected log decoded LP output B quantity" - f" {exact_amount_out_b_float} to be within 20% of the tx output quantity" + f" {exact_amount_out_b_float} to be within 20% of the requested tx output quantity" f" {requested_output_b_quantity_float} but it's not.") - async def __decode_deposit_transaction(self, account, transaction, contract_method_name, decoded_func_params): + async def __decode_deposit_tx(self, account, transaction, contract_method_name, decoded_func_params): """Decode transaction receipts/logs from a deposit contract interaction Possible contracts using a 'deposit' method: * Deposit MOVR into WMOVR token contract to receive WMOVR @@ -952,7 +977,7 @@ async def __decode_deposit_transaction(self, account, transaction, contract_meth self.transactions[account][timestamp_key]['output_a_token_name'] = token_info['name'] self.transactions[account][timestamp_key]['output_a_token_symbol'] = token_info['symbol'] - async def __decode_withdraw_transaction(self, account, transaction, contract_method_name, decoded_func_params): + async def __decode_withdraw_tx(self, account, transaction, contract_method_name, decoded_func_params): """Decode transaction receipts/logs from a withdraw contract interaction :param account: the 'owner' account that we're analyzing transactions for @@ -1134,7 +1159,7 @@ async def __decode_withdraw_transaction(self, account, transaction, contract_met # self.transactions[account][timestamp_key]['output_a_token_name'] = output_token_info['name'] # self.transactions[account][timestamp_key]['output_a_token_symbol'] = output_token_info['symbol'] - async def __decode_redeem_transaction(self, account, transaction, contract_method_name, decoded_func_params): + async def __decode_redeem_tx(self, account, transaction, contract_method_name, decoded_func_params): """Decode transaction receipts/logs from a redeem contract interaction :param account: the 'owner' account that we're analyzing transactions for @@ -1235,7 +1260,7 @@ def __extract_quantity_from_params(self, transaction, method_name, decoded_event # decoded_event_quantity_int = decoded_event_params[key] keyword_found = True continue - if not keyword_found: + if not keyword_found and verbose: self.logger.warning(f"For transaction {tx_hash} with contract {contract_address} and method" f" '{method_name}', no param keyword found for quantity. This indicates subscrape" f" doesn't handle this particular contract implementation yet." @@ -1275,7 +1300,7 @@ def __extract_source_address_from_params(self, transaction, method_name, decoded # There's no "source" for the "Deposit" event decoded_event_source_address = transaction_source_address keyword_found = True - if not keyword_found: + if not keyword_found and verbose: self.logger.warning(f"For transaction {tx_hash} with contract {contract_address} and method" f" '{method_name}', no param keyword found for source address. This indicates" f" subscrape doesn't handle this particular contract implementation yet." @@ -1311,7 +1336,7 @@ def __extract_destination_address_from_params(self, transaction, method_name, de decoded_event_destination_address = decoded_event_params[key].lower() keyword_found = True continue - if not keyword_found: + if not keyword_found and verbose: self.logger.warning(f"For transaction {tx_hash} with contract {contract_address} and method" f" '{method_name}', no param keyword found for destination address. This indicates" f" subscrape doesn't handle this particular contract implementation yet." diff --git a/tests/test_moonbeam_scraper.py b/tests/test_moonbeam_scraper.py index 626224f..e3f12a6 100644 --- a/tests/test_moonbeam_scraper.py +++ b/tests/test_moonbeam_scraper.py @@ -66,7 +66,7 @@ async def test__no_entries_returned(): @pytest.mark.skipif(new_only or test_scope not in {'all', 'swaps'}, reason="reduce API queries during debug/dev") @pytest.mark.asyncio -async def test__decode_token_swap_transaction_Solarbeam__swapExactTokensForTokens(): +async def test__decode_token_swap_tx_Solarbeam__swapExactTokensForTokens(): # also testing: swap transaction on Solarbeam DEX. filter range on "timeStamp" test_acct = "0xBa4123F4b2da090aeCef69Fd0946D42Ecd4C788E" config = { @@ -80,7 +80,7 @@ async def test__decode_token_swap_transaction_Solarbeam__swapExactTokensForToken } } - logging.info(f"begin 'test__decode_token_swap_transaction_Solarbeam__swapExactTokensForTokens'" + logging.info(f"begin 'test__decode_token_swap_tx_Solarbeam__swapExactTokensForTokens'" f" scraping at {time.strftime('%X')}") items_scraped = await subscrape.scrape(config) assert len(items_scraped) >= 2 @@ -109,7 +109,7 @@ async def test__decode_token_swap_transaction_Solarbeam__swapExactTokensForToken @pytest.mark.skipif(new_only or test_scope not in {'all', 'swaps'}, reason="reduce API queries during debug/dev") @pytest.mark.asyncio -async def test__decode_token_swap_transaction_Zenlink__swapExactTokensForTokens(): +async def test__decode_token_swap_tx_Zenlink__swapExactTokensForTokens(): # also testing: swap transaction on Zenlink DEX. two hops. test_acct = "0x2b46c40b6d1f4d77a6719f92864cf40bb049e366" config = { @@ -123,7 +123,7 @@ async def test__decode_token_swap_transaction_Zenlink__swapExactTokensForTokens( } } - logging.info(f"begin 'test__decode_token_swap_transaction_Zenlink__swapExactTokensForTokens'" + logging.info(f"begin 'test__decode_token_swap_tx_Zenlink__swapExactTokensForTokens'" f" scraping at {time.strftime('%X')}") items_scraped = await subscrape.scrape(config) assert len(items_scraped) >= 1 @@ -145,7 +145,118 @@ async def test__decode_token_swap_transaction_Zenlink__swapExactTokensForTokens( @pytest.mark.skipif(new_only or test_scope not in {'all', 'swaps'}, reason="reduce API queries during debug/dev") @pytest.mark.asyncio -async def test__decode_token_swap_transaction__swapExactTokensForETH(): +async def test__decode_token_swap_tx_Zenlink__swapTokensForExactTokens(): + # also testing: swap transaction on Zenlink DEX. + test_acct = "0x79a8a9ff5717248a5fc0f00cc9920bd4bba77823" + config = { + "moonriver": { + "account_transactions": { + "accounts": [ + test_acct + ], + "_filter": [{"blockNumber": [{"==": 1224237}]}] + } + } + } + + logging.info(f"begin 'test__decode_token_swap_tx_Zenlink__swapTokensForExactTokens'" + f" scraping at {time.strftime('%X')}") + items_scraped = await subscrape.scrape(config) + assert len(items_scraped) >= 1 + transactions = _get_archived_transactions_from_json(test_acct, 'moonriver') + transaction_found = False + for timestamp in transactions: + tx = transactions[timestamp] + if tx['hash'] == '0x3b10057c7d19702732e48d7ec63f1b4f2299dccef79780263d28edbe61e2ed29': + transaction_found = True + logging.debug(f'for hash {tx["hash"]} the full transaction is {tx}') + assert tx['input_a_token_symbol'] == 'USDC' + assert tx['output_a_token_symbol'] == 'WBTC' + _assert_value_within_range(tx['input_a_quantity'], 462.79385) + _assert_value_within_range(tx['output_a_quantity'], 0.01) + else: + continue + assert transaction_found + + +@pytest.mark.skipif(new_only or test_scope not in {'all', 'swaps'}, reason="reduce API queries during debug/dev") +@pytest.mark.asyncio +async def test__decode_token_swap_tx_Huckleberry__swapTokensForExactTokens(): + # also testing: swap transaction on Huckleberry DEX. "timeStamp" range in <= >= order. Two hop. + test_acct = "0xfc2f3c2b6872d6ad347e78f096026274326ab081" + config = { + "moonriver": { + "account_transactions": { + "accounts": [ + test_acct + ], + "_filter": [{"timeStamp": [{"<=": 1672581700}, {">=": 1672581680}]}] + } + } + } + + logging.info(f"begin 'test__decode_token_swap_tx_Huckleberry__swapTokensForExactTokens'" + f" scraping at {time.strftime('%X')}") + items_scraped = await subscrape.scrape(config) + assert len(items_scraped) >= 1 + transactions = _get_archived_transactions_from_json(test_acct, 'moonriver') + transaction_found = False + for timestamp in transactions: + tx = transactions[timestamp] + if tx['hash'] == '0xbdacc65d82e8e7273e42ba3c0c33d3ec884809087c718fcd4076709577cbd2f7': + transaction_found = True + logging.debug(f'for hash {tx["hash"]} the full transaction is {tx}') + assert tx['input_a_token_symbol'] == 'BNB.m' + assert tx['output_a_token_symbol'] == 'FTM.m' + _assert_value_within_range(tx['input_a_quantity'], 0.038442263813382655) + _assert_value_within_range(tx['output_a_quantity'], 51.8294) + else: + continue + assert transaction_found + + +@pytest.mark.skipif(new_only or test_scope not in {'all', 'swaps'}, reason="reduce API queries during debug/dev") +@pytest.mark.asyncio +async def test__decode_token_swap_tx_Huckleberry__swapExactTokensForTokensSupportingFeeOnTransferTokens(): + # also testing: swap transaction on Huckleberry DEX. filter '==' on blockNumber + test_acct = "0x85cf0915c8d3695b03da739e3eaefd5388eb5eef" + config = { + "moonriver": { + "account_transactions": { + "accounts": [ + test_acct + ], + "_filter": [{"blockNumber": [{"==": 3313868}]}] + } + } + } + + logging.info(f"begin 'test__decode_token_swap_tx_Huckleberry__swapExactTokensForTokensSupportingFeeOnTransferTokens'" + f" scraping at {time.strftime('%X')}") + items_scraped = await subscrape.scrape(config) + logging.info('Note: for this specific unit test, subscrape will emit a warning like "expected log decoded output' + ' quantity 3.02e-06 to be within 20% of the requested tx output quantity 0.0 but its not."' + ' Evidently the original contract call set amountOutMin=0 so this is expected behavior.') + assert len(items_scraped) >= 1 + transactions = _get_archived_transactions_from_json(test_acct, 'moonriver') + transaction_found = False + for timestamp in transactions: + tx = transactions[timestamp] + if tx['hash'] == '0xbc065d9aa4fd90a0fb1df3ddfaab633cd3866e5dc069c6ce47f33593d3aa8972': + transaction_found = True + logging.debug(f'for hash {tx["hash"]} the full transaction is {tx}') + assert tx['input_a_token_symbol'] == 'WMOVR' + assert tx['output_a_token_symbol'] == 'BTC.m' + _assert_value_within_range(tx['input_a_quantity'], 0.008232571613605909) + _assert_value_within_range(tx['output_a_quantity'], 0.00000302) + else: + continue + assert transaction_found + + +@pytest.mark.skipif(new_only or test_scope not in {'all', 'swaps'}, reason="reduce API queries during debug/dev") +@pytest.mark.asyncio +async def test__decode_token_swap_tx_Solarbeam__swapExactTokensForETH(): # also testing: swap transaction on Solarbeam DEX. filter '==' on blockNumber test_acct = "0x299cd1c791464827ddfb147612244a2c59da91a0" config = { @@ -159,7 +270,7 @@ async def test__decode_token_swap_transaction__swapExactTokensForETH(): } } - logging.info(f"begin 'test__decode_token_swap_transaction__swapExactTokensForETH'" + logging.info(f"begin 'test__decode_token_swap_tx_Solarbeam__swapExactTokensForETH'" f" scraping at {time.strftime('%X')}") items_scraped = await subscrape.scrape(config) assert len(items_scraped) >= 1 @@ -181,7 +292,7 @@ async def test__decode_token_swap_transaction__swapExactTokensForETH(): @pytest.mark.skipif(new_only or test_scope not in {'all', 'swaps'}, reason="reduce API queries during debug/dev") @pytest.mark.asyncio -async def test__decode_token_swap_transaction__swapExactETHForTokens(): +async def test__decode_token_swap_tx_Huckleberry__swapExactETHForTokens(): # also testing: swap transaction on Huckleberry DEX. "blockNumber" range in <= >= order. two hop tx test_acct = "0x8e7fbb49f436d0e8a50c02f631e729a57a9a0aca" config = { @@ -195,7 +306,7 @@ async def test__decode_token_swap_transaction__swapExactETHForTokens(): } } - logging.info(f"begin 'test__decode_token_swap_transaction__swapExactETHForTokens'" + logging.info(f"begin 'test__decode_token_swap_tx_Huckleberry__swapExactETHForTokens'" f" scraping at {time.strftime('%X')}") items_scraped = await subscrape.scrape(config) assert len(items_scraped) >= 1 @@ -217,21 +328,21 @@ async def test__decode_token_swap_transaction__swapExactETHForTokens(): @pytest.mark.skipif(new_only or test_scope not in {'all', 'swaps'}, reason="reduce API queries during debug/dev") @pytest.mark.asyncio -async def test__decode_token_swap_transaction__swapExactTokensForTokensSupportingFeeOnTransferTokens(): - # also testing: swap transaction on Huckleberry DEX. filter '==' on blockNumber - test_acct = "0x85cf0915c8d3695b03da739e3eaefd5388eb5eef" +async def test__decode_token_swap_tx_Huckleberry__swapTokensForExactETH(): + # also testing: swap transaction on Huckleberry DEX. "blockNumber" range in <= >= with equal block number. Two hops. + test_acct = "0xfc2f3c2b6872d6ad347e78f096026274326ab081" config = { "moonriver": { "account_transactions": { "accounts": [ test_acct ], - "_filter": [{"blockNumber": [{"==": 3313868}]}] + "_filter": [{"blockNumber": [{"<=": 3427502}, {">=": 3427502}]}] } } } - logging.info(f"begin 'test__decode_token_swap_transaction__swapExactTokensForTokensSupportingFeeOnTransferTokens'" + logging.info(f"begin 'test__decode_token_swap_tx_Huckleberry__swapTokensForExactETH'" f" scraping at {time.strftime('%X')}") items_scraped = await subscrape.scrape(config) assert len(items_scraped) >= 1 @@ -239,35 +350,108 @@ async def test__decode_token_swap_transaction__swapExactTokensForTokensSupportin transaction_found = False for timestamp in transactions: tx = transactions[timestamp] - if tx['hash'] == '0xbc065d9aa4fd90a0fb1df3ddfaab633cd3866e5dc069c6ce47f33593d3aa8972': + if tx['hash'] == '0x1b036cd6d161622a5f610680b2fc15aee96103a4a884d9338ecb59f65b31753f': + transaction_found = True + logging.debug(f'for hash {tx["hash"]} the full transaction is {tx}') + assert tx['input_a_token_symbol'] == 'FTM.m' + assert tx['output_a_token_symbol'] == 'WMOVR' + _assert_value_within_range(tx['input_a_quantity'], 1.507652227912820355) + _assert_value_within_range(tx['output_a_quantity'], 0.06) + else: + continue + assert transaction_found + + +# @pytest.mark.skipif(not new_only or test_scope not in {'all', 'swaps'}, reason="reduce API queries during debug/dev") +# @pytest.mark.asyncio +# async def test__decode_token_swap_tx__swapETHForExactTokens(): +# # also testing: +# test_acct = "XXXXXXXXXXXXX" +# config = { +# "moonriver": { +# "account_transactions": { +# "accounts": [ +# test_acct +# ], +# "_filter": [{"blockNumber": [{"==": XXXXXXX}]}] +# } +# } +# } +# +# logging.info(f"begin 'test__decode_token_swap_tx__swapETHForExactTokens'" +# f" scraping at {time.strftime('%X')}") +# items_scraped = await subscrape.scrape(config) +# assert len(items_scraped) >= 1 +# transactions = _get_archived_transactions_from_json(test_acct, 'moonriver') +# transaction_found = False +# for timestamp in transactions: +# tx = transactions[timestamp] +# if tx['hash'] == 'XXXXXXXXXXXXXXXXX': +# transaction_found = True +# logging.debug(f'for hash {tx["hash"]} the full transaction is {tx}') +# assert tx['input_a_token_symbol'] == 'XXXX' +# assert tx['output_a_token_symbol'] == 'XXXX' +# _assert_value_within_range(tx['input_a_quantity'], 1111111) +# _assert_value_within_range(tx['output_a_quantity'], 1111111) +# else: +# continue +# assert transaction_found + + +@pytest.mark.skipif(new_only or test_scope not in {'all', 'swaps'}, reason="reduce API queries during debug/dev") +@pytest.mark.asyncio +async def test__decode_token_swap_tx_Zenlink__swapExactNativeCurrencyForTokens(): + # also testing: swap transaction on Zenlink DEX. + test_acct = "0xe1fa699860444be91d366c21de8fef56e3dec77a" + config = { + "moonriver": { + "account_transactions": { + "accounts": [ + test_acct + ], + "_filter": [{"blockNumber": [{"==": 2971916}]}] + } + } + } + + logging.info(f"begin 'test__decode_token_swap_tx_Zenlink__swapExactNativeCurrencyForTokens'" + f" scraping at {time.strftime('%X')}") + items_scraped = await subscrape.scrape(config) + assert len(items_scraped) >= 1 + transactions = _get_archived_transactions_from_json(test_acct, 'moonriver') + transaction_found = False + for timestamp in transactions: + tx = transactions[timestamp] + if tx['hash'] == '0x20f12bed5fa0c6c61a037196ec344b24e6f473dc54dd932492c7a7643eb33251': transaction_found = True logging.debug(f'for hash {tx["hash"]} the full transaction is {tx}') assert tx['input_a_token_symbol'] == 'WMOVR' - assert tx['output_a_token_symbol'] == 'BTC.m' - _assert_value_within_range(tx['input_a_quantity'], 0.008232571613605909) - _assert_value_within_range(tx['output_a_quantity'], 0.00000302) + assert tx['output_a_token_symbol'] == 'xcKSM' + _assert_value_within_range(tx['input_a_quantity'], 11.1) + _assert_value_within_range(tx['output_a_quantity'], 3.727989001715) else: continue assert transaction_found + @pytest.mark.skipif(new_only or test_scope not in {'all', 'swaps'}, reason="reduce API queries during debug/dev") @pytest.mark.asyncio -async def test__decode_token_swap_transaction__swapTokensForExactETH(): - # also testing: swap transaction on Huckleberry DEX. "blockNumber" range in <= >= with equal block number. Two hops. - test_acct = "0xfc2f3c2b6872d6ad347e78f096026274326ab081" +async def test__decode_token_swap_tx_Zenlink__swapExactTokensForNativeCurrency(): + # also testing: swap transaction on Zenlink DEX. + test_acct = "0x335391f2006c318dc318230bdec020031d7dac75" config = { "moonriver": { "account_transactions": { "accounts": [ test_acct ], - "_filter": [{"blockNumber": [{"<=": 3427502}, {">=": 3427502}]}] + "_filter": [{"blockNumber": [{"==": 1221284}]}] } } } - logging.info(f"begin 'test__decode_token_swap_transaction__swapTokensForExactETH'" + logging.info(f"begin 'test__decode_token_swap_tx_Zenlink__swapExactTokensForNativeCurrency'" f" scraping at {time.strftime('%X')}") items_scraped = await subscrape.scrape(config) assert len(items_scraped) >= 1 @@ -275,13 +459,13 @@ async def test__decode_token_swap_transaction__swapTokensForExactETH(): transaction_found = False for timestamp in transactions: tx = transactions[timestamp] - if tx['hash'] == '0x1b036cd6d161622a5f610680b2fc15aee96103a4a884d9338ecb59f65b31753f': + if tx['hash'] == '0xd1139087f55ac1b9d377c408afed9ca86478b725f7266d7c5d15e22ba5cd1a81': transaction_found = True logging.debug(f'for hash {tx["hash"]} the full transaction is {tx}') - assert tx['input_a_token_symbol'] == 'FTM.m' + assert tx['input_a_token_symbol'] == 'USDC' assert tx['output_a_token_symbol'] == 'WMOVR' - _assert_value_within_range(tx['input_a_quantity'], 1.507652227912820355) - _assert_value_within_range(tx['output_a_quantity'], 0.06) + _assert_value_within_range(tx['input_a_quantity'], 1185.327082) + _assert_value_within_range(tx['output_a_quantity'], 6.056467880187909967) else: continue assert transaction_found @@ -289,21 +473,21 @@ async def test__decode_token_swap_transaction__swapTokensForExactETH(): @pytest.mark.skipif(new_only or test_scope not in {'all', 'swaps'}, reason="reduce API queries during debug/dev") @pytest.mark.asyncio -async def test__decode_token_swap_transaction__swapTokensForExactTokens(): - # also testing: swap transaction on Huckleberry DEX. "timeStamp" range in <= >= order. Two hop. - test_acct = "0xfc2f3c2b6872d6ad347e78f096026274326ab081" +async def test__decode_token_swap_tx_Zenlink__swapTokensForExactNativeCurrency(): + # also testing: swap transaction on Zenlink DEX. + test_acct = "0x8fbebbb93019dc8e56630507e206d8ad00842d41" config = { "moonriver": { "account_transactions": { "accounts": [ test_acct ], - "_filter": [{"timeStamp": [{"<=": 1672581700}, {">=": 1672581680}]}] + "_filter": [{"blockNumber": [{"==": 1219176}]}] } } } - logging.info(f"begin 'test__decode_token_swap_transaction__swapTokensForExactTokens'" + logging.info(f"begin 'test__decode_token_swap_tx_Zenlink__swapTokensForExactNativeCurrency'" f" scraping at {time.strftime('%X')}") items_scraped = await subscrape.scrape(config) assert len(items_scraped) >= 1 @@ -311,13 +495,13 @@ async def test__decode_token_swap_transaction__swapTokensForExactTokens(): transaction_found = False for timestamp in transactions: tx = transactions[timestamp] - if tx['hash'] == '0xbdacc65d82e8e7273e42ba3c0c33d3ec884809087c718fcd4076709577cbd2f7': + if tx['hash'] == '0xf72916c26906789b1b6768d8347652e47d042181f8a1545ebbe80018fa2ce111': transaction_found = True logging.debug(f'for hash {tx["hash"]} the full transaction is {tx}') - assert tx['input_a_token_symbol'] == 'BNB.m' - assert tx['output_a_token_symbol'] == 'FTM.m' - _assert_value_within_range(tx['input_a_quantity'], 0.038442263813382655) - _assert_value_within_range(tx['output_a_quantity'], 51.8294) + assert tx['input_a_token_symbol'] == 'ZLK' + assert tx['output_a_token_symbol'] == 'WMOVR' + _assert_value_within_range(tx['input_a_quantity'], 963.663306508578689591) + _assert_value_within_range(tx['output_a_quantity'], 10) else: continue assert transaction_found @@ -325,21 +509,21 @@ async def test__decode_token_swap_transaction__swapTokensForExactTokens(): @pytest.mark.skipif(new_only or test_scope not in {'all', 'swaps'}, reason="reduce API queries during debug/dev") @pytest.mark.asyncio -async def test__decode_token_swap_transaction__swapExactNativeCurrencyForTokens(): +async def test__decode_token_swap_tx_Zenlink__swapNativeCurrencyForExactTokens(): # also testing: swap transaction on Zenlink DEX. - test_acct = "0xe1fa699860444be91d366c21de8fef56e3dec77a" + test_acct = "0x9191c75bb0681a71f7d254484f2eb749c8934dac" config = { "moonriver": { "account_transactions": { "accounts": [ test_acct ], - "_filter": [{"blockNumber": [{"==": 2971916}]}] + "_filter": [{"blockNumber": [{"==": 1218897}]}] } } } - logging.info(f"begin 'test__decode_token_swap_transaction__swapExactNativeCurrencyForTokens'" + logging.info(f"begin 'test__decode_token_swap_tx_Zenlink__swapNativeCurrencyForExactTokens'" f" scraping at {time.strftime('%X')}") items_scraped = await subscrape.scrape(config) assert len(items_scraped) >= 1 @@ -347,13 +531,13 @@ async def test__decode_token_swap_transaction__swapExactNativeCurrencyForTokens( transaction_found = False for timestamp in transactions: tx = transactions[timestamp] - if tx['hash'] == '0x20f12bed5fa0c6c61a037196ec344b24e6f473dc54dd932492c7a7643eb33251': + if tx['hash'] == '0xc28ed3d98f9e6404828b73280eda5db1cc19aaca70dac03fed62bfdbf2273431': transaction_found = True logging.debug(f'for hash {tx["hash"]} the full transaction is {tx}') assert tx['input_a_token_symbol'] == 'WMOVR' - assert tx['output_a_token_symbol'] == 'xcKSM' - _assert_value_within_range(tx['input_a_quantity'], 11.1) - _assert_value_within_range(tx['output_a_quantity'], 3.727989001715) + assert tx['output_a_token_symbol'] == 'ZLK' + _assert_value_within_range(tx['input_a_quantity'], 10.591632213710223118) + _assert_value_within_range(tx['output_a_quantity'], 1000) else: continue assert transaction_found @@ -362,7 +546,7 @@ async def test__decode_token_swap_transaction__swapExactNativeCurrencyForTokens( @pytest.mark.skipif(not new_only or test_scope not in {'kbtc'}, reason="reduce API queries during debug/dev") @pytest.mark.asyncio -async def test__decode_token_swap_transaction__swap(): +async def test__decode_token_swap_tx_SolarbeamStableSwap__swap(): # also testing: StableSwap AMM on Solarbeam DEX. test_acct = "0x27e6a60146c5341d2e5577b219a2961f2d180579" config = { @@ -376,7 +560,7 @@ async def test__decode_token_swap_transaction__swap(): } } - logging.info(f"begin 'test__decode_token_swap_transaction__swap'" + logging.info(f"begin 'test__decode_token_swap_tx_SolarbeamStableSwap__swap'" f" scraping at {time.strftime('%X')}") items_scraped = await subscrape.scrape(config) assert len(items_scraped) >= 1 @@ -396,8 +580,9 @@ async def test__decode_token_swap_transaction__swap(): assert transaction_found +# @pytest.mark.skipif(not new_only or test_scope not in {'all', 'swaps'}, reason="reduce API queries during debug/dev") # @pytest.mark.asyncio -# async def test__decode_token_swap_transaction__(): +# async def test__decode_token_swap_tx__(): # # also testing: swap transaction on Zenlink DEX. # test_acct = "XXXXXXXXXXXXX" # config = { @@ -411,7 +596,7 @@ async def test__decode_token_swap_transaction__swap(): # } # } # -# logging.info(f"begin 'test__decode_token_swap_transaction__'" +# logging.info(f"begin 'test__decode_token_swap_tx__'" # f" scraping at {time.strftime('%X')}") # items_scraped = await subscrape.scrape(config) # assert len(items_scraped) >= 1 @@ -437,7 +622,7 @@ async def test__decode_token_swap_transaction__swap(): @pytest.mark.skipif(new_only or test_scope not in {'all', 'liquidity'}, reason="reduce API queries during debug/dev") @pytest.mark.asyncio -async def test__decode_add_liquidity_transaction__addLiquidity(): +async def test__decode_add_liquidity_tx_Zenlink__addLiquidity(): # also testing: Zenlink transactions test_acct = "0xa3d2c4af7496069d264e9357f9f39f79a656a1c8" config = { @@ -451,7 +636,7 @@ async def test__decode_add_liquidity_transaction__addLiquidity(): } } - logging.info(f"begin 'test__decode_add_liquidity_transaction__addLiquidity'" + logging.info(f"begin 'test__decode_add_liquidity_tx_Zenlink__addLiquidity'" f" scraping at {time.strftime('%X')}") items_scraped = await subscrape.scrape(config) assert len(items_scraped) >= 1 @@ -473,10 +658,48 @@ async def test__decode_add_liquidity_transaction__addLiquidity(): assert transaction_found +@pytest.mark.skipif(new_only or test_scope not in {'all', 'liquidity'}, reason="reduce API queries during debug/dev") +@pytest.mark.asyncio +async def test__decode_add_liquidity_tx_Solarbeam__addLiquidityETH(): + # also testing: + test_acct = "0xc794047d59f11bef4035241ab403c9a419d7da8d" + config = { + "moonriver": { + "account_transactions": { + "accounts": [ + test_acct + ], + "_filter": [{"blockNumber": [{"==": 3513919}]}] + } + } + } + + logging.info(f"begin 'test__decode_add_liquidity_tx_Solarbeam__addLiquidityETH'" + f" scraping at {time.strftime('%X')}") + items_scraped = await subscrape.scrape(config) + assert len(items_scraped) >= 1 + transactions = _get_archived_transactions_from_json(test_acct, 'moonriver') + transaction_found = False + for timestamp in transactions: + tx = transactions[timestamp] + if tx['hash'] == '0x6c56b95468fc04805d4d714ddd8edd46e61548a095460b7f912de7303383c3bf': + transaction_found = True + logging.debug(f'for hash {tx["hash"]} the full transaction is {tx}') + assert tx['input_a_token_symbol'] == 'MFAM' + assert tx['input_b_token_symbol'] == 'WMOVR' + assert tx['output_a_token_symbol'] == 'SLP' + _assert_value_within_range(tx['input_a_quantity'], 109972.053560515580176075) + _assert_value_within_range(tx['input_b_quantity'], 23.840224856355448048) + _assert_value_within_range(tx['output_a_quantity'], 1520.682022451843464574) + else: + continue + assert transaction_found + + @pytest.mark.skipif(new_only or test_scope not in {'all', 'liquidity'}, reason="reduce API queries during debug/dev") @pytest.mark.asyncio -async def test__decode_add_liquidity_transaction__addLiquidityNativeCurrency(): +async def test__decode_add_liquidity_tx_Zenlink__addLiquidityNativeCurrency(): # also testing: Zenlink transactions test_acct = "0x725f5b2e92164c38ef25a70f0807b71a7f0e770a" config = { @@ -490,7 +713,7 @@ async def test__decode_add_liquidity_transaction__addLiquidityNativeCurrency(): } } - logging.info(f"begin 'test__decode_add_liquidity_transaction__addLiquidityNativeCurrency'" + logging.info(f"begin 'test__decode_add_liquidity_tx_Zenlink__addLiquidityNativeCurrency'" f" scraping at {time.strftime('%X')}") items_scraped = await subscrape.scrape(config) assert len(items_scraped) >= 1 @@ -512,46 +735,85 @@ async def test__decode_add_liquidity_transaction__addLiquidityNativeCurrency(): assert transaction_found -# this Zenlink ETH/vETH -> LP requires a large refactor. Therefore ignore for now. -# @pytest.mark.asyncio -# async def test__decode_add_liquidity_transaction__addLiquiditySingleToken(): -# # also testing: Zenlink exchange ETH for vETH and receive ZLK-LP -# test_acct = "0x0a83985e4a6e8dae2b67bed4f2d9268f6806ce00" -# config = { -# "moonriver": { -# "account_transactions": { -# "accounts": [ -# test_acct -# ], -# "_filter": [{"blockNumber": [{"==": 2411562}]}] -# } -# } -# } -# -# logging.info(f"begin 'test__decode_add_liquidity_transaction__addLiquiditySingleToken'" -# f" scraping at {time.strftime('%X')}") -# items_scraped = await subscrape.scrape(config) -# assert len(items_scraped) >= 1 -# transactions = _get_archived_transactions_from_json(test_acct, 'moonriver') -# transaction_found = False -# for timestamp in transactions: -# tx = transactions[timestamp] -# if tx['hash'] == '0x311d38bf46501961abdee8c9d808f9990fb7d91bd658039022bedd05ccad4581': -# transaction_found = True -# logging.debug(f'for hash {tx["hash"]} the full transaction is {tx}') -# assert tx['input_a_token_symbol'] == 'ETH' -# assert tx['output_a_token_symbol'] == 'ZLK-LP' -# _assert_value_within_range(tx['input_a_quantity'], 0.0218565212226356) -# _assert_value_within_range(tx['output_a_quantity'], 0.023807051928692573) -# else: -# continue -# assert transaction_found +@pytest.mark.skipif(new_only or test_scope not in {'all', 'liquidity'}, + reason="reduce API queries during debug/dev") +@pytest.mark.asyncio +async def test__decode_add_liquidity_tx_Zenlink__addLiquiditySingleNativeCurrency(): + test_acct = "0x9f7aa4f003817352e9770e579be7efcd37ba1990" + config = { + "moonriver": { + "account_transactions": { + "accounts": [ + test_acct + ], + "_filter": [{"blockNumber": [{"==": 1222466}]}] + } + } + } + + logging.info(f"begin 'test__decode_add_liquidity_tx_Zenlink__addLiquiditySingleNativeCurrency'" + f" scraping at {time.strftime('%X')}") + items_scraped = await subscrape.scrape(config) + assert len(items_scraped) >= 1 + transactions = _get_archived_transactions_from_json(test_acct, 'moonriver') + transaction_found = False + for timestamp in transactions: + tx = transactions[timestamp] + if tx['hash'] == '0x67f837a13804964808ff8d92a20ecadd9087ec352a2ce0524667c52022775169': + transaction_found = True + logging.debug(f'for hash {tx["hash"]} the full transaction is {tx}') + assert tx['input_a_token_symbol'] == 'WMOVR' + assert tx['output_a_token_symbol'] == 'ZLK-LP' + _assert_value_within_range(tx['input_a_quantity'], 7.523520565458797) + _assert_value_within_range(tx['output_a_quantity'], 0.000051110515327605) + else: + continue + assert transaction_found + + +@pytest.mark.skipif(new_only or test_scope not in {'all', 'liquidity'}, + reason="reduce API queries during debug/dev") +@pytest.mark.asyncio +async def test__decode_add_liquidity_tx_Zenlink__addLiquiditySingleToken(): + # Testing Zenlink exchange ETH for vETH and receive ZLK-LP + # Oddly this transaction only specifies ETH->vETH in the token path instead of full ETH->vETH->ZLK-LP. + # The series of events are ETH->vETH, send vETH back to acct, then provide both ETH and vETH for ZLK-LP. + test_acct = "0x0a83985e4a6e8dae2b67bed4f2d9268f6806ce00" + config = { + "moonriver": { + "account_transactions": { + "accounts": [ + test_acct + ], + "_filter": [{"blockNumber": [{"==": 2411562}]}] + } + } + } + + logging.info(f"begin 'test__decode_add_liquidity_tx_Zenlink__addLiquiditySingleToken'" + f" scraping at {time.strftime('%X')}") + items_scraped = await subscrape.scrape(config) + assert len(items_scraped) >= 1 + transactions = _get_archived_transactions_from_json(test_acct, 'moonriver') + transaction_found = False + for timestamp in transactions: + tx = transactions[timestamp] + if tx['hash'] == '0x311d38bf46501961abdee8c9d808f9990fb7d91bd658039022bedd05ccad4581': + transaction_found = True + logging.debug(f'for hash {tx["hash"]} the full transaction is {tx}') + assert tx['input_a_token_symbol'] == 'ETH' + assert tx['output_a_token_symbol'] == 'ZLK-LP' + _assert_value_within_range(tx['input_a_quantity'], 0.04364793008913) + _assert_value_within_range(tx['output_a_quantity'], 0.023807051928692573) + else: + continue + assert transaction_found @pytest.mark.skipif(not new_only or test_scope not in {'kbtc'}, reason="reduce API queries during debug/dev") @pytest.mark.asyncio -async def test__decode_add_liquidity_transaction__kbtc_stableswap_solarbeam(): +async def test__decode_add_liquidity_tx_SolarbeamStableSwap__kbtc_stableswap_solarbeam(): # also testing: Add liquidity to kBTC-BTC StableSwap on Solarbeam DEX. test_acct = "0xc365926c71dae2c7e39d176c9406239318301a3c" config = { @@ -565,7 +827,7 @@ async def test__decode_add_liquidity_transaction__kbtc_stableswap_solarbeam(): } } - logging.info(f"begin 'test__decode_add_liquidity_transaction__kbtc_stableswap_solarbeam'" + logging.info(f"begin 'test__decode_add_liquidity_tx_SolarbeamStableSwap__kbtc_stableswap_solarbeam'" f" scraping at {time.strftime('%X')}") items_scraped = await subscrape.scrape(config) assert len(items_scraped) >= 1 @@ -585,46 +847,10 @@ async def test__decode_add_liquidity_transaction__kbtc_stableswap_solarbeam(): assert transaction_found +# @pytest.mark.skipif(not new_only or test_scope not in {'all', 'liquidity'}, reason="reduce API queries during debug/dev") # @pytest.mark.asyncio -# async def test__decode_add_liquidity_transaction__(): -# # also testing: Zenlink transactions -# test_acct = "XXXXXXXXXX" -# config = { -# "moonriver": { -# "account_transactions": { -# "accounts": [ -# test_acct -# ], -# "_filter": [{"blockNumber": [{"==": }]}] -# } -# } -# } -# -# logging.info(f"begin 'test__decode_add_liquidity_transaction__XXXXXXXXXXXXXXXX'" -# f" scraping at {time.strftime('%X')}") -# items_scraped = await subscrape.scrape(config) -# assert len(items_scraped) >= 1 -# transactions = _get_archived_transactions_from_json(test_acct, 'moonriver') -# transaction_found = False -# for timestamp in transactions: -# tx = transactions[timestamp] -# if tx['hash'] == 'XXXXXXXXXXX': -# transaction_found = True -# logging.debug(f'for hash {tx["hash"]} the full transaction is {tx}') -# assert tx['input_a_token_symbol'] == 'xcKSM' -# assert tx['input_b_token_symbol'] == 'WMOVR' -# assert tx['output_a_token_symbol'] == 'ZLK-LP' -# _assert_value_within_range(tx['input_a_quantity'], 3.635995421334) -# _assert_value_within_range(tx['input_b_quantity'], 14.999999999996282704) -# _assert_value_within_range(tx['output_a_quantity'], 1) -# else: -# continue -# assert transaction_found - - -# @pytest.mark.asyncio -# async def test__decode_add_liquidity_transaction__(): -# # also testing: Zenlink transactions +# async def test__decode_add_liquidity_tx__(): +# # also testing: # test_acct = "XXXXXXXXXX" # config = { # "moonriver": { @@ -632,12 +858,12 @@ async def test__decode_add_liquidity_transaction__kbtc_stableswap_solarbeam(): # "accounts": [ # test_acct # ], -# "_filter": [{"blockNumber": [{"==": }]}] +# "_filter": [{"blockNumber": [{"==": xxxxxxxxxx}]}] # } # } # } # -# logging.info(f"begin 'test__decode_add_liquidity_transaction__XXXXXXXXXXXXXXXX'" +# logging.info(f"begin 'test__decode_add_liquidity_tx__XXXXXXXXXXXXXXXX'" # f" scraping at {time.strftime('%X')}") # items_scraped = await subscrape.scrape(config) # assert len(items_scraped) >= 1 @@ -648,12 +874,12 @@ async def test__decode_add_liquidity_transaction__kbtc_stableswap_solarbeam(): # if tx['hash'] == 'XXXXXXXXXXX': # transaction_found = True # logging.debug(f'for hash {tx["hash"]} the full transaction is {tx}') -# assert tx['input_a_token_symbol'] == 'xcKSM' -# assert tx['input_b_token_symbol'] == 'WMOVR' -# assert tx['output_a_token_symbol'] == 'ZLK-LP' -# _assert_value_within_range(tx['input_a_quantity'], 3.635995421334) -# _assert_value_within_range(tx['input_b_quantity'], 14.999999999996282704) -# _assert_value_within_range(tx['output_a_quantity'], 1) +# assert tx['input_a_token_symbol'] == 'xxxxxx' +# assert tx['input_b_token_symbol'] == 'xxxxxx' +# assert tx['output_a_token_symbol'] == 'xxxxxxx' +# _assert_value_within_range(tx['input_a_quantity'], xxxxxx) +# _assert_value_within_range(tx['input_b_quantity'], xxxxxxx) +# _assert_value_within_range(tx['output_a_quantity'], xxxxxxxxx) # else: # continue # assert transaction_found @@ -663,8 +889,9 @@ async def test__decode_add_liquidity_transaction__kbtc_stableswap_solarbeam(): # ######### REMOVE LIQUIDITY TESTS ############################ # ############################################################# +# @pytest.mark.skipif(not new_only or test_scope not in {'all', 'liquidity'}, reason="reduce API queries during debug/dev") # @pytest.mark.asyncio -# async def test__decode_remove_liquidity_transaction__(): +# async def test__decode_remove_liquidity_tx__(): # # also testing: Zenlink transactions # test_acct = "XXXXXXXXXX" # config = { @@ -678,7 +905,7 @@ async def test__decode_add_liquidity_transaction__kbtc_stableswap_solarbeam(): # } # } # -# logging.info(f"begin 'test__decode_remove_liquidity_transaction__XXXXXXXXXXXXXXXX'" +# logging.info(f"begin 'test__decode_remove_liquidity_tx__XXXXXXXXXXXXXXXX'" # f" scraping at {time.strftime('%X')}") # items_scraped = await subscrape.scrape(config) # assert len(items_scraped) >= 1 @@ -703,7 +930,7 @@ async def test__decode_add_liquidity_transaction__kbtc_stableswap_solarbeam(): @pytest.mark.skipif(new_only or test_scope not in {'all', 'liquidity'}, reason="reduce API queries during debug/dev") @pytest.mark.asyncio -async def test__decode_remove_liquidity_transaction__removeLiquidityNativeCurrency(): +async def test__decode_remove_liquidity_tx_Zenlink__removeLiquidityNativeCurrency(): # also testing: Zenlink transactions test_acct = "0x96cc80292fa3a7045611eb84ae09df8bd15936d2" config = { @@ -717,7 +944,7 @@ async def test__decode_remove_liquidity_transaction__removeLiquidityNativeCurren } } - logging.info(f"begin 'test__decode_remove_liquidity_transaction__removeLiquidityNativeCurrency'" + logging.info(f"begin 'test__decode_remove_liquidity_tx_Zenlink__removeLiquidityNativeCurrency'" f" scraping at {time.strftime('%X')}") items_scraped = await subscrape.scrape(config) assert len(items_scraped) >= 1 @@ -742,7 +969,7 @@ async def test__decode_remove_liquidity_transaction__removeLiquidityNativeCurren @pytest.mark.skipif(not new_only or test_scope not in {'kbtc'}, reason="reduce API queries during debug/dev") @pytest.mark.asyncio -async def test__decode_remove_liquidity_transaction__kbtc_stableswap_solarbeam(): +async def test__decode_remove_liquidity_tx_SolarbeamStableSwap__kbtc_stableswap_solarbeam(): # also testing: Remove liquidity from kBTC-BTC StableSwap on Solarbeam DEX. test_acct = "0xb4c9531a60e252c871d51923bc9f153f1d371ca8" config = { @@ -756,7 +983,7 @@ async def test__decode_remove_liquidity_transaction__kbtc_stableswap_solarbeam() } } - logging.info(f"begin 'test__decode_remove_liquidity_transaction__kbtc_stableswap_solarbeam'" + logging.info(f"begin 'test__decode_remove_liquidity_tx_SolarbeamStableSwap__kbtc_stableswap_solarbeam`'" f" scraping at {time.strftime('%X')}") items_scraped = await subscrape.scrape(config) assert len(items_scraped) >= 1