Skip to content

Commit 2f26b07

Browse files
authored
Tests/custom tx params and nonce (#22569)
* e2e: added new test for setting custom tx params and replacing tx nonce
1 parent 0900eb9 commit 2f26b07

File tree

7 files changed

+233
-13
lines changed

7 files changed

+233
-13
lines changed

test/appium/requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ mnemonic==0.21
1616
bip-utils==2.9.3
1717
testrail==0.3.15
1818
webcolors==24.8.0
19-
CurrencyConverter==0.17.34
19+
CurrencyConverter==0.17.34
20+
bs4==0.0.2
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import requests
2+
from bs4 import BeautifulSoup
3+
import re
4+
5+
6+
class LightweightBrowserHandler:
7+
8+
def __init__(self, base_url: str):
9+
self.base_url = base_url
10+
11+
12+
def load_tx_etherscan_page(self, tx_hash: str) -> BeautifulSoup:
13+
headers = {
14+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36",
15+
"Accept-Language": "en-US,en;q=0.9",
16+
"Cache-Control": "no-cache",
17+
"Connection": "keep-alive",
18+
"DNT": "1",
19+
}
20+
tx_url = '%s/tx/%s' % (self.base_url, tx_hash)
21+
response = requests.get(tx_url, headers=headers, timeout=10)
22+
response.raise_for_status()
23+
return BeautifulSoup(response.text, "html.parser")
24+
25+
def extract_tx_details(self, soup: BeautifulSoup) -> dict:
26+
tx_fee_elem = soup.select_one("#txfeebutton")
27+
tx_fee = tx_fee_elem.text.strip() if tx_fee_elem else "Not Found"
28+
29+
confirmation_time_elem = soup.find(string=re.compile('Confirmed within'))
30+
confirmation_time = (
31+
confirmation_time_elem.text.replace("Confirmed within", "").strip()
32+
if confirmation_time_elem
33+
else "Not Found"
34+
)
35+
36+
return {'fee': tx_fee, 'confirmation_time': confirmation_time}
37+
38+
def find_text(self, soup: BeautifulSoup, text: str) -> bool:
39+
return soup.find(string=re.compile(re.escape(text))) is not None
40+
41+
def find_success_badge(self, soup: BeautifulSoup):
42+
for span in soup.find_all("span", class_=lambda x: x and "text-green-600" in x):
43+
if "Success" in span.get_text():
44+
return span
45+
return None
46+
47+
def get_fee_and_confirmation_time(self, tx_url: str) -> dict:
48+
try:
49+
soup = self.load_tx_etherscan_page(tx_url)
50+
return self.extract_tx_details(soup)
51+
except requests.RequestException as e:
52+
print(f"An error occurred while fetching transaction details: {e}")
53+
return {'fee': None, 'confirmation_time': None}
54+

test/appium/support/api/network_api.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from json import JSONDecodeError
55
from os import environ
66
from typing import List
7+
import json
78

89
import pytest
910
import pytz
@@ -83,3 +84,20 @@ def wait_for_balance_to_be(self, address: str, expected_balance: int):
8384
time.sleep(20)
8485
raise TimeoutException(
8586
'balance is not updated on Etherscan, it is %s but expected to be %s' % (balance, expected_balance))
87+
88+
89+
class SepoliaNetworkApi:
90+
def __init__(self):
91+
self.rpc_url = 'https://ethereum-sepolia.rpc.subquery.network/public'
92+
93+
def is_tx_successful(self, tx_hash):
94+
response = requests.post(headers={"Content-Type": "application/json"},
95+
url=self.rpc_url,
96+
data=json.dumps(
97+
{"jsonrpc": "2.0", "method": "eth_getTransactionReceipt", "params": [tx_hash],
98+
"id": 1})).json()
99+
result = response.get('result')
100+
if result is not None:
101+
return result.get('status') == '0x1'
102+
return None
103+

test/appium/tests/base_test_case.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,18 @@
2626

2727
implicit_wait = 5
2828

29+
def get_app_package():
30+
"""
31+
Determines the app package dynamically based on the APK configuration.
32+
:return: The app package name.
33+
"""
34+
apk = pytest_config_global['apk']
35+
app_folder = "im.status.ethereum" # Default app package name
36+
37+
if re.findall(r'pr\d\d\d\d\d', apk) or re.findall(r'\d\d\d\d\d.apk', apk):
38+
app_folder += ".pr" # Append `.pr` in specific cases
39+
40+
return app_folder
2941

3042
def get_lambda_test_capabilities_real_device():
3143
capabilities = {
@@ -78,12 +90,7 @@ def get_lambda_test_capabilities_emulator(platform_version: int = 14, device_nam
7890

7991

8092
def get_app_path():
81-
app_folder = 'im.status.ethereum'
82-
apk = pytest_config_global['apk']
83-
if re.findall(r'pr\d\d\d\d\d', apk) or re.findall(r'\d\d\d\d\d.apk', apk):
84-
app_folder += '.pr'
85-
app_path = '/storage/emulated/0/Android/data/%s/files/Download/' % app_folder
86-
return app_path
93+
return '/storage/emulated/0/Android/data/%s/files/Download/' % get_app_package()
8794

8895

8996
def pull_logs_folder(driver):

test/appium/tests/wallet_txs/test_wallet_mainnet_txs.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def test_wallet_swap_dai_snt_real_tx(self):
6565
network=network_name)
6666
self.errors.append(self.wallet, tx_errors)
6767

68-
self.wallet.just_fyi("Check that balance is updated for receiver")
68+
self.wallet.just_fyi("Check that From and To balance is updated")
6969
self.wallet.wait_for_wallet_balance_to_update(fiat_expected_amount_from, asset_name_from, fiat=True)
7070
self.wallet.wait_for_wallet_balance_to_update(fiat_expected_amount_to, asset_name_to, fiat=True)
7171

test/appium/tests/wallet_txs/test_wallet_testnet.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from tests import marks, run_in_parallel
66
from users import transaction_senders
77
from views.sign_in_view import SignInView
8+
from support.api.network_api import SepoliaNetworkApi
89

910

1011
@pytest.mark.xdist_group(name="four_2")
@@ -99,4 +100,92 @@ def test_wallet_send_erc20_from_drawer(self, network, asset, asset_ticker, decim
99100

100101
self.errors.verify_no_errors()
101102

103+
@pytest.mark.xdist_group(name="two_1")
104+
@marks.nightly
105+
@marks.smoke
106+
class TestWalletCustomParamOneDevice(MultipleSharedDeviceTestCase):
107+
108+
def prepare_devices(self):
109+
self.drivers, self.loop = create_shared_drivers(1)
110+
self.driver = self.drivers[0]
111+
self.sign_in = SignInView(self.drivers[0])
112+
self.sender, self.receiver = transaction_senders['ETH_ADI_STT_2'], transaction_senders['ETH_3']
113+
self.sender['wallet_address'] = '0x' + self.sender['address']
114+
self.receiver['wallet_address'] = '0x' + self.receiver['address']
115+
self.home = self.sign_in.recover_access(self.sender['passphrase'])
116+
self.wallet = self.sign_in.get_wallet_view()
117+
118+
@marks.testrail_id(742910)
119+
def test_send_snt_custom_tx_params(self):
120+
wallet = self.wallet
121+
wallet.navigate_back_to_wallet_view()
122+
network = 'Sepolia'
123+
address, asset_name, amount, asset_ticker = self.receiver['wallet_address'], 'Status Test Token', '1', 'STT'
124+
max_base_fee, max_prio, max_gas_amount = '10', '1', '60000'
125+
126+
wallet.just_fyi("Set amount and address")
127+
wallet.set_amount_and_address(address, asset_name, amount)
128+
wallet.button_one.click_until_presence_of_element(wallet.advanced_tx_button)
129+
130+
wallet.just_fyi("Set custom tx params to make pending tx")
131+
wallet.advanced_tx_button.wait_and_click()
132+
wallet.custom_tx_params_button.wait_and_click()
133+
wallet.custom_max_base_fee_button.click()
134+
wallet.clear_value_and_set_with_custom_keyboard(max_base_fee)
135+
wallet.custom_max_prio_fee_button.wait_and_click()
136+
wallet.clear_value_and_set_with_custom_keyboard(max_prio)
137+
wallet.custom_max_gas_amount_button.wait_and_click()
138+
wallet.clear_value_and_set_with_custom_keyboard(max_gas_amount)
139+
wallet.button_one.click()
140+
141+
wallet.just_fyi("Check that custom tx params are preserved")
142+
wallet.advanced_tx_button.wait_and_click()
143+
wallet.custom_tx_params_button.wait_and_click()
144+
for value in (max_base_fee, max_prio, max_gas_amount):
145+
if not wallet.get_custom_tx_element(value).is_element_displayed():
146+
self.errors.append(wallet, "Value %s is not preserved in custom tx settings drawer!" % value)
147+
wallet.button_one.click()
148+
149+
wallet.just_fyi("Sign first tx (for dropping) and copy tx hash")
150+
wallet.slide_and_confirm_with_password()
151+
152+
wallet.just_fyi("Verify send tx in the list for sender")
153+
device_time_before_sending = wallet.driver.device_time
154+
tx_errors = wallet.check_last_transaction_in_activity(device_time_before_sending, amount,
155+
send_to_account=self.receiver['wallet_address'],
156+
asset=asset_ticker,
157+
tx_type='Send',
158+
network=network,
159+
navigate_to_main_screen=False)
160+
self.errors.append(wallet, tx_errors)
161+
dropped_tx_hash = wallet.copy_tx_hash()
162+
163+
wallet.just_fyi("Send second tx (for replacing) with custom nonce")
164+
wallet.navigate_back_to_wallet_view()
165+
wallet.set_amount_and_address(address, asset_name, amount)
166+
wallet.button_one.click_until_presence_of_element(wallet.advanced_tx_button)
167+
wallet.advanced_tx_button.wait_and_click()
168+
wallet.custom_tx_params_button.wait_and_click()
169+
wallet.custom_nonce_button.wait_and_click()
170+
current_nonce = int(wallet.amount_input.text)
171+
wallet.clear_nonce_field()
172+
wallet.set_amount(str(current_nonce - 1))
173+
wallet.button_one.click()
174+
wallet.button_one.click()
175+
wallet.slide_and_confirm_with_password()
176+
replacemant_tx_hash = wallet.copy_tx_hash()
177+
178+
if replacemant_tx_hash == dropped_tx_hash:
179+
wallet.just_fyi("Scenario1, nonce is too low: initial tx failed, check the status")
180+
if SepoliaNetworkApi().is_tx_successful(dropped_tx_hash) is not False:
181+
self.errors.append(wallet,
182+
"Initial tx %s is not failed, so custom nonce is not set" % dropped_tx_hash)
183+
184+
else:
185+
wallet.just_fyi("Scenario2, Nonce is replaced: check that initial tx was dropped as custom nonce has been set")
186+
if SepoliaNetworkApi().is_tx_successful(dropped_tx_hash) is None:
187+
self.errors.append(wallet, "Dropped tx %s receipt is available, so custom nonce is not set" % dropped_tx_hash)
188+
189+
self.errors.verify_no_errors()
190+
102191

test/appium/views/wallet_view.py

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import time
33
from typing import Literal
44

5+
56
import pytest
67
from selenium.common import NoSuchElementException
78

@@ -94,6 +95,10 @@ def header(self):
9495
def timestamp(self):
9596
return Text(self.driver, prefix=self.locator, xpath="//*[@content-desc='transaction-timestamp']").text
9697

98+
@property
99+
def options(self):
100+
return Button(self.driver, prefix=self.locator, xpath="//*[@content-desc='transaction-timestamp']/following::*[@content-desc='icon'][1]")
101+
97102
@property
98103
def amount(self):
99104
return Text(self.driver, prefix=self.locator,
@@ -190,6 +195,14 @@ def __init__(self, driver):
190195
self.to_data_container = ConfirmationViewInfoContainer(self.driver, label_name='To')
191196
self.on_data_container = ConfirmationViewInfoContainer(self.driver, label_name='On')
192197

198+
# Advanced tx settings
199+
self.advanced_tx_button = Button(self.driver, accessibility_id='advanced-button')
200+
self.custom_tx_params_button = Button(self.driver, translation_id='custom')
201+
self.custom_max_base_fee_button = Button(self.driver, translation_id='max-base-fee')
202+
self.custom_max_prio_fee_button = Button(self.driver, translation_id='priority-fee')
203+
self.custom_nonce_button = Button(self.driver, translation_id='nonce')
204+
self.custom_max_gas_amount_button = Button(self.driver, translation_id='max-gas-amount')
205+
193206
# Swap flow
194207
self.approve_swap_button = Button(self.driver, accessibility_id='Approve')
195208
self.spending_cap_approval_info_container = BaseElement(
@@ -234,6 +247,10 @@ def __init__(self, driver):
234247
self.driver, xpath="//*[@content-desc='expanded-collectible']//android.widget.ImageView")
235248
self.send_from_collectible_info_button = Button(self.driver, accessibility_id="icon, Send")
236249

250+
251+
# Tx activity
252+
self.copy_tx_hash_button = Button(self.driver, accessibility_id="copy-transaction-hash")
253+
237254
# dApp adding
238255
self.add_dapp_button = Button(self.driver, accessibility_id='connected-dapps-add')
239256
self.wallet_connect_button = Button(self.driver, accessibility_id='wc-connect')
@@ -309,13 +326,34 @@ def set_amount(self, amount: str):
309326
for i in amount:
310327
Button(self.driver, accessibility_id='keyboard-key-%s' % i).click()
311328

312-
def send_asset(self, address: str, asset_name: str, amount, network_name: str, account='Account 1'):
313-
self.send_button.click()
329+
def clear_value_and_set_with_custom_keyboard(self, value: str):
330+
self.clear_amount()
331+
self.set_amount(value)
332+
self.button_one.click()
333+
334+
def clear_nonce_field(self):
335+
Button(self.driver, xpath='//com.horcrux.svg.SvgView[@content-desc="icon"]').click()
336+
337+
def clear_amount(self):
338+
"""
339+
Tap on the clear button until the amount input field is empty.
340+
"""
341+
clear_button = Button(self.driver, accessibility_id="icon-label")
342+
edit_box = EditBox(self.driver, xpath="//android.widget.EditText")
343+
while edit_box.text != "" and edit_box.text != "0":
344+
clear_button.click()
345+
346+
def set_amount_and_address(self, address: str, asset_name: str, amount, network_name=""):
347+
self.send_button.click_until_presence_of_element(self.address_text_input)
314348
self.address_text_input.send_keys(address)
315349
self.continue_button.click()
316350
self.select_asset(asset_name)
317-
self.select_network(network_name)
351+
if network_name:
352+
self.select_network(network_name)
318353
self.set_amount(str(amount))
354+
355+
def send_asset(self, address: str, asset_name: str, amount, network_name: str, account='Account 1'):
356+
self.set_amount_and_address(address, asset_name, amount, network_name)
319357
self.confirm_transaction()
320358

321359
def send_asset_from_drawer(self, address: str, asset_name: str, amount, network_name: str):
@@ -345,6 +383,11 @@ def swap_asset_from_drawer(self, asset_name: str, amount, network_name: str, dec
345383
'est_slippage': slippage,
346384
'max_fees': fees}
347385

386+
def copy_tx_hash(self):
387+
self.get_activity_element().options.click_until_presence_of_element(self.copy_tx_hash_button)
388+
self.copy_tx_hash_button.click()
389+
return self.driver.get_clipboard_text()
390+
348391
def add_regular_account(self, account_name: str):
349392
if not self.add_account_button.is_element_displayed():
350393
self.get_account_element().swipe_left_on_element()
@@ -479,7 +522,8 @@ def check_last_transaction_in_activity(self, device_time, amount,
479522
send_to_account='',
480523
swap_asset_to='SNT',
481524
swap_amount_to='0.000000000000000000',
482-
network='Status Network'):
525+
network='Status Network',
526+
navigate_to_main_screen=True):
483527
errors = list()
484528
current_time = datetime.datetime.strptime(device_time, "%Y-%m-%dT%H:%M:%S%z")
485529
expected_time = "Today %s" % current_time.strftime('%-I:%M %p')
@@ -528,7 +572,8 @@ def check_last_transaction_in_activity(self, device_time, amount,
528572
except NoSuchElementException:
529573
errors.append("Can't find the last transaction")
530574
finally:
531-
self.close_account_button.click_until_presence_of_element(self.show_qr_code_button)
575+
if navigate_to_main_screen:
576+
self.close_account_button.click_until_presence_of_element(self.show_qr_code_button)
532577
return errors
533578

534579
def get_balance(self, asset='Ether', fiat=False):
@@ -539,6 +584,10 @@ def get_balance(self, asset='Ether', fiat=False):
539584
def get_receive_swap_amount(self, decimals=18):
540585
self.just_fyi("Getting swap Receive amount for on review page")
541586
return self.round_amount_float(self.swap_receive_amount_summary_text.text.split()[0], decimals)
587+
588+
def get_custom_tx_element(self, text):
589+
return Text(self.driver, xpath="//*[@content-desc[contains(., '%s')]]" % text)
590+
542591

543592
def get_connected_dapp_element_by_name(self, dapp_name: str):
544593
class ConnectedDAppElement(BaseElement):
@@ -557,3 +606,5 @@ def disconnect(self):
557606
def select_account_to_connect_dapp(self, account_name: str):
558607
self.select_account_to_connect_dapp_button.click()
559608
Button(self.driver, xpath="//*[@content-desc='container']/*[@text='%s']" % account_name).click()
609+
610+

0 commit comments

Comments
 (0)