diff --git a/.github/workflows/test-suite-desktop-e2e.yml b/.github/workflows/test-suite-desktop-e2e.yml index 55b06b9ec63..fc73243cb97 100644 --- a/.github/workflows/test-suite-desktop-e2e.yml +++ b/.github/workflows/test-suite-desktop-e2e.yml @@ -120,10 +120,3 @@ jobs: COMPOSE_FILE: ./docker/docker-compose.suite-desktop-ci.yml run: docker compose down - - name: Upload Playwright report - if: ${{ ! cancelled() }} - uses: actions/upload-artifact@v4 - with: - name: playwright-report-${{ matrix.TEST_GROUP }} - path: ./packages/suite-desktop-core/playwright-report/ - retention-days: 30 diff --git a/.github/workflows/test-suite-web-e2e-pw.yml b/.github/workflows/test-suite-web-e2e-pw.yml index e3399555529..dc8edce213a 100644 --- a/.github/workflows/test-suite-web-e2e-pw.yml +++ b/.github/workflows/test-suite-web-e2e-pw.yml @@ -155,6 +155,7 @@ jobs: CURRENTS_PROJECT_ID: Og0NOQ CURRENTS_RECORD_KEY: ${{ secrets.CURRENTS_RECORD_KEY }} CURRENTS_CI_BUILD_ID: pr-run-${{github.run_id}} + PASSPHRASE: ${{ secrets.E2E_TEST_PASSPHRASE }} run: | docker compose up -d ${{ matrix.CONTAINERS }} echo "Starting Playwright Web test group ${{ matrix.TEST_GROUP }}" @@ -185,10 +186,3 @@ jobs: COMPOSE_FILE: ./docker/docker-compose.suite-ci-pw.yml run: docker compose down - - name: Upload Playwright report - if: ${{ ! cancelled() }} - uses: actions/upload-artifact@v4 - with: - name: playwright-report-${{ matrix.TEST_GROUP }} - path: ./packages/suite-desktop-core/playwright-report/ - retention-days: 30 diff --git a/docs/tests/e2e-playwright-suite.md b/docs/tests/e2e-playwright-suite.md index ab2ffae4e0b..f7107bcaa55 100644 --- a/docs/tests/e2e-playwright-suite.md +++ b/docs/tests/e2e-playwright-suite.md @@ -25,6 +25,7 @@ Steps: - ``export HOSTNAME=`hostname` `` - `export DISPLAY=${HOSTNAME}:0` 1. In terminal window, navigate to `trezor-user-env` repo root and run `./run.sh`. +1. In workspace `packages/suite-desktop-core` create a `.env` file according to the `.example.env` ### Web diff --git a/packages/suite-desktop-core/.example.env b/packages/suite-desktop-core/.example.env new file mode 100644 index 00000000000..c9d832122f3 --- /dev/null +++ b/packages/suite-desktop-core/.example.env @@ -0,0 +1,3 @@ + +# Secret wallet passphrase dedicated for automated Trading tests +PASSPHRASE= \ No newline at end of file diff --git a/packages/suite-desktop-core/e2e/fixtures/invity/buy/trade-request.json b/packages/suite-desktop-core/e2e/fixtures/invity/buy/requests/trade-request-bitcoin.json similarity index 100% rename from packages/suite-desktop-core/e2e/fixtures/invity/buy/trade-request.json rename to packages/suite-desktop-core/e2e/fixtures/invity/buy/requests/trade-request-bitcoin.json diff --git a/packages/suite-desktop-core/e2e/fixtures/invity/buy/requests/trade-request-solana.json b/packages/suite-desktop-core/e2e/fixtures/invity/buy/requests/trade-request-solana.json new file mode 100644 index 00000000000..99053d7d6f3 --- /dev/null +++ b/packages/suite-desktop-core/e2e/fixtures/invity/buy/requests/trade-request-solana.json @@ -0,0 +1,26 @@ +{ + "trade": { + "exchange": "banxa", + "fiatCurrency": "CZK", + "receiveCurrency": "solana", + "rate": 6106.42, + "wantCrypto": true, + "exp": "cRPhcsJWUUq9brG+zMXmog==", + "country": "CZ", + "paymentMethodName": "Credit Card", + "tags": ["wantCrypto"], + "fiatStringAmount": "3053.21", + "receiveStringAmount": "0.5", + "minFiat": 600, + "maxFiat": 50000, + "minCrypto": 0.10408553, + "maxCrypto": 8.67379421, + "paymentMethod": "creditCard", + "quoteId": "f9e89339-a0bb-4960-ae8e-cc543d651de3", + "partnerData2": "6098", + "paymentId": "8abad787-6328-46bf-9c65-4d9797008f93", + "orderId": "3e44be68-6827-4fa0-962b-85e1cbcbca2f", + "receiveAddress": "o6AySDQRrUa5Sp6XM9gqLogoibdufvxVyviFvR1PfaP" + }, + "returnUrl": "http://localhost:8000/coinmarket-redirect#detail/sol/normal/0/8abad787-6328-46bf-9c65-4d9797008f93" +} diff --git a/packages/suite-desktop-core/e2e/fixtures/invity/buy/watch-request.json b/packages/suite-desktop-core/e2e/fixtures/invity/buy/requests/watch-request.json similarity index 100% rename from packages/suite-desktop-core/e2e/fixtures/invity/buy/watch-request.json rename to packages/suite-desktop-core/e2e/fixtures/invity/buy/requests/watch-request.json diff --git a/packages/suite-desktop-core/e2e/fixtures/invity/buy/trade-bitcoin.json b/packages/suite-desktop-core/e2e/fixtures/invity/buy/trade-bitcoin.json index d243038dcde..c0c983be82e 100644 --- a/packages/suite-desktop-core/e2e/fixtures/invity/buy/trade-bitcoin.json +++ b/packages/suite-desktop-core/e2e/fixtures/invity/buy/trade-bitcoin.json @@ -4,7 +4,7 @@ "paymentId": "5e895ff7-c444-4371-a41f-c1735edca46c", "status": "SUBMITTED", "originalPaymentId": "e17a2bed-86c6-4974-9d87-9fc926b16614", - "partnerData": "", + "partnerData": "https://app.topperpay.com/?bt=eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6IjEwZjdkZGE4LTNlOGEtNDcyYi1hM2FhLWQzM2RjOTgwMDM4YyJ9.eyJpYXQiOjE3Mzg3NzU4ODQsImV4cCI6MTczODc3NTk0NCwianRpIjoiNWUxZTFhOGUtM2FiZi00NDRhLTk4YmUtOTAxNzk2OWY3YmY4Iiwic3ViIjoiNTUzNzI3NDgtZDRiZS00MTVmLWI0MDgtZDI2OWFkY2I3OWZlIiwicGFydG5lciI6eyJkaXNwbGF5TmFtZSI6IlRyZXpvciBTdWl0ZSIsImNvbnRpbnVlVXJsIjoiaHR0cDovL2xvY2FsaG9zdDo4MDAwL2NvaW5tYXJrZXQtcmVkaXJlY3QjZGV0YWlsL2J0Yy9ub3JtYWwvMC8zZWRkNTkyYi0wYWM4LTQ3ZGItYmVhMy00ODU2NzQ3NTI4ODMiLCJmZWUiOnsicGVyY2VudGFnZSI6IjEifX0sInNvdXJjZSI6eyJhbW91bnQiOiIxMjM0IiwiYXNzZXQiOiJDWksiLCJwYXltZW50TWV0aG9kIjp7Im5ldHdvcmsiOiJjYXJkIn19LCJ0YXJnZXQiOnsiYWRkcmVzcyI6ImJjMXE3Y2VxdmFxN2ZxeXl3eHFjeDdxbmZ4a2ZrMnlrcHNsYTlwZTgwcSIsImFzc2V0IjoiQlRDIiwibmV0d29yayI6ImJpdGNvaW4iLCJyZWNpcGllbnRFZGl0TW9kZSI6Im5vdC1lZGl0YWJsZSJ9fQ.AYeoeDtYLjZFeACH9-Rjtkd1R7hH5EpdIzSp30baeSLfwm1Val_3HVDLVtUNLsBHFeAx3-kacJxUts6ReX2N5R0-AC2_1lVxOCJhkRvzI5VDujguo-btsgFFBUkhWBlbHkXs5rV_861UOFSprdExrD4TL_r-wro73q7OAwPKsjIK8fPQ&theme=light", "exchange": "topper", "fiatCurrency": "CZK", "receiveCurrency": "bitcoin", @@ -26,7 +26,7 @@ "tradeForm": { "form": { "formMethod": "GET", - "formAction": "", + "formAction": "https://app.topperpay.com/?bt=eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6IjEwZjdkZGE4LTNlOGEtNDcyYi1hM2FhLWQzM2RjOTgwMDM4YyJ9.eyJpYXQiOjE3Mzg3NzU4ODQsImV4cCI6MTczODc3NTk0NCwianRpIjoiNWUxZTFhOGUtM2FiZi00NDRhLTk4YmUtOTAxNzk2OWY3YmY4Iiwic3ViIjoiNTUzNzI3NDgtZDRiZS00MTVmLWI0MDgtZDI2OWFkY2I3OWZlIiwicGFydG5lciI6eyJkaXNwbGF5TmFtZSI6IlRyZXpvciBTdWl0ZSIsImNvbnRpbnVlVXJsIjoiaHR0cDovL2xvY2FsaG9zdDo4MDAwL2NvaW5tYXJrZXQtcmVkaXJlY3QjZGV0YWlsL2J0Yy9ub3JtYWwvMC8zZWRkNTkyYi0wYWM4LTQ3ZGItYmVhMy00ODU2NzQ3NTI4ODMiLCJmZWUiOnsicGVyY2VudGFnZSI6IjEifX0sInNvdXJjZSI6eyJhbW91bnQiOiIxMjM0IiwiYXNzZXQiOiJDWksiLCJwYXltZW50TWV0aG9kIjp7Im5ldHdvcmsiOiJjYXJkIn19LCJ0YXJnZXQiOnsiYWRkcmVzcyI6ImJjMXE3Y2VxdmFxN2ZxeXl3eHFjeDdxbmZ4a2ZrMnlrcHNsYTlwZTgwcSIsImFzc2V0IjoiQlRDIiwibmV0d29yayI6ImJpdGNvaW4iLCJyZWNpcGllbnRFZGl0TW9kZSI6Im5vdC1lZGl0YWJsZSJ9fQ.AYeoeDtYLjZFeACH9-Rjtkd1R7hH5EpdIzSp30baeSLfwm1Val_3HVDLVtUNLsBHFeAx3-kacJxUts6ReX2N5R0-AC2_1lVxOCJhkRvzI5VDujguo-btsgFFBUkhWBlbHkXs5rV_861UOFSprdExrD4TL_r-wro73q7OAwPKsjIK8fPQ&theme=light", "fields": {} } } diff --git a/packages/suite-desktop-core/e2e/fixtures/invity/index.ts b/packages/suite-desktop-core/e2e/fixtures/invity/index.ts index 607ac94baca..c1a459308b9 100644 --- a/packages/suite-desktop-core/e2e/fixtures/invity/index.ts +++ b/packages/suite-desktop-core/e2e/fixtures/invity/index.ts @@ -1,11 +1,12 @@ import { cloneDeep } from 'lodash'; -import { NetworkSymbol } from '@suite-common/wallet-config'; - import buyList from './buy/list.json'; import buyQuotesBTC from './buy/quotes-bitcoin.json'; import buyQuotesEthereum from './buy/quotes-ethereum.json'; import buyQuotesSolana from './buy/quotes-solana.json'; +import buyTradeBTCPayload from './buy/requests/trade-request-bitcoin.json'; +import buyTradeSolanaPayload from './buy/requests/trade-request-solana.json'; +import buyWatchPayload from './buy/requests/watch-request.json'; import buyTradeBTC from './buy/trade-bitcoin.json'; import buyTradeEthereum from './buy/trade-ethereum.json'; import buyTradeSolana from './buy/trade-solana.json'; @@ -17,7 +18,15 @@ import exchangeTrade from './exchange/trade.json'; import exchangeWatch from './exchange/watch.json'; import info from './info.json'; import sellList from './sell/list.json'; -import { TradeRequest } from './types'; +import sellQuotesBTC from './sell/quotes-bitcoin.json'; +//Payloads +import sellQuotesPayload from './sell/requests/quotes-request.json'; +import sellTradePayload from './sell/requests/trade-request.json'; +import sellWatchPayload from './sell/requests/watch-request.json'; +import sellTradeBTC from './sell/trade-bitcoin.json'; +import sellWatch from './sell/watch.json'; +//Types +import { SellTradeResponse, TradeResponse } from './types'; const invityUrl = 'https://exchange.trezor.io'; @@ -33,6 +42,18 @@ export const invityEndpoint = { buyTrade: `${invityUrl}/api/v3/buy/trade`, buyWatch: `${invityUrl}/api/v3/buy/watch/*`, sellList: `${invityUrl}/api/v3/sell/list`, + sellQuotes: `${invityUrl}/api/v3/sell/fiat/quotes`, + sellTrade: `${invityUrl}/api/v3/sell/fiat/trade`, + sellWatch: `${invityUrl}/api/v3/sell/fiat/watch/*`, +}; + +export const invityRequest = { + buyTradeBTCPayload, + buyTradeSolanaPayload, + buyWatchPayload, + sellQuotesPayload, + sellTradePayload, + sellWatchPayload, }; export const invityResponses = { @@ -46,20 +67,39 @@ export const invityResponses = { [invityEndpoint.buyQuotes]: buyQuotesBTC, [invityEndpoint.buyWatch]: buyWatch, [invityEndpoint.sellList]: sellList, + [invityEndpoint.sellQuotes]: sellQuotesBTC, + [invityEndpoint.sellTrade]: sellTradeBTC, + [invityEndpoint.sellWatch]: sellWatch, }; -// This modification allows us to skip the provider's part of the flow and go directly to the transaction detail. -export const createRedirectedTradeResponse = (params: { - symbol: NetworkSymbol; - tradeRequest: TradeRequest; - url: string; -}) => { - const redirectToDetail = `${params.url}coinmarket-redirect#detail/${params.symbol}/normal/0/${params.tradeRequest.trade.paymentId}`; - const modifiedTrade = cloneDeep(params.tradeRequest); - modifiedTrade.trade.partnerData = redirectToDetail; - modifiedTrade.tradeForm.form.formAction = redirectToDetail; +// This modification allows us to skip the provider's part of the flow and continue further. +export const createRedirectedTradeResponse = ( + tradeResponse: TradeResponse | SellTradeResponse, + tradeRequest: any, +) => { + const modifiedResponse = cloneDeep(tradeResponse); + modifiedResponse.trade.partnerData = tradeRequest.returnUrl; + modifiedResponse.tradeForm.form.formAction = tradeRequest.returnUrl; + modifiedResponse.trade.paymentId = tradeRequest.trade.paymentId; + modifiedResponse.trade.orderId = tradeRequest.trade.orderId; + if ('refundAddress' in modifiedResponse.trade && tradeRequest.refundAddress) { + modifiedResponse.trade.refundAddress = tradeRequest.refundAddress; + } + + return modifiedResponse; +}; + +export const getCompanyNameFromList = (name: string, type: 'buyList' | 'sellList') => { + const list = type === 'buyList' ? buyList : sellList; + const filteredItems = list.providers.filter(item => item.name === name); + + if (filteredItems.length !== 1) { + throw new Error( + `Expected exactly one item, but found ${filteredItems.length}\n${JSON.stringify(filteredItems, null, 2)}`, + ); + } - return modifiedTrade; + return filteredItems[0].companyName; }; export { @@ -78,4 +118,7 @@ export { buyTradeSolana, buyWatch, sellList, + sellQuotesBTC, + sellTradeBTC, + sellWatch, }; diff --git a/packages/suite-desktop-core/e2e/fixtures/invity/sell/list.json b/packages/suite-desktop-core/e2e/fixtures/invity/sell/list.json index a611d5b1091..5c1292d6eb4 100644 --- a/packages/suite-desktop-core/e2e/fixtures/invity/sell/list.json +++ b/packages/suite-desktop-core/e2e/fixtures/invity/sell/list.json @@ -109,7 +109,6 @@ "cosmos", "binance-smart-chain--0x0e09fabb73bd3ade0a17ecc321fd13a19e81ce82", "flow", - "fantom", "ethereum--0xf57e7e7c23978c3caec3c3548e3d615c346e79ff", "ethereum--0xc944e90c64b2c07662a292be6244bdf05cda44a7", "near", @@ -374,7 +373,9 @@ "tradedCoins": [ "cardano", "avalanche-2", + "ethereum--0xbb0e17ef65f82ab018d8edd776e8dd940327b28b", "bitcoin-cash", + "binancecoin", "bitcoin", "dogecoin", "ethereum", @@ -383,6 +384,12 @@ "ethereum--0x455e53cbb86018ac2b8092fdcd39d8444affc3f6", "polygon-ecosystem-token", "solana", + "tron", + "ethereum--0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "polygon-pos--0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", + "solana--EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "ethereum--0xdac17f958d2ee523a2206206994597c13d831ec7", + "tron--TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", "stellar", "ripple" ], diff --git a/packages/suite-desktop-core/e2e/fixtures/invity/sell/quotes-bitcoin.json b/packages/suite-desktop-core/e2e/fixtures/invity/sell/quotes-bitcoin.json new file mode 100644 index 00000000000..cb1267eb892 --- /dev/null +++ b/packages/suite-desktop-core/e2e/fixtures/invity/sell/quotes-bitcoin.json @@ -0,0 +1,91 @@ +[ + { + "exchange": "moonpay-sell", + "fiatCurrency": "EUR", + "cryptoCurrency": "bitcoin", + "rate": 85907.69230769231, + "amountInCrypto": true, + "exp": "qtn7k13GIY/wauu7BvapWA==", + "country": "CZ", + "paymentMethodName": "Credit Card", + "fiatStringAmount": "55.84", + "cryptoStringAmount": "0.00065", + "minFiat": 20, + "maxFiat": 30000, + "minCrypto": 0.00006, + "maxCrypto": 30000, + "paymentMethod": "creditCard" + }, + { + "exchange": "btcdirect-sell", + "fiatCurrency": "EUR", + "cryptoCurrency": "bitcoin", + "rate": 92153.84615384616, + "amountInCrypto": true, + "exp": "qtn7k13GIY/wauu7BvapWA==", + "country": "CZ", + "paymentMethodName": "Bank Transfer", + "fiatStringAmount": "59.90", + "cryptoStringAmount": "0.00065", + "minFiat": 31.04, + "maxFiat": 51736.36, + "minCrypto": 0.00032244, + "maxCrypto": 0.53742565, + "paymentMethod": "bankTransfer", + "validUntil": "2025-02-07T12:43:29Z" + }, + { + "exchange": "btcdirect-sell", + "fiatCurrency": "EUR", + "cryptoCurrency": "bitcoin", + "rate": 92153.84615384616, + "amountInCrypto": true, + "exp": "qtn7k13GIY/wauu7BvapWA==", + "country": "CZ", + "paymentMethodName": "SEPA", + "fiatStringAmount": "59.90", + "cryptoStringAmount": "0.00065", + "minFiat": 31.04, + "maxFiat": 51736.36, + "minCrypto": 0.00032244, + "maxCrypto": 0.53742565, + "paymentMethod": "sepa", + "validUntil": "2025-02-07T12:43:29Z" + }, + { + "exchange": "moonpay-sell", + "fiatCurrency": "EUR", + "cryptoCurrency": "bitcoin", + "rate": 85907.69230769231, + "amountInCrypto": true, + "exp": "qtn7k13GIY/wauu7BvapWA==", + "country": "CZ", + "paymentMethodName": "SEPA", + "fiatStringAmount": "55.84", + "cryptoStringAmount": "0.00065", + "minFiat": 20, + "maxFiat": 30000, + "minCrypto": 0.00006, + "maxCrypto": 30000, + "paymentMethod": "sepa" + }, + { + "error": "Amount too low, minimum is 0.00065 0.00083102.", + "exchange": "banxa-sell", + "fiatCurrency": "EUR", + "cryptoCurrency": "bitcoin", + "rate": 90969.23076923077, + "amountInCrypto": true, + "exp": "qtn7k13GIY/wauu7BvapWA==", + "country": "CZ", + "paymentMethodName": "Bank Transfer", + "fiatStringAmount": "59.13", + "cryptoStringAmount": "0.00065000", + "minFiat": 80, + "maxFiat": 50000, + "minCrypto": 0.00083102, + "maxCrypto": 0.51938878, + "paymentMethod": "bankTransfer", + "partnerData2": "6057" + } +] diff --git a/packages/suite-desktop-core/e2e/fixtures/invity/sell/requests/quotes-request.json b/packages/suite-desktop-core/e2e/fixtures/invity/sell/requests/quotes-request.json new file mode 100644 index 00000000000..db3e1461fa1 --- /dev/null +++ b/packages/suite-desktop-core/e2e/fixtures/invity/sell/requests/quotes-request.json @@ -0,0 +1,9 @@ +{ + "amountInCrypto": true, + "cryptoCurrency": "bitcoin", + "fiatCurrency": "EUR", + "country": "CZ", + "cryptoStringAmount": "0.00065", + "fiatStringAmount": "", + "flows": ["BANK_ACCOUNT", "PAYMENT_GATE"] +} diff --git a/packages/suite-desktop-core/e2e/fixtures/invity/sell/requests/trade-request.json b/packages/suite-desktop-core/e2e/fixtures/invity/sell/requests/trade-request.json new file mode 100644 index 00000000000..aea70e18831 --- /dev/null +++ b/packages/suite-desktop-core/e2e/fixtures/invity/sell/requests/trade-request.json @@ -0,0 +1,23 @@ +{ + "trade": { + "exchange": "moonpay-sell", + "fiatCurrency": "EUR", + "cryptoCurrency": "bitcoin", + "rate": 85907.69230769231, + "amountInCrypto": true, + "exp": "qtn7k13GIY/wauu7BvapWA==", + "country": "CZ", + "paymentMethodName": "Credit Card", + "fiatStringAmount": "55.84", + "cryptoStringAmount": "0.00065", + "minFiat": 20, + "maxFiat": 30000, + "minCrypto": 0.00006, + "maxCrypto": 30000, + "paymentMethod": "creditCard", + "paymentId": "227c5679-53df-45a6-ae83-9f4ebf9ecce7", + "orderId": "4839ba2d-dd85-49b3-ae73-5ab1b526e66f", + "refundAddress": "bc1qw0r95wvsqxumq3km0q8kqyjdard2u8ahaw4y2n" + }, + "returnUrl": "http://localhost:8000/coinmarket-redirect#sell-offers/btc/normal/0/p-qc/CZ/EUR/0.00065/bitcoin/4839ba2d-dd85-49b3-ae73-5ab1b526e66f" +} diff --git a/packages/suite-desktop-core/e2e/fixtures/invity/sell/requests/watch-request.json b/packages/suite-desktop-core/e2e/fixtures/invity/sell/requests/watch-request.json new file mode 100644 index 00000000000..8302cad00f3 --- /dev/null +++ b/packages/suite-desktop-core/e2e/fixtures/invity/sell/requests/watch-request.json @@ -0,0 +1,22 @@ +{ + "refundAddress": "bc1qw0r95wvsqxumq3km0q8kqyjdard2u8ahaw4y2n", + "paymentId": "227c5679-53df-45a6-ae83-9f4ebf9ecce7", + "status": "SUBMITTED", + "partnerData": "https://sell.moonpay.com/?apiKey=pk_live_1wuWa2Z1O2qG8izvpLkMOvBFbAPgWpfn&baseCurrencyCode=btc"eCurrencyCode=eur&baseCurrencyAmount=0.00065&lockAmount=true&externalTransactionId=227c5679-53df-45a6-ae83-9f4ebf9ecce7&redirectURL=http%3A%2F%2Flocalhost%3A8000%2Fcoinmarket-redirect%23sell-offers%2Fbtc%2Fnormal%2F0%2Fp-qc%2FCZ%2FEUR%2F0.00065%2Fbitcoin%2F4839ba2d-dd85-49b3-ae73-5ab1b526e66f&showWalletAddressForm=true&colorCode=%2300bfd9&language=CZ&signature=Nduwv4haCHiSgzeb1%2FaBUu8lN1r8aKg%2FN%2F2Io9JDYY0%3D", + "exchange": "moonpay-sell", + "fiatCurrency": "EUR", + "cryptoCurrency": "bitcoin", + "rate": 85907.69230769231, + "amountInCrypto": true, + "exp": "qtn7k13GIY/wauu7BvapWA==", + "country": "CZ", + "paymentMethodName": "Credit Card", + "fiatStringAmount": "55.84", + "cryptoStringAmount": "0.00065", + "minFiat": 20, + "maxFiat": 30000, + "minCrypto": 0.00006, + "maxCrypto": 30000, + "paymentMethod": "creditCard", + "orderId": "4839ba2d-dd85-49b3-ae73-5ab1b526e66f" +} diff --git a/packages/suite-desktop-core/e2e/fixtures/invity/sell/trade-bitcoin.json b/packages/suite-desktop-core/e2e/fixtures/invity/sell/trade-bitcoin.json new file mode 100644 index 00000000000..9bbafb868d8 --- /dev/null +++ b/packages/suite-desktop-core/e2e/fixtures/invity/sell/trade-bitcoin.json @@ -0,0 +1,31 @@ +{ + "trade": { + "refundAddress": "bc1qw0r95wvsqxumq3km0q8kqyjdard2u8ahaw4y2n", + "paymentId": "227c5679-53df-45a6-ae83-9f4ebf9ecce7", + "status": "SUBMITTED", + "partnerData": "https://sell.moonpay.com/?apiKey=pk_live_1wuWa2Z1O2qG8izvpLkMOvBFbAPgWpfn&baseCurrencyCode=btc"eCurrencyCode=eur&baseCurrencyAmount=0.00065&lockAmount=true&externalTransactionId=227c5679-53df-45a6-ae83-9f4ebf9ecce7&redirectURL=http%3A%2F%2Flocalhost%3A8000%2Fcoinmarket-redirect%23sell-offers%2Fbtc%2Fnormal%2F0%2Fp-qc%2FCZ%2FEUR%2F0.00065%2Fbitcoin%2F4839ba2d-dd85-49b3-ae73-5ab1b526e66f&showWalletAddressForm=true&colorCode=%2300bfd9&language=CZ&signature=Nduwv4haCHiSgzeb1%2FaBUu8lN1r8aKg%2FN%2F2Io9JDYY0%3D", + "exchange": "moonpay-sell", + "fiatCurrency": "EUR", + "cryptoCurrency": "bitcoin", + "rate": 85907.69230769231, + "amountInCrypto": true, + "exp": "qtn7k13GIY/wauu7BvapWA==", + "country": "CZ", + "paymentMethodName": "Credit Card", + "fiatStringAmount": "55.84", + "cryptoStringAmount": "0.00065", + "minFiat": 20, + "maxFiat": 30000, + "minCrypto": 0.00006, + "maxCrypto": 30000, + "paymentMethod": "creditCard", + "orderId": "4839ba2d-dd85-49b3-ae73-5ab1b526e66f" + }, + "tradeForm": { + "form": { + "formMethod": "GET", + "formAction": "https://sell.moonpay.com/?apiKey=pk_live_1wuWa2Z1O2qG8izvpLkMOvBFbAPgWpfn&baseCurrencyCode=btc"eCurrencyCode=eur&baseCurrencyAmount=0.00065&lockAmount=true&externalTransactionId=227c5679-53df-45a6-ae83-9f4ebf9ecce7&redirectURL=http%3A%2F%2Flocalhost%3A8000%2Fcoinmarket-redirect%23sell-offers%2Fbtc%2Fnormal%2F0%2Fp-qc%2FCZ%2FEUR%2F0.00065%2Fbitcoin%2F4839ba2d-dd85-49b3-ae73-5ab1b526e66f&showWalletAddressForm=true&colorCode=%2300bfd9&language=CZ&signature=Nduwv4haCHiSgzeb1%2FaBUu8lN1r8aKg%2FN%2F2Io9JDYY0%3D", + "fields": {} + } + } +} diff --git a/packages/suite-desktop-core/e2e/fixtures/invity/sell/watch.json b/packages/suite-desktop-core/e2e/fixtures/invity/sell/watch.json new file mode 100644 index 00000000000..486f6e9125d --- /dev/null +++ b/packages/suite-desktop-core/e2e/fixtures/invity/sell/watch.json @@ -0,0 +1,5 @@ +{ + "status": "SEND_CRYPTO", + "destinationAddress": "bc1q3tzqaakjpjdljaudkhusr95s23fl48wh4xdmvy", + "destinationPaymentExtraId": "6d666a5f-b99c-4482-b8bc-2df04fc11b7b" +} diff --git a/packages/suite-desktop-core/e2e/fixtures/invity/types.ts b/packages/suite-desktop-core/e2e/fixtures/invity/types.ts index 3460a9a0a2f..4aaedc667dc 100644 --- a/packages/suite-desktop-core/e2e/fixtures/invity/types.ts +++ b/packages/suite-desktop-core/e2e/fixtures/invity/types.ts @@ -25,6 +25,29 @@ type Trade = { partnerData2?: string; }; +type SellTrade = { + refundAddress: string; + paymentId: string; + status: string; + partnerData: string; + exchange: string; + fiatCurrency: string; + cryptoCurrency: string; + rate: number; + amountInCrypto: boolean; + exp: string; + country: string; + paymentMethodName: string; + fiatStringAmount: string; + cryptoStringAmount: string; + minFiat: number; + maxFiat: number; + minCrypto: number; + maxCrypto: number; + paymentMethod: string; + orderId: string; +}; + type TradeForm = { form: { formMethod: string; @@ -33,7 +56,12 @@ type TradeForm = { }; }; -export type TradeRequest = { +export type TradeResponse = { trade: Trade; tradeForm: TradeForm; }; + +export type SellTradeResponse = { + trade: SellTrade; + tradeForm: TradeForm; +}; diff --git a/packages/suite-desktop-core/e2e/playwright.config.ts b/packages/suite-desktop-core/e2e/playwright.config.ts index fb9fcaf73e7..0a7465ae224 100644 --- a/packages/suite-desktop-core/e2e/playwright.config.ts +++ b/packages/suite-desktop-core/e2e/playwright.config.ts @@ -1,7 +1,10 @@ import type { PlaywrightTestConfig } from '@playwright/test'; import { devices } from '@playwright/test'; +import dotenv from 'dotenv'; import path from 'path'; +dotenv.config({ path: path.resolve(__dirname, '../.env') }); + export enum PlaywrightProjects { Web = 'web', Desktop = 'desktop', @@ -41,7 +44,7 @@ const config: PlaywrightTestConfig = { }, reportSlowTests: null, reporter: process.env.GITHUB_ACTION - ? [['list'], ['@currents/playwright'], ['html', { open: 'never' }]] + ? [['list'], ['@currents/playwright']] : [['list'], ['html', { open: 'never' }]], timeout: process.env.GITHUB_ACTION ? timeoutCIRun : timeoutLocalRun, outputDir: path.join(__dirname, 'test-results'), diff --git a/packages/suite-desktop-core/e2e/support/fixtures.ts b/packages/suite-desktop-core/e2e/support/fixtures.ts index a36c92bff65..099f2fe43df 100644 --- a/packages/suite-desktop-core/e2e/support/fixtures.ts +++ b/packages/suite-desktop-core/e2e/support/fixtures.ts @@ -185,8 +185,8 @@ const test = base.extend({ const recoveryPage = new RecoveryActions(page); await use(recoveryPage); }, - marketPage: async ({ page, url }, use) => { - const marketPage = new MarketActions(page, url); + marketPage: async ({ page }, use) => { + const marketPage = new MarketActions(page); await use(marketPage); }, assetsPage: async ({ page }, use) => { diff --git a/packages/suite-desktop-core/e2e/support/pageActions/devicePromptActions.ts b/packages/suite-desktop-core/e2e/support/pageActions/devicePromptActions.ts index db2e8d8a678..db8c18c218b 100644 --- a/packages/suite-desktop-core/e2e/support/pageActions/devicePromptActions.ts +++ b/packages/suite-desktop-core/e2e/support/pageActions/devicePromptActions.ts @@ -15,7 +15,14 @@ export class DevicePromptActions { readonly outputValueOf = ( section: 'default' | 'address' | 'data' | 'amount' | 'fee' | 'total', ) => this.page.getByTestId(`@modal/output-${section}`).getByTestId('@modal/output-value'); + readonly cryptoAmountOf = (section: 'amount' | 'fee' | 'total') => + this.page + .getByTestId(`@modal/output-${section}`) + .getByTestId('@modal/crypto-amount-with-symbol'); + readonly fiatAmountOf = (section: 'amount' | 'fee' | 'total') => + this.page.getByTestId(`@modal/output-${section}`).getByTestId('@modal/fiat-amount'); readonly reviewAmount: Locator; + readonly sellButton: Locator; constructor(private page: Page) { this.confirmOnDevicePrompt = page.getByTestId('@prompts/confirm-on-device'); @@ -26,6 +33,7 @@ export class DevicePromptActions { this.chunkedText = page.getByTestId('@device-display/chunked-text'); this.outputValue = page.getByTestId('@modal/output-value'); this.reviewAmount = page.getByTestId('@modal/transaction-review/amount'); + this.sellButton = page.getByTestId('@modal/send'); } @step() diff --git a/packages/suite-desktop-core/e2e/support/pageActions/marketActions.ts b/packages/suite-desktop-core/e2e/support/pageActions/marketActions.ts index d577da59237..bfec86d68b8 100644 --- a/packages/suite-desktop-core/e2e/support/pageActions/marketActions.ts +++ b/packages/suite-desktop-core/e2e/support/pageActions/marketActions.ts @@ -10,11 +10,10 @@ import { invityEndpoint, invityResponses, } from '../../fixtures/invity'; -import expectedTradeRequestPayload from '../../fixtures/invity/buy/trade-request.json'; import { TrezorUserEnvLinkProxy, step } from '../common'; import { expect } from '../customMatchers'; import { DevicePromptActions } from './devicePromptActions'; -import { TradeRequest } from '../../fixtures/invity/types'; +import { SellTradeResponse, TradeResponse } from '../../fixtures/invity/types'; const quoteProviderLocator = '@trading/offers/quote/provider'; const quoteAmountLocator = '@trading/offers/quote/crypto-amount'; @@ -48,6 +47,7 @@ export class MarketActions { readonly offerSpinner: Locator; readonly section: Locator; readonly form: Locator; + readonly sellTabButton: Locator; readonly quoteProvider: Locator; readonly bestOfferSection: Locator; readonly bestOfferAmount: Locator; @@ -86,8 +86,10 @@ export class MarketActions { // Confirmation modal readonly modal: Locator; readonly buyTermsConfirmButton: Locator; + readonly sellTermsConfirmButton: Locator; readonly confirmOnTrezorButton: Locator; readonly confirmationSection: Locator; + readonly confirmationAccount: Locator; readonly confirmationAccountDropdown: Locator; readonly confirmationCryptoAmount: Locator; readonly confirmationFiatAmount: Locator; @@ -109,16 +111,16 @@ export class MarketActions { readonly transactionDetailStatus: Locator; readonly proceedToPayButton: Locator; readonly transactionDetail: Locator; - readonly transactionWatchPeriod = '00:30'; + readonly watchPeriod = '00:30'; + // Sell + readonly formSellButton: Locator; - constructor( - private page: Page, - private url: string, - ) { + constructor(private page: Page) { this.devicePrompt = new DevicePromptActions(page); this.offerSpinner = this.page.getByTestId('@trading/offers/loading-spinner'); this.section = this.page.getByTestId('@trading'); this.form = this.page.getByTestId('@trading/form'); + this.sellTabButton = this.page.getByTestId('@trading/menu/wallet-trading-sell'); this.quoteProvider = this.page.getByTestId(quoteProviderLocator); this.bestOfferSection = this.page.getByTestId('@trading/best-offer'); this.bestOfferAmount = this.page.getByTestId('@trading/best-offer/amount'); @@ -148,12 +150,16 @@ export class MarketActions { this.selectThisQuoteButton = this.page.getByTestId('@trading/offers/get-this-deal-button'); this.modal = this.page.getByTestId('@modal'); this.buyTermsConfirmButton = this.page.getByTestId( - '@trading/buy/offers/buy-terms-confirm-button', + '@trading/buy/offers/trade-terms-confirm-button', + ); + this.sellTermsConfirmButton = this.page.getByTestId( + '@trading/sell/offers/trade-terms-confirm-button', ); this.confirmOnTrezorButton = this.page.getByTestId( '@trading/offer/confirm-on-trezor-button', ); this.confirmationSection = this.page.getByTestId('@trading/selected-offer'); + this.confirmationAccount = this.page.getByTestId('@trading/form/verify/account'); this.confirmationAccountDropdown = this.page.getByTestId( '@trading/verify-options/account/input', ); @@ -177,16 +183,25 @@ export class MarketActions { this.transactionDetailStatus = this.page.getByTestId('@trading/transaction/detail/status'); this.proceedToPayButton = this.page.getByRole('button', { name: 'Proceed to pay' }); this.transactionDetail = this.page.getByTestId('@trading/transaction/detail'); + this.formSellButton = this.page.getByTestId('@trading/form/sell-button'); } @step() - async waitForOffersSyncToFinish() { + async waitForBuyOffersSync() { await expect(this.offerSpinner).toBeHidden({ timeout: 30000 }); //Even though the offer sync is finished, the best offer might not be displayed correctly yet and show 0 BTC await expect(this.bestOfferAmount).not.toHaveText('0 BTC'); await expect(this.buyBestOfferButton).toBeEnabled(); } + @step() + async waitForSellOffersSync() { + await expect(this.offerSpinner).toBeHidden({ timeout: 30000 }); + //Even though the offer sync is finished, the best offer might not be displayed correctly yet and show 0 BTC + await expect(this.bestOfferAmount).not.toHaveText('0'); + await expect(this.formSellButton).toBeEnabled(); + } + @step() async selectCountryOfResidence(countryCode: string) { const countryLabel = getCountryLabel(countryCode); @@ -223,30 +238,30 @@ export class MarketActions { } @step() - async setYouPayFiatAmount( + async setYouPayAmount( amount: string, - currency: FiatCurrencyCode = 'czk', + cryptoCurrency: string = 'bitcoin', + wantCrypto: boolean = false, + fiatCurrencyCode: FiatCurrencyCode = 'czk', country: string = 'CZ', ) { - // Warning: the field is initialized empty and gets default value after the first offer sync - await expect(this.youPayFiatInput).not.toHaveValue(''); + const inputField = wantCrypto ? this.youPayCryptoInput : this.youPayFiatInput; + await expect(inputField).not.toHaveValue(''); await this.selectCountryOfResidence(country); - await this.selectFiatCurrency(currency); - const quotesPromise = this.page.waitForResponse(invityEndpoint.buyQuotes); - await this.youPayFiatInput.fill(amount); - await quotesPromise; - // Warning: Bug #16054, as a workaround we wait for offer sync after setting the amount - await this.waitForOffersSyncToFinish(); - } - - @step() - async setYouPayCryptoAmount(amount: string, country: string = 'CZ') { - // Warning: the field is initialized empty and gets default value after the first offer sync - await expect(this.youPayCryptoInput).not.toHaveValue(''); - await this.selectCountryOfResidence(country); - await this.youPayCryptoInput.fill(amount); - // Warning: Bug #16054, as a workaround we wait for offer sync after setting the amount - await this.waitForOffersSyncToFinish(); + await this.selectFiatCurrency(fiatCurrencyCode); + const quotesRequestPromise = this.page.waitForRequest(invityEndpoint.buyQuotes); + const quotesResponsePromise = this.page.waitForResponse(invityEndpoint.buyQuotes); + await inputField.fill(amount); + await expect(quotesRequestPromise).toHavePayload({ + wantCrypto, + fiatCurrency: fiatCurrencyCode.toUpperCase(), + receiveCurrency: cryptoCurrency, + country, + fiatStringAmount: wantCrypto ? '2500' : amount, + ...(wantCrypto && { cryptoStringAmount: amount }), + }); + await quotesResponsePromise; + await this.waitForBuyOffersSync(); } @step() @@ -262,26 +277,16 @@ export class MarketActions { await this.devicePrompt.confirmOnDevicePromptIsHidden(); } - @step() - async finishMockedTrade() { - const tradeRequestPromise = this.page.waitForRequest(invityEndpoint.buyTrade); - await this.confirmTradeButton.click(); - await expect(tradeRequestPromise).toHavePayload(expectedTradeRequestPayload, { - omit: ['returnUrl', 'trade.orderId', 'trade.paymentId'], - }); - } - // We bypass the provider part of the flow by having a modified redirect in trade response. // This redirect is provided by Invity and normaly leads to provider's page. // But our mocked response redirects us to transaction detail where our flow continues. @step() - async mockInvityTrade(tradeRequest: TradeRequest, symbol: NetworkSymbol) { - const redirectedTradeResponse = createRedirectedTradeResponse({ - symbol, - tradeRequest, - url: this.url, - }); - await this.page.route(invityEndpoint.buyTrade, async route => { + async mockInvityTrade(tradeResponse: TradeResponse | SellTradeResponse, endpointUrl: string) { + await this.page.route(endpointUrl, async (route, request) => { + const redirectedTradeResponse = createRedirectedTradeResponse( + tradeResponse, + request.postDataJSON(), + ); await route.fulfill({ json: redirectedTradeResponse }); }); } @@ -296,7 +301,7 @@ export class MarketActions { } @step() - async changeTransactionWatchResponseTo(status: 'SUBMITTED' | 'SUCCESS') { + async changeBuyWatchResponseTo(status: 'SUBMITTED' | 'SUCCESS') { await this.page.route(invityEndpoint.buyWatch, async route => { await route.fulfill({ json: { status } }); }); diff --git a/packages/suite-desktop-core/e2e/tests/trading/buy-bitcoin.test.ts b/packages/suite-desktop-core/e2e/tests/trading/buy-bitcoin.test.ts index 42ee3cdcb25..bd589bc0174 100644 --- a/packages/suite-desktop-core/e2e/tests/trading/buy-bitcoin.test.ts +++ b/packages/suite-desktop-core/e2e/tests/trading/buy-bitcoin.test.ts @@ -1,8 +1,7 @@ import { localizeNumber } from '@suite-common/wallet-utils'; import { capitalizeFirstLetter } from '@trezor/utils'; -import { buyQuotesBTC, buyTradeBTC, invityEndpoint } from '../../fixtures/invity'; -import expectedWatchRequestPayload from '../../fixtures/invity/buy/watch-request.json'; +import { buyQuotesBTC, buyTradeBTC, invityEndpoint, invityRequest } from '../../fixtures/invity'; import { formatAddress } from '../../support/common'; import { expect, test } from '../../support/fixtures'; @@ -19,7 +18,7 @@ const { receiveAddress, paymentMethodName } = buyTradeBTC.trade; test.describe('Trading - Buy BTC', { tag: ['@group=other', '@webOnly'] }, () => { test.beforeEach(async ({ marketPage, onboardingPage, dashboardPage, walletPage }) => { await marketPage.mockInvity(); - await marketPage.mockInvityTrade(buyTradeBTC, 'btc'); + await marketPage.mockInvityTrade(buyTradeBTC, invityEndpoint.buyTrade); await onboardingPage.completeOnboarding(); await dashboardPage.discoveryShouldFinish(); await walletPage.openTrading(); @@ -27,7 +26,7 @@ test.describe('Trading - Buy BTC', { tag: ['@group=other', '@webOnly'] }, () => test('Select compared offers to buy', async ({ marketPage }) => { await test.step('Fill input amount and opens offer comparison', async () => { - await marketPage.setYouPayFiatAmount(fiatAmount); + await marketPage.setYouPayAmount(fiatAmount); await expect(marketPage.bestOfferAmount).toHaveText(bestBuyCryptoAmount); await expect(marketPage.quoteProvider).toHaveText(bestBuyProvider); await marketPage.compareButton.click(); @@ -54,19 +53,23 @@ test.describe('Trading - Buy BTC', { tag: ['@group=other', '@webOnly'] }, () => test('Buy crypto from best offer', async ({ page, marketPage }) => { await test.step('Request a trade', async () => { - await marketPage.setYouPayFiatAmount(fiatAmount); + await marketPage.setYouPayAmount(fiatAmount); await marketPage.buyBestOfferButton.click(); await marketPage.confirmTrade(); }); - const watchRequestPromise = page.waitForRequest(invityEndpoint.buyWatch); await page.clock.install(); await test.step('Confirm the trade and get redirected to transaction detail', async () => { - await marketPage.changeTransactionWatchResponseTo('SUBMITTED'); - await marketPage.finishMockedTrade(); - await expect(watchRequestPromise).toHavePayload(expectedWatchRequestPayload, { - omit: ['partnerData'], + await marketPage.changeBuyWatchResponseTo('SUBMITTED'); + const tradeRequestPromise = page.waitForRequest(invityEndpoint.buyTrade); + const watchRequestPromise = page.waitForRequest(invityEndpoint.buyWatch); + await marketPage.confirmTradeButton.click(); + await expect(tradeRequestPromise).toHavePayload(invityRequest.buyTradeBTCPayload, { + omit: ['returnUrl', 'trade.orderId', 'trade.paymentId'], + }); + await expect(watchRequestPromise).toHavePayload(invityRequest.buyWatchPayload, { + omit: ['partnerData', 'orderId', 'paymentId'], }); await expect(marketPage.transactionDetailStatus).toHaveText( 'Waiting for your payment...', @@ -75,8 +78,8 @@ test.describe('Trading - Buy BTC', { tag: ['@group=other', '@webOnly'] }, () => }); await test.step('Wait 30s for watch refresh and change of status to Approved', async () => { - await marketPage.changeTransactionWatchResponseTo('SUCCESS'); - await page.clock.fastForward(marketPage.transactionWatchPeriod); + await marketPage.changeBuyWatchResponseTo('SUCCESS'); + await page.clock.fastForward(marketPage.watchPeriod); await expect(marketPage.transactionDetailStatus).toHaveText('Approved'); await expect(marketPage.confirmationFiatAmount).toHaveText(formattedFiatAmount); await expect(marketPage.confirmationCryptoAmount).toHaveText(bestBuyCryptoAmount); diff --git a/packages/suite-desktop-core/e2e/tests/trading/buy-ethereum.test.ts b/packages/suite-desktop-core/e2e/tests/trading/buy-ethereum.test.ts index 401aeebc297..1bceef9a12e 100644 --- a/packages/suite-desktop-core/e2e/tests/trading/buy-ethereum.test.ts +++ b/packages/suite-desktop-core/e2e/tests/trading/buy-ethereum.test.ts @@ -15,7 +15,7 @@ const { receiveAddress, paymentMethodName } = buyTradeEthereum.trade; test.describe('Trading - Buy Ethereum', { tag: ['@group=other', '@webOnly'] }, () => { test.beforeEach(async ({ page, marketPage, onboardingPage, dashboardPage }) => { await marketPage.mockInvity(); - await marketPage.mockInvityTrade(buyTradeEthereum, 'eth'); + await marketPage.mockInvityTrade(buyTradeEthereum, invityEndpoint.buyTrade); await page.route(invityEndpoint.buyQuotes, async route => { await route.fulfill({ json: buyQuotesEthereum }); }); @@ -35,7 +35,7 @@ test.describe('Trading - Buy Ethereum', { tag: ['@group=other', '@webOnly'] }, ( await test.step('Request to buy Ethereum', async () => { await walletPage.tradingBuyButton.click(); await marketPage.selectAccount('Ethereum', 'eth'); - await marketPage.setYouPayFiatAmount(fiatAmount); + await marketPage.setYouPayAmount(fiatAmount, 'ethereum'); await expect(marketPage.bestOfferAmount).toHaveText(formattedCryptoAmount); await expect(marketPage.quoteProvider).toHaveText(provider); await marketPage.buyBestOfferButton.click(); diff --git a/packages/suite-desktop-core/e2e/tests/trading/buy-solana.test.ts b/packages/suite-desktop-core/e2e/tests/trading/buy-solana.test.ts index 01edf72ad57..abfb23485e9 100644 --- a/packages/suite-desktop-core/e2e/tests/trading/buy-solana.test.ts +++ b/packages/suite-desktop-core/e2e/tests/trading/buy-solana.test.ts @@ -1,7 +1,12 @@ import { localizeNumber } from '@suite-common/wallet-utils'; import { capitalizeFirstLetter } from '@trezor/utils'; -import { buyQuotesSolana, buyTradeSolana, invityEndpoint } from '../../fixtures/invity'; +import { + buyQuotesSolana, + buyTradeSolana, + invityEndpoint, + invityRequest, +} from '../../fixtures/invity'; import { formatAddress } from '../../support/common'; import { expect, test } from '../../support/fixtures'; @@ -16,7 +21,7 @@ const { receiveAddress, paymentMethodName } = buyTradeSolana.trade; test.describe('Trading - Buy Solana', { tag: ['@group=other', '@webOnly'] }, () => { test.beforeEach(async ({ page, marketPage, onboardingPage, dashboardPage }) => { await marketPage.mockInvity(); - await marketPage.mockInvityTrade(buyTradeSolana, 'sol'); + await marketPage.mockInvityTrade(buyTradeSolana, invityEndpoint.buyTrade); await page.route(invityEndpoint.buyQuotes, async route => { await route.fulfill({ json: buyQuotesSolana }); }); @@ -25,6 +30,7 @@ test.describe('Trading - Buy Solana', { tag: ['@group=other', '@webOnly'] }, () }); test('Buy specific crypto amount of Solana token', async ({ + page, settingsPage, dashboardPage, walletPage, @@ -39,9 +45,10 @@ test.describe('Trading - Buy Solana', { tag: ['@group=other', '@webOnly'] }, () }); await test.step('Request a specific crypto amount to buy', async () => { - await marketPage.waitForOffersSyncToFinish(); + await marketPage.waitForBuyOffersSync(); await marketPage.youPayFiatCryptoSwitchButton.click(); - await marketPage.setYouPayCryptoAmount(cryptoAmount); + const isCryptoInput = true; + await marketPage.setYouPayAmount(cryptoAmount, 'solana', isCryptoInput); await expect(marketPage.bestOfferAmount).toHaveText(fiatAmount); await expect(marketPage.quoteProvider).toHaveText(provider); await marketPage.buyBestOfferButton.click(); @@ -55,7 +62,11 @@ test.describe('Trading - Buy Solana', { tag: ['@group=other', '@webOnly'] }, () capitalizeFirstLetter(provider), ); await expect(marketPage.confirmationPaymentMethod).toHaveText(paymentMethodName); + const tradeRequestPromise = page.waitForRequest(invityEndpoint.buyTrade); await marketPage.confirmTradeButton.click(); + await expect(tradeRequestPromise).toHavePayload(invityRequest.buyTradeSolanaPayload, { + omit: ['returnUrl', 'trade.orderId', 'trade.paymentId'], + }); }); await test.step('Verify transaction detail', async () => { diff --git a/packages/suite-desktop-core/e2e/tests/trading/sell-bitcoin.test.ts b/packages/suite-desktop-core/e2e/tests/trading/sell-bitcoin.test.ts new file mode 100644 index 00000000000..9b59c327b80 --- /dev/null +++ b/packages/suite-desktop-core/e2e/tests/trading/sell-bitcoin.test.ts @@ -0,0 +1,101 @@ +import { capitalizeFirstLetter } from '@trezor/utils'; + +import { + getCompanyNameFromList, + invityEndpoint, + invityRequest, + sellQuotesBTC, + sellTradeBTC, + sellWatch, +} from '../../fixtures/invity'; +import { expect, test } from '../../support/fixtures'; + +const mnemonic = + 'academic again academic academic academic academic academic academic academic academic academic academic academic academic academic academic academic pecan provide remember'; + +// Expected values based on our mocked responses +const fiatAmount = sellQuotesBTC[0].fiatStringAmount; +const cryptoAmount = sellQuotesBTC[0].cryptoStringAmount; +const provider = getCompanyNameFromList(sellQuotesBTC[0].exchange, 'sellList'); +const providerAddress = sellWatch.destinationAddress; +const providerPaymentId = sellWatch.destinationPaymentExtraId; +const formattedCryptoAmount = `${cryptoAmount} BTC`; +const formattedFiatAmount = `€${fiatAmount}`; +const { paymentMethodName } = sellTradeBTC.trade; + +test.describe('Trading - Sell', { tag: ['@group=other', '@webOnly'] }, () => { + test.use({ + emulatorSetupConf: { mnemonic, passphrase_protection: true }, + }); + test.beforeEach(async ({ marketPage, onboardingPage, dashboardPage, walletPage }) => { + if (!process.env.PASSPHRASE) { + throw new Error( + 'PASSPHRASE not provided in env variables. Check docs/tests/e2e-playwright-suite.md.', + ); + } + await marketPage.mockInvity(); + await marketPage.mockInvityTrade(sellTradeBTC, invityEndpoint.sellTrade); + await onboardingPage.completeOnboarding(); + await dashboardPage.discoveryShouldFinish(); + await dashboardPage.deviceSwitchingOpenButton.click(); + await dashboardPage.addHiddenWallet(process.env.PASSPHRASE!); + await dashboardPage.discoveryShouldFinish(); + await walletPage.openTrading(); + await marketPage.sellTabButton.click(); + }); + + test('Sell Bitcoin for best offer', async ({ + page, + marketPage, + devicePrompt, + trezorUserEnvLink, + }) => { + await test.step('Fill in a sell request', async () => { + await marketPage.selectCountryOfResidence('CZ'); + const quoteRequestPromise = page.waitForRequest(invityEndpoint.sellQuotes); + await marketPage.youPayCryptoInput.fill(cryptoAmount); + await expect(quoteRequestPromise).toHavePayload(invityRequest.sellQuotesPayload); + await marketPage.waitForSellOffersSync(); + await expect(marketPage.bestOfferAmount).toHaveText(fiatAmount); + await expect(marketPage.quoteProvider).toHaveText(capitalizeFirstLetter(provider)); + }); + + await test.step('Confirm sell', async () => { + await marketPage.formSellButton.click(); + const tradeRequestPromise = page.waitForRequest(invityEndpoint.sellTrade); + await marketPage.sellTermsConfirmButton.click(); + await expect(tradeRequestPromise).toHavePayload(invityRequest.sellTradePayload, { + omit: ['returnUrl', 'trade.orderId', 'trade.paymentId', 'trade.refundAddress'], + }); + }); + await test.step('Wait for the redirection to complete', async () => { + await expect(page.getByText('Buy & sell')).not.toBeVisible(); + await expect(page.getByText('Buy & sell')).toBeVisible({ timeout: 15_000 }); + }); + + await test.step('Verify all confirmation values', async () => { + await expect(marketPage.confirmationFiatAmount).toHaveText(formattedFiatAmount); + await expect(marketPage.confirmationCryptoAmount).toHaveText(formattedCryptoAmount); + await expect(marketPage.confirmationProvider).toHaveText(provider); + await expect(marketPage.confirmationPaymentMethod).toHaveText(paymentMethodName); + await expect(marketPage.confirmationAddress).toHaveText(providerAddress); + await expect(marketPage.confirmationAccount).toHaveText('Bitcoin #1'); + await expect(page.getByTestId('@trading/form/verify/extra-id')).toHaveText( + providerPaymentId, + ); + }); + + await test.step('Initiate send', async () => { + await marketPage.confirmTradeButton.click(); + await expect(devicePrompt.sellButton).toBeDisabled(); + await devicePrompt.confirmOnDevicePromptIsShown(); + await trezorUserEnvLink.pressYes(); + await expect(devicePrompt.cryptoAmountOf('amount')).toHaveText(formattedCryptoAmount); + await devicePrompt.confirmOnDevicePromptIsShown(); + await trezorUserEnvLink.pressYes(); + // Note: We intentionally skip clicking the sell button in tests to prevent actual cryptocurrency transactions. + // In a real scenario, the user would complete the transaction by clicking this button. + await expect(devicePrompt.sellButton).toBeEnabled(); + }); + }); +}); diff --git a/packages/suite-desktop-core/package.json b/packages/suite-desktop-core/package.json index 8310333bfb3..7224e1b9620 100644 --- a/packages/suite-desktop-core/package.json +++ b/packages/suite-desktop-core/package.json @@ -58,6 +58,7 @@ "@types/electron-localshortcut": "^3.1.3", "@types/lodash": "^4", "@types/ws": "^8.5.13", + "dotenv": "^16.4.7", "electron": "34.1.0", "fs-extra": "^11.2.0", "glob": "^10.3.10", diff --git a/packages/suite/src/components/suite/FormattedCryptoAmount.tsx b/packages/suite/src/components/suite/FormattedCryptoAmount.tsx index 19cd7daa7af..da2bf3ae9b0 100644 --- a/packages/suite/src/components/suite/FormattedCryptoAmount.tsx +++ b/packages/suite/src/components/suite/FormattedCryptoAmount.tsx @@ -103,7 +103,7 @@ export const FormattedCryptoAmount = ({ } const content = ( - + {!!signValue && } diff --git a/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewOutputList/TransactionReviewOutputElement.tsx b/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewOutputList/TransactionReviewOutputElement.tsx index b0ecbc7e737..5f7563b85f9 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewOutputList/TransactionReviewOutputElement.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewOutputList/TransactionReviewOutputElement.tsx @@ -86,6 +86,7 @@ const Value = ({ value, type, symbol, token, isFee, isFiatVisible, state }: Valu return ( <> {symbol && isFiatVisible && ( - + { decision.resolve(true); onCancel(); diff --git a/packages/suite/src/views/wallet/trading/common/TradingSelectedOffer/TradingOfferSell/TradingOfferSellTransaction.tsx b/packages/suite/src/views/wallet/trading/common/TradingSelectedOffer/TradingOfferSell/TradingOfferSellTransaction.tsx index 2c131bd5efd..8f08830c6da 100644 --- a/packages/suite/src/views/wallet/trading/common/TradingSelectedOffer/TradingOfferSell/TradingOfferSellTransaction.tsx +++ b/packages/suite/src/views/wallet/trading/common/TradingSelectedOffer/TradingOfferSell/TradingOfferSellTransaction.tsx @@ -67,7 +67,7 @@ export const TradingSelectedOfferSellTransaction = () => { - + @@ -75,7 +75,7 @@ export const TradingSelectedOfferSellTransaction = () => { - +
{destinationAddress}
@@ -94,7 +94,7 @@ export const TradingSelectedOfferSellTransaction = () => { )} - +
{destinationPaymentExtraId}
@@ -106,6 +106,7 @@ export const TradingSelectedOfferSellTransaction = () => { isLoading={callInProgress} isDisabled={!device?.connected} onClick={sendTransaction} + data-testid="@trading/offer/continue-transaction-button" > diff --git a/yarn.lock b/yarn.lock index bc8e75c9aa4..72d5df8749b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12618,6 +12618,7 @@ __metadata: "@types/lodash": "npm:^4" "@types/ws": "npm:^8.5.13" chalk: "npm:^4.1.2" + dotenv: "npm:^16.4.7" electron: "npm:34.1.0" electron-localshortcut: "npm:^3.2.1" electron-store: "npm:8.2.0" @@ -21156,10 +21157,10 @@ __metadata: languageName: node linkType: hard -"dotenv@npm:^16.0.0, dotenv@npm:^16.0.3, dotenv@npm:^16.3.1, dotenv@npm:^16.4.1, dotenv@npm:^16.4.4, dotenv@npm:^16.4.5, dotenv@npm:~16.4.5": - version: 16.4.5 - resolution: "dotenv@npm:16.4.5" - checksum: 10/55a3134601115194ae0f924e54473459ed0d9fc340ae610b676e248cca45aa7c680d86365318ea964e6da4e2ea80c4514c1adab5adb43d6867fb57ff068f95c8 +"dotenv@npm:^16.0.0, dotenv@npm:^16.0.3, dotenv@npm:^16.3.1, dotenv@npm:^16.4.1, dotenv@npm:^16.4.4, dotenv@npm:^16.4.5, dotenv@npm:^16.4.7, dotenv@npm:~16.4.5": + version: 16.4.7 + resolution: "dotenv@npm:16.4.7" + checksum: 10/f13bfe97db88f0df4ec505eeffb8925ec51f2d56a3d0b6d916964d8b4af494e6fb1633ba5d09089b552e77ab2a25de58d70259b2c5ed45ec148221835fc99a0c languageName: node linkType: hard