Repository: stellar/django-polaris (python)

Issue:
watch_transactions: support fee-bump transactions
### What problem does your feature solve?

If a wallet or SEP-31 sender uses fee-bump transactions to initiate a withdrawal or remittance, Polaris' `watch_transactions` transaction streamer will fail to deserialize it.

### What would you like to see?

We need to add support for streaming these fee-bump transactions.

Pull request:
support streaming fee-bump transactions
resolves #653 

This PR adds support for parsing fee-bump transactions in our transaction monitoring process, `watch_transactions`. Previously, sending payments to Polaris anchors using fee-bumps would cause the process to crash and the transaction would never be detected.

Patch:
diff --git a/polaris/management/commands/watch_transactions.py b/polaris/management/commands/watch_transactions.py
index 1a5a30c..e260aa0 100644
--- a/polaris/management/commands/watch_transactions.py
+++ b/polaris/management/commands/watch_transactions.py
@@ -6,6 +6,7 @@ from decimal import Decimal
 
 from django.core.management.base import BaseCommand
 from django.db.models import Q
+from stellar_sdk import FeeBumpTransactionEnvelope
 from stellar_sdk.exceptions import NotFoundError
 from stellar_sdk.transaction import Transaction as HorizonTransaction
 from stellar_sdk.transaction_envelope import TransactionEnvelope
@@ -172,11 +173,19 @@ class Command(BaseCommand):
             f"Matched transaction object {transaction.id} for stellar transaction {response['id']}"
         )
 
