Skip to content

Commit 48bccfa

Browse files
moneymanolisk9ert
andauthored
Bugfix: Updating the auto-withdrawal in Swan integration could lead to an error (#1981)
* if enough addresses reserved return an empty list + tests * client test fixed + test for reserve_addresses for swan service * ignore_cleanup_errors=True + dev doc updated * fix select of threshold in settings * test for set_autowithdrawal_settings * fix test Co-authored-by: k9ert <[email protected]>
1 parent dd4b106 commit 48bccfa

File tree

13 files changed

+244
-60
lines changed

13 files changed

+244
-60
lines changed

docs/development.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,9 @@ Set up the dependencies:
150150
pip3 install -r test_requirements.txt
151151
pip3 install -e .
152152
```
153+
154+
You need a virtual environment based on Python 3.10 for the tests to run successfully, otherwise you get this error:
155+
`TypeError: __init__() got an unexpected keyword argument 'ignore_cleanup_errors'`
153156

154157
If you have a local bitcoind already installed:
155158
```

src/cryptoadvance/specter/services/service.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ def reserve_addresses(
129129
)
130130
logger.debug(f"Already have {len(addresses)} addresses reserved for {cls.id}")
131131

132+
# More addresses ought to be reserved as there are reserved addresses
132133
if len(addresses) < num_addresses:
133134
if addresses:
134135
# Continuing reserving from where we left off
@@ -149,7 +150,7 @@ def reserve_addresses(
149150
continue
150151

151152
# Mark an Address in a persistent way as being reserved by a Service
152-
cls.reserve_address(wallet=wallet, address=address)
153+
cls.reserve_address(wallet=wallet, address=address, label=label)
153154
logger.debug(f"Reserved {address} for {cls.id}")
154155

155156
addresses.append(address)
@@ -158,9 +159,18 @@ def reserve_addresses(
158159
annotations_storage.set_addr_annotations(
159160
addr=address, annotations=annotations, autosave=False
160161
)
162+
# There are enough addresses already reserved
163+
else:
164+
logger.debug(
165+
f"Returning an empty list from reserve_addresses() since there are enough addresses already reserved."
166+
)
167+
return []
168+
161169
if annotations:
162170
annotations_storage.save()
163-
171+
logger.debug(
172+
f"Returning this list of addresses {addresses} from reserve_addresses()."
173+
)
164174
return addresses
165175

166176
@classmethod

src/cryptoadvance/specter/wallet.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -628,7 +628,8 @@ def update(self):
628628
self.check_addresses()
629629

630630
def check_unused(self):
631-
"""Check current receive address is unused and get new if needed"""
631+
"""Check current receive address is unused and get new if needed
632+
Used means: the given address having a non-zero amount received in transactions with zero confirmations."""
632633
addr = self.address
633634
try:
634635
while self.rpc.getreceivedbyaddress(addr, 0) != 0:

src/cryptoadvance/specterext/swan/client.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -238,17 +238,13 @@ def update_autowithdrawal_addresses(
238238
specter_wallet_name: str,
239239
specter_wallet_alias: str,
240240
addresses: List[str],
241-
) -> dict:
241+
) -> str:
242242
"""
243243
* If SWAN_WALLET_ID is known, any existing unused addresses are cleared.
244244
* If there is no known SWAN_WALLET_ID, we `POST` to create an initial Swan wallet and store the resulting SWAN_WALLET_ID.
245245
* Sends the list of new addresses for SWAN_WALLET_ID.
246+
* Returns the swan wallet id if the response provides it or raises an SwanApiException.
246247
"""
247-
248-
# normalize the strucure compatible with what swan will accept:
249-
# like: [{"address": "bcrt1q8k8a73crvjs06jhdj7xee8mace3mhlxj4pdvna"}, {"address": "bcrt ...
250-
addresses = [address["address"] for address in addresses]
251-
252248
if swan_wallet_id:
253249
# We already have a Swan walletId. DELETE the existing unused addresses...
254250
self.delete_autowithdrawal_addresses(swan_wallet_id)
@@ -261,6 +257,7 @@ def update_autowithdrawal_addresses(
261257
endpoint = "/apps/v20210824/wallets"
262258
method = "POST"
263259

260+
# For the required structure of the paylod see: https://developers.swanbitcoin.com/api/create-a-new-wallet
264261
resp = self.authenticated_request(
265262
endpoint=endpoint,
266263
method=method,

src/cryptoadvance/specterext/swan/controller.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ def settings():
102102
wallets=wallets,
103103
cookies=request.cookies,
104104
num_reserved_addrs=SwanService.MIN_PENDING_AUTOWITHDRAWAL_ADDRS,
105+
autowithdrawal_threshold=SwanService.get_current_user_service_data().get(
106+
SwanService.AUTOWITHDRAWAL_THRESHOLD
107+
),
105108
)
106109

107110

src/cryptoadvance/specterext/swan/service.py

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ def set_associated_wallet(cls, wallet: Wallet):
111111
@classmethod
112112
def reserve_addresses(
113113
cls, wallet: Wallet, label: str = None, num_addresses: int = 10
114-
) -> List[str]:
114+
):
115115
"""
116116
* Reserves addresses for Swan auto-withdrawals
117117
* Sets the associated Specter `Wallet` that will receive auto-withdrawals
@@ -123,9 +123,14 @@ def reserve_addresses(
123123
from . import client as swan_client
124124

125125
# Update Addresses as reserved (aka "associated") with Swan in our Wallet
126+
# addresses is list of address strings, like so: ["bcrt1qxak08ykhf7r4js9yncysy5p05xp0fwxhamewc8", ... , "bcrt1qpys58dndrn9sxnk0z7ngm6wsxskpvs9jsjq7q6"]
127+
# or it is an empty list if we are requesting to reserve less addresses as we already have
126128
addresses = super().reserve_addresses(
127129
wallet=wallet, label=label, num_addresses=num_addresses
128130
)
131+
logger.debug(
132+
f"The addresses that go in as arguments to update_autowithdrawal_addresses: {addresses}"
133+
)
129134

130135
# Clear out any prior unused reserved addresses if this is a different Wallet
131136
cur_wallet = cls.get_associated_wallet()
@@ -136,17 +141,19 @@ def reserve_addresses(
136141
cls.set_associated_wallet(wallet)
137142

138143
# Send the new list to Swan (DELETES any unused ones; creates a new SWAN_WALLET_ID if needed)
139-
swan_wallet_id = cls.client().update_autowithdrawal_addresses(
140-
cls.get_current_user_service_data().get(cls.SWAN_WALLET_ID),
141-
specter_wallet_name=wallet.name,
142-
specter_wallet_alias=wallet.alias,
143-
addresses=addresses,
144-
)
145-
logger.debug(f"Updating the Swan wallet id to {swan_wallet_id}")
146-
if swan_wallet_id:
147-
cls.update_current_user_service_data({cls.SWAN_WALLET_ID: swan_wallet_id})
148-
149-
return addresses
144+
# Only do this if we are requesting to reserve less addresses as we already have
145+
if addresses != []:
146+
swan_wallet_id = cls.client().update_autowithdrawal_addresses(
147+
cls.get_current_user_service_data().get(cls.SWAN_WALLET_ID),
148+
specter_wallet_name=wallet.name,
149+
specter_wallet_alias=wallet.alias,
150+
addresses=addresses,
151+
)
152+
logger.debug(f"Updating the Swan wallet id to {swan_wallet_id}")
153+
if swan_wallet_id:
154+
cls.update_current_user_service_data(
155+
{cls.SWAN_WALLET_ID: swan_wallet_id}
156+
)
150157

151158
@classmethod
152159
def set_autowithdrawal_settings(cls, wallet: Wallet, btc_threshold: str):

src/cryptoadvance/specterext/swan/templates/swan/settings.jinja

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@
3030

3131
<div>Auto-withdrawal threshold:</div>
3232
<select name="threshold">
33-
<option value="0" {% if threshold == '0' %}selected{% endif %}>Weekly</option>
34-
<option value="0.01" {% if threshold == '0.01' %}selected{% endif %}>0.010 BTC</option>
35-
<option value="0.025" {% if threshold == '0.025' %}selected{% endif %}>0.025 BTC</option>
36-
<option value="0.05" {% if threshold == '0.05' %}selected{% endif %}>0.050 BTC</option>
33+
<option value="0" {% if autowithdrawal_threshold == '0' %}selected{% endif %}>Weekly</option>
34+
<option value="0.01" {% if autowithdrawal_threshold == '0.01' %}selected{% endif %}>0.010 BTC</option>
35+
<option value="0.025" {% if autowithdrawal_threshold == '0.025' %}selected{% endif %}>0.025 BTC</option>
36+
<option value="0.05" {% if autowithdrawal_threshold == '0.05' %}selected{% endif %}>0.050 BTC</option>
3737
</select>
3838
<br/>
3939
<br/>

tests/conftest.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
logger = logging.getLogger(__name__)
3636

3737
pytest_plugins = [
38-
# "conftest_visibility",
38+
"conftest_visibility",
3939
"fix_ghost_machine",
4040
"fix_keys_and_seeds",
4141
"fix_devices_and_wallets",
@@ -268,13 +268,17 @@ def elements_elreg(request):
268268
@pytest.fixture
269269
def empty_data_folder():
270270
# Make sure that this folder never ever gets a reasonable non-testing use-case
271-
with tempfile.TemporaryDirectory(prefix="specter_home_tmp_") as data_folder:
271+
with tempfile.TemporaryDirectory(
272+
prefix="specter_home_tmp_", ignore_cleanup_errors=False
273+
) as data_folder:
272274
yield data_folder
273275

274276

275277
@pytest.fixture
276278
def devices_filled_data_folder(empty_data_folder):
277-
os.makedirs(empty_data_folder + "/devices")
279+
devices_folder = empty_data_folder + "/devices"
280+
if not os.path.isdir(devices_folder):
281+
os.makedirs(devices_folder)
278282
with open(empty_data_folder + "/devices/trezor.json", "w") as text_file:
279283
text_file.write(
280284
"""

tests/conftest_visibility.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ def should_intercept(call):
2828
return not (
2929
isinstance(call.excinfo.value, RpcError)
3030
or isinstance(call.excinfo.value, SpecterError)
31+
or isinstance(call.excinfo.value, AssertionError)
3132
)
3233

3334

tests/fix_devices_and_wallets.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ def wallet(devices_filled_data_folder, device_manager, node):
194194
device_manager,
195195
)
196196
device = device_manager.get_by_alias("trezor")
197-
wm.create_wallet("test_wallet", 1, "wpkh", [device.keys[5]], [device])
198-
wallet = wm.wallets["test_wallet"]
197+
wallet_name = f"test_wallet_{random.randint(0, 999999)}"
198+
wm.create_wallet(wallet_name, 1, "wpkh", [device.keys[5]], [device])
199+
wallet = wm.wallets[wallet_name]
199200
return wallet

0 commit comments

Comments
 (0)