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.
"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
- Data is only collected with explicit permission.
- Your sensitive data is not collected.
- We use AWS logging for data analytics and Sentry for error tracking.
- We store the data concerning errors for the period of 90 days.
- 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
- User with enabled analytics interacts with the application
- Events are sent to specific endpoints
- Collected data are parsed and analysed (can be seen in Keboola)
- Charts and metrics are created (in Tableau)
- 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.
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.
Breadcrumbs
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
-
Complete the firmware release process including firmware signing.
-
Add firmwares to
packages/connect-common/files/firmware/*
and modify itsreleases.json
file. See Firmwarereleases.json
files structure for an explanation and 90bb548 for an example. -
Remove older binaries so they are not bundled in the desktop app any more, but always keep:
- the intermediary FW for T1B1 packages/connect-common/files/firmware/t1b1/trezor-inter-v{1 | 2 | 3}.bin
- and 2.1.1 for T2T1 packages/connect-common/files/firmware/t2t1/trezor-2.1.1.bin
See #4262 for explanation.
-
Test it locally (at least by running
yarn build:libs
to rebuild connect files andyarn suite:dev
to use/copy them). -
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.
key | type | example value | description |
---|---|---|---|
required | boolean | false | If 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. |
url | string | firmware/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_bitcoinonly | string | firmware/t1b1/trezor-t1b1-1.11.1-bitcoinonly.bin" | Same as url , just for Bitcoin only FW. |
fingerprint | string | "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_bitcoinonly | string | "8e17b95b5d302f203de3a8fe27959efd25e3d5140ac9b5e60412f1b3f624995d" | Same as fingerprint , just for Bitcoin only FW. |
notes | string | https://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. |
changelog | string | "* 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.
route | source | assetPrefix |
---|---|---|
/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
- Update
CHANGELOG.md
and list all changes since the last release of the package. - Bump the version in
packages/<PACKAGE-NAME>/package.json
. Use the semver convention.
Production
- Create new branch with
npm-release/
prefix. - Commit your changes as
release: @trezor/<PACKAGE-NAME> X.X.X
. - Use
<PACKAGE-NAME> deploy npm
job.
Beta
If you want to publish to npm as beta
(from any branch) do the following:
- Change the version in
packages/<PACKAGE-NAME>/package.json
fromX.X.X
toX.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. - Commit your changes as
release: @trezor/<PACKAGE-NAME> X.X.X-beta.X
. - 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 202020.10.3
third release in Oct 202019.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 stable20.10.2
second release on Oct 22nd to stable20.11.0
release on Oct 29th 2020 to beta20.11.0
another release on Nov 5th to beta20.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.
- Update
trezor-common
submodule:
yarn update-submodules
- Build
src/data/coins.json
file usingtrezor-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 orArray
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];
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, customfeePerUnit
is calculated ifethereumData
is usedsendFormActionsRipple
does calculation locally, additionalaccount.reserve
check on recipient address
PrecomposedLevel
are calculated for all possibleFeeLevel
at once.- if
FeeLevel
wasn't changed by the user and currentPrecomposedLevel
has error then tries to switch to a lower/custom possibleFeeLevel
- if
PrecomposedLevel
has error set this error inreact-hook-form
- if
PrecomposedLevel
hasset-max
set calculated amount inreact-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.)
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 configiframe.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 regularfetch
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 asapp.asar
file, and they are downloaded usingfs.readFile
API. see @trezor/connect/src/utils/assets
- Trezor Bridge (trezord)
- Tor
App ID and name by environment
Environment | App ID | App name | User data dir name |
---|---|---|---|
production (codesign) | com.trezor.suite | Trezor Suite | @trezor/suite-desktop |
development (sldev) | com.trezor.suite.dev | Trezor Suite Dev | @trezor/suite-desktop-dev |
local | com.github.Electron | Trezor 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)
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:
name | commands |
---|---|
Reload app | F5, Ctrl+R, Cmd+R |
Hard Reload app | Shift+F5, Shift+Ctrl+R, Shift+Cmd+R |
Restart app | Alt+F5, Option+F5, Alt+Shift+R, Option+Shift+R |
Open DevTools | F12, 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:
name | description |
---|---|
--open-devtools | Open DevTools on app launch. |
--pre-release | Tells the auto-updater to fetch pre-release updates. |
--bridge-legacy | Use Legacy (trezord-go) Bridge implementation |
--bridge-dev | Instruct Bridge to support emulator on port 21324 |
--log-level=NAME | Set 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-write | Write log to disk |
--log-ui | Enables printing of UI console messages in the console. |
--log-file=FILENAME | Name of the output file (defaults to trezor-suite-log-%tt.txt ) |
--log-path=PATHNAME | Path for the output file (defaults to /logs subfolder of Suite data directory or current working directory) |
--enable-updater | Enables the auto updater (if disabled in feature flags) |
--disable-updater | Disables the auto updater (if enabled in feature flags) |
--updater-url=URL | Set 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?
- Use
yarn generate-package @scope/new-package-name
- it will generate package boilerplate inscope/new-package-name
folder.
How to use this new package?
- Place this package to dependency field of package.json in package where you want to use it.
- Run
yarn refs
to generate tsconfig refs. - Run
yarn
to let yarn symlink this package.
Features
This directory contains description of various Trezor Suite features.
- coin-handler
- transactions export
- transactions search
- metadata labeling
- fiat rates
- guide
- localization
- messaging system
- feature flags
- desktop logger
- application log
- onboarding
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.
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.
Useful links
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.
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.
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:
- User opens App for the first time. Metadata is disabled. "Add label" buttons are present on mouse hover over labelable data.
- User clicks "Add label" button.
- Device metadata key is generated.
- Using device metadata key, account metadata keys are created.
- Open modal with metadata providers and connect.
- Fetch data from metadata provider and set interval for fetching data.
- Activate editable input.
Metadata enabled during discovery process:
Controlled by discoveryActions
and metadataMiddleware
- If passphrase is not used, device metadata key is generated before discovery process starts. (
discoveryActions
) - 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
)
- in the middle of the discovery process after successfully receiving the first bundle of accounts and at least one account is not empty. (
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:
- sets metadata.enabled bool value
- if setting to false, it triggers removal of all metadata (including keys) from devices and accounts.
- if setting to false, disconnects metadata provider (Dropbox, Google Drive)
- there is a button "disconnect provider" which:
- triggers removal of all metadata values (excluding metadata keys) from devices and accounts. This way provider might be reconnected without reconnecting device
- 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 prefixTR_
, or expanded variantTR_<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 adefaultMessage
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.
- Go to Settings in Suite
- Rapidly click on the "Settings" heading 5 times
- Click the three dot context menu on the right
- "Debug Settings" should've appeared. Click it. If "Debug Settings" hasn't appeared, repeat step 2.
- 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.
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
(oryarn build:libs
) script for local development.
- in CI job in
- The results are saved into
suite-common/message-system/files
as two files:config.v1.jws
to be uploaded tohttps://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 inpackages/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
- Config is fetched on load of application and is stored in Redux state. To be persisted between sessions, is is mirrored into IndexDB.
- Conditions of config are evaluated on specific Redux actions. See
messageSystemMiddleware.ts
file. - 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:
- Add your flag to the
FLAGS
constant and set its default value. When naming your flag, bear in mind the following conventions:- Always explain what the flag is about using a comment next to it.
- The name of the flag should always be in capitals.
- The name of the flag should never contain the world
enable
ordisable
because the name should always towards an enabled state. Its value should reflect whether the feature is enabled or not. - The name of the flag should never contain the word
flag
because it's inferred.
- (optional) You can override the flag for each environment (web, desktop) using their specific constants.
- Use the
isFeatureFlagEnabled
function from@suite-utils/features
to check if the flag is enabled or not. - 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)
name | type | default value | description |
---|---|---|---|
colors | boolean | true | Console output has colors |
writeToConsole | boolean | true | Output is displayed in the console |
writeToDisk | boolean | false | Output is written to a file |
outputFile | string | 'trezor-suite-log-%tt.txt' | file name for the output |
outputPath | string | /logs subfolder in Suite data directory or CWD | path for the output |
logFormat | string | '%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
Expression | Example output | Description |
---|---|---|
%tt | 2021-01-19T11-07-40 | Date and time in filename friendly format |
%ts | 1611054460306 | Timestamp |
%dt | 2021-01-19T11:08:22.244Z | Date and time in ISO format (ISO 8601) |
logFormat
Expression | Example output | Description |
---|---|---|
%lvl | INFO | Level in letters and upper case |
%top | Example | Topic |
%msg | Example message | Message |
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.
- 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
- 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
- Firmware
- Note about normal and bootloader mode
- Firmware update or installation
- Device with older firmware installed
- Device with no firmware installed
- State of currently shipped devices, intermediary firmware
- Generating seed
- Backup seed
- PIN Setup
- Suite Settings (enabled coins, custom backends, TOR)
- Final step (device label, homescreen)
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)
- Device connected in normal mode
- Part of happy path. After the device is connected we proceed to Data analytics (only in initial run) or to Device security (genuinity) check
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.
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).
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 tonone
) - We don't need to prompt the user to switch to bootloader mode in this case.
- Fresh device (
- 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
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).
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.
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)
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:
- Does the user want to recover from 12, 28 or 24 words?
- 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
- Run
xhost +
to add yourself to the X access control list. - Run
docker/docker-suite-install.sh
. - 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).
- 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;
- On NixOS: Make sure that docker is enabled in your configuration.nix:
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
- Run XQuartz. Wait till it is launched. Leave it running in the background.
- In XQuartz settings go to Preferences -> Security and enable "Allow connections from network clients".
- 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.
- Run Docker and go to Preferences -> Resources -> Advanced and increase RAM to at least 4GB. Otherwise, the app during tests does not even load.
- In terminal window run
docker/docker-suite-install.sh
- In the terminal window, set two environment variables:
export HOSTNAME=`hostname`
export DISPLAY=${HOSTNAME}:0
- 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).
- 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 runSuite
andCypress
locally.
Prerequisites
- Docker
- XQuartz (to share your screen with Docker)
- Trezor user env
- No other instance of
Suite
ortrezord
service is running
Steps:
- Run XQuartz. Wait till it is launched. Leave it running in the background.
- In XQuartz settings go to Preferences -> Security and enable "Allow connections from network clients".
- 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.
- Run Docker and go to Preferences -> Resources -> Advanced and increase RAM to at least 4GB. Otherwise, the app during tests does not even load.
- In the terminal window, set two environment variables:
export HOSTNAME=`hostname`
export DISPLAY=${HOSTNAME}:0
- In terminal window, navigate to
trezor-user-env
repo root and run./run.sh
. - In another window, run web
Suite
withyarn suite:dev
. - 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
ortrezord
is running
- make sure that no other instance of
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 asdocker/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 asweb.webpack.config.ts
ordesktop.webpack.config.ts
forsuite-web
andsuite-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):
Command | Description |
---|---|
dev:web | Runs a watch build of suite-web with development settings and serves it. |
build:web | Builds a production build of suite-web . |
dev:desktop | Runs a watch build of suite-desktop with development settings, serves it and runs the Electron wrapper. |
build:desktop | Builds a production build of suite-desktop . |
lint | Runs the linter on the package. |
type-check | Runs the TypeScript checker on the package. |
type-check:watch | Same 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:
- Install an Ubuntu WSL2 (must be v2, you may upgrade existing v1 WSL to v2)
- Install USBIPD
In WSL:
- Run
sudo apt-get install build-essential
- Install these Electron dependencies for Linux
- Install udev rules as per the Trezor docs
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 walletplatform
-'1'
for STM32F207,'2'
for STM32F42xfeature_class
-'B'
for Buttons,'T'
for Touchgeneration
Official name | Development name |
---|---|
Trezor Model 1 | T1B1 |
Trezor Model T | T2T1 |
Trezor Safe 3 | T2B1, T3B1 |
Trezor Safe 5 | T3T1 |
- 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
Encode videos using command line (recommended)
WebM - currently supported by Chrome/Firefox
ffmpeg -i source.mov -pix_fmt yuva420p -an encoded_file.webm
Encode videos using desktop application
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