Trezor Suite documentation

This documentation provides technical information for Suite developers, third-party wallet developers integrating Trezor, and users interested in implementation details. It is available at docs.trezor.io/trezor-suite in an HTML version compiled using mdBook. More guidance on Suite development can be found in package READMEs of the trezor-suite monorepo.

Data Analytics

Anonymous1 data volunteered by Trezor users directly contributes to improved performance across all the platforms you use Trezor Suite on.

1

"Anonymous" means that we do not collect any sensitive personal information. AWS and Sentry are able to view IP addresses but they are not tracked or collected by Trezor and Trezor removes such information from the logs automatically. Enable Tor to mask your IP address from third parties when using Trezor Suite.

Participation is easy and completely optional. Enable or disable usage data sharing with one click at any time in Trezor Suite Settings. With full control over what you contribute, you can safely take part in making Bitcoin more secure.

TL;DR

  1. Data is only collected with explicit permission.
  2. Your sensitive data is not collected.
  3. We use AWS logging for data analytics and Sentry for error tracking.
  4. We store the data concerning errors for the period of 90 days.
  5. Only limited amount of users is able to access the data.

What data is collected?

When enabled, purely functional data about how the app is used will be collected and analyzed to find defects and inefficiencies. With explicit consent, both web and desktop applications may collect anonymous data such as user interactions with app functions, errors, hardware specifications and app response times.

If usage data is disabled only the decision not to share any data is recorded. This means we do not collect any data, automated Sentry reports (see below) or any other data. An exception is when a user specifically chooses to submit feedback or bug reports through Trezor Suite.

How are the data processed?

Data are logged in the form of HTTPS requests to an AWS S3 bucket. Those data logs are then transformed into sets which can be analyzed to give meaningful information. See AWS for more detailed info about the particular events which are tracked.

Error tracking using Sentry

To catch errors quickly and deliver you the best experience with your Trezor, we use Sentry.io, a tool for error tracking and performance monitoring. Data is only available to Sentry when usage data tracking is enabled. See our page about Sentry for more information on how it works.

Retention period

By principle, the logs collected are destroyed without delay once the purpose of use is met. However, the minimum retention period equals to 90 days when the data is processed to improve Trezor Suite. The 90 days are related to the data concerning any errors occurring in Trezor Suite. Performance related data may be stored for longer periods of time. When the retention period ends, all event data and most metadata is eradicated from the storage and from the servers without additional archiving in order to prevent the threat of intrusion.

Security of data

Access to the data is limited strictly to the members of the development, security and IT team. All users are provided access on the need-to-know basis and the accesses are regularly reviewed. Users accessing the data log in using a strong combination of username and password and use two-factor authentication (where provided by a service provider).

Contents

AWS Analytics: Info

For a deeper technical writeup of analytics processes intended for developers, please see README.md.

Trezor Suite can be set to collect real-world data to improve the performance of both web and desktop apps. This anonymous data is only shared by users who have usage data tracking enabled.

During the first run Trezor Suite prompts the user whether they wish to participate in the data collection and such setting can be changed later on in Settings at any time.

Anonymous data

Collected data are anonymous. This means that Suite does not track personal information and can not be used to view particular users' balances.

Among the data not collected by analytics:

  • Device IDs
  • Public keys
  • Particular amounts
  • Transaction IDs

When data tracking is enabled, Trezor Suite collects functional information that can be used to directly improve the app, such as:

  • Events triggered by a user during a session
  • Hardware, operating system and setup of the connected device
  • Errors encountered during a session

Data process

  1. User with enabled analytics interacts with the application
  2. Events are sent to specific endpoints
  3. Collected data are parsed and analysed (can be seen in Keboola)
  4. Charts and metrics are created (in Tableau)
  5. We know how to improve the application

Sentry

Trezor Suite uses Sentry.io to track errors and monitor app performance on the machines of users who have enabled anonymous data collection. This allows us to optimize Trezor Suite and fix compatibility issues across many different usage environments.

No data is shared with Sentry if users have disabled usage data tracking 1.

1

In rare cases where an error would occur before Suite loading its internal storage an error to Sentry might be sent, because Suite is not yet informed whether analytics are enabled or not. As the storage is not yet loaded no private data can be sent.

What is being tracked

General data

Browser (User Agent), System and HW specifications, Suite version, instance id shared with analytics.

timestamps, clicks, navigation, analytics, network requests from Suite.

Extra data:

  • Enabled-coins e.g.: [btc, ltc, eth, xrp, doge]
  • Wallet discovery e.g.:
[
  {
    authConfirm:False,
    bundleSize: 0,
    deviceState: [redacted],
    failed: [],
    index: 0,
    loaded: 14,
    networks: [btc, btc, btc, ltc, ltc, ltc, eth, xrp, doge],
    running: [undefined],
    status: 4,
    total: 14,
  },
]
  • Device information (slightly redacted):
{
  authConfirm: False,
  available: False,
  buttonRequests: [],
  connected: False,
  features:
    {
      auto_lock_delay_ms: 600000,
      backup_type: Bip39,
      bootloader_hash: None,
      bootloader_mode: None,
      capabilities:
        [
          Capability_Bitcoin,
          Capability_Bitcoin_like,
          Capability_Binance,
          Capability_Cardano,
          Capability_Crypto,
          Capability_EOS,
          Capability_Ethereum,
          Capability_Monero,
          Capability_NEM,
          Capability_Ripple,
          Capability_Stellar,
          Capability_Tezos,
          Capability_U2F,
          Capability_Shamir,
          Capability_ShamirGroups,
          [Filtered],
        ],
      device_id: [redacted],
      display_rotation: 0,
      experimental_features: False,
      firmware_present: None,
      flags: 0,
      fw_major: None,
      fw_minor: None,
      fw_patch: None,
      fw_vendor: None,
      fw_vendor_keys: None,
      imported: None,
      initialized: True,
      label: [redacted],
      language: en-US,
      major_version: 2,
      minor_version: 4,
      model: T,
      backup_availability: 0,
      no_backup: False,
      passphrase_always_on_device: False,
      passphrase_protection: True,
      patch_version: 2,
      pin_protection: True,
      recovery_status: 0,
      revision: 9276b1702361f70e094286e2f89e919d8a230d5c,
      safety_checks: Strict,
      sd_card_present: False,
      sd_protection: False,
      session_id: [redacted],
      unfinished_backup: False,
      unlocked: True,
      vendor: trezor.io,
      wipe_code_protection: False,
    },
  firmware: valid,
  firmwareRelease:
    {
      changelog: [],
      isNewer: False,
      isRequired: None,
      release: {},
    },
  id: [redacted],
  instance: 1,
  label: [redacted],
  metadata: { status: disabled },
  mode: normal,
  passphraseOnDevice: False,
  path,
  remember: True,
  state: [redacted],
  status: used,
  ts: 1632094494156,
  type: acquired,
  unavailableCapabilities: {},
  useEmptyPassphrase: False,
  walletNumber: 1,
}
  • Action logs:
[
  { time: 1634644852099, type: @suite/online-status },
  { action: {}, time: 1634644852104, type: @suite/init },
  { time: 1634644852966, type: @message-system/save-valid-messages },
  { time: 1634644852967, type: @suite/tor-status },
  { time: 1634644853131, type: @resize/update-window-size },
  { time: 1634644853306, type: @desktop-update/enable },
  { time: 1634644853361, type: @desktop-update/checking },
  { time: 1634644853449, type: @message-system/save-valid-messages },
  { action: {}, time: 1634644853453, type: @suite/set-language },
  { time: 1634644853455, type: @storage/loaded },
  { time: 1634644853717, type: @message-system/fetch-config-success },
  { time: 1634644853744, type: @analytics/init },
  { time: 1634644854072, type: @desktop-update/not-available },
  { time: 1634644854166, type: iframe-loaded },
  { time: 1634644854168, type: @suite/trezor-connect-initialized },
  { time: 1634644854187, type: @blockchain/update-fee },
  { action: {}, time: 1634644854188, type: @suite/app-changed },
  { time: 1634644854189, type: @router/location-change },
  { time: 1634644854191, type: @suite/ready },
  { time: 1634644854192, type: @wallet-settings/clear-tor-blockbook-urls },
  { time: 1634644854192, type: @blockchain/ready },
]

Releases

This chapter contains information about the release process.

Adding New Firmwares

In case we are about to release both Suite and firmwares we want to add the signed binaries during the Freeze so QA has the whole thing to test.

