diff --git a/app/api/swap/models.py b/app/api/swap/models.py index 51dc8e4..49fa84c 100644 --- a/app/api/swap/models.py +++ b/app/api/swap/models.py @@ -231,6 +231,11 @@ class SwapQuoteRequest(SwapSupportRequest): class SwapStatusRequest(SwapBaseModel): tx_hash: str = Field(description="Transaction hash of the swap") + source_coin: Coin = Field(description="Source coin of the swap") + source_chain_id: str = Field(description="Source chain ID of the swap") + destination_coin: Coin = Field(description="Destination coin of the swap") + destination_chain_id: str = Field(description="Destination chain ID of the swap") + deposit_address: str = Field(description="Deposit address of the swap") deposit_memo: str | None = Field( default=None, @@ -457,61 +462,12 @@ class SwapQuote(SwapBaseModel): routes: list[SwapRoute] = Field(description="Available swap routes") -class SwapTransactionDetails(SwapBaseModel): - coin: Coin = Field(description="Coin identifier") - chain_id: str = Field(description="Chain identifier") - hash: str = Field(description="Transaction hash") - explorer_url: str | None = Field( - default=None, - description="Block explorer URL for this transaction", - ) - - @property - def chain(self) -> Chain | None: - return Chain.get(self.coin, self.chain_id) - - -class SwapDetails(SwapBaseModel): - amount_in: str | None = Field( - default=None, - description="Actual input amount in smallest unit", - ) - amount_in_formatted: str | None = Field( - default=None, - description="Actual input amount in readable format", - ) - - amount_out: str | None = Field( - default=None, - description="Actual output amount in smallest unit", - ) - amount_out_formatted: str | None = Field( - default=None, - description="Actual output amount in readable format", - ) - - refunded_amount: str | None = Field( - default=None, - description="Refunded amount in smallest unit (if any)", - ) - refunded_amount_formatted: str | None = Field( - default=None, - description="Refunded amount in readable format", - ) - - transactions: list[SwapTransactionDetails] = Field( - default_factory=list, - description="All transactions involved in the swap", - ) - - -class SwapStatusResponse(SwapSupportRequest): +class SwapStatusResponse(SwapBaseModel): status: SwapStatus = Field(description="Current status of the swap") - swap_details: SwapDetails | None = Field( + internal_status: str | None = Field( default=None, - description="Detailed swap information", + description="Provider-specific status of the swap", ) - provider: SwapProviderEnum = Field(description="Provider handling this swap") explorer_url: str | None = Field( default=None, description="Block explorer URL for the swap transaction", diff --git a/app/api/swap/providers/near_intents/client.py b/app/api/swap/providers/near_intents/client.py index cd5ee7c..9a02d13 100644 --- a/app/api/swap/providers/near_intents/client.py +++ b/app/api/swap/providers/near_intents/client.py @@ -222,7 +222,6 @@ async def get_status(self, request: SwapStatusRequest) -> SwapStatusResponse: data = response.json() near_response = NearIntentsStatusResponse.model_validate(data) - supported_tokens = await self.get_supported_tokens() - return from_near_intents_status(near_response, supported_tokens) + return from_near_intents_status(near_response, request) self._handle_error_response(response) diff --git a/app/api/swap/providers/near_intents/models.py b/app/api/swap/providers/near_intents/models.py index 25f531e..7a0d72d 100644 --- a/app/api/swap/providers/near_intents/models.py +++ b/app/api/swap/providers/near_intents/models.py @@ -75,43 +75,8 @@ class NearIntentsQuoteResponse(BaseModel): model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) -class NearIntentsTransactionDetails(BaseModel): - hash: str - explorer_url: str | None = Field(default=None) - - model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) - - -class NearIntentsSwapDetails(BaseModel): - intent_hashes: list[str] = Field(default_factory=list) - near_tx_hashes: list[str] = Field(default_factory=list) - - amount_in: str | None = Field(default=None) - amount_in_formatted: str | None = Field(default=None) - amount_in_usd: str | None = Field(default=None) - - amount_out: str | None = Field(default=None) - amount_out_formatted: str | None = Field(default=None) - amount_out_usd: str | None = Field(default=None) - - refunded_amount: str | None = Field(default=None) - refunded_amount_formatted: str | None = Field(default=None) - - origin_chain_tx_hashes: list[NearIntentsTransactionDetails] = Field( - default_factory=list - ) - destination_chain_tx_hashes: list[NearIntentsTransactionDetails] = Field( - default_factory=list - ) - - model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) - - class NearIntentsStatusResponse(BaseModel): - quote_response: NearIntentsQuoteResponse status: str - updated_at: datetime - swap_details: NearIntentsSwapDetails model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) diff --git a/app/api/swap/providers/near_intents/test_client.py b/app/api/swap/providers/near_intents/test_client.py index da47ae1..4312900 100644 --- a/app/api/swap/providers/near_intents/test_client.py +++ b/app/api/swap/providers/near_intents/test_client.py @@ -10,6 +10,7 @@ SwapErrorKind, SwapProviderEnum, SwapQuoteRequest, + SwapStatus, SwapStatusRequest, SwapSupportRequest, SwapType, @@ -998,6 +999,10 @@ async def test_post_submit_hook_success(client, mock_httpx_client): request = SwapStatusRequest( tx_hash="4jLC9UPQJUyEK9dbgTywQQHeJTngX54FjJ6ZLPb1BUspGX4ZZGrg3u4P5tjHGqzpuq1c73rD2QwhyFQETvPgWdm5", + source_coin=Chain.SOLANA.coin, + source_chain_id=Chain.SOLANA.chain_id, + destination_coin=Chain.BITCOIN.coin, + destination_chain_id=Chain.BITCOIN.chain_id, deposit_address="4Rqnz7SPU4EqSUravxbKTSBti4RNf1XGaqvBmnLfvH83", deposit_memo=None, provider=SwapProviderEnum.NEAR_INTENTS, @@ -1032,6 +1037,10 @@ async def test_post_submit_hook_with_memo(client, mock_httpx_client): request = SwapStatusRequest( tx_hash="test_hash", + source_coin=Chain.SOLANA.coin, + source_chain_id=Chain.SOLANA.chain_id, + destination_coin=Chain.BITCOIN.coin, + destination_chain_id=Chain.BITCOIN.chain_id, deposit_address="test_address", deposit_memo="test_memo", provider=SwapProviderEnum.NEAR_INTENTS, @@ -1054,6 +1063,10 @@ async def test_post_submit_hook_error(client, mock_httpx_client): request = SwapStatusRequest( tx_hash="invalid_hash", + source_coin=Chain.SOLANA.coin, + source_chain_id=Chain.SOLANA.chain_id, + destination_coin=Chain.BITCOIN.coin, + destination_chain_id=Chain.BITCOIN.chain_id, deposit_address="test_address", deposit_memo=None, provider=SwapProviderEnum.NEAR_INTENTS, @@ -1085,7 +1098,10 @@ async def test_get_status_success( mock_response.json.return_value = { "quoteResponse": { "quoteRequest": MOCK_QUOTE_REQUEST, - "quote": MOCK_FIRM_QUOTE, + "quote": { + **MOCK_FIRM_QUOTE, + "depositAddress": "4Rqnz7SPU4EqSUravxbKTSBti4RNf1XGaqvBmnLfvH83", + }, }, "status": "SUCCESS", "updatedAt": "2025-12-11T13:48:50.883000Z", @@ -1110,6 +1126,10 @@ async def test_get_status_success( request = SwapStatusRequest( tx_hash="4jLC9UPQJUyEK9dbgTywQQHeJTngX54FjJ6ZLPb1BUspGX4ZZGrg3u4P5tjHGqzpuq1c73rD2QwhyFQETvPgWdm5", + source_coin=Chain.SOLANA.coin, + source_chain_id=Chain.SOLANA.chain_id, + destination_coin=Chain.BITCOIN.coin, + destination_chain_id=Chain.BITCOIN.chain_id, deposit_address="4Rqnz7SPU4EqSUravxbKTSBti4RNf1XGaqvBmnLfvH83", deposit_memo=None, provider=SwapProviderEnum.NEAR_INTENTS, @@ -1124,11 +1144,12 @@ async def test_get_status_success( assert call_args[1]["params"]["depositAddress"] == request.deposit_address # Verify response - assert result.status.value == "SUCCESS" - assert result.provider == SwapProviderEnum.NEAR_INTENTS - assert result.swap_details is not None - assert result.swap_details.amount_in == "2005138" - assert result.swap_details.amount_out == "711" + assert result.status == SwapStatus.SUCCESS + assert result.internal_status == "SUCCESS" + assert ( + result.explorer_url + == "https://explorer.near-intents.org/transactions/4Rqnz7SPU4EqSUravxbKTSBti4RNf1XGaqvBmnLfvH83" + ) @pytest.mark.asyncio @@ -1152,7 +1173,7 @@ async def test_get_status_with_memo( "quoteRequest": MOCK_QUOTE_REQUEST, "quote": MOCK_INDICATIVE_QUOTE, }, - "status": "PENDING", + "status": "PENDING_DEPOSIT", "updatedAt": "2025-12-11T13:48:50.883000Z", "swapDetails": {}, } @@ -1160,17 +1181,25 @@ async def test_get_status_with_memo( request = SwapStatusRequest( tx_hash="test_hash", + source_coin=Chain.SOLANA.coin, + source_chain_id=Chain.SOLANA.chain_id, + destination_coin=Chain.BITCOIN.coin, + destination_chain_id=Chain.BITCOIN.chain_id, deposit_address="test_address", deposit_memo="test_memo", provider=SwapProviderEnum.NEAR_INTENTS, ) - await client.get_status(request) + result = await client.get_status(request) # Verify memo was included in params call_args = mock_httpx_client.get.call_args assert call_args[1]["params"]["depositMemo"] == "test_memo" + # Verify status and internal_status + assert result.status == SwapStatus.PENDING + assert result.internal_status == "PENDING_DEPOSIT" + @pytest.mark.asyncio async def test_route_price_impact_negative( @@ -1236,6 +1265,10 @@ async def test_get_status_error(client, mock_httpx_client, mock_supported_tokens request = SwapStatusRequest( tx_hash="invalid_hash", + source_coin=Chain.SOLANA.coin, + source_chain_id=Chain.SOLANA.chain_id, + destination_coin=Chain.BITCOIN.coin, + destination_chain_id=Chain.BITCOIN.chain_id, deposit_address="invalid_address", deposit_memo=None, provider=SwapProviderEnum.NEAR_INTENTS, diff --git a/app/api/swap/providers/near_intents/transformations.py b/app/api/swap/providers/near_intents/transformations.py index f5b8c20..65be89c 100644 --- a/app/api/swap/providers/near_intents/transformations.py +++ b/app/api/swap/providers/near_intents/transformations.py @@ -8,15 +8,14 @@ CardanoTransactionParams, EvmTransactionParams, SolanaTransactionParams, - SwapDetails, SwapProviderEnum, SwapQuoteRequest, SwapRoute, SwapRouteStep, SwapStatus, + SwapStatusRequest, SwapStatusResponse, SwapStepToken, - SwapTransactionDetails, SwapType, TransactionParams, ZcashTransactionParams, @@ -326,91 +325,20 @@ def normalize_near_intents_status(status: str) -> SwapStatus: return status_mapping.get(status, SwapStatus.PENDING) -def _find_token_by_asset_id( - asset_id: str, - supported_tokens: list[TokenInfo], -) -> TokenInfo | None: - """Find a token by its NEAR Intents asset ID.""" - return next( - (t for t in supported_tokens if t.near_intents_asset_id == asset_id), - None, - ) - - def from_near_intents_status( response: NearIntentsStatusResponse, - supported_tokens: list[TokenInfo], + request: SwapStatusRequest, ) -> SwapStatusResponse: - origin_asset = _find_token_by_asset_id( - response.quote_response.quote_request.origin_asset_id, - supported_tokens, - ) - destination_asset = _find_token_by_asset_id( - response.quote_response.quote_request.destination_asset_id, - supported_tokens, - ) - - if not origin_asset or not destination_asset: - raise ValueError("Invalid origin or destination asset") - - swap_details_data = response.swap_details - - # Collect all transactions - transactions = [] - - # Add origin chain transactions - for tx in swap_details_data.origin_chain_tx_hashes: - transactions.append( - SwapTransactionDetails( - coin=origin_asset.coin, - chain_id=origin_asset.chain_id, - hash=tx.hash, - explorer_url=tx.explorer_url, - ), - ) - - # Add destination chain transactions - for tx in swap_details_data.destination_chain_tx_hashes: - transactions.append( - SwapTransactionDetails( - coin=destination_asset.coin, - chain_id=destination_asset.chain_id, - hash=tx.hash, - explorer_url=tx.explorer_url, - ), - ) - - swap_details = SwapDetails( - amount_in=swap_details_data.amount_in, - amount_in_formatted=swap_details_data.amount_in_formatted, - amount_out=swap_details_data.amount_out, - amount_out_formatted=swap_details_data.amount_out_formatted, - refunded_amount=swap_details_data.refunded_amount, - refunded_amount_formatted=swap_details_data.refunded_amount_formatted, - transactions=transactions, - ) - # Generate explorer URL for NEAR Intents using deposit address explorer_url = None - deposit_address = response.quote_response.quote.deposit_address + deposit_address = request.deposit_address if deposit_address: explorer_url = ( f"https://explorer.near-intents.org/transactions/{deposit_address}" ) return SwapStatusResponse( - # source - source_coin=origin_asset.coin, - source_chain_id=origin_asset.chain_id, - source_token_address=origin_asset.address, - # destination - destination_coin=destination_asset.coin, - destination_chain_id=destination_asset.chain_id, - destination_token_address=destination_asset.address, - recipient=response.quote_response.quote_request.recipient, - # status fields status=normalize_near_intents_status(response.status), - swap_details=swap_details, - provider=SwapProviderEnum.NEAR_INTENTS, + internal_status=response.status, explorer_url=explorer_url, )