Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(trading): otc market banner #17202

Merged
merged 1 commit into from
Feb 28, 2025
Merged

feat(trading): otc market banner #17202

merged 1 commit into from
Feb 28, 2025

Conversation

adderpositive
Copy link
Contributor

Description

Add a banner with a link to the Invity OTC.

Related Issue

Resolve #17198

Screenshots

obrazek

@adderpositive adderpositive added feature Product related issue visible for end user +Invity Related to Invity project labels Feb 24, 2025
@adderpositive adderpositive self-assigned this Feb 24, 2025
Copy link

github-actions bot commented Feb 26, 2025

🚀 Expo preview is ready!

  • Project → trezor-suite-preview
  • Platforms → android, ios
  • Scheme → trezorsuitelite
  • Runtime Version → 26
  • More info

Learn more about 𝝠 Expo Github Action

@@ -1420,6 +1420,18 @@ export default defineMessages({
defaultMessage: 'View details',
id: 'TR_TRADING_VIEW_DETAILS',
},
TR_TRADING_OTC_INFO: {
defaultMessage: 'For purchases over USD (EUR) 50,000, consider our OTC partner:',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would choose careful wording like:
For buy and sell orders exceeding USD (EUR) 50,000, consider our OTC partner:
Because the banner will be also for sell.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make the amount as a placeholder with value, so it reflects what is coming from backend.

Comment on lines 23 to 24
const ALLOWED_FIAT_CURRENCIES = ['usd', 'eur'];
const FIAT_AMOUNT_ALLOWANCE = '50001';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would rather:

  1. get these values from API
  2. lets set FIAT_AMOUNT_ALLOWANCE to 50000 and modify comparison operator accordingly. (it looks weird to have it like this - but once it will be over API we could set any value)

@adderpositive adderpositive marked this pull request as ready for review February 27, 2025 10:16
Copy link

coderabbitai bot commented Feb 27, 2025

Walkthrough

This update introduces enhancements to support over-the-counter (OTC) trading functionality across the application. Four new message entries are added to provide users with guidance and action prompts for OTC trading transactions. A new React component, TradingFormOfferOTC, is implemented to conditionally display OTC offers based on the trading context. This component retrieves trading context values and fetches OTC data from a dedicated API endpoint asynchronously. On the backend, the InvityAPI class is extended with a new private property for the OTC service endpoint and a method, getOTCData, to retrieve OTC data, including error handling for failed requests. Additionally, a new type, TradingOTC, is defined to structure the OTC trading details required across these features.

✨ Finishing Touches
  • 📝 Generate Docstrings

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (6)
suite-common/trading/src/invityAPI.ts (1)

441-451: Fix error logging message.

The error logging message '[getInfo]' appears to be a copy-paste error. It should be '[getOTCData]' to maintain consistency with other error logging patterns in this file.

-            console.error('[getInfo]', error);
+            console.error('[getOTCData]', error);
packages/suite/src/support/messages.ts (1)

1423-1434: Well-structured messaging for OTC market feature, but consider refining the wording.

The addition of these three messages nicely supports the OTC market banner feature mentioned in the PR objectives. You've included dedicated messages for both buying and selling through Mercuryo.

However, I notice that the wording in TR_TRADING_OTC_INFO still primarily emphasizes "purchases" despite LukasRada's earlier feedback suggesting more inclusive language for both buy and sell operations. Consider revising to something like:

-        defaultMessage: 'For purchases over USD (EUR) 50,000, consider our OTC partner:',
+        defaultMessage: 'For buy and sell orders exceeding USD (EUR) 50,000, consider our OTC partner:',

This would make it clearer that the OTC service is available for both buying and selling high-value amounts.

packages/suite/src/views/wallet/trading/common/TradingForm/TradingFormOfferOTC.tsx (4)

26-32: Improve numeric comparison logic.

The current implementation compares strings numerically with Number(fiatAmount) < Number(FIAT_AMOUNT_ALLOWANCE). This works but could be improved for clarity since FIAT_AMOUNT_ALLOWANCE is already defined as a string.

-    const FIAT_AMOUNT_ALLOWANCE = '50001';
+    const FIAT_AMOUNT_ALLOWANCE = 50000;

-        Number(fiatAmount) < Number(FIAT_AMOUNT_ALLOWANCE);
+        Number(fiatAmount) <= FIAT_AMOUNT_ALLOWANCE;

33-56: Consider fetching OTC URL on demand.

The current implementation fetches the OTC URL on component render. As suggested in a previous review, consider fetching the URL only when the user clicks on the link to reduce unnecessary API calls.

You could implement this by:

  1. Moving the getOtcData function outside the useEffect
  2. Adding a click handler that calls this function if the URL hasn't been fetched yet
  3. Showing a loading state while fetching

Example implementation:

const handleLinkClick = async (event: React.MouseEvent) => {
  if (!otcUrl) {
    event.preventDefault();
    await getOtcData();
    // Open URL programmatically after fetching
  }
};

return (
  // ...
  <TrezorLink 
    href={otcUrl || '#'} 
    onClick={handleLinkClick} 
    target="_blank" 
    typographyStyle="hint"
  >
  // ...
);

41-51: Add URL validation to prevent potential errors.

The code creates a URL object without validation. While this is unlikely to cause issues in this specific context, adding validation for the otcData.apiUrl would improve robustness.

-            const apiUrl = new URL(otcData.apiUrl);
+            try {
+                const apiUrl = new URL(otcData.apiUrl);
+                const params = new URLSearchParams({
+                    widget_id: otcData.idWidget,
+                    otc_id: otcData.idOtcUser,
+                    fiat_amount: fiatAmount,
+                    fiat_currency: currencySelect,
+                    type: context.type,
+                });
+                
+                apiUrl.search = params.toString();
+                setOtcUrl(apiUrl.toString());
+            } catch (error) {
+                console.error('Invalid OTC API URL:', error);
+            }

62-77: Consider adding accessibility attributes to the link.

The link to the OTC market could benefit from additional accessibility attributes such as aria-label to provide more context for screen readers.

<TrezorLink 
  href={otcUrl} 
  target="_blank" 
+ aria-label={context.type === 'buy' ? 'Open OTC buy page in new tab' : 'Open OTC sell page in new tab'}
  typographyStyle="hint"
>
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fce4f90 and 713cfad.

📒 Files selected for processing (5)
  • packages/suite/src/support/messages.ts (1 hunks)
  • packages/suite/src/views/wallet/trading/common/TradingForm/TradingFormOffer.tsx (2 hunks)
  • packages/suite/src/views/wallet/trading/common/TradingForm/TradingFormOfferOTC.tsx (1 hunks)
  • suite-common/trading/src/invityAPI.ts (3 hunks)
  • suite-common/trading/src/types.ts (1 hunks)
🔇 Additional comments (5)
suite-common/trading/src/types.ts (1)

90-94: LGTM! The type definition looks good.

Adding the TradingOTC type is clean and follows the project's typing conventions with well-named properties.

packages/suite/src/views/wallet/trading/common/TradingForm/TradingFormOffer.tsx (2)

31-31: LGTM! Clean import.

The import for the new OTC component follows the existing import pattern.


162-162: LGTM! Conditional rendering looks good.

The conditional rendering shows the OTC banner only for 'buy' or 'sell' trading types, which aligns with the PR objective of adding an OTC market banner.

suite-common/trading/src/invityAPI.ts (1)

88-89: LGTM! Endpoint property follows convention.

The OTC_INFO property is correctly declared as private readonly and follows the naming convention used for other service endpoints.

packages/suite/src/views/wallet/trading/common/TradingForm/TradingFormOfferOTC.tsx (1)

23-24: Consider fetching constants from API instead of hardcoding.

The comment indicates these constants will be refactored to be more dynamic in the future. As mentioned in the previous review, consider fetching these values from the API now instead of hardcoding them, which would provide more flexibility.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (5)
packages/suite/src/views/wallet/trading/common/TradingForm/TradingFormOfferOTC.tsx (5)

30-42: Consider adding error handling to API call

The API call to get OTC data doesn't include error handling. If the call fails, users won't know why the OTC offer isn't showing.

const getOtcData = async () => {
-    const otcData = await invityAPI.getOTCData();
+    try {
+        const otcData = await invityAPI.getOTCData();
+        
+        if (!otcData) return;
+        
+        setOtcData(otcData);
+    } catch (error) {
+        console.error('Failed to fetch OTC data:', error);
+        // Optionally add user-facing error handling if appropriate
+    }
-
-    if (!otcData) return;
-
-    setOtcData(otcData);
};

80-88: Add security attributes to external link

When opening external links in new tabs, it's a security best practice to include the rel="noopener noreferrer" attribute to prevent potential exploitation through the window.opener API.

- <TrezorLink href={apiUrl.toString()} target="_blank" typographyStyle="hint">
+ <TrezorLink href={apiUrl.toString()} target="_blank" rel="noopener noreferrer" typographyStyle="hint">

30-42: Consider implementing "fetch OTC data on click" approach

As suggested in a previous comment, fetching the OTC URL on click rather than on render would improve performance and reduce unnecessary API calls, especially if many users view the form but few click the link.

This would involve:

  1. Remove the current useEffect
  2. Create a click handler that fetches data only when needed
  3. Show a loading state during the fetch

Example implementation:

- useEffect(() => {
-     if (!apiKey) return;
- 
-     const getOtcData = async () => {
-         const otcData = await invityAPI.getOTCData();
- 
-         if (!otcData) return;
- 
-         setOtcData(otcData);
-     };
- 
-     getOtcData();
- }, [apiKey]);

+ const [isLoading, setIsLoading] = useState(false);
+ 
+ const handleOtcLinkClick = async (e: React.MouseEvent) => {
+     e.preventDefault();
+     if (!apiKey) return;
+     
+     setIsLoading(true);
+     try {
+         const fetchedOtcData = await invityAPI.getOTCData();
+         
+         if (!fetchedOtcData) {
+             setIsLoading(false);
+             return;
+         }
+         
+         setOtcData(fetchedOtcData);
+         
+         // Create URL with parameters
+         const apiUrl = new URL(fetchedOtcData.apiUrl);
+         const params = new URLSearchParams({
+             widget_id: fetchedOtcData.idWidget,
+             otc_id: fetchedOtcData.idOtcUser,
+             fiat_amount: fiatAmount,
+             fiat_currency: currencySelect,
+             type: context.type,
+         });
+         
+         apiUrl.search = params.toString();
+         
+         // Open the URL in a new tab
+         window.open(apiUrl.toString(), '_blank', 'noopener,noreferrer');
+     } catch (error) {
+         console.error('Failed to fetch OTC data:', error);
+     } finally {
+         setIsLoading(false);
+     }
+ };

And update the link in the render function:

- <TrezorLink href={apiUrl.toString()} target="_blank" typographyStyle="hint">
+ <TrezorLink onClick={handleOtcLinkClick} href="#" typographyStyle="hint">

44-56: Improve condition readability with early returns pattern

The current condition block can be restructured for better readability by using early returns for each check.

- if (
-     !otcData ||
-     amountInCrypto ||
-     !isCurrencyAllowed ||
-     !fiatAmount ||
-     Number(fiatAmount) <= Number(otcData.minimumFiat)
- ) {
-     return null;
- }

+ if (!otcData) return null;
+ if (amountInCrypto) return null;
+ if (!isCurrencyAllowed) return null;
+ if (!fiatAmount) return null;
+ if (Number(fiatAmount) <= Number(otcData.minimumFiat)) return null;

Alternatively, if you prefer to keep them together:

- if (
-     !otcData ||
-     amountInCrypto ||
-     !isCurrencyAllowed ||
-     !fiatAmount ||
-     Number(fiatAmount) <= Number(otcData.minimumFiat)
- ) {
+ // Don't show OTC banner if any of these conditions are met
+ if (!otcData || 
+     amountInCrypto || 
+     !isCurrencyAllowed || 
+     !fiatAmount || 
+     Number(fiatAmount) <= Number(otcData.minimumFiat)) {
      return null;
  }

53-53: Add safety check for potential NaN comparison

Converting strings to numbers using Number() could result in NaN if the input is not a valid number. Consider adding a safety check to handle this edge case.

- Number(fiatAmount) <= Number(otcData.minimumFiat)
+ const numFiatAmount = Number(fiatAmount);
+ const minFiat = Number(otcData.minimumFiat);
+ isNaN(numFiatAmount) || isNaN(minFiat) || numFiatAmount <= minFiat
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3b424cc and 0913097.

📒 Files selected for processing (3)
  • packages/suite/src/support/messages.ts (1 hunks)
  • packages/suite/src/views/wallet/trading/common/TradingForm/TradingFormOfferOTC.tsx (1 hunks)
  • suite-common/trading/src/invityAPI.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • suite-common/trading/src/invityAPI.ts
  • packages/suite/src/support/messages.ts
⏰ Context from checks skipped due to timeout of 90000ms (5)
  • GitHub Check: EAS Update
  • GitHub Check: prepare_android_test_app
  • GitHub Check: Analyze with CodeQL (javascript)
  • GitHub Check: build-web
  • GitHub Check: Setup and Cache Dependencies

if (!apiKey) return;

const getOtcData = async () => {
const otcData = await invityAPI.getOTCData();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetching the data every time user opens buy/sell form seems a bit like a waste of resources to me

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, I will add it on loading trading section in new refactored trading part? What do you think? 😁

@tomasklim tomasklim added the release Will be included in the upcoming release. Needs to be backported to the release branch. label Feb 27, 2025

if (
!otcData ||
amountInCrypto ||
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it would make sense to convert it? so that you can also show it if user wants to buy 1 btc?

Copy link
Member

@tomasklim tomasklim left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
packages/suite/src/views/wallet/trading/common/TradingForm/TradingFormOfferOTC.tsx (3)

58-67: Consider implementing lazy loading of OTC URL.

As suggested in the past review comments, fetching the OTC URL on click rather than on render could improve performance and address potential security concerns with URL parameters.

Instead of constructing the URL on render, consider implementing a click handler that fetches and constructs the URL only when needed:

const handleOtcLinkClick = async (e: React.MouseEvent) => {
    e.preventDefault();
    
    // Construct URL parameters only on click
    const apiUrl = new URL(otcData.apiUrl);
    const params = new URLSearchParams({
        widget_id: otcData.idWidget,
        otc_id: otcData.idOtcUser,
        fiat_amount: fiatAmount,
        fiat_currency: currencySelect,
        type: context.type,
    });
    
    apiUrl.search = params.toString();
    window.open(apiUrl.toString(), '_blank');
};

Then update the TrezorLink component to use this handler instead of the pre-constructed URL.


34-34: Add error handling for the API call.

The current implementation doesn't handle errors that might occur during the API call to getOTCData().

Consider adding error handling and potentially a way to communicate to users if something goes wrong:

     const getOtcData = async () => {
-            const otcData = await invityAPI.getOTCData();
+            try {
+                const otcData = await invityAPI.getOTCData();
+                
+                if (!otcData) return;
+                
+                setOtcData(otcData);
+            } catch (error) {
+                // Consider logging the error or showing a user-friendly message
+                console.error('Failed to fetch OTC data:', error);
+            }
-
-            if (!otcData) return;
-
-            setOtcData(otcData);
        };

50-50: Consider converting crypto amounts to fiat for comparison.

As mentioned in the review comments, it might be valuable to convert crypto amounts to fiat to enable showing the OTC option even when users enter crypto values.

If users enter an amount in crypto, consider converting it to fiat for comparison with the minimum threshold instead of simply not showing the banner. This would provide a more consistent experience regardless of how the user inputs the amount.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9a5401c and 87df0a5.

📒 Files selected for processing (3)
  • packages/suite/src/support/messages.ts (1 hunks)
  • packages/suite/src/views/wallet/trading/common/TradingForm/TradingFormOffer.tsx (2 hunks)
  • packages/suite/src/views/wallet/trading/common/TradingForm/TradingFormOfferOTC.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/suite/src/views/wallet/trading/common/TradingForm/TradingFormOffer.tsx
  • packages/suite/src/support/messages.ts
⏰ Context from checks skipped due to timeout of 90000ms (5)
  • GitHub Check: EAS Update
  • GitHub Check: prepare_android_test_app
  • GitHub Check: build-web
  • GitHub Check: Setup and Cache Dependencies
  • GitHub Check: Analyze with CodeQL (javascript)
🔇 Additional comments (6)
packages/suite/src/views/wallet/trading/common/TradingForm/TradingFormOfferOTC.tsx (6)

30-42: Consider optimizing data fetching pattern.

The current implementation fetches OTC data on every component mount which could lead to unnecessary API calls, especially since this component will render whenever the trading form is opened.

Consider moving this data fetching logic to a higher level component that loads once when the trading section is initialized, as you mentioned in response to previous feedback. This would prevent redundant API calls and improve performance.


44-46: Add null check for otcData.allowedCurrencies.

The optional chaining on otcData?.allowedCurrencies is good, but there's no guarantee that allowedCurrencies is always an array even if otcData exists.

Add additional type safety to ensure this won't cause runtime errors:

-    const isCurrencyAllowed = otcData?.allowedCurrencies?.includes(
+    const isCurrencyAllowed = Array.isArray(otcData?.allowedCurrencies) && otcData.allowedCurrencies.includes(
         currencySelect as FiatCurrencyCode,
     );

48-56: Consider adding a check for excessive amounts.

There appears to be a check for minimum fiat amounts, but no check for maximum allowed amounts which might be relevant for OTC trading.

Based on the review comments, there was a suggestion to add a FIAT_AMOUNT_ALLOWANCE. Consider implementing a maximum amount check as well:

     if (
         !otcData ||
         amountInCrypto ||
         !isCurrencyAllowed ||
         !fiatAmount ||
-        Number(fiatAmount) <= Number(otcData.minimumFiat)
+        Number(fiatAmount) <= Number(otcData.minimumFiat) ||
+        (otcData.maximumFiat && Number(fiatAmount) > Number(otcData.maximumFiat))
     ) {
         return null;
     }

69-94: Ensure the banner design matches the specified style.

According to the previous review comments, there was a specific style guideline for how the Trezor banner should look.

Verify that the current implementation of the Banner matches the design specification shared in the review comments. If adjustments are needed to match the reference image, consider styling props or additional wrapper elements.


1-15: The imports are well-organized and appropriate for the component's functionality.

The imports are logically grouped and include all necessary dependencies for the component.


16-29: Good use of context and state management.

The component correctly leverages the trading form context to access form values and determine whether it's in a buy or sell context. The state management with useState is appropriate for storing the OTC data.

@tomasklim tomasklim merged commit 0ec5da3 into develop Feb 28, 2025
30 of 31 checks passed
@tomasklim tomasklim deleted the feat/trading-otc branch February 28, 2025 11:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Product related issue visible for end user +Invity Related to Invity project release Will be included in the upcoming release. Needs to be backported to the release branch.
Projects
Status: 🤝 Needs QA
Development

Successfully merging this pull request may close these issues.

Trading - add possibility to buy through the OTC
4 participants