Binaries and release definitions are stored in packages/connect-common/files/firmware/* in the same folder structure as they are in https://github.com/trezor/webwallet-data deployed on https://data.trezor.io.

Latest and intermediary firmware binaries are bundled in suite-desktop and suite-web so the whole onboarding process in desktop app – including FW installation – can be completed offline. It means that only the latest and intermediary Firmware binaries are currently available in Suite (both web and desktop). There is no fallback on https://data.trezor.io.

Package @trezor/connect-common is a public NPM package used as dependency of @trezor/connect.

Add firmwares

  1. Complete the firmware release process including firmware signing.

  2. Add firmwares to packages/connect-common/files/firmware/* and modify its releases.json file. See Firmware releases.json files structure for an explanation and 90bb548 for an example.

  3. Remove older binaries so they are not bundled in the desktop app any more, but always keep:

    See #4262 for explanation.

  4. Test it locally (at least by running yarn build:libs to rebuild connect files and yarn suite:dev to use/copy them).

  5. Freeze Suite. At this moment you are all good to Freeze and forward to QA. They should be able to test Suite in its wholeness along with the new firmwares.

Release for 3rd party users of @trezor/connect

After Suite is released, distribute new firmware by releasing new @trezor/connect with an updated @trezor/connect-common package.

Follow instructions on how to release a new version of @trezor/connect.


Firmware releases.json files structure

Firmware releases.json files provide data about all available firmware versions and they are used to offer the correct firmware version for the user to update depending on the current version of firmware, bootloader and bridge. See the table below for a description of every param.

Those releases.json files are bundled inside @trezor/connect in /static/connect/data folder. Therefore, suite-web takes if from https://suite.trezor.io/web/static/connect/data/firmware/{deviceModel}/releases.json?r={timestamp to prevent caching} and suite-desktop has it on file:///static/connect/data/firmware/{deviceModel}/releases.json. Neither the suite-web nor the suite-desktop take it from https://data.trezor.io.

keytypeexample valuedescription
requiredbooleanfalseIf true, user will be forced to update older FW in order to continue using Suite.
version[number, number, number][1, 11, 1]Firmware version. Has to be unique.
bootloader_version[number, number, number][1, 11, 0]Bootloader version. If you are adding new firmwares, ask & verify if there is new BL included (by running it on the device and checking the version shown)
min_firmware_version[number, number, number][1, 6, 2]Minimal supported FW version. See getInfo for the usage.
min_bootloader_version[number, number, number][1, 5, 0]Minimal supported bootloader version. See getInfo for the usage.
urlstringfirmware/t1b1/trezor-t1b1-1.11.1".bin"Where to find the binary. Depends on the filename. While adding new FW, keep the structure, just update the version number. suite-web downloads binaries from https://data.trezor.io, suite-desktop has them bundled.
url_bitcoinonlystringfirmware/t1b1/trezor-t1b1-1.11.1-bitcoinonly.bin"Same as url, just for Bitcoin only FW.
fingerprintstring"f7c60d0b8c2853afd576867c6562aba5ea52bdc2ce34d0dbb8751f52867c3665"Fingerprint of FW binary. Run trezorctl firmware-update -f {path-to-the-bin} to retrieve it (you don't have to confirm the update on device unless you want to). Look for Firmware fingerprint: row.
fingerprint_bitcoinonlystring"8e17b95b5d302f203de3a8fe27959efd25e3d5140ac9b5e60412f1b3f624995d"Same as fingerprint, just for Bitcoin only FW.
notesstringhttps://blog.trezor.io/trezor-suite-and-firmware-updates-may-2022-b1af60742291"Link to blog with info about the changes in this FW version. You could find it on internal Notion page for the release even before it's published.
changelogstring"* Remove Lisk.\n* Re-enabled Firo support."Short description of main changes, displayed to the user on FW update page. Split lines by * sign. You can find it on internal Notion page for the release.

Desktop Updates

The desktop build of Trezor Suite uses an auto-updating feature to keep the application up to date with our latest published release.

Internals

The package electron-updater (part of electron-builder) is used to manage updates. Information about updates is displayed in our UI and the user can perform actions related to them (trigger update, skip, etc...).

In addition of what electron-updater provides us, we check signatures of downloaded files. For this to work, all files uploaded on Github need to have a signature attached with them. The signature will be checked against the SL signing key which is included in the application at build time. The key is located in packages/suite-desktop-core/build/app-key.asc and should be updated if the private key is changed.

Environments

Staging

Staging is available at staging-suite.trezor.io and is only accessible within SatoshiLabs internal IP range (office + VPN).

Before releasing publicly we deploy to so-called staging environment which should be 1:1 with production. QA tests the release there.

Production (suite.trezor.io)

Stable version is hosted on suite.trezor.io.

routesourceassetPrefix
/web@trezor/suite-web/web

Publishing @trezor package to npm registry

yarn npm publish should be done only on gitlab CI in deploy npm phase.

Purpose

@trezor packages are dependencies of @trezor/connect public API. Publish is required to distribute changes to @trezor/connect and make them available for 3rd party implementations.

Prerequisites

  1. Update CHANGELOG.md and list all changes since the last release of the package.
  2. Bump the version in packages/<PACKAGE-NAME>/package.json. Use the semver convention.

Production

  1. Create new branch with npm-release/ prefix.
  2. Commit your changes as release: @trezor/<PACKAGE-NAME> X.X.X.
  3. Use <PACKAGE-NAME> deploy npm job.

Beta

If you want to publish to npm as beta (from any branch) do the following:

  1. Change the version in packages/<PACKAGE-NAME>/package.json from X.X.X to X.X.(X + 1)-beta.1. The -beta.<n> suffix is important because NPM registry doesn't allow overriding already published versions. With this suffix we can publish multiple beta versions for a single patch.
  2. Commit your changes as release: @trezor/<PACKAGE-NAME> X.X.X-beta.X.
  3. Use beta <PACKAGE-NAME> deploy npm job.

Signing binaries win

The desktop build of Trezor Suite uses electron-builder for signing the package and the binaries inside.

In order to be able to sign all the binaries for windows in other operating systems [electron-builder] uses osslsigncode.

Check if binaries are signed for windows in Linux

The installer .exe can be unpacked with 7za x Trezor-Suite-22.2.1-win-x64.exe on Linux. The chktrust is from mono-develop package (Ubuntu LTS, other distros will have it under similar name).

7za x Trezor-Suite-22.2.1-win-x64.exe

After unpacked, test signatures:

for I in **/*.exe **/*.dll; do echo "---Checking $I"---; chktrust "$I"; done

CI signing details for windows

Certificate file is with extension: .pfx Env variables for signing: WIN_CSC_KEY_PASSWORD WIN_CSC_LINK.

Creating Self-signed pfx and cer certificates with OpenSSL

Generate directly the pem:

openssl req -x509 -days 365 -newkey rsa:2048 -keyout cert.pem -out cert.pem

The pem cannot be used with Microsoft products, so we need to convert it to PKCS#12/PFX Format which is what Microsoft uses.

openssl pkcs12 -export -in cert.pem -inkey cert.pem -out cert.pfx

Versioning

This repo contains a mix of packages with 3 different versioning schemes and schedules.

Private Packages

That is, all packages that have private: true in their package.json and are not consumed by third parties nor published to NPM. Because they get only consumed by other packages in this repo (eg. @trezor/suite-data or @trezor/suite) by the Yarn's workspace resolution or are distributed in other forms like, for example, bundled applications (eg. @trezor/suite-desktop) we do not version them. That is, their version is kept at 1.0.0 all the time.

Public Packages

That is, packages published to NPM consumed by third parties. At the moment of writing this there is one public package: blockchain-link. They follow the SemVer scheme on irregular schedule.

Suite App

The version of the Suite App itself is tracked in the suiteVersion field of the suite package.json. This version is a way to communicate the steps in evolution of the Suite app between our product, marketing, support teams and the users.

We are using so-called calendar versioning in the format YY.MM.PATCH where

  • YY stands for the current year.
  • MM stands for the current month.
  • PATCH is increased on every release in the given month.

For example:

  • 20.10.1 first release in Oct 2020
  • 20.10.3 third release in Oct 2020
  • 19.12.1 first release in Dec 2019

Beta versions

We version beta in a similar way as production versions but we always set PATCH to 0 and increase the MM.

That means that every release on beta has 0 as the patch version. This has a drawback that you can't distinguish beta deployments by a version number, but beta testers should be able to read and report the commit hash.

Only stable releases have patch version >1 and this increases with each stable release: 1, 2, 3, 4.

Beta also has +1 MM version when compared to stable indicating this is upcoming release which will be deployed on stable next month.

For example:

  • 20.10.1 first release on Oct 15th to stable
  • 20.10.2 second release on Oct 22nd to stable
  • 20.11.0 release on Oct 29th 2020 to beta
  • 20.11.0 another release on Nov 5th to beta
  • 20.11.1 public release on Nov 14th to stable

Development versions

We use the same scheme as beta. That is, develop branch has always YY.MM.0 version where MM is the upcoming month's release. When we fork develop to release/20YY-MM branch, we bump the release branch version to YY.MM.1 and increase the develop version to YY.(MM+1).0 indicating we are already brewing next release in the develop.

Packages

This directory contains description of various Trezor Suite packages.

@trezor/connect @trezor/suite @trezor/suite-desktop creating new package

Trezor javascript SDK

Most of Trezor Connect documentation has been moved to Connect Explorer.

This page contains some remaining documentation useful for developers or internal purposes.

Debugging

?trezor-connect-src

We have introduced a feature to significantly help us debugging Connect issues. You can substitute the Connect version easily with the ?trezor-connect-src parameter.

You can simply visit https://suite.trezor.io/web/?trezor-connect-src=YOUR_CONNECT_BUILD and Connect will be replaced by your own build.

This is extremely helpful along with the Connect's build and deploy features in its CI. You can create a new branch in Connect, push it, CI will build it, and if you run the manual deploy job it will also deploy it to https://connect.corp.sldev.cz/[BRANCH_NAME]. And then you can use https://suite.trezor.io/web/?trezor-connect-src=https://connect.corp.sldev.cz/[BRANCH_NAME] and you are testing the production build with your Connect build.

And of course the Suite build does not have to be the production suite.trezor.io one. You can use this feature anywhere.

Is it safe to have this enabled in production?

Only whitelisted domains are allowed so you can't replace the Connect URL with any random one. The list of whitelisted domains is:

  • trezor.io (production)
  • trezoriovpjcahpzkrewelclulmszwbqpzmzgub37gbcjlvluxtruqad.onion (Tor production)
  • sldev.cz (development)
  • localhost

Also Bridge will not talk to any other endpoints than the above mentioned due to its CORS policy.

Dependencies

Webpack:

Since webpack@5 auto polyfills for nodejs are not provided. see https://webpack.js.org/blog/2020-10-10-webpack-5-release/#automatic-nodejs-polyfills-removed

List of essential libraries to produce build:

  • assert (polyfill)
  • crypto-browserify (polyfill)
  • html-webpack-plugin
  • process (polyfill)
  • stream-browserify (polyfill)
  • style-loader
  • terser-webpack-plugin
  • util (polyfill)
  • webpack-*
  • worker-loader

From Protobuf to TypeScript and Flow

This document was moved to @trezor/transport

Supported coins

The pipeline

Do not change @trezor/connect-common/files/coins.json manually.

The one and only source of truth are *.json definitions declared and maintained in the firmware repository.

These are exported to a read-only trezor-common repository.

trezor-common is included as git submodule mounted at submodules/trezor-common.

Update and maintenance in @trezor/connect

Make sure that desired [coin].json definition is present in trezor-firmware repository and corresponding support for connect is enabled.

  1. Update trezor-common submodule:
 yarn update-submodules
  1. Build src/data/coins.json file using trezor-common/cointool:
yarn update-coins

Events

Handling events

Once user grants permission for hosting page to communicate with API TrezorConnect will emits events about device state. Events can be distinguished by "type" field of event object (TODO structure) Constants of all types can be imported from package

ES6

import TrezorConnect, { DEVICE_EVENT, DEVICE } from '@trezor/connect';

TrezorConnect.on(DEVICE_EVENT, event => {
    if (event.type === DEVICE.CONNECT) {
    } else if (event.type === DEVICE.DISCONNECT) {
    }
});

List of published events

Full list of events is unfortunately beyond the scope of this documentation but you may refer to the source code for:

Path

  • path - string | Array<number> in BIP44 path scheme or Array of hardended numbers.

Examples

Bitcoin account 1 using BIP44 derivation path

"m/49'/0/'0'";

Bitcoin account 1 using hardended path

[(49 | 0x80000000) >>> 0, 0 | (0x80000000 >>> 0), (0 | 0x80000000) >>> 0];

Bitcoin first address address of account 1 using BIP44 derivation path

"m/49'/0/'0'/0/0";

Bitcoin first address address of account 1 using hardended path

[(49 | 0x80000000) >>> 0, (0 | 0x80000000) >>> 0, (0 | 0x80000000) >>> 0, 0, 0];

See more examples

Connect Methods

Connect methods documentation has been moved to Connect Explorer.

Suite docs

Send form active elements description

Outputs (BTC coins only):

  • regular (transfer) output is set by default
  • add OP_RETURN: if default output has any values then OP_RETURN is added as a second output otherwise will replace the first input
  • remove OP_RETURN: if there is only 1 output (OP_RETURN) then switch to regular otherwise just remove it
  • add recipient
  • remove recipient
  • Clear all

Address:

  • on address input change
  • on QR scan
  • on Import (to be done)

Address errors:

  • RECIPIENT_IS_NOT_SET (empty field)
  • RECIPIENT_IS_NOT_VALID (not valid address)
  • RECIPIENT_CANNOT_SEND_TO_MYSELF (XRP only: cannot send to myself)

Amount:

  • on amount input change
  • on Fiat input change
  • on QR scan (optional if defined in QR code)
  • on Import (to be done, optional if defined in file)
  • IF sendmax is ON
  • IF sendmax is set AND has second(or multiple) output(s): on second output Amount change
  • IF sendmax is set: on every fee level change
  • IF sendmax is set: on custom fee change
  • IF sendmax is set: on BTC opreturn data changed
  • IF sendmax is set: on ETH data changed
  • (ETH only) IF sendmax is set AND switching between ETH (base currency) and TOKEN

Amount errors:

  • AMOUNT_IS_NOT_SET (empty field)
  • AMOUNT_IS_TOO_LOW (lower/equal than zero + ETH exception: 0 amount is possible ONLY for tx with DATA)
  • AMOUNT_IS_BELOW_DUST lower than network dust limit
  • AMOUNT_IS_NOT_ENOUGH (not enough funds on account)
  • AMOUNT_NOT_ENOUGH_CURRENCY_FEE (ETH only: trying to send TOKEN without enough ETH to cover TX fee)
  • AMOUNT_IS_MORE_THAN_RESERVE (XRP only: trying to spend the reserve)
  • AMOUNT_IS_LESS_THAN_RESERVE (XRP only: trying to send less XRP than required reserve to the empty account)
  • AMOUNT_IS_NOT_IN_RANGE_DECIMALS (amount with invalid decimal places)
  • AMOUNT_IS_NOT_INTEGER (ERC20 only: token doesn't accept decimal places)
  • REMAINING_BALANCE_LESS_THAN_RENT (solana only: account has to keep a minimal balance equal to rent)

Fiat:

  • on fiat input change
  • on Amount input change (any reason listed above)
  • on Currency select change (recalculation)
  • on Import (to be done, optional if defined in file AND amount is not defined in file)

Fiat errors:

  • AMOUNT_IS_NOT_SET (empty field)
  • AMOUNT_IS_TOO_LOW (lower than 0, 0 is still possible if recalculated amount is lower than 1 cent)
  • AMOUNT_IS_NOT_IN_RANGE_DECIMALS (max. 2 decimals allowed)

Fee:

  • on fee level click
  • on custom fee level input change
  • on BTC OP_RETURN data changed
  • on ETH data changed
  • switching from "regular" fee level to "custom" should set value from last selected fee
  • IF fee level wasn't changed yet (normal) and there is not enough coins to satisfy normal level should be automatically switched to first possible (lower) level, either LOW or CUSTOM...
  • last used fee level will be remembered globally for this coin
  • estimated time is only available for BTC-like coins

Fee errors (custom level):

  • CUSTOM_FEE_IS_NOT_SET (empty field)
  • CUSTOM_FEE_IS_NOT_INTEGER (BTC and XRP: decimals not allowed)
  • AMOUNT_IS_NOT_IN_RANGE_DECIMALS (ETH only: decimals are allowed but with max. 9 decimals - GWEI is not satoshi)
  • CUSTOM_FEE_NOT_IN_RANGE (must be between minFee and maxFee specified in coins.json, in @trezor/connect)

(BTC only) OP_RETURN output:

  • HEX field, (on the right) should be changed on every ASCII field (on the left) change
  • ASCII field should be changed ONLY if HEX is valid, otherwise should be empty

OP_RETURN output errors:

  • DATA_NOT_SET (empty fields)
  • DATA_NOT_VALID_HEX (not valid hexadecimal)
  • DATA_HEX_TOO_BIG (data size limited to 80 bytes)

(BTC only) Locktime:

Additional field in send form, activated by "Add locktime" option.

If the number is greater than 500000000 then it is a timestamp otherwise is block number

  • on "add locktime" input change
  • on RBF option enable
  • should disable RBF option if set
  • should disable BROADCAST option if set

Locktime errors:

  • LOCKTIME_IS_NOT_SET
  • LOCKTIME_IS_NOT_NUMBER
  • LOCKTIME_IS_TOO_LOW (lower/equal zero)
  • LOCKTIME_IS_NOT_INTEGER (decimals not allowed)
  • LOCKTIME_IS_TOO_BIG (locktime larger than max unix timestamp * 2 = 4294967294)

(BTC only) RBF:

Additional checkbox in send form, since this could be only true/false there is no validation for that filed


(ETH only) Data:

Additional field in send form, activated by "Add data" option. Same behavior as BTC OP_RETURN output.

  • HEX field, (on the right) should be changed on every ASCII field (on the left) change
  • ASCII field should be changed ONLY if HEX is valid, otherwise should be empty

Data errors:

  • DATA_NOT_VALID_HEX
  • DATA_HEX_TOO_BIG (data size limit: 8192 bytes for protobuf single message encoding)

(XRP only) Destination tag:

Additional field in send form, activated by "Add destination tag" option It doesn't have impact on transaction itself (fee, amount etc)

Destination tag errors:

  • DESTINATION_TAG_NOT_SET
  • DESTINATION_TAG_IS_NOT_NUMBER
  • DESTINATION_TAG_IS_NOT_VALID (decimals not allowed, in range: 0 - 4294967295)

Broadcast:

  • toggle "Sign transaction" / "Send transaction" button
  • "Review transaction" modal with different options at the last step (copy or download signed tx)

Drafts:

  • draft should be saved on change of any field (if this field is valid)
  • draft should be loaded after changing url (going back to send form from any other page)

Send RAW:

  • Broadcast signed tx to the network regardless of tx OWNER, this tx doesn't have to be signed by currently selected account, only selected NETWORK matters

Precomposed transaction ("Total Sent" field)

  • on load draft
  • on address change
  • on amount change
  • on fee change
  • on additional option change

Review modal

  • can be cancelled at any time during signing
  • mirroring data displayed on the device
  • if there is BTC OP_RETURN data or ETH DATA present and those data are larger than 10 chars additional "expand button" will appear next to it
  • (BTC only) Expandable "Transaction detail" section
  • Regarding to BROADCAST option "Send transaction" or "Copy/download transaction" buttons are available on the last step

Send component architecture

Send component is a mix of hooks and redux.

Hooks are used to control and validate form fields using react-hook-form library

Redux is used for persistent data like drafts, fiatRates, settings etc...

@wallet-views/send

Entry point of send form component. Implements useSendForm hook by passing Redux props to it.

@wallet-hooks/useSendForm

Hook and set of sub-hooks The whole logic of send form pre/post validation, working with field (recalculation), async transaction composing and sending

@wallet-actions/sendFormActions

Called from useSendForm hook. A set of operations with @trezor/connect and post validation (see: Transaction signing)

@wallet-reducers/sendFormReducer

Storing transaction drafts and temporary data used in TransactionReviewModal

Transaction compose process

Validation of react-hook-form state occurs in React.useEffect so potential errors are available after render tick.

In order to work with properly validated state useSendFormCompose.composeRequest also needs to be handled in React.useEffect after render tick.

Every networkType has own sendFormActions.composeTransaction method

  • sendFormActionsBitcoin does calculation using @trezor/connect
  • sendFormActionsEthereum does calculation locally, custom feePerUnit is calculated if ethereumData is used
  • sendFormActionsRipple does calculation locally, additional account.reserve check on recipient address

img

  • PrecomposedLevel are calculated for all possible FeeLevel at once.
  • if FeeLevel wasn't changed by the user and current PrecomposedLevel has error then tries to switch to a lower/custom possible FeeLevel
  • if PrecomposedLevel has error set this error in react-hook-form
  • if PrecomposedLevel has set-max set calculated amount in react-hook-form

Transaction signing process

Every networkType has own sendFormActions.signTransaction method. This process is async may be interrupted by the user (ReviewTransaction cancel, disconnect device etc.)

img

Suite Desktop

Main differences between suite-web and suite-desktop builds

- @trezor/connect API

  • suite-web

    @trezor/connect is hosted at [url]/build/static/connect and injected as an iframe into DOM.

    @trezor/connect imports from @trezor/suite are replaced to @trezor/connect-web see webpack config

    iframe.postMessage/iframe.onmessage interface is used as communication channel between suite and connect API.

  • suite-desktop

    @trezor/connect is installed as regular node_module and works in nodejs context (electron main process).

    @trezor/connect files are not hosted on the electron renderer context, there is no iframe or /build/static/connect dir.

    On the renderer context all @trezor/connect methods from are replaced by @trezor/ipc-proxy methods. see index

- Firmware binaries

  • suite-web

    newest firmware binaries are hosted at [url]/build/static/connect/data/firmware and they are downloaded using regular fetch API.

  • suite-desktop

    firmware binaries are bundled as application resources in bin directory, full path depends on OS but it could be found on the as level as app.asar file, and they are downloaded using fs.readFile API. see @trezor/connect/src/utils/assets

- Trezor Bridge (trezord)

- Tor

App ID and name by environment

EnvironmentApp IDApp nameUser data dir name
production (codesign)com.trezor.suiteTrezor Suite@trezor/suite-desktop
development (sldev)com.trezor.suite.devTrezor Suite Dev@trezor/suite-desktop-dev
localcom.github.ElectronTrezor Suite Local@trezor/suite-desktop-local

Suite app name and ID are set by the environment so that Suite uses different user data dir and it's not mixed between environments. The main benefit is that you can switch back and forth between Suite dev versions without losing your remembered production wallets. One disadvantage of this solution is checking of other instance running is not so straightforward between environments.

Same concept (user data separated by environment) works on web out of the box (storage per domain name).

Debugging main process (Chrome dev tools)

Source

Open chrome and go to chrome://inspect

In "Devices" tab make sure that "Discover network targets" is enabled and "localhost:5858" is added (use Configure button)

dev mode

modify packages/suite-desktop/package.json

"dev:run": "electron ."
// to
"dev:run": "electron --inspect=5858 ."

prod mode

Run production build with --inspect=5858 runtime flag

Logging

Logging can be enabled by running Suite with the command line flag --log-level=LEVEL (replace LEVEL with error, warn, info or debug based on the logging you wish to display). Additional command line flags can be found below.

More technical information can be found on the Desktop Logger page.

Shortcuts

Available shortcuts:

namecommands
Reload appF5, Ctrl+R, Cmd+R
Hard Reload appShift+F5, Shift+Ctrl+R, Shift+Cmd+R
Restart appAlt+F5, Option+F5, Alt+Shift+R, Option+Shift+R
Open DevToolsF12, Cmd+Shift+I,Ctrl+Shift+I, Cmd+Alt+I, Ctrl+Alt+I

Runtime flags

Runtime flags can be used when running the Suite Desktop executable, enabling or disabling certain features. For example: ./Trezor-Suite-22.7.2.AppImage --open-devtools will run with this flag turned on, which will result in opening DevTools on app launch.

Available flags:

namedescription
--open-devtoolsOpen DevTools on app launch.
--pre-releaseTells the auto-updater to fetch pre-release updates.
--bridge-legacyUse Legacy (trezord-go) Bridge implementation
--bridge-devInstruct Bridge to support emulator on port 21324
--log-level=NAMESet the logging level. Available levels are [name (value)]: error (1), warn (2), info(3), debug (4). All logs with a value equal or lower to the selected log level will be displayed.
--log-writeWrite log to disk
--log-uiEnables printing of UI console messages in the console.
--log-file=FILENAMEName of the output file (defaults to trezor-suite-log-%tt.txt)
--log-path=PATHNAMEPath for the output file (defaults to /logs subfolder of Suite data directory or current working directory)
--enable-updaterEnables the auto updater (if disabled in feature flags)
--disable-updaterDisables the auto updater (if enabled in feature flags)
--updater-url=URLSet custom URL for auto-updater (default is github)

Debugging build

Linux

./Trezor-Suite-22.7.2.AppImage --log-level=debug

MacOS

./Trezor\ Suite.app/Contents/MacOS/Trezor\ Suite --log-level=debug

NixOS

appimage-run ./Trezor-Suite.AppImage --log-level=debug

Extract application

MacOS

npx asar extract ./Trezor\ Suite.app/Contents/Resources/app.asar ./decompiled

NixOS

Run application to get mount-id like:

Trezor-Suite.AppImage installed in ~/.cache/appimage-run/e4f67ae8624c4079527c669d8a3c4bbc1dd00b83b2e1d15807a5863b11bd4f38

npx asar extract ~/.cache/appimage-run/[mount-id]/resources/app.asar ./decompiled

How to create new package?

  1. Use yarn generate-package @scope/new-package-name - it will generate package boilerplate in scope/new-package-name folder.

How to use this new package?

  1. Place this package to dependency field of package.json in package where you want to use it.
  2. Run yarn refs to generate tsconfig refs.
  3. Run yarn to let yarn symlink this package.

Features

This directory contains description of various Trezor Suite features.

Coin Protocol Handler

Trezor Suite, both on web and desktop, can handle opening of coin URLs in coin:address and coin:address?amount= formats. Currently all bitcoin-like coins schemes are supported (Applies for bitcoin-like coins supported by Trezor Suite). bitcoin: is supported in both web and desktop app environment. Other coins, such as litecoin:, dogecoin: are supported only in desktop app. MDN docs

Behavior

Behavior on web

When opening Suite on web, in Firefox it will prompt the user to associate the opening of bitcoin: URLs with Suite. On Chrome, you have to click on small icon in address bar and allow bitcoin: URLs. By accepting, all bitcoin: URLs in the browser will open Suite.

Chrome Prompt Firefox Prompt

Behavior on desktop

By installing the desktop application, the bitcoin: protocol handler will be automatically registered in the system. In Firefox, the user will have the choice to use Suite Desktop to open bitcoin: URLs or browser if the protocol handler is also registered there. In Chrome, if the desktop application protocol handler is registered but web is not, Chrome will offer the desktop app. If both desktop and web app is registered, Chrome will open the web app without asking.

More apps with same handler (macOS)

If user has more desktop apps with same handler installed, the behavior is not defined. However, it looks like that the last installed application is launched.

Structure

The implementation adheres to the BIP29 specification (with the exception of the label and message parameters).

Example

bitcoin:39FpPxnR1gji9LJpNDZsB1bBeZttgTtg4L?amount=0.001 bitcoin:39FpPxnR1gji9LJpNDZsB1bBeZttgTtg4L

UI

When an app is opened using bitcoin: protocol handler, it shows a notification informing a user about stored address and an amount ready to be filled into bitcoin send form.

Bitcoin Notification Bitcoin Notification Form

Figma

Notion

GitHub #3294 GitHub #5266 GitHub #5508

Handlers manipulation

Chrome

Navigate to chrome://settings/handlers and remove handlers you want.

Firefox

Navigate to about:preferences#general and scroll to Applications. Find handler, click on chevron in Action column, hit Application details and remove apps you want.

Desktop app - macOS

Delete desktop app and use terminal open -a Xcode ~/Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plist. Find and delete bitcoin and other coin handlers. If it still wants to open a desktop app. Find all occurrences of Trezor Suite.app and delete them, even the .dmg files in suite-desktop/build-electron.

Transactions - Export

You have the possibility to export your data in multiple formats: PDF, CSV and JSON. The export menu is available by clicking the three little dots near to the search button. Export Button

The JSON format contains the most extensive amount of information. This export can be used for data vizualisation for example.

Transactions - Search

Transactions can be searched using an input field located above the transaction list. Clicking the magnifying glass will open the input and focus in it. You can use the keyboard shortcut CTRL/⌘ + F for the same result. Search Button

You can search for the following information:

  • Transaction ID (txid)
  • Input and Output addresses
  • Output labels
  • Address labels
  • Amounts
  • Dates

All searches, apart from the two last ones (Amounts and Dates), are free text searches. No specific format is required, anything (txid, addresses, labels) matching the search will be returned.

Any numbers will be treated as amounts. For a given number, partially matching amounts will be shown. Operators, prefixed to the number, can be used for different results:

  • > will return amounts greater than the value (i.e. > 0.001 for all amounts greater than 0.001).
  • < will return amounts lower than the value (i.e. < 0 for all negative amounts).
  • = will return amounts that strictly match the value (i.e. = 0.01 will return all amounts that are exactly 0.01).
  • != will return amounts that do not match the value (i.e. != 0.01 will return all amounts that are not 0.01).

Dates provided in the format yyyy-mm-dd will return transactions matching that date. Just like amount searches, prefixed operators can be used:

  • > will return all transactions after the date, including the given date itself (i.e. > 2020-12-14 will return all transactions after December 14th, 2020).
  • < will return all transactions before the date, including the given date itself (i.e. < 2020-12-14 will return all transactions before December 14th, 2020).
  • != will return all transactions that are not matching the date (i.e. != 2020-12-14 will return all transactions except the ones on December 14th, 2020).

Multiple fields can be searched at the same time using AND (&) and OR (|) operators. A few examples using these operators might be pretty self-explanatory:

  • > 2020-12-01 & < 2020-12-31 will return all transactions from December 2020.
  • > 2020-12-01 & < 2020-12-31 & > 0 will return all incoming (positive) transactions from December 2020.
  • > 0.01 | > 2020-12-01 will return all transactions with an amount higher than 0.01 OR that have happened since December 1st, 2020.
  • > 2019-12-01 & < 2019-12-31 | > 2020-12-01 & < 2020-12-31 will return all transactions from December 2020 and 2019.

As you may see in the last example, and similarly to SQL, the AND (&) operator has precedence over the OR (|) operator.

Metadata (labeling)

Metadata is a feature that allows the user to associate persistent data with their wallets, accounts, receive addresses, and outputs. Trezor Suite refers to metadata as to "labeling" in the user interface.

For non-technical introduction, see Trezor Learn.

Data stores

Because Trezor Suite is not a typical application with a backend server, data must be stored elsewhere. Currently supported providers are:

  • Dropbox
  • Google Drive
  • Local file system (desktop only)

Google Drive specifics

Google Drive authentication has differing implementations for desktop and web version of Suite. For security reasons, Google does not allow the authorization code flow for web apps, thus only allowing the user of web Suite to log in via the implicit flow with an access token lasting for lasts one hour. Authorization code flow used in the desktop app leverages a refresh token to enable the user to stay logged in permanently while using desktop Suite. To implement this flow, we had to establish an authorization backend holding a client_secret, see @trezor/auth-server. If the authorization via our backend fails for some reason, there is a fallback to the implicit flow in the desktop app. TODO: switch from the fallback flow to the authorization code flow as soon as possible so that the user does not have to re-authenticate every hour.

Data structure in store

version 1.0.0 (current)

device metadata example

{
  "version": "1.0.0",
  "walletLabel": "my hidden wallet label",
},

account metadata example

{
  "version": "1.0.0",
  "accountLabel": "my cool account label",
  "outputLabels": {
    "9f472739fa7034dfb9736fa4d98915f2e8ddf70a86ee5e0a9ac0634f8c1d0007": {
      "0": "transaction 1"
    },
  },
  "addressLabels": {
    "bc1qannfxke2tfd4l7vhepehpvt05y83v3qsf6nfkk": "my cool address label",
  }
}

version 2.0.0 (future)

Each record will have timestamp which will allow user to resolve potential conflicts. (Implementation not currently planned.)

Data encryption

Data is stored in encrypted form using aes-256-gcm cipher. Master key for encryption is generated by users Trezor with constants defined in suite/src/actions/suite/constants/metadataConstants.ts Files encryption-decryption logic is located in suite/src/utils/suite/metadata.ts

Where data lives in the App

Settings related data is defined in suite/src/reducers/suite/metadataReducer which contains

{
 enabled: bool,
 initiating: bool,
 provider: {
  isCloud: bool,
   type: "dropbox" | "google" | "fileSystem",
   tokens: {
    accessToken?: string,
    refreshToken?: string
   },
   user: string
 }
}

Metadata itself is divided into 2 groups (device metadata and account metadata). They are stored together with concrete records.

Device has metadata property:

{
  /*
   - disabled is initial state. user has not interacted with metadata before, metadata keys are not available
   - enabled means that metadata is enabled for this device and application has metadata keys. App will try to download and decipher metadata
   - cancelled means that user rejected cipherKeyValue call on device.
  */
  status: "disabled" |"enabled" | "cancelled",
  key: string,
  fileName: string,
  aesKey: string,
  walletLabel: string
}

Account has metadata property which is an object of following shape:

{
  key: string(xpub),
  fileName: string,
  aesKey: string,
  outputLabels: {
    [string(txid)]: {
      [number(outputIndex)]: string
    },
  },
  addressLabels: {
    [string(address)]: string,
  },
  accountLabel: string
}

Where metadata is set and displayed

  • Wallet label is set in the modal where wallet is selected.
  • Account label is set in account header.
  • Receiving address label is set in the receive address list.
  • Output label is set in send form address field, coin control or transaction history

Note that transaction history displays output label and address next to each other. If the output does not have a label, only the address is shown. If it does, the address is shown when the label is hovered. The address is displayed as follows:

  • If a receive address has a label, its label is displayed.
  • If a receive address has a label and it belongs to the same account, it is replaced by "Sent to myself".
  • If an outgoing address belongs to another discovered wallet or account, it is replaced by the account label (and wallet label, if it is a different wallet).
  • If none of the above is true, plain address is displayed.

Wallet and account labels can also be displayed in other places in Suite as read-only, e.g. in send form when sending to an address belonging to another discovered wallet or account.

Device name set in device settings is not part of metadata.

User stories

First time user:

  1. User opens App for the first time. Metadata is disabled. "Add label" buttons are present on mouse hover over labelable data.
  2. User clicks "Add label" button.
  3. Device metadata key is generated.
  4. Using device metadata key, account metadata keys are created.
  5. Open modal with metadata providers and connect.
  6. Fetch data from metadata provider and set interval for fetching data.
  7. Activate editable input.

Metadata enabled during discovery process:

Controlled by discoveryActions and metadataMiddleware

  1. If passphrase is not used, device metadata key is generated before discovery process starts. (discoveryActions)
  2. If passphrase is used, metadata key is generated either:
    • in the middle of the discovery process after successfully receiving the first bundle of accounts and at least one account is not empty. (discoveryActions)
    • after passphrase confirmation process. (metadataMiddleware)

How to turn metadata off

  • controls for common metadata related actions are located in general settings under the labeling section
  • there is a switch which:
    1. sets metadata.enabled bool value
    2. if setting to false, it triggers removal of all metadata (including keys) from devices and accounts.
    3. if setting to false, disconnects metadata provider (Dropbox, Google Drive)
  • there is a button "disconnect provider" which:
    1. triggers removal of all metadata values (excluding metadata keys) from devices and accounts. This way provider might be reconnected without reconnecting device
    2. disconnects metadata provider

Fiat rates

Suite provides several types of fiat rates:

  • Current fiat rate: Used on Dashboard, next to the account balance, for converting a crypto amount in send form to a fiat currency, etc...
  • Weekly rates: In addition to current rate we also fetch 7 days old rate and based on the difference we either show green or red arrow next to an exchange rate (can be seen in assets table on Dashboard).
  • Historical fiat rate: Exchange rate at the time of facilitating a transaction. Used in list of transactions to calculate daily deltas and in transaction detail modal. ERC-20 tokens are not yet implemented.

Providers

  • Blockbook: For all main networks (BTC, LTC, ETH + ERC-20 tokens, ...) except XRP, SOL + SOL tokens and ADA + ADA tokens
  • CoinGecko: Used for XRP, SOL + SOL tokens and ADA + ADA tokens and as a fallback for failed requests to blockbook.

First fetch

Current fiat rates

For main networks: On app launch for enabled networks and then immediately after enabling new coin/network

Current fiat rates for ERC-20 tokens

ERC-20 tokens: On ACCOUNT.CREATE which is triggered during account discovery (if account were not remembered), on ACCOUNT.UPDATE when account.tokens has some new items.

Weekly fiat rates

Weekly rates are downloaded on app launch and on enabling new network.

Historical fiat rates (for transactions)

Historical rates for transactions: On addTransaction action, which means after a new transaction is added, stored within the tx object,

Update intervals

Current fiat rates

Every rate stored in wallet.fiat reducer is checked in 3-minute interval. If the rate is older then 10 minutes then it is refetched.

Current fiat rates for ERC-20 tokens

List of tokens is part of the account object (account.tokens). Fiat rates for ERC-20 tokens are fetched on ACCOUNT.CREATE (fired during account discovery) and ACCOUNT.UPDATE (new token can appear after receiving a token transaction). These actions are intercepted in fiatRatesMiddleware. We don't fetch rates for tokens without definitions.

Weekly fiat rates

Check for deciding if a weekly rate for a coin needs to be updated runs every hour. Fetched rates are cached also for 1 hour. Eg. If user opens app and there are already weekly fiat rates, no older than 1 hour, stored in redux state then Suite won't fire new fetch.

Historical fiat rates (for transactions)

They are stored as part of the transaction. They don't need to be periodically updated as the exchange rate in the past cannot change anymore. If fetch fails for some reason we will retry on next BLOCKCHAIN.CONNECTED.

Usage

To make your life easier use FiatValue component.

Most straightforward usage is to just pass amount and symbol, if you need to work with tokens also add tokenAddress property:

<FiatValue amount={amount} symbol={assetSymbol} tokenAddress={tokenTransfer?.address} />

For converting to fiat amount using rates from history use useHistoricRate in combination with historicRate property:

<FiatValue
    amount={targetAmount}
    symbol={transaction.symbol}
    historicRate={historicRate}
    useHistoricRate
/>

To support more complex use-cases we are leveraging render props. When passing function as a children it will get called with one parameter, object with value, rate and timestamp. This allows us to handle cases where fiat rates are missing (all fields in the object are set null) or show not only fiat amount, but also used exchange rate.

<FiatValue amount="1" symbol={symbol}>
    {({ _value, rate, timestamp }) =>
        rate && timestamp ? (
            // we got rates!
            // show the exchange rate and provide information about last update in tooltip
            <Tooltip
                content={
                    <LastUpdate>
                        <Translation
                            id="TR_LAST_UPDATE"
                            values={{
                                value: (
                                    <FormattedRelativeTime
                                        value={rateAge(timestamp) * 60}
                                        numeric="auto"
                                        updateIntervalInSeconds={10}
                                    />
                                ),
                            }}
                        />
                    </LastUpdate>
                }
            >
                <FiatRateWrapper>{rate}</FiatRateWrapper>
            </Tooltip>
        ) : (
            // no rates available!
            <NoRatesTooltip />
        )
    }
</FiatValue>

There are other handy props like showApproximationIndicator (self explanatory) and disableHiddenPlaceholder which disables blurred overlay used when discreet-mode is activated.

Suite Guide

Guide is a feature that allows us to write content on various topics like basics of deterministic wallets, cryptocurrencies, Suite specifics or generally anything a Suite user might be interested in and then provide it to the user directly in the app.

Content

The content is maintained in a GitBook project. (You'll need an account with appropriate privileges to access it.) GitBook mirrors the content to a GitHub repository from where it's fetched when Suite builds. The fetch happens in the suite-data package on build:lib yarn task. Once fetched the content is indexed and transformed to a format usable in the Suite app. This format is then copied into the static directories of web, desktop and native builds.

Indexing, transforming and copying of the content is handled by scripts in packages/suite-data/src/guide. These are configured to fetch a particular commit of the GitBook mirror by the GITBOOK_REVISION constant in packages/suite-data/src/guide/constants.ts. This constant must/can be changed accordingly to propagate changes from GitBook to suite builds. (One could set it to the master branch to always use the latest content version, however that is discouraged as we rather want to precisely control which version of the content will get included in each release.)

How to update

Change GITBOOK_REVISION constant in packages/suite-data/src/guide/constants.ts to the new revision. Example: 40855097. That's it.

Localization

Suite uses react-intl package for all in-app localization needs. Definitions of all messages are stored in messages.ts.

To allow non-developers to edit these messages through user-friendly interface, we upload them to Crowdin via their CLI.

After strings have been translated we use Crowdin CLI again to download the translated json files to suite-data package. To finish the process these files need to be committed to the repository.

Message definitions

messages.ts is the place where you add new messages to be used in Suite. It's basically just a huge object where a key is an ID of the message and a value is the message definition.

Do not manually edit language json files in suite-data/files/translations/ directory. These are auto-generated, changing them directly is plausible only for development purposes.

Structure

  • id: We don't have strict conventions for generating these IDs, although using a prefix TR_, or expanded variant TR_<SCOPE>, where scope is, for example, "ONBOARDING" is really handy. ID must be the same as the object's key.
  • defaultMessage: Used as a source string for translator. It's also a text that is shown in the app as a fallback till someone changes/improves it in Crowdin.
  • description: Optional. Useful for describing the context in which the message occurs, especially if it is not clear from a defaultMessage field.
  • dynamic: Optional. Must be set to true for programmatically constructed keys. Otherwise, the keys will be deleted by the list-duplicates script.

Example:

{
  ...
  TR_ADDRESS: {
      id: 'TR_ADDRESS',
      defaultMessage: 'Address',
      description: 'Used as label for receive/send address input',
  },
  ...
}

Usage in Suite

To render a message use our wrapper for react-intl's FormattedMessage, Translation. It will always return JSX.Element. If, for some reason, you need to render the message as a string (for example for passing it as a placeholder prop to an input) use useTranslation hook.

Translation accepts the same parameters as FormattedMessage and adds a little bit of magic.

Most straightforward usage is to just pass message's ID to id prop:

<Translation id="TR_CONTINUE" />

or for string variant using hook:

const { translationString } = useTranslation();
translationString('TR_CONTINUE');

There are cases where you need to pass a variable which needs to be part of the translated message:

<Translation id="TR_ENTERED_PIN_NOT_CORRECT" values={{ deviceLabel: device.label }} />

or for string variant using hook:

const { translationString } = useTranslation();
translationString('TR_ENTERED_PIN_NOT_CORRECT', { deviceLabel: device.label });

Definition for TR_ENTERED_PIN_NOT_CORRECT:

TR_ENTERED_PIN_NOT_CORRECT: {
    defaultMessage: 'Entered PIN for "{deviceLabel}" is not correct',
    id: 'TR_ENTERED_PIN_NOT_CORRECT',
}

Sometimes you need to provide a translator the ability to emphasize some words in a sentence AKA rich text formatting. In this example a text enclosed in <strong> will be wrapped in StrongStyling component.

<Translation
    id="TR_TRANSACTIONS_SEARCH_TIP_2"
    values={{
        strong: chunks => <StrongStyling>{chunks}</StrongStyling>, // search string is wrapped in strong tag for additional styling
    }}
/>

Definition for TR_TRANSACTIONS_SEARCH_TIP_2:

TR_TRANSACTIONS_SEARCH_TIP_2: {
  id: 'TR_TRANSACTIONS_SEARCH_TIP_2',
  defaultMessage:
      'Tip: You can use the greater than (>) and lesser than (<) symbols on amount searches. For example <strong>> 1</strong> will show all transactions that have an amount of 1 or higher.',
},

For even more shenanigans (like handling plural form) check this great overview on ICU Message syntax.

Translation mode

Section shamelessly stolen from Crowdin contributions.

There's a hidden feature in Suite, intended for translators, called Translation mode that redirects you into Crowdin upon clicking any particular string. This is immensely handy in comparison to blindly translating strings within Crowdin as it allows you to understand the context of a certain string before being taken to Crowdin to translate it.

  1. Go to Settings in Suite
  2. Rapidly click on the "Settings" heading 5 times
  3. Click the three dot context menu on the right
  4. "Debug Settings" should've appeared. Click it. If "Debug Settings" hasn't appeared, repeat step 2.
  5. Enable "Translation mode"

After enabling it each string, which is rendered via Translation component, is now underlined with red and shows a popup with the message's ID when you hover the mouse over it.

To join the ranks of translators follow Crowdin contributions guide.

Synchronization with Crowdin

With the automated CI job from GitHub.

Navigate to the Crowdin translations update action and trigger manual job with a base branch develop Before triggering the job, make sure there is no pull request already opened with the title Crowdin translations update

Action will create a pull request with the title Crowdin translations update, review it and merge.

Locally

All work could be done with shortcuts defined in package.json scripts section. In order to interact with Crowdin you need to ask the project owner for access token and either store it in your $HOME/.crowdin.yml file:

'api_token': xxxx

or, alternatively, add it as an option for each called script:

yarn workspace @trezor/suite translations:download --token xxxx

Extract

To extract message definitions from Suite into master.json file run:

yarn workspace @trezor/suite translations:extract

The newly created master.json file is generated from messages.ts and serves only as a base for translations in Crowdin, therefore it is not committed into Git repository.

Upload

To upload extracted master.json file with updated message definitions from Suite to Crowdin run:

yarn workspace @trezor/suite translations:upload

You can even do that from your branch with messages that are not yet merged in develop branch, just be sure you have rebased your branch on latest develop before doing so. This process replaces all definitions in Crowdin, meaning if your branch is missing some definitions, that are already in develop branch and uploaded in Crowdin, they will be removed.

Download

To download new translations from Crowdin run:

yarn workspace @trezor/suite translations:download

and then open a PR with updated language files.

Workflow for regular Crowdin Synchronization

BRANCH_NAME=feat/crowdin-sync

git checkout develop
git pull
git checkout -b $BRANCH_NAME

# Extract message definitions from Suite
yarn workspace @trezor/suite translations:extract
# Upload to sync the key set.
yarn workspace @trezor/suite translations:upload
# Download to fetch values for all keys.
yarn workspace @trezor/suite translations:download

git add packages/suite-data/files/translations
git commit -m 'feat(translations): Sync with Crowdin'
git push origin $BRANCH_NAME

Plus creating, reviewing and merging the PR.

Message System

Message system was implemented to allow sending emergency messages to Trezor Suite app to a user with specific stack.

Example messages

Issue on Github

Notion for production deployment

Types of in-app messages

There are multiple ways of displaying message to a user:

  • banner
    • looks like a cookie bar above the page
  • modal
    • TODO: missing implementation
  • context
    • messages on specific places in app (e.g. settings page, banner in account page)
  • feature
    • disabling some feature with an explanation message

Implementation

Config

The system of messages is based on a configuration file in which messages with specific conditions ​are described. If specific conditions are satisfied, the message is shown to a user.

Current configuration file is located in suite-common/message-system/config folder. Its name is config.vX.json. The X express current version messaging system.

The config is fetched at launch of the application and then every minute. It remembers the previously fetched config to inform the user even if he is offline. For this reason, the latest available config during build time is bundled with the application.

If fetching of a new config fails, the fetching process is repeated every 30 seconds.

Schema

The configuration structure is specified in JSON file using JSON schema. The file can be found in suite-common/message-system/schema folder. Its name is config.schema.vX.json.

We use JSON schema for 2 reasons:

  • generating TypeScript types
  • validating configuration file

Types

Types are generated from JSON-schema during the build:libs process or can be generated manually by yarn workspace @suite-common/message-system msg-system-types. A messageSystem.ts file is created in suite-common/suite-types/src folder.

  • This file should never be changed manually.
  • This file is committed into the repository.

Signing

To ensure the authenticity of a configuration file, JSON Web Signatures are used. The configuration file is signed by a private key using elliptic curves (ES256) and the data specified in the config are used. The authenticity is verified on client side using corresponding public key.

CI job

Validation

  • Validation of configuration file is performed in CI job in validation phase. It is used to detect possible structure and semantic errors.
  • It can be run locally by yarn workspace @suite-common/message-system validate-config script.

Signing

  • Signing of the configuration file is performed:
    • in CI job in prebuild phase for distribution and
    • manually by yarn message-system-sign-config (or yarn build:libs) script for local development.
  • The results are saved into suite-common/message-system/files as two files:
    • config.v1.jws to be uploaded to https://data.trezor.io/config/$environment/config.vX.jws
    • config.v1.ts to be bundled with application
  • Development private key is baked into project structure together with public keys for both development and production.
  • Production private key is available only on codesign branch in CI (both Gitlab and Github).
  • Development private key can be found in suite-common/message-system/scripts/sign-config.ts file, the public keys can be found in packages/suite-build/utils/jws.ts file.

Versioning of implementation

If changes made to the message system are incompatible with the previous version, the version number should be bumped in messageSystemConstants.ts file and CI jobs has to be adapted. There are also few places where the version is defined statically so search for config.v1 over the whole project.

Config Structure

Structure of config, types and optionality of specific keys can be found in the schema or in generated types. Example config is commented below.

{
    // Version of message system implementation. Bump if new version is not backward compatible.
    "version": 1,
    // Datetime in ISO8601 when was config created.
    "timestamp": "2021-03-03T03:48:16+00:00",
    // Version of config. New config is accepted only if sequence number is higher.
    "sequence": 1,
    "actions": [
        {
            /*
            - User's stack has to match one of the condition objects to show this message.
            - The bitwise operation is OR.
            */
            "conditions": [
                /*
                - All keys are optional (duration, os, environment, browser, settings, transport,
                  devices, architecture (To be implemented))
                - If a value is specified then all its subkeys have to be specified
                - The bitwise operation is AND.
                */
                {
                    /*
                    - Datetime in ISO8601 from / to which date this message is valid.
                    - If duration category is used, then both times have to be set.
                    */
                    "duration": {
                        "from": "2021-03-01T12:10:00.000Z",
                        "to": "2022-01-31T12:10:00.000Z"
                    },
                    /*
                    For os, environment, browser and transport.
                    - All values (except for revision in environment) are version definitions
                    - Semver npm library is used for working with versions https://www.npmjs.com/package/semver.
                    - "*" = all versions; "!" = not for this type of browser/os/...
                    - Options: gte, lt, ranges, tildes, carets,... are supported, see semver lib for more info.
                    */
                    "os": {
                        "macos": [
                            "10.14",
                            "10.18",
                            "11"
                        ],
                        "linux": "*",
                        "windows": "!",
                        "android": "*",
                        "ios": "13",
                        "chromeos": "*"
                    },
                    // revision is optional
                    "environment": {
                        "desktop": "<21.5",
                        "mobile": "!",
                        "web": "<22",
                        "revision": "7281ac61483e38d974625c2505bfe5efd519aacb"
                    },
                    "browser": {
                        "firefox": [
                            "82",
                            "83"
                        ],
                        "chrome": "*",
                        "chromium": "!"
                    },
                    "transport": {
                        "bridge": [
                            "2.0.30",
                            "2.0.27"
                        ],
                        "webusbplugin": "*"
                    },
                    /*
                    - If key is not available (undefined), then it can be whatever.
                    - Currently supported keys are "tor" and coin symbols from "enabledNetworks".
                    - The bitwise operation is OR.
                    */
                    "settings": [
                        {
                            "tor": true,
                            "btc": true
                        },
                        {
                            "tor": false,
                            "ltc": true
                        }
                    ],
                    // Empty device array is targeting users without a connected device.
                    "devices": [
                        {
                            // Possible values: "1" +  "T1B1", "T" + "T2T1", "T2B1", "T3B1", "T3T1"
                            // in case of targeting "T1B1" or "T2T1", for backwards compatibility use old (1, T) and new naming (T1B1, T2T1 together in a new object
                            // in case of targeting "T2B1" in Suites before device release, please use all three "T2B1", "Safe 3" and empty string ""
                            "model": "T1B1",
                            /*
                            Beware
                            - firmware version in bootloader mode is unavailable on T1B1
                            - bootloader version is available only in bootloader mode
                            */
                            "firmware": "2.4.1",
                            "bootloader": "2.0.4",
                            // Possible values: "*", "bitcoin-only", and "regular"
                            "variant": "bitcoin-only",
                            "firmwareRevision": "*",
                            "vendor": "trezor.io"
                        }
                    ]
                }
            ],
            "message": {
                // Used for remembering dismissed messages.
                "id": "0f3ec64d-c3e4-4787-8106-162f3ac14c34",
                /*
                - Existing banners have defined priorities.
                - The range is 0 to 100.
                */
                "priority": 100,
                // When a user closes the message, it will never show again until the user clear app storage.
                "dismissible": true,
                /*
                Variants:
                - info (blue)
                - warning (orange)
                - critical (red)
                */
                "variant": "warning",
                // Options: banner, modal, context, feature
                "category": "banner",
                /*
                - Message in language of Suite app is shown to a user.
                - Currently 'en', 'es', 'cs', 'ru', 'ja' are supported.
                - 'en-GB' is used for backward compatibility and should match value of 'en'.
                */
                "content": {
                    "en-GB": "New Trezor firmware is available!",
                    "en": "New Trezor firmware is available!",
                    "de": "Neue Trezor Firmware ist verfügbar!"
                },
                // optional headline following the language structure of content
                "headline": {
                    "en-GB": "Update your Trezor",
                    "en": "Update your Trezor",
                    "de": "Neue"
                },
                // Call to action. Used only for banner and context.
                "cta": {
                    /*
                    Options: "internal-link" or "external-link"
                    - internal-link is route name, see routes.ts file
                        - anchor property can be used, see anchors.ts file
                    - external-link is url address
                    */
                    "action": "internal-link",
                    // Route name or url address according to action.
                    "link": "settings-device",
                    "anchor": "@device-settings/firmware-version",
                    /*
                    - Label of call to action button shown to a user.
                    */
                    "label": {
                        "en-GB": "Update now",
                        "en": "Update now",
                        "de": "Jetzt aktualisieren"
                    }
                },
                // Used only for modals. (To be implemented)
                "modal": {
                    "title": {
                        "en-GB": "Update now",
                        "en": "Update now",
                        "de": "Jetzt aktualisieren"
                    },
                    "image": "https://example.com/example.png"
                },
                // Used only for context.
                "context": {
                    "domain": [
                        "coins.receive",
                        "coins.btc"
                  ]
                }
                 // Used only for feature
                "feature": [
                    {
                        "domain": "coinjoin",
                        "flag": false
                    }
                ]
            }
        }
    ]
}

How to update

When updating message system config, sequence number must always be higher than the previous one. Once released config cannot be rolled back to the previous one with lower sequence number. A new one with higher sequence number has to be created.

Updated config is automatically uploaded by CI job to the corresponding S3 bucket based on the current branch.

Priorities of messages

Based on the priority of the message, the message is displayed to the user. 0 is the lowest priority, 100 is the highest priority. Current priorities of existing banners can be found here.

Targeting Linux version

Unfortunately, it is not possible to target specific distributions and versions of Linux. It is possible to only target all Linux users using * or exclude all Linux users using !.

Application steps

  1. Config is fetched on load of application and is stored in Redux state. To be persisted between sessions, is is mirrored into IndexDB.
  2. Conditions of config are evaluated on specific Redux actions. See messageSystemMiddleware.ts file.
  3. If conditions of a message satisfy the user's stack, the message is accordingly propagated. If it is dismissible, its ID is saved to Redux state (IndexDB) on close, to avoid displaying it next time.

Followup

Ideas and non-critical bugs can be added to the followup issue.

Feature Flags

Feature flags allow us to enable and disable certain features at build time, helping us to produce specific builds for specific environments.

Workflow

All feature flags are located in suite-common/suite-config/src/features.ts. To add a new flag, start by doing the following:

  1. Add your flag to the FLAGS constant and set its default value. When naming your flag, bear in mind the following conventions:
    1. Always explain what the flag is about using a comment next to it.
    2. The name of the flag should always be in capitals.
    3. The name of the flag should never contain the world enable or disable because the name should always towards an enabled state. Its value should reflect whether the feature is enabled or not.
    4. The name of the flag should never contain the word flag because it's inferred.
  2. (optional) You can override the flag for each environment (web, desktop) using their specific constants.
  3. Use the isFeatureFlagEnabled function from @suite-utils/features to check if the flag is enabled or not.
  4. Wrap the code you wish to control in a condition checking for your flag being enabled or not.

Example

import { isFeatureFlagEnabled } from '@suite-utils/features';

const main = () => {
    alwaysRunning();

    if (isFeatureFlagEnabled('LABELING')) {
        myLabelingFeature();
    }
};

Future evolutions

  • Control feature flags at runtime.

Desktop Logger

The desktop application includes a logging library to display various types of log in the console or in a file.

Four (or five if we count 'mute') log levels are currently implemented:

  • error (1)
  • warn (2)
  • info (3)
  • debug (4)

All messages with an inferior level to the selected one will be displayed. For example, if the selected log level is info, then it will also display warn and error messages.

How to enable logging

Logging can be enabled by running Suite with the command line flag --log-level=LEVEL (replace LEVEL with error, warn, info or debug based on the logging you wish to display). Additional command line flags can be found on the Suite-Desktop page.

If you activate Debug menu, logging to file is automatically started. When you deactivate Debug menu, logging stopped if the app is not running with the command line flag. If you run the app with --log-write flag and then activate the Debug menu, logging just continue with the same file.

API

Exported Types

LogLevel

Any of the following values:

  • mute (0)
  • error (1)
  • warn (2)
  • info (3)
  • debug (4)

Options (all optional)

nametypedefault valuedescription
colorsbooleantrueConsole output has colors
writeToConsolebooleantrueOutput is displayed in the console
writeToDiskbooleanfalseOutput is written to a file
outputFilestring'trezor-suite-log-%tt.txt'file name for the output
outputPathstring/logs subfolder in Suite data directory or CWDpath for the output
logFormatstring'%dt - %lvl(%top): %msg'Output format of the log

String formatters

The options outputFile and logFormat can be used with some expressions, prefixed with the percent (%) symbol, to apply certain dynamic elements. It is for example possible to display a timestamp or the current date & time in the listed options above. While some expressions can be used in any strings, some strings have their own expressions.

Global

ExpressionExample outputDescription
%tt2021-01-19T11-07-40Date and time in filename friendly format
%ts1611054460306Timestamp
%dt2021-01-19T11:08:22.244ZDate and time in ISO format (ISO 8601)

logFormat

ExpressionExample outputDescription
%lvlINFOLevel in letters and upper case
%topExampleTopic
%msgExample messageMessage

Constructor

The constructor has the following parameters:

  • level (LogLevel): Selected log level (see LogLevels in Exported Types above)
  • options (Options): Optional parameter containing settings for the logger (see Options in Exported Types above)

Log methods

All log methods have the same signature as they are just wrappers around a private logging method.

The following methods are available:

  • error(topic: string, messages: string | string[]); // level: 1
  • warn(topic: string, messages: string | string[]); // level: 2
  • info(topic: string, messages: string | string[]); // level: 3
  • debug(topic: string, messages: string | string[]); // level: 4

Parameters:

  • topic (string): Message topic
  • messages (string | string[]): Single message or array of messages which will be displayed one by line.

Exit method

The exit() method is used to close the write stream of the log file. If you are not planning to write logs to disk, you won't need to use this method. Otherwise it is highly advised to place this inside exit/crash callbacks.

Example

const logger = new Logger('warn', {
    colors: false, // Turning off colors
    logFormat: '%lvl: %msg', // Level and message only
    writeToDisk: true, // Write to disk
    outputFile: 'log-desktop.txt', // Static file name, will be overwritten if it exists
});

// These messages will be printed with the level provided above
logger.error('example', 'This is an example error message with the topic "example"');
logger.warn('example', 'This is an example warn message with the topic "example"');

// And these won't
logger.info('example', 'This is an example info message with the topic "example"');
logger.debug('example', 'This is an example debug message with the topic "example"');

// Closes the write stream (only needed of writing the log file)
logger.exit();

Q&A

How to format a string?

The library does not have any formatting capabilities as JavaScript already has templating features built-in. Simply use string literals (for example: logger.info('Topic', `My string ${myvar}.`)).

How to output an object?

The library does not include any helper for this as there is already a language feature that does this. Simply use JSON.stringify(myObject).

How can I write the log in JSON format?

You can change the logOutput option to be formatted like JSON. For example: logOutput: '{ "ts": "%ts", "lvl": "%lvl", "topic": "%top", "message": "%msg" },'. In order to display/use it properly, you will have to edit the output a little bit. Wrap all the messages in square brackets ([, ]) and remove the comma (,) from the last message.

Onboarding

Incomplete developers guide to Onboarding in Trezor Suite

There are few different ways to trigger the onboarding process:

  • Initial run
    • Initial run is what we call a state when Suite is launched for the first time. It may also be triggered by clearing the app storage (flag is stored inside a reducer suite.flags.initialRun and saved to persistent storage). This is stored per device. If Suite detects initial run it'll automatically launch the onboarding.
  • Connecting an uninitialized device (without a seed)
    • Suite will automatically launch the onboarding
  • Wiping a device from Suite UI and proceeding with device setup

Prerequisites

Button Requests

Each device object inside has its buttonRequests array which gets populated through buttonRequestMiddleware. It it basically a way for a device to request cooperation from the app.

It is used, for example, by firmware update flow for setting correct firmware.status after device requested confirmation on the device:

case SUITE.ADD_BUTTON_REQUEST:
      if (action.payload.code === 'ButtonRequest_FirmwareUpdate') {
          draft.status = 'waiting-for-confirmation';
      }

Another usage is in "Setup Pin" step, where the device, through button request, will let us know if user should enter pin for the first time or 2nd time (for confirmation). It is also leveraged outside of Onboarding every time user needs to enter a PIN. Thanks to these buttons we can reuse the same component that will adjust just based on these button requests.

In onboarding, we clear this array after each step (handled in buttonRequestMiddleware).

Steps

Welcome

Before a user connects a device

As a first step we prompt the user to connect his device in normal mode. In this step we handle various invalid device modes and problems with transport layer used to facilitate communication with the device (webUSB, Trezor Bridge).

List of valid states

  • Waiting for a device
    • We provide some troubleshooting tips and link to download Trezor Bridge as it may happen that user launched Suite for the first time without installing Trezor Bridge (only in Web environment) waiting for device
  • Device connected in normal mode

List of invalid states

Invalid device states:

  • Device connected, but in bootloader mode
  • Device connected, but unreadable
  • Seedless device setup is not supported in Suite (not to be confused with regular device without seed).

Invalid transport states:

  • No transport available (Trezor Bridge is not running)
    • We need to provide link to download Trezor Bridge (only in Web environment)

Data analytics (only in initial run)

During the initial run we ask users to give their consent to collect and process anonymous data in order to help us improve the user experience.

data analytics

Device security (genuinity) check

This sub step is adjusted based on a state of the device. We distinguish between device that was already used before (it has a firmware installed) and most likely unused device (no seed, no firmware).

security check

Device without seed, no firmware installed

This should be the most common case in onboarding.

Scenario:

  • New device bought from trusted seller
  • User wiped the device (and firmware)

Security questions: Check hologram, verify seller, check package...

Primary action: Setup device (starts onboarding)

Secondary action: Contact support

Device without seed, firmware already installed

Scenario:

  • User has wiped the device (without erasing a firmware)
  • Device has been tampered with

Security questions: Have the user used the device before?

Primary action: Setup device (starts onboarding)

Secondary action: Contact support

Device with seed (thus also a firmware)

Scenario:

  • User cleared app storage so the onboarding was started on initial run.
  • Device has been tampered with

Security questions: Have the user used the device before?

Primary action: Setup device (starts onboarding)

Secondary action: Contact support

Firmware

To provide good user and dev experience firmware flow has its own reducer. It is shared between firmware flow used in onboarding and standalone modal for firmware update. Both flows,fw installation via onboarding and firmware update via standalone modal, reuse same components.

Active sub step of firmware step is stored in status field. Explanation of each field and sub steps can be found in firmwareReducer.ts.

Note about normal and bootloader mode

Device can be connected in “normal” mode or in “bootloader" mode which you access by pressing left button (both buttons on old T1B1 fw) or swiping on touchscreen during connecting usb cable to the device. Before starting the installation process, user needs to disconnect the device and reconnect it in bootloader mode.

From the technical perspective, these two modes are seen as 2 different devices and there is no way we can tell that the device, which was reconnected in bootloader mode is indeed the same device which was before connected in normal mode. This basically means that if you are updating a firmware with a device connected via webUSB you will need to do pairing process twice. When device is in bootloader mode device.features.major_version, device.features.minor_version, device.features.patch_version is version of a bootloader, not a firmware.

Another interesting fact is that a device without installed firmware acts as it is in bootloader mode regardless of buttons you are or you are not pressing (device has always some bootloader installed).

Bootloader mode has another catch, not all device's features are accessible while a device is in this state. Such inaccessible fields are set to null. For example, device.features.pin_protection is set to null, but that doesn't mean that device has this feature disabled. After reconnecting the same device in normal mode pin_protection might be set true (or false).

This is the reason why, in Welcome step, we force the user to connect a device in normal mode first. Otherwise we wouldn't know what firmware version is installed or what firmware update we can offer.

Firmware installation

Device could be in various states when the user enters this step.

  • Firmware not installed
    • Fresh device (device.firmware set to none)
    • We don't need to prompt the user to switch to bootloader mode in this case.
  • Firmware already installed and:
    • Update available: It can be optional (skip button is present) or mandatory. Be aware that a device, while in bootloader mode, doesn’t report its fw version, only version of bootloader.
    • Latest firmware already installed (just wiped seed)

Possible error states:

  • Generic firmware installation fail
    • User cancelled installation on a device
    • Device disconnected during the process
    • Some unexpected error
  • Device is connected in bootloader mode from the start
    • We will prompt the user to connect device in normal mode
    • This won't happen in Onboarding so much, but it is handled as this firmware flow is used in standalone firmware update modal which can be triggered from Suite
  • Device disconnected before firmware installation starts
    • Prompt the user to reconnect the device
  • Device disconnected after firmware installation starts
    • Installation will fail with generic error
    • This could happen when a cable is not connected properly and the device will disconnect during the process

Device with older firmware installed

firmware update

If device is connected in normal mode and user proceeds by clicking “install firmware” we will ask the user to disconnect the device and reconnect it in bootloader mode (via a small modal popup). reconnect in bootloader

After the device is reconnected we show a button to trigger an update process. Then the device will request a confirmation from the user. Only after the user confirms it the installation begins.

firmware confirm

Device with no firmware installed

User proceeds by clicking "Install firmware" CTA. Since the device without firmware is always in bootloader mode we don't need any cooperation from the user. Device doesn't ask for confirming the installation of the fw. User of T1B1 is prompted to disconnect the device and reconnect it in normal mode. Newer device models automatically restart. Then we continue to the next step, (Generating seed)

reconnect in normal T1B1 firmware completed

State of currently shipped devices

Intermediary firmware

T1B1 devices with old bootloader cannot be upgraded to latest firmware directly. First we'll install so called intermediary firmware, which will bump bootloader to newer version. After installation is completed, the user will be asked to reconnect the device. Because intermediary firmware only bumps bootloader and doesn't install any firmware, device will be in bootloader mode regardless of how the user reconnects it (whether they press a button or not). Then it triggers an installation of subsequent firmware, which will be the latest firmware available. It will follow basically the same flow as with the first installation.

WebUSB

Support for the WebUSB came pretty late for T1B1 (bootloader 1.6.0 bundled with FW 1.7.1). Currently shipped devices won't support WebUSB out of-the-box and user won't be able to pair such device. In this case user needs to install Trezor Bridge. After finishing fw upgrade WebUSB support will be available.

Caveats

UI.FIRMWARE_PROGRESS
  • Devices won’t dispatch any event after the user confirms the installation on a device. We only detect that the installation has started when we receive UI.FIRMWARE_PROGRESS which is triggered about 10 seconds too late on T1B1.
Remembered wallet, multiple devices

The onboarding inherited few bugs from its predecessor. After the installation of a firmware user is asked to reconnect his device (T1B1) or the device is auto restarted. To prevent Suite from selecting another device while the one we use was disconnected, we force remembering the device (and storing it to persistent storage). However this doesn't work in case of freshly unpacked device (or device with wiped fw), which are in bootloader mode from the start and cannot be "remembered".

When this happens Suite will try to select another available device, which will be the other connected device or remembered wallet and there is no way ho to switch the device back unless you restart the app/refresh the page.

To work around this in suiteActions.onHandleDisconnect, before selecting the next active device, we check if we are in Onboarding (or standalone firmware update flow) and if so we won't allow switching selected device to different one.

Generating seed

User chooses between generating a new seed or seed recovery.

Generating new seed

  • Single seed
  • Shamir (not available on T1B1)

At first it might seem that both options are doing exactly the same, real difference between these two will present itself in Backup seed step

Recovery from mnemonic

T2T1 or newer

The entire process is done on device. All we need to do in Suite UI is to show generic "Confirm on your Trezor" bubble.

T1B1

For T1B1 there are two things the user needs to decide:

  1. Does the user want to recover from 12, 28 or 24 words?
  2. Does the user want to enter the seed by selecting words in Suite UI (Standard recovery) or he/she wants the most secure environment and will enter the words via keyboard matrix shown on the device (advanced recovery)?

Backup seed

User needs to confirm that the seed will be safe, there will be no digital copy of it. Then he can start the process on a device.

T2T1 or newer

The entire process is done on device. All we need to do in Suite UI is to show generic "Confirm on your Trezor" bubble.

T1B1

Process consists of clicking "Next" button too many times and writing down words displayed on a trusty seed card. During this process we will display "Confirm on Trezor" prompt and instructions.

This step is optional and can be skipped and finished later from Suite settings.

PIN Setup

After the user hits CTA button “Create PIN” we need to show confirmation prompt. It is handled by checking device.buttonRequests to see if there is ButtonRequest_Other (T1B1) or ButtonRequest_ProtectCall. Yes, it is hacky. But thanks to clearing buttonRequests array in each step of the onboarding it should be safe and presence of these requests should indeed indicate that a device is asking for a confirmation.

When the user hits cancel on a device, @SUITE/lock-device is fired, buttonRequestsMiddleware will intercept it and fire removeButtonRequests action which clears the array resulting in cancelling confirmation prompt in the Onboarding UI.

Entering PIN

T1B1

After the user confirms setting new PIN on the device we'll receive UI.REQUEST_PIN, which will be stored in modal reducer (as every other request coming from a device). Based on this we display PIN matrix.

The user enters PIN twice, if there is a mismatch, process is stopped and an error shown with a button to try again.

T2T1 or newer

The entire process is done on device (including handling of mismatched pins). All we need to do in Suite UI is to show generic "Confirm on your Trezor" bubble.

Be aware that after the PIN is set, device auto-lock functionality gets activated (starting from some firmware version). Thus if it's taking the user too long to finish the onboarding process, the device will get auto locked. If in the final step, where the user can change device label and/or homescreen, the device is locked and will respond with a request to show a PIN matrix. This is handled globally, in UnexpectedStates component, for the whole onboarding flow.

Suite Settings

User can choose what coins should be enabled right from the start. There is also an option to enable TOR and set custom Blockbook backends. These settings mirror what is already set in Suite. If a user has used Suite before and changed its setting he/she will see these changed settings here as we don't want to reset user's settings just because he/she goes through onboarding process with another (or wiped) device.

Final step

The last step which contains basic device setup such as changing its label and homescreen.

Tests

This chapter contains information about tests.

@trezor/suite-web e2e tests

@trezor/suite-web uses Cypress to run e2e tests. It also uses trezor-user-env which is daily built into a docker image providing all the necessary instrumentation required to run tests (bridge and emulators).

Run it locally

Note: All paths below are relative to the root of trezor-suite repository, if not specified otherwise.

On Linux

Prerequisites

Steps

  1. Run xhost + to add yourself to the X access control list.
  2. Run docker/docker-suite-install.sh.
  3. Run docker/docker-suite-test.sh.
    • A Cypress window should open.
    • Wait until the project is built (a warning about "http://localhost:8000/ is not available", should disappear on the retry button click).
  4. Start a test by clicking its name in the Cypress window.
    • It should open a browser window.
    • If the Suite web app is not loading even after two retries. Stop tests, open a new tab, navigate to http://localhost:8000/, refresh the page until the app is loaded. Close the tab and run tests again.

Troubleshooting

  • Error while fetching server API version: ('Connection aborted.', FileNotFoundError(2, 'No such file or directory'))
    • On NixOS: Make sure that docker is enabled in your configuration.nix:
      virtualisation.docker.enable = true;
  • Error while fetching server API version: ('Connection aborted.', PermissionError(13, 'Permission denied')) - Check the docker.sock permissions:
    sudo chmod 666 /var/run/docker.sock

On MacOS (Intel)

Prerequisites

Steps

  1. Run XQuartz. Wait till it is launched. Leave it running in the background.
  2. In XQuartz settings go to Preferences -> Security and enable "Allow connections from network clients".
  3. Open a new terminal window (not in XQuartz) and add yourself to the X access control list:
    • xhost +127.0.0.1
    • You will probably need to logout/login after XQuartz installation to have xhost command available.
  4. Run Docker and go to Preferences -> Resources -> Advanced and increase RAM to at least 4GB. Otherwise, the app during tests does not even load.
  5. In terminal window run docker/docker-suite-install.sh
  6. In the terminal window, set two environment variables:
    • export HOSTNAME=`hostname`
    • export DISPLAY=${HOSTNAME}:0
  7. In terminal window run docker/docker-suite-test.sh
    • A Cypress window should open.
    • Wait until the project is built (a warning about "http://localhost:8000/ is not available", should disappear on the retry button click).
  8. Start a test by clicking its name in the Cypress window.
    • It should open a browser window.
    • If the Suite web app is not loading even after two retries. Stop tests, open a new tab, navigate to http://localhost:8000/, refresh the page until the app is loaded. Close the tab and run tests again.

Troubleshooting

  • [...ERROR:browser_main_loop.cc(1434)] Unable to open X display.
    • Make sure the XQuartz app is launched and you can see its terminal.
    • Check that environment variables are properly set:
      • echo $HOSTNAME # e.g. name.local
      • echo $DISPLAY # e.g. name.local:0
    • Do not mix native terminal window with terminal window in your IDE (e.g. Visual Studio Code).

On MacOS (ARM)

  • currently, it is not possible to run E2E tests only within a Docker container on ARM mac. With Trezor-user-env run in Docker, it is possible to run Suite and Cypress locally.

Prerequisites

Steps:

  1. Run XQuartz. Wait till it is launched. Leave it running in the background.
  2. In XQuartz settings go to Preferences -> Security and enable "Allow connections from network clients".
  3. Open a new terminal window (not in XQuartz) and add yourself to the X access control list:
    • xhost +127.0.0.1
    • You will probably need to logout/login after XQuartz installation to have xhost command available.
  4. Run Docker and go to Preferences -> Resources -> Advanced and increase RAM to at least 4GB. Otherwise, the app during tests does not even load.
  5. In the terminal window, set two environment variables:
    • export HOSTNAME=`hostname`
    • export DISPLAY=${HOSTNAME}:0
  6. In terminal window, navigate to trezor-user-env repo root and run ./run.sh.
  7. In another window, run web Suite with yarn suite:dev.
  8. In a third window, run npx cypress open --project packages/suite-web/e2e --config 'baseUrl=http://localhost:8000'.

Troubleshooting

  • Cypress could not verify that this server is running ...
    • make sure that the localhost is actually running and is accessible via browser
  • tests fail at the very beginning on screen with "Use trezor here" button
    • make sure that no other instance of Suite or trezord is running

Notes

Best practices and established patterns

Page objects

There should be no direct getting of elements in the test code. We want that and most other logic operating the app to be abstracted to the page objects located in packages/suite-web/e2e/support/pageObjects.

Steps

Give the test a structure by using cy.step() blocks. Example:

cy.step('Setup standard wallet with label', () => {...});

Easily understandable and readable code

If there is a magic constant or hard to understand block of code, put it under a named constant/function.

Image snapshots

It is possible to run tests with image snapshots to test for visual regressions. To enable snapshots, use env variable:

CYPRESS_SNAPSHOT=1 docker/docker-suite-test.sh

When you need to update image snapshots you have 2 options:

  • use CI job. This will generate new snapshots in artifacts together with a handy script that updates your snapshots locally. Check the log output.
  • use docker/docker-suite-snapshots.sh. This does the same as docker/docker-suite-test.sh, the only difference is it won't fail on non-matching snapshots but generate new snapshots.

run_tests script

The run_tests.js script is the entry point for e2e tests. It:

  • picks tests files to be run (see @tags)
  • retries tests if needed (see @retry)
  • reports tests results

tags

Each test should be assigned a tag at the top of the test file. These allow you to add more fine-grained control in run_tests.js.

At the moment, there are the following tags:

  • @group_[string]
  • @retry=[number]

@group

Assigning a @group allows run_tests.js script to sort the test files into groups and run them in parallel on CI. At the moment these groups exist:

  • @group_metadata
  • @group_device-management
  • @group_suite
  • @group_settings

@retry

If there is a test that you for any reason need to retry if it fails you may provide @retry=2 tag. In this case, test will be run 3 times in total and count as failed only if all runs fail.

Results

There is a tool to track tests runs and their results, temporarily hosted here https://track-suite.herokuapp.com/ Repo here: https://github.com/mroz22/track-suite

@trezor/connect-popup

@trezor/connect-popup is end-to-end tested together with @trezor/connect-explorer using playwright test runner.

Playwright + NixOS

Before the first run or occasionally after the update of npm dependencies you will be asked to run playwright install command:

Error: browserType.launch: Executable doesn't exist at .cache/ms-playwright/chromium-[revision]/chrome-linux/chrome

Please run the following command to download new browsers:
npx playwright install

Playwright requires specific revisions defined in node_modules/playwright-core/browsers.json. Unfortunately downloaded browsers are not compatible with NixOS.

Link your system browser instead of downloading:

FILE=path-from-the-error && mkdir -p "${FILE%/*}" && touch "$FILE" && ./nixos-fix-binaries.sh

Fixtures

Fixtures are located in packages/connect-popup

Test results

Checkout latest screenshots

Regtest

Regtest is a private blockchain which has the same rules and address format as testnet, but there is no global p2p network to connect to.

To use custom backend (electrum server) with bitcoind running in regtest mode you can use the docker container by running the command below:

bash docker/docker-regtest-electrum.sh

The previous command will initialize a new fresh regtest bitcoin blockchain with electrum server running and expose to the localhost at TCP port 50001.

In order to be able to use regtest electrum in suite it is required to configured the REGTEST coin with custom backend with URL below:

localhost:50001:t

Assorted knowledge

This directory serves as a dumping ground for important knowledge tidbits that do not clearly fit in any particular location. Please add any information that you think should be written down.

At any time, information stored here might be restructured or moved to a different location, so as to ensure that the documentation is well structured overall.

Front-End Build

The front-end build of Suite is handled by Webpack configurations inside the suite-build package.

The folder structure is as follows:

  • configs: Contains the Webpack configuration files. base.webpack.config.ts serves as a common base for all other configurations. The other files in this folder are project specific such as web.webpack.config.ts or desktop.webpack.config.ts for suite-web and suite-desktop respectively.
  • plugins: Contains custom Webpack plugins.
  • utils: Contains various utils for the build scripts.

These Webpack configurations are using TypeScript and use the tsconfig.json file at the root of the package. This is specified via the TS_NODE_PROJECT environment variable to avoid any issues regardless of the location where the command is run.

The following commands are available in this package (using yarn run at the root of the package or yarn workspace @trezor/suite-build run at the root of the project):

CommandDescription
dev:webRuns a watch build of suite-web with development settings and serves it.
build:webBuilds a production build of suite-web.
dev:desktopRuns a watch build of suite-desktop with development settings, serves it and runs the Electron wrapper.
build:desktopBuilds a production build of suite-desktop.
lintRuns the linter on the package.
type-checkRuns the TypeScript checker on the package.
type-check:watchSame as type-check but in watch mode.

Aliases

Aliases for imports (for example @suite-utils/features) are defined in the tsconfig.json file at the root of the project, in the compilerOptions.paths property. The values are processed at build time for the webpack configuration in order to properly resolve aliases.

Development on Windows

Thanks to Windows Subsystem for Linux (WSL), you can run Trezor Suite dev environment on a Windows machine.

Prerequisites

On Windows:

In WSL:

Setup

Proceed with the general readme instructions.

Connecting USB device

On Windows, run usbipd list, find the bus id of the Trezor device, e.g. 2-1.

Then run:

usbipd bind --busid 2-1
usbipd attach --wsl --busid 2-1

In WSL, run lsusb to confirm the device is visible.

Note: Without udev rules, the device will be visible by lsusb, but not in the app.

# Trezor device naming in codebase

Development/Internal name consists of 4 keys <product_class> <platform> <feature_class> <generation>

  • product_class - 'T' for Trezor hardware wallet
  • platform - '1' for STM32F207, '2' for STM32F42x
  • feature_class - 'B' for Buttons, 'T' for Touch
  • generation
Official nameDevelopment name
Trezor Model 1T1B1
Trezor Model TT2T1
Trezor Safe 3T2B1, T3B1
Trezor Safe 5T3T1
  • Trezor Safe 3 exists in two variants, depending on its chip. It was upgraded post-release.

Review Process

Same as for Trezor Firmware. Please see here.

Videos in Suite

Videos in Suite can be encoded in .mp4 container however we can also use WebM format which have benefits of smaller data footprint, better color accuracy and they also support transparency.

The designers should always provide .mov file with appropriate quality, pixel dimensions and a alpha channel (if e.g. transparent background is needed).

The following process is tested on MacOS:

Prerequisites

Install encoder

brew install ffmpeg

WebM - currently supported by Chrome/Firefox

ffmpeg -i source.mov -pix_fmt yuva420p -an encoded_file.webm

Encode videos using desktop application

Shutterencoder

Example usage

Encoded video files can be then saved to suite-data and linked using resolveStaticPath() function.

<video loop autoPlay muted>
    <source src={resolveStaticPath(`videos/onboarding/encoded_file.webm`)} type="video/webm" />
</video>

References

How to make HEVC, H265 and VP9 videos with an alpha channel for the web

Alpha transparency in Chrome video