-        op_results = TransactionResult.from_xdr(result_xdr).result.results
-        horizon_tx = TransactionEnvelope.from_xdr(
-            envelope_xdr,
-            network_passphrase=settings.STELLAR_NETWORK_PASSPHRASE,
-        ).transaction
+        try:
+            horizon_tx = TransactionEnvelope.from_xdr(
+                envelope_xdr,
+                network_passphrase=settings.STELLAR_NETWORK_PASSPHRASE,
+            ).transaction
+            op_results = TransactionResult.from_xdr(result_xdr).result.results
+        except ValueError:
+            horizon_tx = FeeBumpTransactionEnvelope.from_xdr(
+                envelope_xdr, network_passphrase=settings.STELLAR_NETWORK_PASSPHRASE
+            ).transaction.inner_transaction_envelope.transaction
+            op_results = TransactionResult.from_xdr(
+                result_xdr
+            ).result.inner_result_pair.result.result.results
 
         payment_data = await cls._find_matching_payment_data(
             response, horizon_tx, op_results, transaction


Test patch:
diff --git a/polaris/tests/commands/test_watch_transactions.py b/polaris/tests/commands/test_watch_transactions.py
index f729f68..684dfa8 100644
--- a/polaris/tests/commands/test_watch_transactions.py
+++ b/polaris/tests/commands/test_watch_transactions.py
@@ -15,7 +15,6 @@ SUCCESS_PAYMENT_TRANSACTION_JSON = {
     "result_xdr": "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAA=",
     "memo": "AAAAAAAAAAAAAAAAAAAAAIDqc+oB00EajZzqIpme754=",
     "memo_type": "hash",
-    "source": "GC6Z5AIAP2IGNXB2QO4P34MQNP776H6LNIFBIP6C3IRGBHU5RQVSQ7XM",
     "paging_token": "2007270145658880",
 }
 SUCCESS_STRICT_SEND_PAYMENT = {
@@ -25,9 +24,17 @@ SUCCESS_STRICT_SEND_PAYMENT = {
     "result_xdr": "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAANAAAAAAAAAAAAAAAA0QNr+3v9HSAqKJT6bfuS/r+OXa68V5uK9x/kvH0HX54AAAABVEVTVAAAAAC9noEAfpBm3DqDuP3xkGv//x/LagoUP8LaImCenYwrKAAAAAJUpHqAAAAAAA==",
     "memo": "textmemo",
     "memo_type": "text",
-    "source": "GC6Z5AIAP2IGNXB2QO4P34MQNP776H6LNIFBIP6C3IRGBHU5RQVSQ7XM",
     "paging_token": "2009348909830144",
 }
+SUCCESS_FEE_BUMP_TRANSACTION_JSON = {
+    "id": "fa9fea58c75fa8587b0c84e29c42e52a6f1d880caba2696de26e58f12642b8b0",
+    "paging_token": "3910825421058048",
+    "successful": True,
+    "envelope_xdr": "AAAABQAAAAABlxfkxDiG97LmQTHOHDkdHtisPCg5bhMQq60YoygN4QAAAAAAAADIAAAAAgAAAABND0kC1ZZnQ/B3aSE6fsMCYXuX62OWjh43yMtN2q/qzwAAAGQADd7MAAAABgAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAA0e3uiqDBB+LjfOuUuJ26YAAAAAQAAAAAAAAABAAAAAKRpR8qEOhfot6vtFfpWfSRbLq5q0BTmip1gHpcJLVfrAAAAAVNSVAAAAAAAhtTQMJX+BNlpAJifo4EW88+GFJsORAsrJ5lOKgGODdQAAAAABfXhAAAAAAAAAAAB2q/qzwAAAECZAwm54BEDxFDsn3sv5P+xtD9IkTiTPuclL6W2qFvIuiAFNbxQcgkUok310CAP2v11hx4yfrZlWzHWCE3Z++UFAAAAAAAAAAGjKA3hAAAAQOoJ1uWSE4VPZHqzoRLWYofT1z3mrXeI8rmLaVK8le1K2BfISBMcEr7NdqpsG25FRanIeOtQ7XVoINgdu/AwHQg=",
+    "result_xdr": "AAAAAAAAAMgAAAAB8FlKVLi+X5YNNxJmSxU66RFWDFlKR2MuqKHFmklACVsAAAAAAAAAZAAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAA=",
+    "memo": "AAAAAAAAAAAAAAAAAAAAADR7e6KoMEH4uN865S4nbpg=",
+    "memo_type": "hash",
+}
 TEST_ASSET_ISSUER_SEED = "SADPW3NKRUNSNKXZ3UCNKF5QKXFS3IBNDLIEDV4PEDLIXBXJOGSOQW6J"
 TEST_ASSET_ISSUER_PUBLIC_KEY = Keypair.from_secret(TEST_ASSET_ISSUER_SEED).public_key
 TEST_ASSET_DISTRIBUTION_SEED = (
@@ -104,6 +111,39 @@ def test_process_response_strict_send_success(client):
     assert transaction.amount_in == 1001
 
 
+@pytest.mark.django_db
+def test_fee_bump_tx():
+    asset = Asset.objects.create(
+        code="SRT",
+        issuer="GCDNJUBQSX7AJWLJACMJ7I4BC3Z47BQUTMHEICZLE6MU4KQBRYG5JY6B",
+        distribution_seed=TEST_ASSET_DISTRIBUTION_SEED,
+    )
+    transaction = Transaction.objects.create(
+        asset=asset,
+        stellar_account=Keypair.random().public_key,
+        amount_in=10,
+        amount_expected=10,
+        kind=Transaction.KIND.withdrawal,
+        status=Transaction.STATUS.pending_user_transfer_start,
+        memo=SUCCESS_FEE_BUMP_TRANSACTION_JSON["memo"],
+        protocol=Transaction.PROTOCOL.sep24,
+        receiving_anchor_account="GCSGSR6KQQ5BP2FXVPWRL6SWPUSFWLVONLIBJZUKTVQB5FYJFVL6XOXE",
+    )
+    json = deepcopy(SUCCESS_FEE_BUMP_TRANSACTION_JSON)
+
+    async_to_sync(Command().process_response)(
+        json, "GCSGSR6KQQ5BP2FXVPWRL6SWPUSFWLVONLIBJZUKTVQB5FYJFVL6XOXE"
+    )
+
+    transaction.refresh_from_db()
+    assert transaction.from_address
+    assert transaction.stellar_transaction_id
+    assert transaction.paging_token
+    assert transaction.status == Transaction.STATUS.pending_anchor
+    assert transaction.amount_in == 10
+    assert transaction.amount_expected == 10
+
+
 def test_process_response_unsuccessful(client, acc1_usd_withdrawal_transaction_factory):
     json = {"successful": False}
     try: