From 617ba710277beb84f9393b56a826f56ee65386bd Mon Sep 17 00:00:00 2001 From: Randy Schott <1815175+schottra@users.noreply.github.com> Date: Wed, 31 Jan 2024 10:47:58 -0500 Subject: [PATCH] [PAY-2440] Update indexers for cleaner withdrawal flow (#7394) --- .../tasks/payment_router_mock_transactions.py | 8 +- .../tasks/test_index_payment_router.py | 201 +------- .../tasks/test_index_user_bank.py | 104 +++- .../tasks/user_bank_mock_transactions.py | 444 +++++++++++++++++- .../models/users/usdc_transactions_history.py | 3 + .../src/tasks/index_payment_router.py | 132 +----- .../src/tasks/index_user_bank.py | 45 +- .../discovery-provider/src/utils/helpers.py | 10 + 8 files changed, 592 insertions(+), 355 deletions(-) diff --git a/packages/discovery-provider/integration_tests/tasks/payment_router_mock_transactions.py b/packages/discovery-provider/integration_tests/tasks/payment_router_mock_transactions.py index 9acc83536ea..cd3abc1c490 100644 --- a/packages/discovery-provider/integration_tests/tasks/payment_router_mock_transactions.py +++ b/packages/discovery-provider/integration_tests/tasks/payment_router_mock_transactions.py @@ -2776,9 +2776,7 @@ ) -# Routes $1.00 to a single recipient w/ a recovery memo that does not specify -# a source transaction. The sending address should match the recipient of a previous -# transaction in order to trigger recovery +# Routes $1.00 to a user bank w/ a recovery memo. mock_valid_transfer_single_recipient_recovery_tx = GetTransactionResp.from_json( json.dumps( { @@ -2817,8 +2815,8 @@ { "programIdIndex": 6, "accounts": [0], - # "recovery" - "data": "L8nXdrNsKha", + # "Recover Withdrawal" + "data": "4RDcbHgG9pB3vm5rAKgbs5F47", "stackHeight": None, }, { diff --git a/packages/discovery-provider/integration_tests/tasks/test_index_payment_router.py b/packages/discovery-provider/integration_tests/tasks/test_index_payment_router.py index 5e2f6f4af9e..6b0c64ddd8c 100644 --- a/packages/discovery-provider/integration_tests/tasks/test_index_payment_router.py +++ b/packages/discovery-provider/integration_tests/tasks/test_index_payment_router.py @@ -328,8 +328,7 @@ def test_process_payment_router_tx_details_transfer_from_user_bank_without_purch assert sender_transaction_record.tx_metadata == trackOwnerUserBank -# Should revert the most recent outbound transaction to the sending address -# of the recovery transaction +# Should index recoveries with the correct transaction type def test_process_payment_router_tx_details_transfer_recovery(app): tx_response = mock_valid_transfer_single_recipient_recovery_tx with app.app_context(): @@ -365,208 +364,20 @@ def test_process_payment_router_tx_details_transfer_recovery(app): challenge_event_bus=challenge_event_bus, ) - # Expect original transaction to have been removed + # Expect a recovery transaction record transaction_record = ( - session.query(USDCTransactionsHistory) - .filter(USDCTransactionsHistory.signature == "existingWithdrawal") - .filter(USDCTransactionsHistory.user_bank == trackOwnerUserBank) - .first() - ) - assert transaction_record is None - - -# Recovery transaction is for less than the original, should update original -# to be the difference -def test_process_payment_router_tx_details_transfer_partial_recovery( - app, -): - tx_response = mock_valid_transfer_single_recipient_recovery_tx - with app.app_context(): - db = get_db() - - transaction = tx_response.value.transaction.transaction - - tx_sig_str = str(transaction.signatures[0]) - - challenge_event_bus = create_autospec(ChallengeEventBus) - - test_entries_with_transaction = test_entries.copy() - test_entries_with_transaction["usdc_transactions_history"] = [ - { - "user_bank": trackOwnerUserBank, - "signature": "existingWithdrawal", - "transaction_type": USDCTransactionType.transfer, - "method": USDCTransactionMethod.send, - "change": -2000000, - "balance": 0, - "tx_metadata": transactionSenderOwnerAccount, - } - ] - - populate_mock_db(db, test_entries_with_transaction) - - with db.scoped_session() as session: - process_payment_router_tx_details( - session=session, - tx_info=tx_response, - tx_sig=tx_sig_str, - timestamp=datetime.now(), - challenge_event_bus=challenge_event_bus, - ) - - # Expect original transaction to be modified - transaction_record = ( - session.query(USDCTransactionsHistory) - .filter(USDCTransactionsHistory.signature == "existingWithdrawal") - .filter(USDCTransactionsHistory.user_bank == trackOwnerUserBank) - .filter( - USDCTransactionsHistory.tx_metadata == transactionSenderOwnerAccount - ) - .first() - ) - # Recovery transaction was for half of the original amount, expect the difference - assert transaction_record.change == -1000000 - assert transaction_record.balance == 1000000 - - -def test_process_payment_router_tx_details_transfer_over_recovery( - # Recovery transaction is for more than the original, should index as a new transfer - app, -): - tx_response = mock_valid_transfer_single_recipient_recovery_tx - with app.app_context(): - db = get_db() - - transaction = tx_response.value.transaction.transaction - - tx_sig_str = str(transaction.signatures[0]) - - challenge_event_bus = create_autospec(ChallengeEventBus) - - test_entries_with_transaction = test_entries.copy() - test_entries_with_transaction["usdc_transactions_history"] = [ - { - "user_bank": trackOwnerUserBank, - "signature": "existingWithdrawal", - "transaction_type": USDCTransactionType.transfer, - "method": USDCTransactionMethod.send, - "change": -500000, - "balance": 0, - "tx_metadata": transactionSenderOwnerAccount, - } - ] - - populate_mock_db(db, test_entries_with_transaction) - - with db.scoped_session() as session: - process_payment_router_tx_details( - session=session, - tx_info=tx_response, - tx_sig=tx_sig_str, - timestamp=datetime.now(), - challenge_event_bus=challenge_event_bus, - ) - - # Expect original transaction to be modified - existing_transaction_record = ( - session.query(USDCTransactionsHistory) - .filter(USDCTransactionsHistory.signature == "existingWithdrawal") - .filter(USDCTransactionsHistory.user_bank == trackOwnerUserBank) - .filter( - USDCTransactionsHistory.tx_metadata == transactionSenderOwnerAccount - ) - .first() - ) - # Recovery transaction was for more than the original amount, expect original transaction to be unchanged - assert existing_transaction_record.change == -500000 - - # Expect new transaction to have been added - new_transaction_record = ( session.query(USDCTransactionsHistory) .filter(USDCTransactionsHistory.signature == tx_sig_str) .filter(USDCTransactionsHistory.user_bank == trackOwnerUserBank) - .filter( - USDCTransactionsHistory.tx_metadata == transactionSenderOwnerAccount - ) .first() ) - - assert new_transaction_record is not None - assert new_transaction_record.method == USDCTransactionMethod.receive - assert new_transaction_record.transaction_type == USDCTransactionType.transfer - assert new_transaction_record.change == 1000000 - assert new_transaction_record.balance == 1000000 - - -# Recovery transaction doesn't match the most recent outbound transfer (different addresses). Should index -# as a regular inbound transfer -def test_process_payment_router_tx_details_transfer_recovery_address_mismatch( - app, -): - tx_response = mock_valid_transfer_single_recipient_recovery_tx - with app.app_context(): - db = get_db() - - transaction = tx_response.value.transaction.transaction - - tx_sig_str = str(transaction.signatures[0]) - - challenge_event_bus = create_autospec(ChallengeEventBus) - - test_entries_with_transaction = test_entries.copy() - test_entries_with_transaction["usdc_transactions_history"] = [ - { - "user_bank": trackOwnerUserBank, - "signature": "existingWithdrawal", - "transaction_type": USDCTransactionType.transfer, - "method": USDCTransactionMethod.send, - "change": -1000000, - "balance": 0, - "tx_metadata": "randomOtherAccount", - } - ] - - populate_mock_db(db, test_entries_with_transaction) - - with db.scoped_session() as session: - process_payment_router_tx_details( - session=session, - tx_info=tx_response, - tx_sig=tx_sig_str, - timestamp=datetime.now(), - challenge_event_bus=challenge_event_bus, - ) - - # Original transaction should remain unchanged - existing_transaction_record = ( - session.query(USDCTransactionsHistory) - .filter(USDCTransactionsHistory.signature == "existingWithdrawal") - .filter(USDCTransactionsHistory.user_bank == trackOwnerUserBank) - .filter(USDCTransactionsHistory.tx_metadata == "randomOtherAccount") - .first() - ) - assert existing_transaction_record is not None - assert existing_transaction_record.change == -1000000 - assert existing_transaction_record.balance == 0 - assert existing_transaction_record.method == USDCTransactionMethod.send + assert transaction_record.method == USDCTransactionMethod.receive assert ( - existing_transaction_record.transaction_type == USDCTransactionType.transfer - ) - - # Expect new transaction to have been added - transaction_record = ( - session.query(USDCTransactionsHistory) - .filter(USDCTransactionsHistory.signature == tx_sig_str) - .filter(USDCTransactionsHistory.user_bank == trackOwnerUserBank) - .first() + transaction_record.transaction_type + == USDCTransactionType.recover_withdrawal ) - assert transaction_record is not None - assert transaction_record.user_bank == trackOwnerUserBank - # Regular transfer, not a purchase - assert transaction_record.transaction_type == USDCTransactionType.transfer - assert transaction_record.method == USDCTransactionMethod.receive assert transaction_record.change == 1000000 - # For transfers, source is the owning wallet unless it's a transfer from a user bank + assert transaction_record.balance == 1000000 assert transaction_record.tx_metadata == transactionSenderOwnerAccount diff --git a/packages/discovery-provider/integration_tests/tasks/test_index_user_bank.py b/packages/discovery-provider/integration_tests/tasks/test_index_user_bank.py index 1c3d9a35a5b..687e43d0004 100644 --- a/packages/discovery-provider/integration_tests/tasks/test_index_user_bank.py +++ b/packages/discovery-provider/integration_tests/tasks/test_index_user_bank.py @@ -7,8 +7,9 @@ from integration_tests.tasks.user_bank_mock_transactions import ( EXTERNAL_ACCOUNT_ADDRESS, RECEIVER_ACCOUNT_WAUDIO_ADDRESS, - RECIPIENT_ACCOUNT_USDC_ADDRESS, - SENDER_ACCOUNT_USDC_ADDRESS, + RECIPIENT_USDC_USER_BANK_ADDRESS, + SENDER_ROOT_WALLET_USDC_ACCOUNT_OWNER, + SENDER_USDC_USER_BANK_ADDRESS, SENDER_ACCOUNT_WAUDIO_ADDRESS, mock_failed_track_purchase_tx, mock_invalid_track_purchase_bad_splits_tx, @@ -22,6 +23,8 @@ mock_valid_transfer_without_purchase_tx, mock_valid_waudio_transfer_between_user_banks, mock_valid_waudio_transfer_from_user_bank_to_external_address, + mock_valid_transfer_prepare_withdrawal_tx, + mock_valid_transfer_withdrawal_tx, ) from integration_tests.utils import populate_mock_db from src.challenges.challenge_event import ChallengeEvent @@ -45,10 +48,10 @@ from src.utils.redis_connection import get_redis track_owner_id = 1 -track_owner_usdc_user_bank = RECIPIENT_ACCOUNT_USDC_ADDRESS +track_owner_usdc_user_bank = RECIPIENT_USDC_USER_BANK_ADDRESS track_owner_eth_address = "0xe66402f9a6714a874a539fb1689b870dd271dfb2" track_buyer_id = 2 -track_buyer_user_bank = SENDER_ACCOUNT_USDC_ADDRESS +track_buyer_user_bank = SENDER_USDC_USER_BANK_ADDRESS sender_user_id = 3 sender_eth_address = "0x7d273271690538cf855e5b3002a0dd8c154bb060" @@ -57,6 +60,9 @@ receiver_eth_address = "0x627D8D6D0f06C5663809d29905db7A88317C6240" receiver_waudio_user_bank = RECEIVER_ACCOUNT_WAUDIO_ADDRESS +# Used as the source wallet for all the mock transactions +transactionSenderOwnerAccount = SENDER_ROOT_WALLET_USDC_ACCOUNT_OWNER + test_entries = { "users": [ { @@ -111,13 +117,13 @@ "track_price_history": [ { # pay full price to trackOwner "track_id": 1, - "splits": {RECIPIENT_ACCOUNT_USDC_ADDRESS: 1000000}, + "splits": {RECIPIENT_USDC_USER_BANK_ADDRESS: 1000000}, "total_price_cents": 100, }, { # pay $1 each to track owner and third party "track_id": 2, "splits": { - RECIPIENT_ACCOUNT_USDC_ADDRESS: 1000000, + RECIPIENT_USDC_USER_BANK_ADDRESS: 1000000, EXTERNAL_ACCOUNT_ADDRESS: 1000000, }, "total_price_cents": 200, @@ -537,7 +543,7 @@ def test_process_user_bank_txs_details_create_usdc_user_bank(app): ) assert user_bank is not None - assert user_bank.bank_account == RECIPIENT_ACCOUNT_USDC_ADDRESS + assert user_bank.bank_account == RECIPIENT_USDC_USER_BANK_ADDRESS assert user_bank.ethereum_address == track_owner_eth_address @@ -674,6 +680,90 @@ def test_process_user_bank_txs_details_skip_unknown_instructions(app): assert transaction_record is None +def test_process_user_bank_tx_details_prepare_withdrawal( + app, +): + tx_response = mock_valid_transfer_prepare_withdrawal_tx + with app.app_context(): + db = get_db() + redis = get_redis() + solana_client_manager_mock = create_autospec(SolanaClientManager) + + transaction = tx_response.value.transaction.transaction + + tx_sig_str = str(transaction.signatures[0]) + + challenge_event_bus = create_autospec(ChallengeEventBus) + + populate_mock_db(db, test_entries) + + with db.scoped_session() as session: + process_user_bank_tx_details( + solana_client_manager=solana_client_manager_mock, + redis=redis, + session=session, + tx_info=tx_response, + tx_sig=tx_sig_str, + timestamp=datetime.now(), + challenge_event_bus=challenge_event_bus, + ) + + transaction_record = ( + session.query(USDCTransactionsHistory) + .filter(USDCTransactionsHistory.signature == tx_sig_str) + .filter(USDCTransactionsHistory.user_bank == track_buyer_user_bank) + .first() + ) + assert transaction_record is not None + assert ( + transaction_record.transaction_type + == USDCTransactionType.prepare_withdrawal + ) + assert transaction_record.method == USDCTransactionMethod.send + assert transaction_record.change == -1000000 + assert transaction_record.tx_metadata == transactionSenderOwnerAccount + + +def test_process_user_bank_tx_details_withdrawal( + app, +): + tx_response = mock_valid_transfer_withdrawal_tx + with app.app_context(): + db = get_db() + redis = get_redis() + solana_client_manager_mock = create_autospec(SolanaClientManager) + + transaction = tx_response.value.transaction.transaction + + tx_sig_str = str(transaction.signatures[0]) + + challenge_event_bus = create_autospec(ChallengeEventBus) + + populate_mock_db(db, test_entries) + + with db.scoped_session() as session: + process_user_bank_tx_details( + solana_client_manager=solana_client_manager_mock, + redis=redis, + session=session, + tx_info=tx_response, + tx_sig=tx_sig_str, + timestamp=datetime.now(), + challenge_event_bus=challenge_event_bus, + ) + + transaction_record = ( + session.query(USDCTransactionsHistory) + .filter(USDCTransactionsHistory.signature == tx_sig_str) + .filter(USDCTransactionsHistory.user_bank == track_buyer_user_bank) + .first() + ) + assert transaction_record is not None + assert transaction_record.transaction_type == USDCTransactionType.withdrawal + assert transaction_record.method == USDCTransactionMethod.send + assert transaction_record.change == -1000000 + + def test_process_user_bank_txs_details_ignore_payment_router_transfers(app): # Payment router transaction, should be ignored by user bank indexer tx_response = ( diff --git a/packages/discovery-provider/integration_tests/tasks/user_bank_mock_transactions.py b/packages/discovery-provider/integration_tests/tasks/user_bank_mock_transactions.py index c216985e297..b34dc2592e3 100644 --- a/packages/discovery-provider/integration_tests/tasks/user_bank_mock_transactions.py +++ b/packages/discovery-provider/integration_tests/tasks/user_bank_mock_transactions.py @@ -29,9 +29,11 @@ UNKNOWN_PDA = "2HYsaffLbtDuMNNiUkvnQ1i9bHdMwtzEfB4win2bHkaj" # Used as sender / purchaser in tracks below -SENDER_ACCOUNT_USDC_ADDRESS = "38YSndmPWVF3UdzczbB3UMYUgPQtZrgvvPVHa3M4yQVX" +SENDER_USDC_USER_BANK_ADDRESS = "38YSndmPWVF3UdzczbB3UMYUgPQtZrgvvPVHa3M4yQVX" +SENDER_ROOT_WALLET_USDC_ACCOUNT = "3XmVeZ6M1FYDdUQaNeQZf8dipvtzNP6NVb5xjDkdeiNb" +SENDER_ROOT_WALLET_USDC_ACCOUNT_OWNER = "HXLN9UWwAjMPgHaFZDfgabT79SmLSdTeu2fUha2xHz9W" # Used as recipient / track owner in transactions below -RECIPIENT_ACCOUNT_USDC_ADDRESS = "7G1angvMtUZLFMyrMDGj7bxsduB4bjLD7VXRR7N4FXqe" +RECIPIENT_USDC_USER_BANK_ADDRESS = "7G1angvMtUZLFMyrMDGj7bxsduB4bjLD7VXRR7N4FXqe" SENDER_ACCOUNT_WAUDIO_ADDRESS = "9yqbTsyhgH6XFxZXVWKC3mEoxFnToodWiDgyx3YWZbL" RECEIVER_ACCOUNT_WAUDIO_ADDRESS = "ECohA2z8a9VGbicoFU7aJGt7ENRsJcsrnfnyG5e4qYkp" EXTERNAL_ACCOUNT_ADDRESS = "7hxqJmiPkSAP1zbtu8w2gWXUzEvNp8u9Ms5pKcKwXiNn" @@ -54,6 +56,10 @@ PURCHASE_TRACK1_MEMO_DATA = "7YSwHDhdZsHu6X" # base58.b58encode("track:2:10").decode("utf-8") PURCHASE_TRACK2_MEMO_DATA = "7YSwHDhdZtmtNs" +# base58.b58encode("Prepare Withdrawal").decode("utf-8") +PREPARE_WITHDRAWAL_MEMO = "4LXeTxmZydvvx9jk2DnmBAwcX" +# base58.b58encode("Withdrawal").decode("utf-8") +WITHDRAWAL_MEMO = "5uqUSXZpY6AMST" # contentType:contentId:blockNumber:purchaserUserId # base58.b58encode("track:1:10:2").decode("utf-8") @@ -77,9 +83,9 @@ }, "accountKeys": [ FEE_PAYER, - SENDER_ACCOUNT_USDC_ADDRESS, + SENDER_USDC_USER_BANK_ADDRESS, NONCE_ACCOUNT_ADDRESS, - RECIPIENT_ACCOUNT_USDC_ADDRESS, + RECIPIENT_USDC_USER_BANK_ADDRESS, "11111111111111111111111111111111", CLAIMABLE_TOKENS_PDA, USDC_PDA, @@ -250,9 +256,9 @@ }, "accountKeys": [ FEE_PAYER, - SENDER_ACCOUNT_USDC_ADDRESS, + SENDER_USDC_USER_BANK_ADDRESS, NONCE_ACCOUNT_ADDRESS, - RECIPIENT_ACCOUNT_USDC_ADDRESS, + RECIPIENT_USDC_USER_BANK_ADDRESS, "11111111111111111111111111111111", CLAIMABLE_TOKENS_PDA, USDC_PDA, @@ -424,9 +430,9 @@ }, "accountKeys": [ FEE_PAYER, - SENDER_ACCOUNT_USDC_ADDRESS, + SENDER_USDC_USER_BANK_ADDRESS, NONCE_ACCOUNT_ADDRESS, - RECIPIENT_ACCOUNT_USDC_ADDRESS, + RECIPIENT_USDC_USER_BANK_ADDRESS, "11111111111111111111111111111111", CLAIMABLE_TOKENS_PDA, USDC_PDA, @@ -572,6 +578,406 @@ ) +# Transfer $1 to a non user bank account as preparation for withdrawal +mock_valid_transfer_prepare_withdrawal_tx = GetTransactionResp.from_json( + json.dumps( + { + "jsonrpc": "2.0", + "result": { + "slot": 227246439, + "transaction": { + "signatures": [MOCK_SIGNATURE], + "message": { + "header": { + "numRequiredSignatures": 1, + "numReadonlySignedAccounts": 0, + "numReadonlyUnsignedAccounts": 8, + }, + "accountKeys": [ + FEE_PAYER, + SENDER_USDC_USER_BANK_ADDRESS, + NONCE_ACCOUNT_ADDRESS, + SENDER_ROOT_WALLET_USDC_ACCOUNT_OWNER, + "11111111111111111111111111111111", + CLAIMABLE_TOKENS_PDA, + USDC_PDA, + "KeccakSecp256k11111111111111111111111111111", + "Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo", + "Sysvar1nstructions1111111111111111111111111", + "SysvarRent111111111111111111111111111111111", + "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + ], + "recentBlockhash": "5H434VMiHgK7RaJZaBKKcriu4eky8erb9QGfcHJSZquU", + "instructions": [ + { + "programIdIndex": 7, + "accounts": [], + "data": "H4eCheRWTZDTCFYUcyMzE6EhQMZvvvLKJ9g6YaUpbZeoLLgVj1uvwCTdzcb2MzbKHsRjN8DjLYdqxuQEZe2TjUKCuBMrFtpnnLd4RcvBnr4ieHCdH8ZU1N6XDfiqyKB4zenQ9S4viza4ob4gbtmiRS6o6KGEtL3fJQRvaA3tdtSx1rfFogZzwMXAxHrkuxHrpAqfm", + "stackHeight": None, + }, + { + "programIdIndex": 5, + "accounts": [0, 1, 3, 2, 6, 10, 9, 4, 11], + "data": "6dMrrkPeSzw2r5huQ6RToaJCaVuu", + "stackHeight": None, + }, + { + "programIdIndex": 8, + "accounts": [0], + "data": PREPARE_WITHDRAWAL_MEMO, + "stackHeight": None, + }, + ], + }, + }, + "meta": { + "err": None, + "status": {"Ok": None}, + "fee": 10000, + "preBalances": [ + 1689358166, + 2039280, + 953520, + 2039280, + 1, + 1141440, + 0, + 1, + 121159680, + 0, + 1009200, + 934087680, + ], + "postBalances": [ + 1689348166, + 2039280, + 953520, + 2039280, + 1, + 1141440, + 0, + 1, + 121159680, + 0, + 1009200, + 934087680, + ], + "innerInstructions": [ + { + "index": 1, + "instructions": [ + { + "programIdIndex": 11, + "accounts": [1, 3, 6, 6], + "data": "3mhiKuxuaKy1", + "stackHeight": 2, + } + ], + } + ], + "logMessages": [ + f"Program {CLAIMABLE_TOKENS_PDA} invoke [1]", + "Program log: Instruction: Transfer", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]", + "Program log: Instruction: Transfer", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4728 of 581084 compute units", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", + f"Program {CLAIMABLE_TOKENS_PDA} consumed 24149 of 600000 compute units", + f"Program {CLAIMABLE_TOKENS_PDA} success", + "Program Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo invoke [1]", + "Program Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo consumed 588 of 575851 compute units", + "Program Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo success", + ], + "preTokenBalances": [ + { + "accountIndex": 1, + "mint": USDC_MINT, + "uiTokenAmount": { + "uiAmount": 1.0, + "decimals": 6, + "amount": "1000000", + "uiAmountString": "1", + }, + "owner": USDC_PDA, + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + }, + { + "accountIndex": 3, + "mint": USDC_MINT, + "uiTokenAmount": { + "uiAmount": None, + "decimals": 6, + "amount": "0", + "uiAmountString": "0", + }, + "owner": SENDER_ROOT_WALLET_USDC_ACCOUNT_OWNER, + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + }, + ], + "postTokenBalances": [ + { + "accountIndex": 1, + "mint": USDC_MINT, + "uiTokenAmount": { + "uiAmount": None, + "decimals": 6, + "amount": "0", + "uiAmountString": "0", + }, + "owner": USDC_PDA, + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + }, + { + "accountIndex": 3, + "mint": USDC_MINT, + "uiTokenAmount": { + "uiAmount": 1.0, + "decimals": 6, + "amount": "1000000", + "uiAmountString": "1", + }, + "owner": SENDER_ROOT_WALLET_USDC_ACCOUNT_OWNER, + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + }, + ], + "rewards": [], + "loadedAddresses": {"writable": [], "readonly": []}, + "computeUnitsConsumed": 24737, + }, + "blockTime": 1698802811, + }, + "id": 0, + } + ) +) + + +# Transfer $1 to a user bank and then out to an external address with a withdrawal memo +# Flow is root wallet usdc acct -> user bank -> external address +mock_valid_transfer_withdrawal_tx = GetTransactionResp.from_json( + json.dumps( + { + "jsonrpc": "2.0", + "result": { + "slot": 245096140, + "transaction": { + "signatures": [MOCK_SIGNATURE], + "message": { + "header": { + "numRequiredSignatures": 1, + "numReadonlySignedAccounts": 0, + "numReadonlyUnsignedAccounts": 9, + }, + "accountKeys": [ + FEE_PAYER, + "3nmLmEzwBBjERiV9UU1a4JXzESwDjrKZdSjP1KG4M9Mc", # Destination + SENDER_USDC_USER_BANK_ADDRESS, # User Bank Sender + NONCE_ACCOUNT_ADDRESS, # Nonce + SENDER_ROOT_WALLET_USDC_ACCOUNT, # Root wallet usdc account + "11111111111111111111111111111111", + USDC_MINT, + CLAIMABLE_TOKENS_PDA, + USDC_PDA, + "KeccakSecp256k11111111111111111111111111111", + "Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo", + "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr", + "Sysvar1nstructions1111111111111111111111111", + "SysvarRent111111111111111111111111111111111", + "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + ], + "recentBlockhash": "B1jNkfQheX2m3NosedANVueUCw9sekihw5KNAYACwKWg", + "instructions": [ + { + "programIdIndex": 13, + "accounts": [4, 6, 2, 0], + "data": "gvPShZQhKrzGM", + "stackHeight": None, + }, + { + "programIdIndex": 9, + "accounts": [], + "data": "H4eCsH1NV1T5Rrwb1xTxXJaqbsXvM53nh9nFhr4guwnTgJUjzbiPWBg4vTg44xzSgQCs1uEz1MYyFnynuyWtVKqMyUUvTNMU3giPhXtdfJLMrPhtaPz1sYN6qcd7YGwyk6cMvEZPPjGjMceYxumjPN4aYeFsrazCisbpp8FYiN4bVsV1HV5BkEqfH1wgDaezzBmFd", + "stackHeight": None, + }, + { + "programIdIndex": 7, + "accounts": [0, 2, 1, 3, 8, 13, 12, 5, 14], + "data": "6dMrrkPeSzw2r5huQ6RToaJCaVuu", + "stackHeight": None, + }, + { + "programIdIndex": 10, + "accounts": [0], + "data": WITHDRAWAL_MEMO, + "stackHeight": None, + }, + { + "programIdIndex": 11, + "accounts": [0], + "data": "4sHBbAWcJZWyJ5TcmKVbb1xNF1kEk9KKgfjJ1pB8aiYY6b3jYCg3NRpegK2XUEdUdtZtEhiT9sxDTanxBg7QNo74XiqnQ6rmtsSP8Ky42NQ9oZQbMhptMz5hmVxzEfYUYFyQCDToEjfQT2daE3CtsXSdgt3BnmZnpck", + "stackHeight": None, + }, + ], + }, + }, + "meta": { + "err": None, + "status": {"Ok": None}, + "fee": 10000, + "preBalances": [ + 49083651, + 2039280, + 2039280, + 953520, + 2039280, + 1, + 227469696505, + 1141440, + 0, + 1, + 521498880, + 521498880, + 0, + 1009200, + 934087680, + ], + "postBalances": [ + 49073651, + 2039280, + 2039280, + 953520, + 2039280, + 1, + 227469696505, + 1141440, + 0, + 1, + 521498880, + 521498880, + 0, + 1009200, + 934087680, + ], + "innerInstructions": [ + { + "index": 2, + "instructions": [ + { + "programIdIndex": 14, + "accounts": [2, 1, 8, 8], + "data": "3QCwqmHZ4mdq", + "stackHeight": 2, + } + ], + } + ], + "logMessages": [ + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [1]", + "Program log: Instruction: TransferChecked", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 6199 of 800000 compute units", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", + f"Program {CLAIMABLE_TOKENS_PDA} invoke [1]", + "Program log: Instruction: Transfer", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]", + "Program log: Instruction: Transfer", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4728 of 774885 compute units", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", + f"Program {CLAIMABLE_TOKENS_PDA} consumed 24149 of 793801 compute units", + f"Program {CLAIMABLE_TOKENS_PDA} success", + "Program Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo invoke [1]", + "Program Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo consumed 588 of 575851 compute units", + "Program Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo success", + "Program MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr invoke [1]", + "Program MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr consumed 56953 of 769652 compute units", + "Program MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr success", + ], + "preTokenBalances": [ + { + "accountIndex": 1, + "mint": USDC_MINT, + "uiTokenAmount": { + "uiAmount": None, + "decimals": 6, + "amount": "0", + "uiAmountString": "0", + }, + "owner": "41zCUJsKk6cMB94DDtm99qWmyMZfp4GkAhhuz4xTwePu", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + }, + { + "accountIndex": 2, + "mint": USDC_MINT, + "uiTokenAmount": { + "uiAmount": 12.61479, + "decimals": 6, + "amount": "12614790", + "uiAmountString": "12.61479", + }, + "owner": USDC_PDA, + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + }, + { + "accountIndex": 4, + "mint": USDC_MINT, + "uiTokenAmount": { + "uiAmount": 1.0, + "decimals": 6, + "amount": "1000000", + "uiAmountString": "1", + }, + "owner": SENDER_ROOT_WALLET_USDC_ACCOUNT_OWNER, + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + }, + ], + "postTokenBalances": [ + { + "accountIndex": 1, + "mint": USDC_MINT, + "uiTokenAmount": { + "uiAmount": 1.0, + "decimals": 6, + "amount": "1000000", + "uiAmountString": "1", + }, + "owner": "41zCUJsKk6cMB94DDtm99qWmyMZfp4GkAhhuz4xTwePu", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + }, + { + "accountIndex": 2, + "mint": USDC_MINT, + "uiTokenAmount": { + "uiAmount": 12.61479, + "decimals": 6, + "amount": "12614790", + "uiAmountString": "12.61479", + }, + "owner": USDC_PDA, + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + }, + { + "accountIndex": 4, + "mint": USDC_MINT, + "uiTokenAmount": { + "uiAmount": None, + "decimals": 6, + "amount": "0", + "uiAmountString": "0", + }, + "owner": "61DEuBcQzLWLgsr8F8XqJo5NeULfdTdVThKiYz4AtdB7", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + }, + ], + "rewards": [], + "loadedAddresses": {"writable": [], "readonly": []}, + "computeUnitsConsumed": 87301, + }, + "blockTime": 1706633649, + }, + "id": 0, + } + ) +) + # Purchase of track id 2 for $2 USDC with single recipient. # It is assumed track id 2 has multiple splits so this will be an invalid purchase mock_invalid_track_purchase_missing_splits_tx = GetTransactionResp.from_json( @@ -590,9 +996,9 @@ }, "accountKeys": [ FEE_PAYER, - SENDER_ACCOUNT_USDC_ADDRESS, + SENDER_USDC_USER_BANK_ADDRESS, NONCE_ACCOUNT_ADDRESS, - RECIPIENT_ACCOUNT_USDC_ADDRESS, + RECIPIENT_USDC_USER_BANK_ADDRESS, "11111111111111111111111111111111", CLAIMABLE_TOKENS_PDA, USDC_PDA, @@ -764,9 +1170,9 @@ }, "accountKeys": [ FEE_PAYER, - SENDER_ACCOUNT_USDC_ADDRESS, + SENDER_USDC_USER_BANK_ADDRESS, NONCE_ACCOUNT_ADDRESS, - RECIPIENT_ACCOUNT_USDC_ADDRESS, + RECIPIENT_USDC_USER_BANK_ADDRESS, EXTERNAL_ACCOUNT_ADDRESS, "11111111111111111111111111111111", CLAIMABLE_TOKENS_PDA, @@ -979,9 +1385,9 @@ }, "accountKeys": [ FEE_PAYER, - SENDER_ACCOUNT_USDC_ADDRESS, + SENDER_USDC_USER_BANK_ADDRESS, NONCE_ACCOUNT_ADDRESS, - RECIPIENT_ACCOUNT_USDC_ADDRESS, + RECIPIENT_USDC_USER_BANK_ADDRESS, "11111111111111111111111111111111", CLAIMABLE_TOKENS_PDA, USDC_PDA, @@ -1152,9 +1558,9 @@ }, "accountKeys": [ FEE_PAYER, - SENDER_ACCOUNT_USDC_ADDRESS, + SENDER_USDC_USER_BANK_ADDRESS, NONCE_ACCOUNT_ADDRESS, - RECIPIENT_ACCOUNT_USDC_ADDRESS, + RECIPIENT_USDC_USER_BANK_ADDRESS, "11111111111111111111111111111111", CLAIMABLE_TOKENS_PDA, UNKNOWN_PDA, @@ -1328,7 +1734,7 @@ }, "accountKeys": [ FEE_PAYER, - RECIPIENT_ACCOUNT_USDC_ADDRESS, + RECIPIENT_USDC_USER_BANK_ADDRESS, "11111111111111111111111111111111", USDC_MINT, USDC_PDA, @@ -1882,9 +2288,9 @@ }, "accountKeys": [ FEE_PAYER, - SENDER_ACCOUNT_USDC_ADDRESS, + SENDER_USDC_USER_BANK_ADDRESS, NONCE_ACCOUNT_ADDRESS, - RECIPIENT_ACCOUNT_USDC_ADDRESS, + RECIPIENT_USDC_USER_BANK_ADDRESS, "11111111111111111111111111111111", CLAIMABLE_TOKENS_PDA, USDC_PDA, diff --git a/packages/discovery-provider/src/models/users/usdc_transactions_history.py b/packages/discovery-provider/src/models/users/usdc_transactions_history.py index c8fc45b02a4..b8c4356ecf1 100644 --- a/packages/discovery-provider/src/models/users/usdc_transactions_history.py +++ b/packages/discovery-provider/src/models/users/usdc_transactions_history.py @@ -9,6 +9,9 @@ class USDCTransactionType(str, enum.Enum): purchase_content = "PURCHASE_CONTENT" transfer = "TRANSFER" + prepare_withdrawal = "PREPARE_WITHDRAWAL" + recover_withdrawal = "RECOVER_WITHDRAWAL" + withdrawal = "WITHDRAWAL" purchase_stripe = "PURCHASE_STRIPE" diff --git a/packages/discovery-provider/src/tasks/index_payment_router.py b/packages/discovery-provider/src/tasks/index_payment_router.py index ebb1e3ee2c3..3256e9aea64 100644 --- a/packages/discovery-provider/src/tasks/index_payment_router.py +++ b/packages/discovery-provider/src/tasks/index_payment_router.py @@ -47,6 +47,7 @@ BalanceChange, decode_all_solana_memos, get_account_index, + get_account_owner_from_balance_change, get_solana_tx_token_balance_changes, get_valid_instruction, has_log, @@ -93,7 +94,7 @@ # The amount of USDC that represents one USD cent USDC_PER_USD_CENT = 10000 -RECOVERY_MEMO_STRING = "recovery" +RECOVERY_MEMO_STRING = "Recover Withdrawal" # Used to limit tx history if needed MIN_SLOT = int(shared_config["solana"]["payment_router_min_slot"]) @@ -180,16 +181,6 @@ def get_highest_payment_router_tx_slot(session: Session): return slot -def get_account_owner_from_balance_change( - sender_account: str, balance_changes: dict[str, BalanceChange] -): - """Finds the owner of the sender account by looking at the balance changes""" - if sender_account in balance_changes: - return balance_changes[sender_account]["owner"] - else: - return None - - # Cache the latest value committed to DB in redis # Used for quick retrieval in health check def cache_latest_sol_payment_router_db_tx(redis: Redis, tx): @@ -390,88 +381,13 @@ def index_purchase( ) -def attempt_index_recovery_transfer( - session: Session, - sender_account: str, - sender_user_account: UserIdBankAccount | None, - receiver_user_accounts: List[UserIdBankAccount], - balance_changes: dict[str, BalanceChange], - tx_sig: str, -): - if sender_user_account is not None: - raise Exception( - f"Sender account for recovery cannot be a user bank: {sender_user_account}" - ) - if len(receiver_user_accounts) != 1: - raise Exception( - f"Recovery transfer must have exactly one receiver account. Received: {','.join([a['user_bank_account'] for a in receiver_user_accounts])}" - ) - - sender_account_owner = get_account_owner_from_balance_change( - sender_account=sender_account, balance_changes=balance_changes - ) - if sender_account_owner is None: - raise Exception( - f"Unexpectedly found no owner account for sender account {sender_account}" - ) - - receiver_user_account = receiver_user_accounts[0] - receiver_bank_account = receiver_user_account["user_bank_account"] - receiver_balance_change = balance_changes[receiver_bank_account] - receiver_balance_change_amount = receiver_balance_change["change"] - - # Find the previous transaction in the other direction (receiver -> sender) - # So that we can modify it - previous_transaction = ( - session.query(USDCTransactionsHistory) - .filter( - and_( - USDCTransactionsHistory.user_bank == receiver_bank_account, - USDCTransactionsHistory.method == USDCTransactionMethod.send, - # The original transaction will be indexed with the owner account - # of the original recipient - USDCTransactionsHistory.tx_metadata == sender_account_owner, - ) - ) - .order_by(desc(USDCTransactionsHistory.transaction_created_at)) - .first() - ) - if previous_transaction is None: - raise Exception( - f"Failed to find matching previous transcation from {receiver_bank_account} to {sender_account}" - ) - - difference = previous_transaction.change + receiver_balance_change_amount - - # Previous transaction is undone by this recovery - if difference == 0: - logger.info( - f"index_payment_router.py | tx: {tx_sig} | Removing previous transaction {previous_transaction.signature}." - ) - session.delete(previous_transaction) - elif difference < 0: - # Previous transaction is partially undone by this recovery - # Modifying the object returned by the query will update the DB - # when the session is flushed - previous_transaction.change = ( - previous_transaction.change + receiver_balance_change_amount - ) - previous_transaction.balance = Decimal(receiver_balance_change["post_balance"]) - logger.info( - f"index_payment_router.py | tx: {tx_sig} | Found partial recovery for {previous_transaction.signature}. New change value is {previous_transaction.change}." - ) - else: - raise Exception( - f"Recovery transaction is for greater amount than original transaction ({receiver_balance_change_amount} > {-previous_transaction.change})" - ) - - def index_transfer( session: Session, sender_account: str, sender_user_account: UserIdBankAccount | None, receiver_user_accounts: List[UserIdBankAccount], receiver_accounts: List[str], + transaction_type: USDCTransactionType, balance_changes: dict[str, BalanceChange], slot: int, timestamp: datetime, @@ -483,7 +399,7 @@ def index_transfer( if sender_user_account is None: sender_metadata_address = ( get_account_owner_from_balance_change( - sender_account=sender_account, balance_changes=balance_changes + account=sender_account, balance_changes=balance_changes ) or sender_account ) @@ -493,7 +409,7 @@ def index_transfer( user_bank=user_account["user_bank_account"], slot=slot, signature=tx_sig, - transaction_type=USDCTransactionType.transfer, + transaction_type=transaction_type, method=USDCTransactionMethod.receive, transaction_created_at=timestamp, change=Decimal(balance_change["change"]), @@ -501,8 +417,9 @@ def index_transfer( tx_metadata=sender_metadata_address, ) session.add(usdc_tx_received) + logger.debug( - f"index_payment_router.py | tx: {tx_sig} | Creating transfer received tx {usdc_tx_received}" + f"index_payment_router.py | tx: {tx_sig} | Creating {transaction_type} received tx {usdc_tx_received}" ) # If sender was a user bank, index a single transfer with a list of recipients if sender_user_account is not None: @@ -511,7 +428,7 @@ def index_transfer( user_bank=sender_user_account["user_bank_account"], slot=slot, signature=tx_sig, - transaction_type=USDCTransactionType.transfer, + transaction_type=transaction_type, method=USDCTransactionMethod.send, transaction_created_at=timestamp, change=Decimal(balance_change["change"]), @@ -520,7 +437,7 @@ def index_transfer( ) session.add(usdc_tx_received) logger.debug( - f"index_payment_router.py | tx: {tx_sig} | Creating transfer received tx {usdc_tx_received}" + f"index_payment_router.py | tx: {tx_sig} | Creating {transaction_type} sent tx {usdc_tx_received}" ) @@ -558,32 +475,12 @@ def validate_and_index_usdc_transfers( ) # For invalid purchases or transfers not related to a purchase, we'll index # it as a regular transfer from the sender_account. - elif memo is not None and memo["type"] is RouteTransactionMemoType.recovery: - try: - attempt_index_recovery_transfer( - session=session, - sender_account=sender_account, - sender_user_account=sender_user_account, - receiver_user_accounts=receiver_user_accounts, - balance_changes=balance_changes, - tx_sig=tx_sig, - ) - except Exception as e: - logger.warn( - f"index_payment_router.py | tx: {tx_sig} | Failed to index recovery transfer. Will index as plain transfer. Exception received was: {e}." - ) - index_transfer( - session=session, - sender_account=sender_account, - sender_user_account=sender_user_account, - receiver_user_accounts=receiver_user_accounts, - receiver_accounts=receiver_accounts, - balance_changes=balance_changes, - slot=slot, - timestamp=timestamp, - tx_sig=tx_sig, - ) else: + transaction_type = ( + USDCTransactionType.recover_withdrawal + if memo is not None and memo["type"] is RouteTransactionMemoType.recovery + else USDCTransactionType.transfer + ) index_transfer( session=session, sender_account=sender_account, @@ -591,6 +488,7 @@ def validate_and_index_usdc_transfers( receiver_user_accounts=receiver_user_accounts, receiver_accounts=receiver_accounts, balance_changes=balance_changes, + transaction_type=transaction_type, slot=slot, timestamp=timestamp, tx_sig=tx_sig, diff --git a/packages/discovery-provider/src/tasks/index_user_bank.py b/packages/discovery-provider/src/tasks/index_user_bank.py index 64d8142d65b..59e7b4b9a63 100644 --- a/packages/discovery-provider/src/tasks/index_user_bank.py +++ b/packages/discovery-provider/src/tasks/index_user_bank.py @@ -62,6 +62,7 @@ BalanceChange, decode_all_solana_memos, get_account_index, + get_account_owner_from_balance_change, get_solana_tx_token_balance_changes, get_valid_instruction, has_log, @@ -114,6 +115,9 @@ MIN_SLOT = int(shared_config["solana"]["user_bank_min_slot"]) INITIAL_FETCH_SIZE = 10 +PREPARE_WITHDRAWAL_MEMO_STRING = "Prepare Withdrawal" +WITHDRAWAL_MEMO_STRING = "Withdrawal" + # Used to find the correct accounts for sender/receiver in the transaction and the ClaimableTokensPDA TRANSFER_SENDER_ACCOUNT_INDEX = 1 TRANSFER_RECEIVER_ACCOUNT_INDEX = 2 @@ -258,6 +262,16 @@ def process_create_userbank_instruction( logger.error(e) +def get_transfer_type_from_memo(memos: List[str]) -> USDCTransactionType: + """Checks the list of memos for one matching containing a transaction type and returns it if found. Defaults to USDCTransactionType.transfer if no matching memo is found""" + for memo in memos: + if memo == PREPARE_WITHDRAWAL_MEMO_STRING: + return USDCTransactionType.prepare_withdrawal + elif memo == WITHDRAWAL_MEMO_STRING: + return USDCTransactionType.withdrawal + return USDCTransactionType.transfer + + class PurchaseMetadataDict(TypedDict): price: int splits: dict[str, int] @@ -551,7 +565,6 @@ def index_user_tip( def process_transfer_instruction( - solana_client_manager: SolanaClientManager, session: Session, redis: Redis, instruction: CompiledInstruction, @@ -646,29 +659,38 @@ def process_transfer_instruction( # Cannot index receive external transfers this way as those use the spl-token program, # not the claimable tokens program, so we will always have a sender_user_id if receiver_user_id is None: - receiver_account_pubkey = Pubkey.from_string(receiver_account) - receiver_account_info = solana_client_manager.get_account_info_json_parsed( - receiver_account_pubkey + transaction_type = get_transfer_type_from_memo(memos=memos) + + # Attempt to look up account owner, fallback to recipient address + receiver_account_owner = ( + get_account_owner_from_balance_change( + account=receiver_account, balance_changes=balance_changes + ) + or receiver_account ) - if receiver_account_info: - receiver_account_owner = receiver_account_info.data.parsed["info"]["owner"] - else: - receiver_account_owner = receiver_account TransactionHistoryModel = ( AudioTransactionsHistory if is_audio else USDCTransactionsHistory ) + change_amount = Decimal(balance_changes[sender_account]["change"]) + # For withdrawals, the user bank balance change will be zero, since the + # user bank is only used as an intermediate step. So we will index the change + # amount as how much was actually sent to the destination + if transaction_type == USDCTransactionType.withdrawal: + change_amount = -Decimal(balance_changes[receiver_account]["change"]) transfer_sent = TransactionHistoryModel( user_bank=sender_account, slot=slot, signature=tx_sig, - transaction_type=TransactionType.transfer, + transaction_type=transaction_type, method=TransactionMethod.send, transaction_created_at=timestamp, - change=Decimal(balance_changes[sender_account]["change"]), + change=change_amount, balance=Decimal(balance_changes[sender_account]["post_balance"]), tx_metadata=str(receiver_account_owner), ) - logger.debug(f"index_user_bank.py | Creating transfer sent {transfer_sent}") + logger.debug( + f"index_user_bank.py | Creating {transaction_type} sent {transfer_sent}" + ) session.add(transfer_sent) # If there are two userbanks to update, it was a transfer from user to user else: @@ -801,7 +823,6 @@ def process_user_bank_tx_details( elif has_transfer_instruction: process_transfer_instruction( - solana_client_manager=solana_client_manager, session=session, redis=redis, instruction=instruction, diff --git a/packages/discovery-provider/src/utils/helpers.py b/packages/discovery-provider/src/utils/helpers.py index 754628d9c37..68137e477e5 100644 --- a/packages/discovery-provider/src/utils/helpers.py +++ b/packages/discovery-provider/src/utils/helpers.py @@ -511,6 +511,16 @@ def decode_all_solana_memos(tx_message: Message): return [] +def get_account_owner_from_balance_change( + account: str, balance_changes: dict[str, BalanceChange] +): + """Finds the owner of the sender account by looking at the balance changes""" + if account in balance_changes: + return balance_changes[account]["owner"] + else: + return None + + def get_valid_instruction( tx_message: Message, meta: UiTransactionStatusMeta, program_address: str ) -> Optional[CompiledInstruction]: