Trezor Suite documentation

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

Data Analytics

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

1

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

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

TL;DR

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

What data is collected?

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

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

How are the data processed?

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

Error tracking using Sentry

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

Retention period

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

Security of data

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

Contents

AWS Analytics: Info

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

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

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

Anonymous data

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

Among the data not collected by analytics:

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

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

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

Data process

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

Sentry

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

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

1

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

What is being tracked

General data

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

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

Extra data:

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

Releases

This chapter contains information about the release process.

Adding New Firmwares

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

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

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

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

Add firmwares

  1. Complete the firmware release process including firmware signing.

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

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

    See #4262 for explanation.

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

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

Release for 3rd party users of @trezor/connect

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

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


Firmware releases.json files structure

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

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

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

Desktop Updates

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

Internals

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

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

Environments

Staging

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

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

Production (suite.trezor.io)

Stable version is hosted on suite.trezor.io.

routesourceassetPrefix
/web@trezor/suite-web/web

Publishing @trezor package to npm registry

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

Purpose

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

Prerequisites

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

Production

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

Beta

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

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

Signing binaries win

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

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

Check if binaries are signed for windows in Linux

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

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

After unpacked, test signatures:

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

CI signing details for windows

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

Creating Self-signed pfx and cer certificates with OpenSSL

Generate directly the pem:

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

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

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

Versioning

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

Private Packages

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

Public Packages

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

Suite App

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

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

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

For example:

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

Beta versions

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

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

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

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

For example:

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

Development versions

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

Packages

This directory contains description of various Trezor Suite packages.

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

Trezor javascript SDK

There are couple of options how to integrate Trezor devices with your project. This page walks you through installation and lets you explore SDK API.

Chose your SDK

Depending on your environment you need to chose the right package.

packageenvironment
@trezor/connectnode.js
@trezor/connect-webweb based (DOM required)
@trezor/connect-webextensionwebextension using service worker

If you are still unsure which package is the right one you may refer to the following table with a collection of examples.

env examplepackage
node@trezor/connect
web app@trezor/connect-web
web extension mv2 (foreground or background)@trezor/connect-web
web extension mv3 (foreground)@trezor/connect-web
web extension mv3 (background)@trezor/connect-webextension
electron in main layer@trezor/connect
electron in renderer layer with popup@trezor/connect-web
Trezor Suite (desktop) electron app@trezor/connect

Quick start

Import from your selected package

// in node
import TrezorConnect from '@trezor/connect';
// or in web based
import TrezorConnect from '@trezor/connect-web';
// or in webextension service worker
import TrezorConnect from '@trezor/connect-webextension';

Initialize in project

TrezorConnect.init({
    lazyLoad: true, // this param will prevent iframe injection until TrezorConnect.method will be called
    manifest: {
        email: '[email protected]',
        appUrl: 'http://your.application.com',
    },
});

Trezor Connect Manifest requires that you, as a Trezor Connect integrator, share your e-mail and application url with us. This provides us with the ability to reach you in case of any required maintenance. This subscription is mandatory. Trezor Connect raises an error that reads "Manifest not set" if manifest is not provided. It can be either set via manifest method or passed as a param in init method.

TrezorConnect.manifest({
    email: '[email protected]',
    appUrl: 'http://your.application.com',
});

If you need more customization, refer to init method documentation

API methods

Handling events

How it works under the hood

There is a major difference between node.js based package (@trezor/connect) and web based packages (@trezor/connect-web and @trezor/connect-webextension). In the former the entire SDK logic is a module of the 3rd party application whereas in the latter, there is strict isolation between 3rd party application code and SDK core logic.

Node.js

In node.js core SDK is loaded as a javascript module without any specificities.

![connect schema when used in node](./schema-connect.jpg =500x338)

Web

@trezor/connect-web imports only a thin layer with API description into your 3rd party application. When initiated, it injects iframe containing core SDK logic from trezor.io into your app. User input, if needed, is served by popup.html page opened on trezor.io on behalf of your application. This way users input such as pin or passphrase is isolated from you and persistent connection between your app and core SDK is kept so events such as device connected/disconnected or blockchain subscriptions are available.

![connect schema when used on web](./schema-connect-web.jpg =950x379)

Webextension

In case of @trezor/connect-webextension, TrezorConnect object is created in a service worker. In this env we can't inject iframe so in order to uphold the same security model as with @trezor/connect-web we open popup.html and load core SDK logic into it. This however does not build persistent connection between SDK and 3rd party application meaning that events cannot be used.

![connect schema when used in webextension](./schema-connect-webextension.jpg =950x432)

Debugging

?trezor-connect-src

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

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

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

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

Is it safe to have this enabled in production?

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

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

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

Dependencies

Webpack:

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

List of essential libraries to produce build:

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

From Protobuf to TypeScript and Flow

This document was moved to @trezor/transport

Supported coins

The pipeline

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

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

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

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

Update and maintenance in @trezor/connect

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

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

Events

Handling events

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

ES6

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

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

List of published events

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

Path

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

Examples

Bitcoin account 1 using BIP44 derivation path

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

Bitcoin account 1 using hardended path

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

Bitcoin first address address of account 1 using BIP44 derivation path

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

Bitcoin first address address of account 1 using hardended path

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

See more examples

Methods

API call return a Promise. Resolve is guaranteed to get called with a result object, even if user closes the window, network connection times out, etc. In case of failure result.success is set to false and result.payload.error is the error message. It is recommended to log the error message and let user restart the action.

Every method require an Object with combination of common fields and method specific fields.

Bitcoin, Bitcoin Cash, Bitcoin Gold, Litecoin, Dash, ZCash, Testnet

Ethereum

Eos

NEM

Stellar

Cardano

Ripple

Solana

Tezos

Binance

Management

please note that these method are not available from popup mode

Common parameters

Every call requires an Object with a combination of common and method-specified fields. All common parameters are optional.

  • device - optional Object
    • path - required string call to a direct device. Useful when working with multiple connected devices. This value is emitted by TrezorConnectEvent
    • state - optional string sets expected state. This value is emitted by TrezorConnectEvent
    • instance - optional number sets an instance of device. Useful when working with one device and multiple passphrases. This value is emitted by TrezorConnectEvent
  • useEmptyPassphraseoptional boolean method will not ask for a passphrase. Default is set to false
  • allowSeedlessDeviceoptional boolean allows to use TrezorConnect methods with device with seedless setup. Default is set to false
  • keepSessionoptional boolean Advanced feature. After method return a response device session will NOT! be released. Session should be released after all calls are performed by calling any method with keepSession set to false or undefined. Useful when you need to do multiple different calls to TrezorConnect API without releasing. Example sequence loop for 10 account should look like:
    • TrezorConnect.getPublicKey({ device: { path: "web01"}, keepSession: true, ...otherParams }) for first account,
    • Trezor.getAddress({ device: { path: "web01"}, ...otherParams }) for the same account,
    • looking up for balance in external blockchain
    • loop iteration
    • after last iteration call TrezorConnect.getFeatures({ device: { path: "web01"}, keepSession: false, ...otherParams })
  • useCardanoDerivation - optional boolean. default is set to true for all cardano related methods, otherwise it is set to false. This parameter determines whether device should derive cardano seed for current session. Derivation of cardano seed takes longer then it does for other coins. A wallet that works with both cardano and other coins might want to set this param to true for every call or it must be able to cope with the following scenario:
    • Connected device is using passhprase
    • Wallet calls getPublicKey with useCardanoDerivation=false, passhprase is entered, seed derived
    • Wallet calls cardanoGetPublicKey.
    • At this moment user will be prompted to enter passhprase again.
  • override - optional boolean Interrupt previous call, if any.
  • chunkifyoptional boolean determines if address will be displayed in chunks of 4 characters. Default is set to false

Get public key

Retrieves BIP32 extended public derived by given BIP32 path. User is presented with a description of the requested key and asked to confirm the export.

const result = await TrezorConnect.getPublicKey(params);

Params

Optional common params

Exporting single public key

  • pathrequired string | Array<number> minimum length is 1. read more
  • coin - optional string determines network definition specified in coins.json file. Coin shortcut, name or label can be used. If coin is not set API will try to get network definition from path.
  • scriptTypeoptional string used to distinguish between various address formats (non-segwit, segwit, etc.).
  • ignoreXpubMagicoptional boolean ignore SLIP-0132 XPUB magic, use xpub/tpub prefix for all account types.
  • ecdsaCurveNameoptional string ECDSA curve name to use
  • crossChainoptional boolean Advanced feature. Use it only if you are know what you are doing. Allows to generate address between chains. For example Bitcoin path on Litecoin network will display cross chain address in Litecoin format.
  • unlockPath - optional PROTO.UnlockPath, the result of TrezorConnect.unlockPath method.
  • suppressBackupWarning - optional boolean By default, this method will emit an event to show a warning if the wallet does not have a backup. This option suppresses the message.

Exporting bundle of public keys

  • bundle - Array of Objects with path, coin and crossChain fields

Example

Return public key of fifth bitcoin account:

TrezorConnect.getPublicKey({
    path: "m/49'/0'/4'",
    coin: 'btc',
});

Return a bundle of public keys for multiple bitcoin accounts:

TrezorConnect.getPublicKey({
    bundle: [
        { path: "m/49'/0'/0'" }, // account 1
        { path: "m/49'/0'/1'" }, // account 2
        { path: "m/49'/0'/2'" }, // account 3
    ],
});

Result

HDNodeResponse type

Result with only one public key

{
    success: true,
    payload: {
        path: Array<number>, // hardended path
        serializedPath: string, // serialized path
        xpub: string,        // xpub in legacy format
        xpubSegwit?: string, // optional for segwit accounts: xpub in segwit format
        chainCode: string,   // BIP32 serialization format
        childNum: number,    // BIP32 serialization format
        publicKey: string,   // BIP32 serialization format
        fingerprint: number, // BIP32 serialization format
        depth: number,       // BIP32 serialization format
    }
}

Read more about BIP32 serialization format

Result with bundle of public keys

{
    success: true,
    payload: [
        { path, serializedPath, xpub, xpubSegwit?, chainCode, childNum, publicKey, fingerprint, depth }, // account 1
        { path, serializedPath, xpub, xpubSegwit?, chainCode, childNum, publicKey, fingerprint, depth }, // account 2
        { path, serializedPath, xpub, xpubSegwit?, chainCode, childNum, publicKey, fingerprint, depth }  // account 3
    ]
}

Error

{
    success: false,
    payload: {
        error: string // error message
    }
}

Request login

Challenge-response authentication via Trezor. To protect against replay attacks you should use a server-side generated and randomized challengeHidden for every attempt. You can also provide a visual challenge that will be shown on the device.

Service backend needs to check whether the signature matches the generated challengeHidden, provided challengeVisual and stored publicKey fields. If that is the case, the backend either creates an account (if the publicKey identity is seen for the first time) or signs in the user (if the publicKey identity is already a known user).

To understand the full mechanics, please consult the Challenge-Response chapter of SLIP-0013: Authentication using deterministic hierarchy.

const result = await TrezorConnect.requestLogin(params);

Params

Optional common params
Common parameter useEmptyPassphrase - is always set to true and it will be ignored by this method

Login using server-side async challenge

  • callbackrequired function which will be called from API to fetch challengeHidden and challengeVisual from server

Login without async challenge

  • challengeHidden - required string hexadecimal value
  • challengeVisual - required string text displayed on Trezor

Example

Login using server-side async challenge
TrezorConnect.requestLogin({
    callback: function () {
        // here should be a request to server to fetch "challengeHidden" and "challengeVisual"
        return {
            challengeHidden: '0123456789abcdef',
            challengeVisual: 'Login to',
        };
    },
});
Login without async challenge
TrezorConnect.requestLogin({
    challengeHidden: '0123456789abcdef',
    challengeVisual: 'Login to',
});

Result

Login type

{
    success: true,
    payload: {
        address: string,
        publicKey: string,
        signature: string,
    }
}

Error

{
    success: false,
    payload: {
        error: string // error message
    }
}

Server side examples

Here is the reference implementation of the server-side signature verification written in various languages:

Symmetric key-value encryption

Cipher key value provides symmetric encryption in the Trezor device where the user might be forced to confirm the encryption/decryption on the display. The key for the encryption is constructed from the private key on the BIP address, the key displayed on the device, and the two informations about whether to ask for confirmation. It is constructed in such a way, that different path, key or the confirm information will get a different encryption key and IV. So, you cannot "skip" the confirmation by using different input. IV can be either manually set, or it is computed together with the key.The value must be divisible into 16-byte blocks. The application has to pad the blocks itself and ensure safety; for example, by using PKCS7.

More information can be found in SLIP-0011.

const result = await TrezorConnect.cipherKeyValue(params);

Params

Optional common params

Common parameter useEmptyPassphrase - is always set to true and it will be ignored by this method

Encrypt single value

  • pathrequired string | Array<number> minimum length is 1. read more
  • keyoptional string a message shown on device
  • valueoptional string hexadecimal value with length a multiple of 16 bytes (32 letters in hexadecimal). Value is what is actually being encrypted.
  • askOnEncrypt - optional boolean should user confirm encrypt?
  • askOnDecrypt - optional boolean should user confirm decrypt?
  • iv - optional string initialization vector - keep unset if you don't know what it means, it will be computed automatically.

Encrypt multiple values

  • bundle - Array of Objects with path, key, value, askOnEncrypt, askOnDecrypt fields

Example

Return encrypted value:

TrezorConnect.cipherKeyValue({
    path: "m/49'/0'/0'",
    key: 'This text is displayed on Trezor during encrypt',
    value: '1c0ffeec0ffeec0ffeec0ffeec0ffee1',
    encrypt: true,
    askOnEncrypt: true,
    askOnDecrypt: true,
});

Return a bundle of encrypted values:

TrezorConnect.cipherKeyValue({
    bundle: [
        {
            path: "m/49'/0'/0'",
            key: '1 text on Trezor',
            value: '1c0ffeec0ffeec0ffeec0ffeec0ffee1',
            encrypt: true,
        },
        {
            path: "m/49'/0'/1'",
            key: '2 text on Trezor',
            value: '1c0ffeec0ffeec0ffeec0ffeec0ffee1',
            encrypt: false,
        },
        { path: "m/49'/0'/2'", key: '3 text on Trezor', value: '1c0ffeec0ffeec0ffeec0ffeec0ffee1' },
    ],
});

Result

CipheredValue type

Result with only one value

{
    success: true,
    payload: {
        value: string
    }
}

Result with bundle of values

{
    success: true,
    payload: [
        { value: string },
        { value: string },
        { value: string }
    ]
}

Error

{
    success: false,
    payload: {
        error: string // error message
    }
}

Wipe device

Reset device to factory defaults and remove all private data.

const result = await TrezorConnect.wipeDevice(params);

Params

Optional common params

Common parameter useEmptyPassphrase - is set to true Common parameter allowSeedlessDevice - is set to true

Example

TrezorConnect.wipeDevice();

Result

Success type

{
    success: true,
    payload: {
        message: 'Device wiped'
    }
}

Error

{
    success: false,
    payload: {
        error: string // error message
    }
}

Reset device

Perform device setup and generate new seed.

const result = await TrezorConnect.resetDevice(params);

Params

Optional common params

  • strengthoptional number Accepted values are [128|192|256]. Default is set to 256
  • labeloptional string
  • u2fCounteroptional number. Default value is set to current time stamp in seconds.
  • pinProtectionoptional boolean
  • passphraseProtectionoptional boolean
  • skipBackupoptional boolean
  • noBackupoptional boolean create a seedless device

Example

TrezorConnect.resetDevice({
    label: 'My fancy Trezor',
});

Result

Success type

{
    success: true,
    payload: {
        message: 'Device successfully initialized'
    }
}

Error

{
    success: false,
    payload: {
        error: string // error message
    }
}

Get Coin Info

Returns information about a specified coin from the coins.json file.

const result = await TrezorConnect.getCoinInfo(params);

Params

Optional common params

Exporting single address

  • coinrequired string coin symbol (btc, eth, bch, ...).

Example

Get coin info for Bitcoin.

TrezorConnect.getCoinInfo({
    coin: 'btc',
});

Result

Result for Bitcoin

BitcoinNetworkInfo

{
    success: true,
    payload: {
        blockchainLink: Object { type: "blockbook", url: (5) […] },
        blocks: 10,
        blocktime: 10,
        cashAddrPrefix: null,
        curveName: "secp256k1",
        decimals: 8,
        defaultFees: Object { Economy: 70, High: 200, Low: 10, … },
        dustLimit: 546,
        forceBip143: false,
        forkid: null,
        hashGenesisBlock: "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
        isBitcoin: true,
        label: "Bitcoin",
        maxAddressLength: 34,
        maxFee: 2000,
        maxFeeSatoshiKb: 2000000,
        minAddressLength: 27,
        minFee: 1,
        minFeeSatoshiKb: 1000,
        name: "Bitcoin",
        network: Object { messagePrefix: "Bitcoin Signed Message:\n", bech32: "bc", pubKeyHash: 0, … },
        segwit: true,
        shortcut: "BTC",
        slip44: 0,
        support: Object { connect: true, trezor1: "1.5.2", trezor2: "2.0.5", … },
        type: "bitcoin",
        xPubMagic: 76067358,
        xPubMagicSegwit: 77429938,
        xPubMagicSegwitNative: 78792518,
    }
}

Get address

Display requested address derived by given BIP32 path on device and returns it to caller. User is asked to confirm the export on Trezor.

const result = await TrezorConnect.getAddress(params);

Params

Optional common params

Exporting single address

  • pathrequired string | Array<number> minimum length is 5. read more
  • addressoptional string address for validation (read Handle button request section below)
  • showOnTrezoroptional boolean determines if address will be displayed on device. Default is set to true
  • coin - optional string determines network definition specified in coins.json file. Coin shortcut, name or label can be used. If coin is not set API will try to get network definition from path.
  • crossChainoptional boolean Advanced feature. Use it only if you are know what you are doing. Allows to generate address between chains. For example Bitcoin path on Litecoin network will display cross chain address in Litecoin format.
  • multisig - optional MultisigRedeemScriptType, redeem script information (multisig addresses only)
  • scriptType - optional InputScriptType, address script type
  • unlockPath - optional PROTO.UnlockPath, the result of TrezorConnect.unlockPath method.
  • chunkifyoptional boolean determines if address will be displayed in chunks of 4 characters. Default is set to false

Exporting bundle of addresses

  • bundle - Array of Objects with path, showOnTrezor, coin and crossChain fields

Handle button request

Since [email protected] there is a possibility to handle UI.ADDRESS_VALIDATION event which will be triggered once the address is displayed on the device. You can handle this event and display custom UI inside of your application.

If certain conditions are fulfilled popup will not be used at all:

  • the user gave permissions to communicate with Trezor
  • device is authenticated by pin/passphrase
  • application has TrezorConnect.on(UI.ADDRESS_VALIDATION, () => {}); listener registered
  • parameter address is set
  • parameter showOnTrezor is set to true (or not set at all)
  • application is requesting ONLY ONE(!) address

Example

Display third address of first bitcoin account:

TrezorConnect.getAddress({
    path: "m/49'/0'/0'/0/2",
    coin: 'btc',
});

Return a bundle of addresses from first bitcoin account without displaying them on device:

TrezorConnect.getAddress({
    bundle: [
        { path: "m/49'/0'/0'/0/0", showOnTrezor: false }, // address 1
        { path: "m/49'/0'/0'/0/1", showOnTrezor: false }, // address 2
        { path: "m/49'/0'/0'/0/2", showOnTrezor: false }, // address 3
    ],
});

Validate address using custom UI inside of your application:

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

TrezorConnect.on(UI.ADDRESS_VALIDATION, data => {
    console.log('Handle button request', data.address, data.serializedPath);
    // here you can display custom UI inside of your app
});

const result = await TrezorConnect.getAddress({
    path: "m/49'/0'/0'/0/0",
    address: '3L6TyTisPBmrDAj6RoKmDzNnj4eQi54gD2',
});
// dont forget to hide your custom UI after you get the result!

Result

Address type

Result with only one address

{
    success: true,
    payload: {
        address: string,     // displayed address
        path: Array<number>, // hardended path
        serializedPath: string,
    }
}

Result with bundle of addresses

{
    success: true,
    payload: [
        { address: string, path: Array<number>, serializedPath: string }, // address 1
        { address: string, path: Array<number>, serializedPath: string }, // address 2
        { address: string, path: Array<number>, serializedPath: string }, // address 3
    ]
}

Error

{
    success: false,
    payload: {
        error: string // error message
    }
}

Get account info

Gets an info of specified account.

const result = await TrezorConnect.getAccountInfo(params);

Params

Optional common params

Using path

  • pathrequired string | Array<number> minimum length is 3. read more
  • coinrequired string determines network definition specified in coins.json file. Coin shortcut, name or label can be used.

Using public key

  • descriptorrequired string public key of account
  • coinrequired string determines network definition specified in coins.json file. Coin shortcut, name or label can be used.

Using discovery

BIP-0044 account discovery is performed and user is presented with a list of accounts. Result is returned after account selection.

  • coinrequired string determines network definition specified in coins.json file. Coin shortcut, name or label can be used.

Other optional params

params are forwarded to BlockBook backend using @trezor/blockchain-link package

  • details — specifies level of details returned by request

    • basic (default) return only account balances, without any derived addresses or transaction history
    • tokens - response with derived addresses (Bitcoin-like accounts) and ERC20 tokens (Ethereum-like accounts), subject of tokens param
    • tokenBalances - same as tokens with balances, subject of tokens param
    • txs - tokenBalances + complete account transaction history
  • tokens — specifies which tokens (xpub addresses) are returned by the request (default nonzero)

    • nonzero - (Default) return only addresses with nonzero balance
    • used - return addresses with at least one transaction
    • derived - return all derived addresses
  • pagenumber transaction history page index, subject of details: txs

  • pageSizenumber transaction history page size, subject of details: txs

  • fromnumber transaction history from block filter, subject of details: txs

  • tonumber transaction history to block filter, subject of details: txs

  • gapnumber address derivation gap size, subject of details: tokens

  • contractFilterstring Ethereum-like accounts only: get ERC20 token info and balance

  • marker{ ledger: number, seq: number } XRP accounts only, transaction history page marker

  • defaultAccountType'normal' | 'segwit' | 'legacy' Bitcoin-like accounts only: specify which account group is displayed as default in popup, subject of Using discovery

  • suppressBackupWarning - boolean By default, this method will emit an event to show a warning if the wallet does not have a backup. This option suppresses the message.

Example

Get info about first bitcoin account

TrezorConnect.getAccountInfo({
    path: "m/49'/0'/0'",
    coin: 'btc',
});

Get info about account using public key (device is not used)

TrezorConnect.getAccountInfo({
    descriptor:
        'xpub6CVKsQYXc9awxgV1tWbG4foDvdcnieK2JkbpPEBKB5WwAPKBZ1mstLbKVB4ov7QzxzjaxNK6EfmNY5Jsk2cG26EVcEkycGW4tchT2dyUhrx',
    coin: 'btc',
});

Get info about account using BIP-0044 account discovery

TrezorConnect.getAccountInfo({
    coin: 'btc',
});

Result

AccountInfo type

{
    success: true,
    payload: {
        id: number,                           // account id
        path: string,                         // serialized path
        descriptor: string,                   // account public key
        legacyXpub?: string,                  // (optional) account public key in legacy format (only for segwit and segwit native accounts)
        balance: string,                      // account balance (confirmed transactions only)
        availableBalance: string,             // account balance (including unconfirmed transactions)
        addresses: {
            // subject of details:tokens param
            unused: Array<AccountAddress>, // unused addresses
            used: Array<AccountAddress>,   // used addresses
            change: Array<AccountAddress>, // change addresses (internal)
        }, // list of derived addresses grouped by purpose (Bitcoin-like accounts)
        history: Array<{
            total: number,
            unconfirmed: number,
            transactions?: Array<AccountTransaction>, // subject of details:txs param
        }> // account history object
        utxo?: Array<AccountUtxo>, // account utxos (Bitcoin-like accounts), subject of details:tokens param
        tokens?: Array<TokenInfo>, // account ERC20 tokens (Ethereum-like accounts), subject of details:tokens param
        misc?: {
            // Ethereum-like accounts only
            nonce: string,
            contractInfo?: TokenInfo, // subject of contractFilter param
            // XRP accounts only
            sequence?: number,
            reserve?: string,
        },
        page?: {
            // subject of details:txs param
            index: number, // current page index
            size: number,  // current page size
            total: number, // total pages count
        },
        marker?: {
            // XRP accounts only
            // subject of details:txs param
            ledger: number,
            seq: number,
        }

    } //
}

Error

{
    success: false,
    payload: {
        error: string // error message
    }
}

Bitcoin: get Ownership identifier

Export SLIP-0019 ownership identifier. Read more

const result = await TrezorConnect.getOwnershipId(params);

:note: Supported only by T2T1 with Firmware 2.4.4 or higher!

Params

Optional common params

Exporting single id

  • pathrequired string | Array<number> minimum length is 5. read more
  • coin - optional string

    Determines network definition specified in coins.json file. Coin shortcut, name or label can be used.

  • scriptTypeoptional InputScriptType
  • multisigoptional MultisigRedeemScriptType

Exporting bundle of ids

  • bundle - Array of Objects with fields listed above.

Example

Display id of the first bitcoin address:

TrezorConnect.getOwnershipId({
    path: "m/86'/0'/0'/0/0",
});

Return a bundle of ids:

TrezorConnect.getOwnershipId({
    bundle: [
        { path: "m/86'/0'/0'/0/0" }, // taproot
        { path: "m/84'/0'/0'/0/0" }, // bech32
        { path: "m/49'/0'/0'/0/0" }, // segwit
    ],
});

Result

OwnershipId type

Result with single id:

{
    success: true,
    payload: {
        ownership_id: string,
        path: number[],
        serializedPath: string
    }
}

Result with bundle of ids sorted by FIFO

{
    success: true,
    payload: [
        { ownership_id: string, path: number[], serializedPath: string }, // taproot
        { ownership_id: string, path: number[], serializedPath: string }, // bech32
        { ownership_id: string, path: number[], serializedPath: string }  // segwit
    ]
}

Error

{
    success: false,
    payload: {
        error: string // error message
    }
}

Bitcoin: get Ownership proof

Export SLIP-0019 ownership proof. Read more

const result = await TrezorConnect.getOwnershipProof(params);

:note: Supported only by T2T1 with Firmware 2.4.4 or higher!

Params

Optional common params

Exporting single proof

  • pathrequired string | Array<number> minimum length is 5. read more
  • coin - optional string

    Determines network definition specified in coins.json file. Coin shortcut, name or label can be used.

  • scriptTypeoptional InputScriptType
  • userConfirmationoptional boolean
  • ownershipIdsoptional Array<string>
  • commitmentDataoptional string
  • multisigoptional MultisigRedeemScriptType
  • preauthorizedoptional boolean read more

Exporting bundle of proofs

  • bundle - Array of Objects with fields listed above.

Example

Display ownership proof of the first bitcoin address:

TrezorConnect.getOwnershipProof({
    path: "m/86'/0'/0'/0/0",
});

Return a bundle of ownership proofs:

TrezorConnect.getOwnershipProof({
    bundle: [
        { path: "m/86'/0'/0'/0/0" }, // taproot
        { path: "m/84'/0'/0'/0/0" }, // bech32
        { path: "m/49'/0'/0'/0/0" }, // segwit
    ],
});

Result

OwnershipProof type

Result with single proof:

{
    success: true,
    payload: {
        ownership_proof: string,
        signature: string,
        path: number[],
        serializedPath: string
    }
}

Result with bundle of proofs sorted by FIFO

{
    success: true,
    payload: [
        { ownership_proof: string, signature: string, path: number[], serializedPath: string }, // taproot
        { ownership_proof: string, signature: string, path: number[], serializedPath: string }, // bech32
        { ownership_proof: string, signature: string, path: number[], serializedPath: string }  // segwit
    ]
}

Error

{
    success: false,
    payload: {
        error: string // error message
    }
}

Compose transaction

This method works only for Bitcoin and Bitcoin-like coins.

Can be used in two different ways and returned result depends on used parameters.

Requests a payment to a set of given outputs.

An automated payment process separated in to following steps:

  1. Account discovery for requested coin is performed and the user is asked for source account selection. [1]
  2. User is asked for fee level selection.
  3. Transaction is calculated, change output is added automatically if needed. [2]
  4. Signing transaction with Trezor, user is asked for confirmation on device.

Returned response is a signed transaction in hexadecimal format same as in signTransaction method.

Params:

Optional common params

  • outputsrequired Array of output objects described below
  • coinrequired string determines network definition specified in coins.json file. Coin shortcut, name or label can be used.
  • pushoptional boolean determines if composed transaction will be broadcasted into blockchain network. Default is set to false.
  • sequenceoptional number transaction input field used in RBF or locktime transactions

Precompose, prepare transaction to be signed by Trezor.

Skip first two steps of payment request (described above) by providing account and feeLevels params and perform only transaction calculation. [2]

The result, internally called PrecomposedTransaction is a set of params that can be used in signTransaction method afterwards.

This useful for the quick preparation of multiple variants for the same transaction using different fee levels or using incomplete data (like missing output addresses just to calculate fee)

Device and backend connection is not required for this case since all data are provided.

Params:

  • outputsrequired Array of output objects described below
  • coinrequired string determines network definition specified in coins.json file. Coin shortcut, name or label can be used.
  • accountrequired Object containing essential data, partial result of getAccountInfo method
    • path - required string
    • utxo - required Array
    • addresses - required string
  • feeLevelsrequired Array of objects. set of requested variants, partial result of blockchainEstimateFee method
    • feePerUnit - required string satoshi per transaction byte.
  • baseFeeoptional number base fee of transaction in satoshi. used in replacement transactions calculation (RBF) and DOGE
  • floorBaseFeeoptional boolean decide whenever baseFee should be floored to the nearest baseFee unit, prevents from fee overpricing. used in DOGE
  • sequenceoptional number transaction input field used in RBF or locktime transactions
  • skipPermutationoptional boolean do not sort calculated inputs/outputs (usage: RBF transactions)

Accepted output objects:

  • regular output
    • amount - required string value to send in satoshi
    • address - required string recipient address
  • send-max - spends all available inputs from account
    • type - required with send-max value
    • address - required string recipient address
  • opreturn - read more
    • type - required with opreturn value
    • dataHex - required hexadecimal string with arbitrary data
  • payment-noaddress - incomplete output, target address is not known yet. used only in precompose
    • type - required with payment-noaddress value
    • amount - required string value to send in satoshi
  • send-max-noaddress - incomplete output, target address is not known yet. used only in precompose
    • type - required with send-max-noaddress value

Examples

Payment example

Send 0.002 BTC to "18WL2iZKmpDYWk1oFavJapdLALxwSjcSk2"

TrezorConnect.composeTransaction({
    outputs: [
        { amount: "200000", address: "18WL2iZKmpDYWk1oFavJapdLALxwSjcSk2" }
    ]
    coin: "btc",
    push: true
});

Payment result

SignedTransaction type

{
    success: true,
    payload: {
        signatures: Array<string>, // signer signatures
        serializedTx: string,      // serialized transaction
        txid?: string,             // blockchain transaction id
    }
}

Error

{
    success: false,
    payload: {
        error: string // error message
    }
}

Precompose example

Prepare multiple variants of the same transaction

TrezorConnect.composeTransaction({
    outputs: [{ amount: '200000', address: 'tb1q9l0rk0gkgn73d0gc57qn3t3cwvucaj3h8wtrlu' }],
    coin: 'btc',
    account: {
        path: "m/84'/0'/0'",
        addresses: {
            used: [
                {
                    address: 'bc1qannfxke2tfd4l7vhepehpvt05y83v3qsf6nfkk',
                    path: "m/84'/0'/0'/0/0",
                    transfers: 1,
                },
            ],
            unused: [
                {
                    address: '',
                    path: "m/84'/0'/0'/0/1",
                    transfers: 0,
                },
            ],
            change: [
                {
                    address: 'bc1qktmhrsmsenepnnfst8x6j27l0uqv7ggrg8x38q',
                    path: "m/84'/0'/0'/1/0",
                    transfers: 0,
                },
            ],
        },
        utxo: [
            {
                txid: '86a6e02943dcd057cfbe349f2c2274478a3a1be908eb788606a6950e727a0d36',
                vout: 0,
                amount: '300000',
                blockHeight: 590093,
                address: 'bc1qannfxke2tfd4l7vhepehpvt05y83v3qsf6nfkk',
                path: "m/84'/0'/0'/0/0",
                confirmations: 100,
            },
        ],
    },
    feeLevels: [{ feePerUnit: '1' }, { feePerUnit: '5' }, { feePerUnit: '30' }],
});

Precompose result

{
    success: true,
    payload: [
        {
            type: 'final',
            totalSpent: '200167',
            fee: '167',
            feePerByte: '1',
            bytes: 167,
            inputs: [
                {
                    address_n: [84 | 0x80000000, 1 | 0x80000000, 0 | 0x80000000, 0, 0],
                    amount: "300000",
                    prev_hash: "86a6e02943dcd057cfbe349f2c2274478a3a1be908eb788606a6950e727a0d36",
                    prev_index: 0,
                    script_type: "SPENDWITNESS",
                }
            ],
            outputs: [
                {
                    address_n: [84 | 0x80000000, 1 | 0x80000000, 0 | 0x80000000, 1, 0],
                    amount: "99833",
                    script_type: "PAYTOWITNESS",
                },
                {
                    address: 'tb1q9l0rk0gkgn73d0gc57qn3t3cwvucaj3h8wtrlu',
                    amount: '200000',
                    script_type: 'PAYTOADDRESS',
                }
            ],
            outputsPermutation: [1, 0],
        },
        {
            type: 'final',
            totalSpent: '200835',
            fee: '835',
            feePerByte: '5',
            bytes: 167,
            inputs: [{ ... }],
            outputs: [{ ... }],
            outputsPermutation: [],
        },
        {
            type: 'error',
        }
    ]
}

For more examples see

Additional notes

  • [1] UI.SELECT_ACCOUNT and UI.SELECT_FEE events are emitted when using trusted mode
  • [2] Account utxo selection, fee and change calculation is performed by @trezor/utxo-lib module

Sign transaction

Asks device to sign given inputs and outputs of pre-composed transaction. User is asked to confirm all transaction details on Trezor.

const result = await TrezorConnect.signTransaction(params);

Params

Optional common params

SignTransaction type

  • coin - required string

    Determines network definition specified in coins.json file. Coin shortcut, name or label can be used. See supported coins

  • inputs - required Array of PROTO.TxInputType,
  • outputs - required Array of PROTO.TxOutputType,
  • paymentRequests - optional Array of PROTO.TxAckPaymentRequest. See SLIP-24
  • refTxs - optional Array of RefTransaction.

    If you don't want to use build-in blockbook backend you can optionally provide those data from your own backend transformed to Trezor format. Since Firmware 2.3.0/1.9.0 referenced transactions are required. Zcash and Komodo refTxs should also contains expiry, version_group_id and extra_data fields.

  • locktime - optional number,
  • version - optional number transaction version,
  • expiry - optional number, only for Decred and Zcash,
  • versionGroupId - optional number only for Zcash, nVersionGroupId when overwintered is set,
  • overwintered - optional boolean only for Zcash
  • timestamp - optional number only for Capricoin, transaction timestamp,
  • branchId - optional number, only for Zcash, BRANCH_ID when overwintered is set
  • push - optional boolean Broadcast signed transaction to blockchain. Default is set to false
  • amountUnitoptional PROTO.AmountUnit

    show amounts in BTC, mBTC, uBTC, sat

  • unlockPath - optional PROTO.UnlockPath, the result of TrezorConnect.unlockPath method.
  • serialize - optional boolean, default true serialize the full transaction, as opposed to only outputting the signatures
  • chunkifyoptional boolean determines if recipient address will be displayed in chunks of 4 characters. Default is set to false

Example

PAYTOADDRESS
TrezorConnect.signTransaction({
    inputs: [
        {
            address_n: [
                (44 | 0x80000000) >>> 0,
                (0 | 0x80000000) >>> 0,
                (2 | 0x80000000) >>> 0,
                1,
                0,
            ],
            prev_index: 0,
            prev_hash: 'b035d89d4543ce5713c553d69431698116a822c57c03ddacf3f04b763d1999ac',
            amount: 3431747,
        },
    ],
    outputs: [
        {
            address_n: [
                (44 | 0x80000000) >>> 0,
                (0 | 0x80000000) >>> 0,
                (2 | 0x80000000) >>> 0,
                1,
                1,
            ],
            amount: 3181747,
            script_type: 'PAYTOADDRESS',
        },
        {
            address: '18WL2iZKmpDYWk1oFavJapdLALxwSjcSk2',
            amount: 200000,
            script_type: 'PAYTOADDRESS',
        },
    ],
    coin: 'btc',
});
SPENDP2SHWITNESS
TrezorConnect.signTransaction({
    inputs: [
        {
            address_n: [
                (49 | 0x80000000) >>> 0,
                (0 | 0x80000000) >>> 0,
                (2 | 0x80000000) >>> 0,
                1,
                0,
            ],
            prev_index: 0,
            prev_hash: 'b035d89d4543ce5713c553d69431698116a822c57c03ddacf3f04b763d1999ac',
            amount: 3431747,
            script_type: 'SPENDP2SHWITNESS',
        },
    ],
    outputs: [
        {
            address_n: [
                (49 | 0x80000000) >>> 0,
                (0 | 0x80000000) >>> 0,
                (2 | 0x80000000) >>> 0,
                1,
                1,
            ],
            amount: 3181747,
            script_type: 'PAYTOP2SHWITNESS',
        },
        {
            address: '18WL2iZKmpDYWk1oFavJapdLALxwSjcSk2',
            amount: 200000,
            script_type: 'PAYTOADDRESS',
        },
    ],
    coin: 'btc',
});
PAYTOADDRESS with refTxs (transaction data provided from custom backend)
TrezorConnect.signTransaction({
    inputs: [
        {
            address_n: [
                (44 | 0x80000000) >>> 0,
                (0 | 0x80000000) >>> 0,
                (2 | 0x80000000) >>> 0,
                1,
                0,
            ],
            prev_index: 0,
            prev_hash: 'b035d89d4543ce5713c553d69431698116a822c57c03ddacf3f04b763d1999ac',
            amount: 3431747,
        },
    ],
    outputs: [
        {
            address_n: [
                (44 | 0x80000000) >>> 0,
                (0 | 0x80000000) >>> 0,
                (2 | 0x80000000) >>> 0,
                1,
                1,
            ],
            amount: 3181747,
            script_type: 'PAYTOADDRESS',
        },
        {
            address: '18WL2iZKmpDYWk1oFavJapdLALxwSjcSk2',
            amount: 200000,
            script_type: 'PAYTOADDRESS',
        },
    ],
    refTxs: [
        {
            hash: 'b035d89d4543ce5713c553d69431698116a822c57c03ddacf3f04b763d1999ac',
            inputs: [
                {
                    prev_hash: '448946a44f1ef514601ccf9b22cc3e638c69ea3900b67b87517ea673eb0293dc',
                    prev_index: 0,
                    script_sig:
                        '47304402202872cb8459eed053dcec0f353c7e293611fe77615862bfadb4d35a5d8807a4cf022015057aa0aaf72ab342b5f8939f86f193ad87b539931911a72e77148a1233e022012103f66bbe3c721f119bb4b8a1e6c1832b98f2cf625d9f59242008411dd92aab8d94',
                    sequence: 4294967295,
                },
            ],
            bin_outputs: [
                {
                    amount: 3431747,
                    script_pubkey: '76a91441352a84436847a7b660d5e76518f6ebb718dedc88ac',
                },
                {
                    amount: 10000,
                    script_pubkey: '76a9141403b451c79d34e6a7f6e36806683308085467ac88ac',
                },
            ],
            lock_time: 0,
            version: 1,
        },
    ],
    coin: 'btc',
});

Result

SignedTransaction type

{
    success: true,
    payload: {
        signatures: Array<string>, // Array of signer signatures
        serializedTx: string,      // serialized transaction
        txid?: string,             // broadcasted transaction id
    }
}

Error

{
    success: false,
    payload: {
        error: string // error message
    }
}

Push transaction

Broadcast the transaction to the selected network.

const result = await TrezorConnect.pushTransaction(params);

Params

Optional common params

PushTransaction type

  • tx - required string serialized transaction,
  • coin - required string Determines network definition specified in coins.json file. Coin shortcut, name or label can be used.

Example

TrezorConnect.pushTransaction({
    tx: '010000000182488650ef25a58fef6788bd71b8212038d7f2bbe4750bc7bcb44701e85ef6d5000000006b4830450221009a0b7be0d4ed3146ee262b42202841834698bb3ee39c24e7437df208b8b7077102202b79ab1e7736219387dffe8d615bbdba87e11477104b867ef47afed1a5ede7810121023230848585885f63803a0a8aecdd6538792d5c539215c91698e315bf0253b43dffffffff0160cc0500000000001976a914de9b2a8da088824e8fe51debea566617d851537888ac00000000',
    coin: 'btc',
});

Result

PushedTransaction type

{
    success: true,
    payload: {
        txid: string, // transaction id
    }
}

Error

{
    success: false,
    payload: {
        error: string // error message
    }
}

Sign message

Asks device to sign a message using the private key derived by given BIP32 path.

const result = await TrezorConnect.signMessage(params);

Params

Optional common params

SignMessage type

  • pathrequired string | Array<number> minimum length is 3. read more
  • message - required string
  • coin - optional string Determines network definition specified in coins.json file. Coin shortcut, name or label can be used. If coin is not set API will try to get network definition from path.
  • hex - optional boolean convert message from hex

Example

TrezorConnect.signMessage({
    path: "m/44'/0'/0'",
    message: 'example message',
});

Result

MessageSignature type

{
    success: true,
    payload: {
        address: string,   // signer address
        signature: string, // signature in base64 format
    }
}

Error

{
    success: false,
    payload: {
        error: string // error message
    }
}

Verify message

Asks device to verify a message using the signer address and signature.

const result = await TrezorConnect.verifyMessage(params);

Params

Optional common params

VerifyMessage type

  • address - required string signer address,
  • message - required string signed message,
  • signature - required string signature in base64 format,
  • coin - required string Determines network definition specified in coins.json file. Coin shortcut, name or label can be used.
  • hex - optional boolean convert message from hex

Example

TrezorConnect.verifyMessage({
    address: '3BD8TL6iShVzizQzvo789SuynEKGpLTms9',
    message: 'example message',
    signature:
        'JO7vL3tOB1qQyfSeIVLvdEw9G1tCvL+lNj78XDAVM4t6UptADs3kXDTO2+2ZeEOLFL4/+wm+BBdSpo3kb3Cnsas=',
    coin: 'btc',
});

Result

Success type

{
    success: true,
    payload: {
        message: "Message verified"
    }
}

Error

{
    success: false,
    payload: {
        error: string // error message
    }
}

Bitcoin: authorize coinjoin

Allow device to do preauthorized operations in signTransaction and getOwnershipProof methods without further user interaction.

Permission persists until physical device disconnection or maxRounds limit is reached.

const result = await TrezorConnect.authorizeCoinjoin(params);

:warning: This feature is experimental! Do not use it in production!

:note: Supported only by T2T1 with Firmware 2.5.3 or higher!

Params

Optional common params

Exporting single id

  • pathrequired string | Array<number>

    prefix of the BIP-32 path leading to the account (m / purpose' / coin_type' / account')read more

  • coordinatorrequired string

    coordinator identifier to approve as a prefix in commitment data (max. 36 ASCII characters)

  • maxRoundsrequired number

    maximum number of rounds that Trezor is authorized to take part in

  • maxCoordinatorFeeRaterequired number

    maximum coordination fee rate in units of 10**6 percent

  • maxFeePerKvbyterequired number

    maximum mining fee rate in units of satoshis per 1000 vbytes

  • coin - optional string

    Determines network definition specified in coins.json file. Coin shortcut, name or label can be used.

  • scriptTypeoptional PROTO.InputScriptType

    used to distinguish between various address formats (non-segwit, segwit, etc.)

  • amountUnitoptional PROTO.AmountUnit

    show amounts in

  • preauthorizedoptional

    Check if device session is already preauthorized and take no further action if so

  • coinjoinRequestoptional PROTO.CoinJoinRequest

    Signing request for a coinjoin transaction

Example:

TrezorConnect.authorizeCoinjoin({
    path: "m/10086'/0'/0'",
    maxRounds: 3,
    maxCoordinatorFeeRate: 500000, // 0.5% => 0.005 * 10**8;
    maxFeePerKvbyte: 3500,
    scriptType: 'SPENDWITNESS',
});

Result

Success type

{
    success: true,
    payload: {
        message: 'Coinjoin authorized'
    }
}

Error

{
    success: false,
    payload: {
        error: string // error message
    }
}

Ethereum: get address

Display requested address derived by given BIP32 path on device and returns it to caller. User is presented with a description of the requested key and asked to confirm the export on Trezor.

const result = await TrezorConnect.ethereumGetAddress(params);

Params

Optional common params

Exporting single address

  • pathrequired string | Array<number> minimum length is 5. read more
  • addressrequired string address for validation (read Handle button request section below)
  • showOnTrezoroptional boolean determines if address will be displayed on device. Default is set to true
  • chunkifyoptional boolean determines if address will be displayed in chunks of 4 characters. Default is set to false

Exporting bundle of addresses

  • bundle - Array of Objects with path and showOnTrezor fields

Handle button request

Since [email protected] there is a possibility to handle UI.ADDRESS_VALIDATION event which will be triggered once the address is displayed on the device. You can handle this event and display custom UI inside of your application.

If certain conditions are fulfilled popup will not be used at all:

  • the user gave permissions to communicate with Trezor
  • device is authenticated by pin/passphrase
  • application has TrezorConnect.on(UI.ADDRESS_VALIDATION, () => {}); listener registered
  • parameter address is set
  • parameter showOnTrezor is set to true (or not set at all)
  • application is requesting ONLY ONE(!) address

Example

Display address of first ethereum account:

TrezorConnect.ethereumGetAddress({
    path: "m/44'/60'/0'/0/0",
});

Return a bundle of ethereum addresses without displaying them on device:

TrezorConnect.ethereumGetAddress({
    bundle: [
        { path: "m/44'/60'/0'/0/0", showOnTrezor: false }, // account 1
        { path: "m/44'/60'/1'/0/0", showOnTrezor: false }, // account 2
        { path: "m/44'/60'/2'/0/0", showOnTrezor: false }, // account 3
    ],
});

Validate address using custom UI inside of your application:

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

TrezorConnect.on(UI.ADDRESS_VALIDATION, data => {
    console.log('Handle button request', data.address, data.serializedPath);
    // here you can display custom UI inside of your app
});

const result = await TrezorConnect.ethereumGetAddress({
    path: "m/44'/60'/0'/0/0",
    address: '0x73d0385F4d8E00C5e6504C6030F47BF6212736A8',
});
// dont forget to hide your custom UI after you get the result!

Result

Address type

Result with only one address

{
    success: true,
    payload: {
        address: string,     // displayed address
        path: Array<number>, // hardended path
        serializedPath: string,
    }
}

Result with bundle of addresses sorted by FIFO

{
    success: true,
    payload: [
        { address: string, path: Array<number>, serializedPath: string }, // account 1
        { address: string, path: Array<number>, serializedPath: string }, // account 2
        { address: string, path: Array<number>, serializedPath: string }  // account 3
    ]
}

Error

{
    success: false,
    payload: {
        error: string // error message
    }
}

Ethereum: Sign transaction

Asks device to sign given transaction using the private key derived by given BIP32 path. User is asked to confirm all transaction details on Trezor.

const result = await TrezorConnect.ethereumSignTransaction(params);

Params

Optional common params

EthereumSignTransaction type

  • pathrequired string | Array<number> minimum length is 3. read more
  • transaction - required Object type of EthereumTransactionEIP1559|EthereumSignTransaction "0x" prefix for each field is optional
  • chunkifyoptional boolean determines if recipient address will be displayed in chunks of 4 characters. Default is set to false

Examples

EIP1559 (after The London Upgrade)

:warning: Supported firmware versions T1B1 v1.10.4 and T2T1 v2.4.2

If both parameters maxFeePerGas and maxPriorityFeePerGas are defined, transaction will be signed as the new type introduced in EIP1559.

TrezorConnect.ethereumSignTransaction({
    path: "m/44'/60'/0'",
    transaction: {
        to: '0xd0d6d6c5fe4a677d343cc433536bb717bae167dd',
        value: '0xf4240',
        data: '0xa',
        chainId: 1,
        nonce: '0x0',
        maxFeePerGas: '0x14',
        maxPriorityFeePerGas: '0x0',
        gasLimit: '0x14',
    },
});

Legacy

For T1B1 and T2T1 with firmware older than 2.4.2 (but supported newer firmware versions too).

TrezorConnect.ethereumSignTransaction({
    path: "m/44'/60'/0'",
    transaction: {
        to: '0x7314e0f1c0e28474bdb6be3e2c3e0453255188f8',
        value: '0xf4240',
        data: '0x01',
        chainId: 1,
        nonce: '0x0',
        gasLimit: '0x5208',
        gasPrice: '0xbebc200',
    },
});

Result

EthereumSignedTx type

{
    success: true,
    payload: {
        v: string, // hexadecimal string with "0x" prefix
        r: string, // hexadecimal string with "0x" prefix
        s: string, // hexadecimal string with "0x" prefix
    }
}

Error

{
    success: false,
    payload: {
        error: string // error message
    }
}

Ethereum: sign message

Asks device to sign a message using the private key derived by given BIP32 path.

const result = await TrezorConnect.ethereumSignMessage(params);

Params

Optional common params EthereumSignMessage type

  • pathrequired string | Array<number> minimum length is 3. read more
  • message - required string message to sign in plain text
  • hex - optional boolean convert message from hex

Example

TrezorConnect.ethereumSignMessage({
    path: "m/44'/60'/0'",
    message: 'example message',
});

Result

MessageSignature type

{
    success: true,
    payload: {
        address: string,
        signature: string,
    }
}

Error

{
    success: false,
    payload: {
        error: string // error message
    }
}

Ethereum: Sign Typed Data

Asks device to sign an EIP-712 typed data message using the private key derived by given BIP32 path.

User is asked to confirm all signing details on T2T1.

const result = await TrezorConnect.ethereumSignTypedData(params);

:warning: Supported only by T2T1 with Firmware 2.4.3 or higher! :warning: Blind signing is supported only on T1B1 with Firmware 1.10.5 or higher!

Params

Optional common params

EthereumSignTypedData type

:warning: Domain-only signing (data.primaryType = "EIP712Domain") is supported only on T2T1 with Firmware 2.4.4 or higher!

Blind signing (optional addition for T1B1 compatibility)

T1B1 firmware currently does not support constructing EIP-712 hashes.

However, it supports signing pre-constructed hashes.

EIP-712 hashes can be constructed with the plugin function at @trezor/connetct-plugin-ethereum.

You may also wish to contruct your own hashes using a different library.

EthereumSignTypedHash type

:warning: Domain-only signing (empty message_hash) is supported only on T1B1 with Firmware 1.10.6 or higher!

  • domain_separator_hash - required string hex-encoded 32-byte hash of the EIP-712 domain.
  • message_hash - optional string hex-encoded 32-byte hash of the EIP-712 message. This is optional for the domain-only hashes where primaryType is EIP712Domain.

Example

const eip712Data = {
    types: {
        EIP712Domain: [
            {
                name: 'name',
                type: 'string',
            },
        ],
        Message: [
            {
                name: 'Best Wallet',
                type: 'string',
            },
            {
                name: 'Number',
                type: 'uint64',
            },
        ],
    },
    primaryType: 'Message',
    domain: {
        name: 'example.trezor.io',
    },
    message: {
        'Best Wallet': 'Trezor Model T',
        // be careful with JavaScript numbers: MAX_SAFE_INTEGER is quite low
        Number: `${2n ** 55n}`,
    },
};

// This functionality is separate from @trezor/connect, as it requires @metamask/eth-sig-utils,
// which is a large JavaScript dependency
const transformTypedDataPlugin = require('@trezor/connect-plugin-ethereum');
const { domain_separator_hash, message_hash } = transformTypedDataPlugin(eip712Data, true);

TrezorConnect.ethereumSignTypedData({
    path: "m/44'/60'/0'",
    data: eip712Data,
    metamask_v4_compat: true,
    // These are optional, but required for T1B1 compatibility
    domain_separator_hash,
    message_hash,
});

Result

EthereumMessageSignature type

{
    success: true,
    payload: {
        address: string,
        signature: string, // hexadecimal string with "0x" prefix
    }
}

Error

{
    success: false,
    payload: {
        error: string // error message
    }
}

Ethereum: verify message

Asks device to verify a message using the signer address and signature.

const result = await TrezorConnect.ethereumVerifyMessage(params);

Params

Optional common params

EthereumVerifyMessage type

  • address - required string signer address. "0x" prefix is optional
  • message - required string signed message in plain text
  • hex - optional boolean convert message from hex
  • signature - required string signature in hexadecimal format. "0x" prefix is optional

Example

TrezorConnect.ethereumVerifyMessage({
    address: '0xdA0b608bdb1a4A154325C854607c68950b4F1a34',
    message: 'Example message',
    signature:
        '11dc86c631ef5d9388c5e245501d571b864af1a717cbbb3ca1f6dacbf330742957242aa52b36bbe7bb46dce6ff0ead0548cc5a5ce76d0aaed166fd40cb3fc6e51c',
});

Result

Success type

{
    success: true,
    payload: {
        message: "Message verified"
    }
}

Error

{
    success: false,
    payload: {
        error: string // error message
    }
}

Eos: get public key

Display requested public key derived by given BIP44 path on device and returns it to caller. User is presented with a description of the requested public key and asked to confirm the export.

const result = await TrezorConnect.eosGetPublicKey(params);

Params

Optional common params

Exporting single address

  • pathrequired string | Array<number> minimum length is 5. read more
  • showOnTrezoroptional boolean determines if address will be displayed on device. Default is set to true
  • chunkifyoptional boolean determines if address will be displayed in chunks of 4 characters. Default is set to false

Exporting bundle of addresses

  • bundle - Array of Objects with path and showOnTrezor fields

Example

Displays public key derived from BIP44 path:

TrezorConnect.eosGetPublicKey({
    path: "m/44'/194'/0'/0/0",
});

Return a bundle of public keys without displaying them on device:

TrezorConnect.eosGetPublicKey({
    bundle: [
        { path: "m/44'/194'/0'/0/0", showOnTrezor: false }, // public key 1
        { path: "m/44'/194'/0'/0/1", showOnTrezor: false }, // public key 2
        { path: "m/44'/194'/0'/0/2", showOnTrezor: false }, // public key 3
    ],
});

Result

EosPublicKey type

Result with only one public key

{
    success: true,
    payload: {
        wifPublicKey: string,
        rawPublicKey: string,
        path: number[],
        serializedPath: string
    }
}

Result with bundle of public keys sorted by FIFO

{
    success: true,
    payload: [
        { wifPublicKey: string, rawPublicKey: string, path: number[], serializedPath: string }, // public key 1
        { wifPublicKey: string, rawPublicKey: string, path: number[], serializedPath: string }, // public key 2
        { wifPublicKey: string, rawPublicKey: string, path: number[], serializedPath: string }  // public key 3
    ]
}

Error

{
    success: false,
    payload: {
        error: string // error message
    }
}

Eos: sign transaction

Asks device to sign given transaction using the private key derived by given BIP44 path. User is asked to confirm all transaction details on Trezor.

const result = await TrezorConnect.eosSignTransaction(params);

Params

Optional common params

EosSignTransaction type

  • pathrequired string | Array<number> minimum length is 3. read more
  • transaction - required Object type of EosSDKTransaction
  • chunkifyoptional boolean determines if recipient address will be displayed in chunks of 4 characters. Default is set to false

Transfer example

TrezorConnect.eosSignTransaction({
    path: "m/44'/194'/0'/0/0",
    transaction: {
        chainId: 'cf057bbfb72640471fd910bcb67639c22df9f92470936cddc1ade0e2f2e7dc4f',
        header: {
            expiration: '2019-12-01T12:00:00',
            refBlockNum: 6439,
            refBlockPrefix: 2995713264,
            maxNetUsageWords: 0,
            maxCpuUsageMs: 0,
            delaySec: 0,
        },
        actions: [
            {
                account: 'eosio.token',
                authorization: [
                    {
                        actor: 'abcdefg12345',
                        permission: 'active',
                    },
                ],
                name: 'transfer',
                data: {
                    from: 'abcdefg12345',
                    to: '12345abcdefg',
                    quantity: '1.0000 EOS',
                    memo: 'memo',
                },
            },
        ],
    },
});

Result

EosSignedTx type

{
    success: true,
    payload: {
        signature: string, // hexadecimal string
    }
}

Error

{
    success: false,
    payload: {
        error: string // error message
    }
}

NEM: get address

Display requested address on device and returns it to caller. User is presented with a description of the requested key and asked to confirm the export.

const result = await TrezorConnect.nemGetAddress(params);

Params

Optional common params

Exporting single address

  • pathrequired string | Array<number> minimum length is 3. read more
  • addressoptional string address for validation (read Handle button request section below)
  • networkoptional number 0x68 - Mainnet, 0x96 - Testnet, 0x60 - Mijin. Default is set to Mainnet
  • showOnTrezoroptional boolean determines if address will be displayed on device. Default is set to true
  • chunkifyoptional boolean determines if address will be displayed in chunks of 4 characters. Default is set to false

Exporting bundle of addresses

  • bundle - Array of Objects with path, network and showOnTrezor fields

Handle button request

Since [email protected] there is a possibility to handle UI.ADDRESS_VALIDATION event which will be triggered once the address is displayed on the device. You can handle this event and display custom UI inside of your application.

If certain conditions are fulfilled popup will not be used at all:

  • the user gave permissions to communicate with Trezor
  • device is authenticated by pin/passphrase
  • application has TrezorConnect.on(UI.ADDRESS_VALIDATION, () => {}); listener registered
  • parameter address is set
  • parameter showOnTrezor is set to true (or not set at all)
  • application is requesting ONLY ONE(!) address

Example

Display address of third nem account:

TrezorConnect.nemGetAddress({
    path: "m/44'/43'/2'",
});

Return a bundle of NEM addresses without displaying them on device:

TrezorConnect.nemGetAddress({
    bundle: [
        { path: "m/44'/43'/0'", showOnTrezor: false }, // account 1
        { path: "m/44'/43'/1'", showOnTrezor: false }, // account 2
        { path: "m/44'/43'/2'", showOnTrezor: false }, // account 3
    ],
});

Validate address using custom UI inside of your application:

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

TrezorConnect.on(UI.ADDRESS_VALIDATION, data => {
    console.log('Handle button request', data.address, data.serializedPath);
    // here you can display custom UI inside of your app
});

const result = await TrezorConnect.nemGetAddress({
    path: "m/44'/43'/0'",
    address: 'TDS7OQUHKNYMSC2WPJA6QUTLJIO22S27B4FMU2AJ',
});
// dont forget to hide your custom UI after you get the result!

Result

Address type

Result with only one address

{
    success: true,
    payload: {
        address: string,
        path: Array<number>,
        serializedPath: string,
    }
}

Result with bundle of addresses

{
    success: true,
    payload: [
        { address: string, path: Array<number>, serializedPath: string }, // account 1
        { address: string, path: Array<number>, serializedPath: string }, // account 2
        { address: string, path: Array<number>, serializedPath: string }, // account 3
    ]
}

Error

{
    success: false,
    payload: {
        error: string // error message
    }
}

NEM: Sign transaction

Asks device to sign given transaction. User is asked to confirm all transaction details on Trezor.

const result = await TrezorConnect.nemSignTransaction(params);

Params

Optional common params

NEMSignTransaction type

  • path - required string | Array<number>
  • transaction - required Object type of NEMTransaction
  • chunkifyoptional boolean determines if recipient address will be displayed in chunks of 4 characters. Default is set to false

Example

Sign simple transaction

// common utility for hexlify message
function hexlify(str) {
    var result = '';
    var padding = '00';
    for (var i=0, l=str.length; i<l; i++) {
        var digit = str.charCodeAt(i).toString(16);
        var padded = (padding+digit).slice(-2);
        result += padded;
    }
    return result;
}

TrezorConnect.nemSignTransaction(
    path: "m/44'/1'/0'/0'/0'",
    transaction: {
        timeStamp: 74649215,
        amount: 2000000,
        fee: 2000000,
        recipient: "TALICE2GMA34CXHD7XLJQ536NM5UNKQHTORNNT2J",
        type: 257,
        deadline: 74735615,
        version: (0x98 << 24),
        message: {
            payload: hexlify('test_nem_transaction_transfer'),
            type: 1,
        },
    }
});

Sign mosaic transaction

TrezorConnect.nemSignTransaction(
    path: "m/44'/1'/0'/0'/0'",
    transaction: {
        timeStamp: 76809215,
        amount: 1000000,
        fee: 1000000,
        recipient: "TALICE2GMA34CXHD7XLJQ536NM5UNKQHTORNNT2J",
        type: 257,
        deadline: 76895615,
        version: (0x98 << 24),
        message: {},
        mosaics: [
            {
                mosaicId: {
                    namespaceId: "nem",
                    name: "xem",
                },
                quantity: 1000000,
            }
        ]
    }
});

Result

NEMSignedTx type

{
    success: true,
    payload: {
        data: string,
        signature: string,
    }
}

Error

{
    success: false,
    payload: {
        error: string // error message
    }
}

Stellar: get address

Display requested address on device and returns it to caller. User is presented with a description of the requested key and asked to confirm the export.

const result = await TrezorConnect.stellarGetAddress(params);

Params

Optional common params

Exporting single address

  • pathrequired string | Array<number> minimum length is 3. read more
  • addressoptional string address for validation (read Handle button request section below)
  • showOnTrezoroptional boolean determines if address will be displayed on device. Default is set to true
  • chunkifyoptional boolean determines if address will be displayed in chunks of 4 characters. Default is set to false

Exporting bundle of addresses

  • bundle - Array of Objects with path and showOnTrezor fields

Handle button request

Since [email protected] there is a possibility to handle UI.ADDRESS_VALIDATION event which will be triggered once the address is displayed on the device. You can handle this event and display custom UI inside of your application.

If certain conditions are fulfilled popup will not be used at all:

  • the user gave permissions to communicate with Trezor
  • device is authenticated by pin/passphrase
  • application has TrezorConnect.on(UI.ADDRESS_VALIDATION, () => {}); listener registered
  • parameter address is set
  • parameter showOnTrezor is set to true (or not set at all)
  • application is requesting ONLY ONE(!) address

Example

Display address of first stellar account:

TrezorConnect.stellarGetAddress({
    path: "m/44'/148'/0'",
});

Return a bundle of stellar addresses without displaying them on device:

TrezorConnect.stellarGetAddress({
    bundle: [
        { path: "m/44'/148'/0'", showOnTrezor: false }, // account 1
        { path: "m/44'/148'/1'", showOnTrezor: false }, // account 2
        { path: "m/44'/148'/2'", showOnTrezor: false }, // account 3
    ],
});

Validate address using custom UI inside of your application:

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

TrezorConnect.on(UI.ADDRESS_VALIDATION, data => {
    console.log('Handle button request', data.address, data.serializedPath);
    // here you can display custom UI inside of your app
});

const result = await TrezorConnect.stellarGetAddress({
    path: "m/44'/148'/0'/0/0",
    address: 'GAXSFOOGF4ELO5HT5PTN23T5XE6D5QWL3YBHSVQ2HWOFEJNYYMRJENBV',
});
// dont forget to hide your custom UI after you get the result!

Result

Address type

Result with only one address

{
    success: true,
    payload: {
        address: string,
        path: Array<number>,
        serializedPath: string,
    }
}

Result with bundle of addresses

{
    success: true,
    payload: [
        { address: string, path: Array<number>, serializedPath: string }, // account 1
        { address: string, path: Array<number>, serializedPath: string }, // account 2
        { address: string, path: Array<number>, serializedPath: string }, // account 3
    ]
}

Error

{
    success: false,
    payload: {
        error: string // error message
    }
}

Stellar: Sign transaction

Asks device to sign given transaction. User is asked to confirm all transaction details on Trezor.

const result = await TrezorConnect.stellarSignTransaction(params);

Params

Optional common params

StellarSignTransaction type

  • pathrequired string | Array<number> minimum length is 3. read more
  • networkPassphrase - required string network passphrase
  • transaction - required Object type of StellarTransaction

Stellar SDK compatibility

@stellar/stellar-sdk is not a part of trezor-connect repository. To transform StellarSDK.Transaction object into TrezorConnect.StellarTransaction, install @trezor/connect-plugin-stellar into your project.

import * as StellarSDK from '@stellar/stellar-sdk';
import transformTrezorTransaction from '<path-to-plugin>/index.js';

const tx = new StellarSdk.TransactionBuilder(...);
...
tx.build();

const params = transformTrezorTransaction(tx);
const result = TrezorConnect.stellarSignTransaction(params);

if (result.success) {
    tx.addSignature('account-public-key', result.payload.signature);
}

Example

TrezorConnect.stellarSignTransaction(
    path: "m/44'/148'/0'",
    networkPassphrase: "Test SDF Network ; September 2015",
    transaction: {
        source: "GAXSFOOGF4ELO5HT5PTN23T5XE6D5QWL3YBHSVQ2HWOFEJNYYMRJENBV",
        fee: 100,
        sequence: 4294967296,
        memo: {
            type: 0,
        },
        operations: [
            {
                type: "payment",
                source: "GAXSFOOGF4ELO5HT5PTN23T5XE6D5QWL3YBHSVQ2HWOFEJNYYMRJENBV",
                destination: "GAXSFOOGF4ELO5HT5PTN23T5XE6D5QWL3YBHSVQ2HWOFEJNYYMRJENBV",
                amount: "10000"
            }
        ]
    }
});

Result

StellarSignedTx type

{
    success: true,
    payload: {
        publicKey: string,
        signature: string,
    }
}

Error

{
    success: false,
    payload: {
        error: string // error message
    }
}

Cardano: get public key

Retrieves BIP32-Ed25519 extended public derived by given BIP32-Ed25519 path. User is presented with a description of the requested key and asked to confirm the export on Trezor.

const result = await TrezorConnect.cardanoGetPublicKey(params);

Params

Optional common params

CardanoGetPublicKey type

Exporting single public key

  • pathrequired string | Array<number> minimum length is 3. read more
  • showOnTrezoroptional boolean determines if publick key will be displayed on device. Default is set to true
  • derivationTypeoptional CardanoDerivationType enum. determines used derivation type. Default is set to ICARUS_TREZOR=2
  • suppressBackupWarning - optional boolean By default, this method will emit an event to show a warning if the wallet does not have a backup. This option suppresses the message.

Exporting bundle of publick keys

  • bundle - Array of Objects with path and showOnTrezor fields

Example

Display public key of first cardano account:

TrezorConnect.cardanoGetPublicKey({
    path: "m/44'/1815'/0'",
});

Return a bundle of cardano public keys without displaying them on device:

TrezorConnect.cardanoGetPublicKey({
    bundle: [
        { path: "m/44'/1815'/0'", showOnTrezor: false }, // account 1
        { path: "m/44'/1815'/1'", showOnTrezor: false }, // account 2
        { path: "m/44'/1815'/2'", showOnTrezor: false }, // account 3
    ],
});

Result

CardanoPublicKey type

Result with only one public key

{
    success: true,
    payload: {
        path: Array<number>, // hardended path
        serializedPath: string,
        publicKey: string,
        node: HDPubNode,
    }
}

Result with a bundle of public keys

{
    success: true,
    payload: [
        { path: Array<number>, serializedPath: string, publicKey: string, node: HDPubNode }, // account 1
        { path: Array<number>, serializedPath: string, publicKey: string, node: HDPubNode}, // account 2
        { path: Array<number>, serializedPath: string, publicKey: string, node: HDPubNode }  // account 3
    ]
}

Error

{
    success: false,
    payload: {
        error: string // error message
    }
}

Cardano: get address

Display requested address derived by given BIP32-Ed25519 path on device and returns it to caller. User is presented with a description of the requested key and asked to confirm the export on Trezor.

const result = await TrezorConnect.cardanoGetAddress(params);

Params

Optional common params

CardanoGetAddress type

Exporting single address

  • addressParametersrequired see description below
  • addressoptional string address for validation (read Handle button request section below)
  • protocolMagic - required Integer 764824073 for Mainnet, 1 for Preprod Testnet, 2 for Preview Testnet
  • networkId - required Integer 1 for Mainnet, 0 for Testnet
  • showOnTrezoroptional boolean determines if address will be displayed on device. Default is set to true
  • derivationTypeoptional CardanoDerivationType enum. determines used derivation type. Default is set to ICARUS_TREZOR=2
  • chunkifyoptional boolean determines if address will be displayed in chunks of 4 characters. Default is set to false

Exporting bundle of addresses

  • bundle - Array of Objects with single address fields

Address Parameters

CardanoAddressParameters type
  • addressType - required CardanoAddressType/number - you can use the flow CARDANO.ADDRESS_TYPE object or typescript CardanoAddressType enum. Supports all address types.
  • pathrequired string | Array<number> minimum length is 5. read more
  • stakingPathoptional string | Array<number> minimum length is 5. read more Used for base and reward address derivation
  • stakingKeyHash - optional string hex string of staking key hash. Used for base address derivation (as an alternative to stakingPath)
  • certificatePointer - optional CardanoCertificatePointer object. Must contain numbers blockIndex, txIndex and certificateIndex. Used for pointer address derivation. read more about pointer address
  • paymentScriptHash - optional string hex string of payment script hash.
  • stakingScriptHash - optional string hex string of staking script hash.

Handle button request

Since [email protected] there is a possibility to handle UI.ADDRESS_VALIDATION event which will be triggered once the address is displayed on the device. You can handle this event and display custom UI inside of your application.

If certain conditions are fulfilled popup will not be used at all:

  • the user gave permissions to communicate with Trezor
  • device is authenticated by pin/passphrase
  • application has TrezorConnect.on(UI.ADDRESS_VALIDATION, () => {}); listener registered
  • parameter address is set
  • parameter showOnTrezor is set to true (or not set at all)
  • application is requesting ONLY ONE(!) address

Example

Display byron address of first cardano account:

TrezorConnect.cardanoGetAddress({
    addressParameters: {
        addressType: CardanoAddressType.BYRON,
        path: "m/44'/1815'/0'/0/0",
    },
    protocolMagic: 764824073,
    networkId: 1,
});

Display base address of first cardano account:

TrezorConnect.cardanoGetAddress({
    addressParameters: {
        addressType: CardanoAddressType.BASE,
        path: "m/1852'/1815'/0'/0/0",
        stakingPath: "m/1852'/1815'/0'/2/0",
    },
    protocolMagic: 764824073,
    networkId: 1,
});

Display base address with script payment part:

TrezorConnect.cardanoGetAddress({
    addressParameters: {
        addressType: CardanoAddressType.BASE_SCRIPT_KEY,
        paymentScriptHash: '0d5acbf6a1dfb0c8724e60df314987315ccbf78bb6c0f9b6f3d568fe',
        stakingPath: "m/1852'/1815'/0'/2/0",
    },
    protocolMagic: 764824073,
    networkId: 1,
});

Display base address with script staking part:

TrezorConnect.cardanoGetAddress({
    addressParameters: {
        addressType: CardanoAddressType.BASE_KEY_SCRIPT,
        path: "m/1852'/1815'/0'/0/0",
        stakingScriptHash: '8d7bebc7a58f1c7b5fb7c9391071ecd3b51b032695522f8c555343a9',
    },
    protocolMagic: 764824073,
    networkId: 1,
});

Display base address with both payment and staking part being a script:

TrezorConnect.cardanoGetAddress({
    addressParameters: {
        addressType: CardanoAddressType.BASE_SCRIPT_SCRIPT,
        paymentScriptHash: '0d5acbf6a1dfb0c8724e60df314987315ccbf78bb6c0f9b6f3d568fe',
        stakingScriptHash: '8d7bebc7a58f1c7b5fb7c9391071ecd3b51b032695522f8c555343a9',
    },
    protocolMagic: 764824073,
    networkId: 1,
});

Display pointer address of first cardano account:

TrezorConnect.cardanoGetAddress({
    addressParameters: {
        addressType: CardanoAddressType.POINTER,
        path: "m/1852'/1815'/0'/0/0",
        certificatePointer: {
            blockIndex: 1,
            txIndex: 2,
            certificateIndex: 3,
        },
    },
    protocolMagic: 764824073,
    networkId: 1,
});

Display pointer script address:

TrezorConnect.cardanoGetAddress({
    addressParameters: {
        addressType: CardanoAddressType.POINTER_SCRIPT,
        paymentScriptHash: '0d5acbf6a1dfb0c8724e60df314987315ccbf78bb6c0f9b6f3d568fe',
        certificatePointer: {
            blockIndex: 1,
            txIndex: 2,
            certificateIndex: 3,
        },
    },
    protocolMagic: 764824073,
    networkId: 1,
});

Display enterprise address of first cardano account:

TrezorConnect.cardanoGetAddress({
    addressParameters: {
        addressType: CardanoAddressType.ENTERPRISE,
        path: "m/1852'/1815'/0'/0/0",
    },
    protocolMagic: 764824073,
    networkId: 1,
});

Display enterprise script address:

TrezorConnect.cardanoGetAddress({
    addressParameters: {
        addressType: CardanoAddressType.ENTERPRISE_SCRIPT,
        paymentScriptHash: '0d5acbf6a1dfb0c8724e60df314987315ccbf78bb6c0f9b6f3d568fe',
    },
    protocolMagic: 764824073,
    networkId: 1,
});

Display reward address of first cardano account:

TrezorConnect.cardanoGetAddress({
    addressParameters: {
        addressType: CardanoAddressType.REWARD,
        stakingPath: "m/1852'/1815'/0'/0/0",
    },
    protocolMagic: 764824073,
    networkId: 1,
});

Display reward script address:

TrezorConnect.cardanoGetAddress({
    addressParameters: {
        addressType: CardanoAddressType.REWARD_SCRIPT,
        stakingScriptHash: '8d7bebc7a58f1c7b5fb7c9391071ecd3b51b032695522f8c555343a9',
    },
    protocolMagic: 764824073,
    networkId: 1,
});

Return a bundle of cardano addresses without displaying them on device:

TrezorConnect.cardanoGetAddress({
    bundle: [
        // byron address, account 1, address 1
        {
            addressParameters: {
                addressType: 8,
                path: "m/44'/1815'/0'/0/0",
            },
            protocolMagic: 764824073,
            networkId: 1,
            showOnTrezor: false,
        },
        // base address with staking key hash, account 1, address 1
        {
            addressParameters: {
                addressType: 0,
                path: "m/1852'/1815'/0'/0/0",
                stakingKeyHash: '1bc428e4720702ebd5dab4fb175324c192dc9bb76cc5da956e3c8dff',
            },
            protocolMagic: 764824073,
            networkId: 1,
            showOnTrezor: false,
        },
        // byron address, account 2, address 3, testnet
        {
            addressParameters: {
                addressType: 8,
                path: "m/44'/1815'/1'/0/2",
            },
            protocolMagic: 1,
            networkId: 0,
            showOnTrezor: false,
        },
    ],
});

Validate address using custom UI inside of your application:

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

TrezorConnect.on(UI.ADDRESS_VALIDATION, data => {
    console.log('Handle button request', data.address, data.serializedPath);
    // here you can display custom UI inside of your app
});

const result = await TrezorConnect.cardanoGetAddress({
    addressParameters: {
        addressType: 8,
        path: "m/44'/1815'/0'/0/0",
    },
    protocolMagic: 764824073,
    networkId: 0,
    address: 'Ae2tdPwUPEZ5YUb8sM3eS8JqKgrRLzhiu71crfuH2MFtqaYr5ACNRdsswsZ',
});
// don't forget to hide your custom UI after you get the result!

Result

CardanoAddress type

Result with only one address

{
    success: true,
    payload: {
        addressParameters: {
            addressType: number,
            path: Array<number>, // hardened path
            stakingPath?: Array<number>, // hardened path
            stakingKeyHash?: string,
            certificatePointer?: {
                blockIndex: number,
                txIndex: number,
                certificatePointer: number,
            },
            paymentScriptHash?: string,
            stakingScriptHash?: string,
        }
        serializedPath?: string,
        serializedStakingPath?: string,
        protocolMagic: number,
        networkId: number,
        address: string,
    }
}

Result with bundle of addresses

{
    success: true,
    payload: [
        {
            addressParameters: {
                addressType: number,
                path: Array<number>, // hardened path
                stakingPath?: Array<number>, // hardened path
                stakingKeyHash?: string,
                certificatePointer?: {
                    blockIndex: number,
                    txIndex: number,
                    certificatePointer: number,
                },
                paymentScriptHash?: string,
                stakingScriptHash?: string,
            }
            serializedPath?: string,
            serializedStakingPath?: string,
            protocolMagic: number,
            networkId: number,
            address: string,
        },
        {
            addressParameters: {
                addressType: number,
                path: Array<number>, // hardened path
                stakingPath?: Array<number>, // hardened path
                stakingKeyHash?: string,
                certificatePointer?: {
                    blockIndex: number,
                    txIndex: number,
                    certificatePointer: number,
                },
                paymentScriptHash?: string,
                stakingScriptHash?: string,
            }
            serializedPath?: string,
            serializedStakingPath?: string,
            protocolMagic: number,
            networkId: number,
            address: string,
        },
        {
            addressParameters: {
                addressType: number,
                path: Array<number>, // hardened path
                stakingPath?: Array<number>, // hardened path
                stakingKeyHash?: string,
                certificatePointer?: {
                    blockIndex: number,
                    txIndex: number,
                    certificatePointer: number,
                },
                paymentScriptHash?: string,
                stakingScriptHash?: string,
            }
            serializedPath?: string,
            serializedStakingPath?: string,
            protocolMagic: number,
            networkId: number,
            address: string,
        },
    ]
}

Error

{
    success: false,
    payload: {
        error: string // error message
    }
}

Cardano: Sign transaction

Asks device to sign given transaction. User is asked to confirm all transaction details on Trezor.

const result = await TrezorConnect.cardanoSignTransaction(params);

Params

Optional common params

CardanoSignTransaction type

  • signingMode - required CardanoTxSigningMode
  • inputs - required Array of CardanoInput
  • outputs - required Array of CardanoOutput
  • fee - required String
  • protocolMagic - required Integer 764824073 for Mainnet, 1 for Preprod Testnet, 2 for Preview Testnet
  • networkId - required Integer 1 for Mainnet, 0 for Testnet
  • ttl - optional String
  • validityIntervalStart - optional String
  • certificates - optional Array of CardanoCertificate
  • withdrawals - optional Array of CardanoWithdrawal
  • auxiliaryData - optional CardanoAuxiliaryData
  • mint - optional CardanoMint
  • scriptDataHash - optional String
  • collateralInputs - optional Array of CardanoCollateralInput
  • requiredSigners - optional Array of CardanoRequiredSigner
  • collateralReturn - optional CardanoOutput
  • totalCollateral - optional String
  • referenceInputs - optional Array of CardanoReferenceInput
  • additionalWitnessRequests - optional Array of string | Array<number> (paths). Used for multi-sig and token minting witness requests as those can not be determined from the transaction parameters.
  • metadata - removed - use auxiliaryData instead
  • derivationTypeoptional CardanoDerivationType enum. Determines used derivation type. Default is set to ICARUS_TREZOR=2.
  • includeNetworkIdoptional Boolean. Determines whether networkId should be explicitly serialized into the transaction body. Default is false.
  • chunkifyoptional boolean determines if recipient address will be displayed in chunks of 4 characters. Default is set to false
  • tagCborSets - optional boolean determines if CBOR arrays intended to be sets will be encoded with tag 258. Default is set to false

CardanoTxSigningMode

ORDINARY_TRANSACTION

Represents an ordinary user transaction transferring funds, delegating stake or withdrawing rewards. The transaction will be witnessed by keys derived from paths included in the inputs, certificates and withdrawals. Additionally, if token minting is present, transaction will also be witnessed by keys derived from paths included in additionalWitnessRequests.

The transaction

  • should have valid path property on all inputs
  • must not contain a pool registration certificate
  • must not contain collateralInputs, collateralReturn, totalCollateral and referenceInputs
  • must contain paths as stake credentials in certificates and withdrawals (no key hashes or script hashes)
  • may contain only 1852 and 1855 paths
  • must not contain 1855 witness requests when transaction is not minting/burning tokens

POOL_REGISTRATION_AS_OWNER

Represents pool registration from the perspective of pool owner.

The transaction

  • must have path undefined on all inputs (i.e., we are not witnessing any UTxO)
  • must have single Pool registration certificate
  • must have single owner given by path on that certificate
  • must not contain withdrawals
  • must not contain token minting
  • must not contain collateralInputs, requiredSigners, collateralReturn, totalCollateral and referenceInputs
  • must contain only staking witness requests

These restrictions are in place due to a possibility of maliciously signing another part of the transaction with the pool owner path as we are not displaying device-owned paths on the device screen.

MULTISIG_TRANSACTION

Represents a multi-sig transaction using native scripts. The transaction will only be signed by keys derived from paths included in additionalWitnessRequests.

The transaction

  • must have path undefined on all inputs
  • must not contain output addresses given by parameters
  • must not contain a pool registration certificate
  • must not contain collateralInputs, collateralReturn, totalCollateral and referenceInputs
  • must contain script hash stake credentials in certificates and withdrawals (no paths or key hashes)
  • may contain only 1854 and 1855 witness requests
  • must not contain 1855 witness requests when transaction is not minting/burning tokens

PLUTUS_TRANSACTION

Represents a transactions containing Plutus script evaluation. The transaction will be witnessed by keys derived from paths included in the inputs, certificates, withdrawals, collateralInputs, requiredSigners and additionalWitnessRequests.

The transaction

  • should contain scriptDataHash and collateralInputs
  • must not contain a pool registration certificate
  • may contain only 1852, 1854 and 1855 required signers
  • may contain only 1852, 1854 and 1855 witness requests

Note: requiredSigners are meant for Plutus transactions (from the blockchain point of view), but some applications utilize them for their own purposes, so we allow them in all signing modes (except for pool registration as owner).

Stake pool registration certificate specifics

Trezor supports signing of stake pool registration certificates as a pool owner. The transaction may contain external inputs (e.g. belonging to the pool operator) and Trezor is not able to verify whether they are actually external or not, so if we allowed signing the transaction with a spending key, there is the risk of losing funds from an input that the user did not intend to spend from. Moreover there is the risk of inadvertedly signing a withdrawal in the transaction if there's any. To mitigate those risks, we introduced special validation rules for stake pool registration transactions which are validated on Trezor as well. The validation rules are the following:

  1. The transaction must not contain any other certificates, not even another stake pool registration
  2. The transaction must not contain any withdrawals
  3. The transaction inputs must all be external, i.e. path must be either undefined or null
  4. Exactly one owner should be passed as a staking path and the rest of owners should be passed as bech32-encoded reward addresses

CIP-36 vote key registration (Catalyst and other)

Trezor supports signing transactions with auxiliary data containing a vote key registration. Vote key registrations used to follow CIP-15, which has been superseded by CIP-36. Currently, Trezor supports both CIP-15 and CIP-36 formats, the intended standard can be specified in the format field (with CIP-15 being the default). They differ in the following:

  • CIP-36 allows delegating the voting power to several vote public keys with different voting power (CardanoCVoteRegistrationDelegation) as an alternative to providing only a single vote public key. Note that Trezor Firmware supports at most 32 delegations in a single registration.
  • CIP-36 registrations contain the votingPurpose field. The value 0 is intended for Catalyst voting and the value 1 is intended for other purposes. If no value is provided, Trezor serializes 0 by default (if the CIP-36 format is used).

Trezor does not support the 1694 derivation paths at the moment.

The payment address to receive rewards can be provided either as a paymentAddress string or as a paymentAddressParameters object. For the smoothest user experience, we recommend providing paymentAddressParameters of a BASE address owned by the device.

Transaction examples

Ordinary transaction

TrezorConnect.cardanoSignTransaction({
    signingMode: CardanoTxSigningMode.ORDINARY_TRANSACTION,
    inputs: [
        {
            path: "m/44'/1815'/0'/0/1",
            prev_hash: '1af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc',
            prev_index: 0,
        },
    ],
    outputs: [
        {
            address: 'Ae2tdPwUPEZCanmBz5g2GEwFqKTKpNJcGYPKfDxoNeKZ8bRHr8366kseiK2',
            amount: '3003112',
        },
        {
            addressParameters: {
                addressType: CardanoAddressType.BASE,
                path: "m/1852'/1815'/0'/0/0",
                stakingPath: "m/1852'/1815'/0'/2/0",
            },
            amount: '7120787',
        },
        {
            format: CardanoTxOutputSerializationFormat.ARRAY_LEGACY,
            address:
                'addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r',
            amount: '2000000',
            tokenBundle: [
                {
                    policyId: '95a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39',
                    tokenAmounts: [
                        {
                            assetNameBytes: '74652474436f696e',
                            amount: '7878754',
                        },
                    ],
                },
            ],
        },
        {
            address: 'addr1w9rhu54nz94k9l5v6d9rzfs47h7dv7xffcwkekuxcx3evnqpvuxu0',
            amount: '1',
            datumHash: '3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7',
        },
        {
            format: CardanoTxOutputSerializationFormat.MAP_BABBAGE,
            address: 'addr1w9rhu54nz94k9l5v6d9rzfs47h7dv7xffcwkekuxcx3evnqpvuxu0',
            amount: '1',
            inlineDatum:
                '3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b73b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b73b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b73b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b73b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b73b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b73b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b73b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7',
            referenceScript:
                '3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b73b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b73b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b73b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b73b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b73b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b73b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b73b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7',
        },
    ],
    fee: '42',
    ttl: '10',
    validityIntervalStart: '20',
    certificates: [
        {
            type: CardanoCertificateType.STAKE_REGISTRATION,
            path: "m/1852'/1815'/0'/2/0",
        },
        {
            type: CardanoCertificateType.STAKE_DEREGISTRATION,
            path: "m/1852'/1815'/0'/2/0",
        },
        {
            type: CardanoCertificateType.STAKE_DELEGATION,
            path: "m/1852'/1815'/0'/2/0",
            pool: 'f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973',
        },
    ],
    withdrawals: [
        {
            path: "m/1852'/1815'/0'/2/0",
            amount: '1000',
        },
    ],
    auxiliaryData: {
        hash: 'ea4c91860dd5ec5449f8f985d227946ff39086b17f10b5afb93d12ee87050b6a',
    },
    scriptDataHash: 'd593fd793c377ac50a3169bb8378ffc257c944da31aa8f355dfa5a4f6ff89e02',
    protocolMagic: 764824073,
    networkId: 1,
    includeNetworkId: false,
});

Stake pool registration

TrezorConnect.cardanoSignTransaction({
    signingMode: CardanoTxSigningMode.POOL_REGISTRATION_AS_OWNER,
    inputs: [
        {
            // notice no path is provided here
            prev_hash: '3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7',
            prev_index: 0,
        },
    ],
    outputs: {
        address:
            'addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r',
        amount: '1000000',
    },
    fee: '300000',
    ttl: '500000000',
    protocolMagic: 764824073,
    networkId: 1,
    includeNetworkId: false,
    certificates: [
        {
            type: CardanoCertificateType.STAKE_POOL_REGISTRATION,
            poolParameters: {
                poolId: 'f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973',
                vrfKeyHash: '198890ad6c92e80fbdab554dda02da9fb49d001bbd96181f3e07f7a6ab0d0640',
                pledge: '500000000', // amount in lovelace
                cost: '340000000', // amount in lovelace
                margin: {
                    // numerator/denominator should be <= 1 which is translated then to a percentage
                    numerator: '1',
                    denominator: '2',
                },
                rewardAccount: 'stake1uya87zwnmax0v6nnn8ptqkl6ydx4522kpsc3l3wmf3yswygwx45el', // bech32-encoded stake pool reward account
                owners: [
                    {
                        stakingKeyPath: "m/1852'/1815'/0'/2/0", // this is the path to the owner's key that will be signing the tx on Trezor
                    },
                    {
                        stakingKeyHash: '3a7f09d3df4cf66a7399c2b05bfa234d5a29560c311fc5db4c490711', // other owner
                    },
                ],
                relays: [
                    {
                        type: CardanoPoolRelayType.SINGLE_HOST_IP,
                        ipv4Address: '192.168.0.1',
                        ipv6Address: '2001:0db8:85a3:0000:0000:8a2e:0370:7334', // ipv6 address in full form
                        port: 1234,
                    },
                    {
                        type: CardanoPoolRelayType.SINGLE_HOST_IP,
                        ipv6Address: '2001:0db8:85a3:0000:0000:8a2e:0370:7334',
                        port: 1234,
                    },
                    {
                        type: CardanoPoolRelayType.SINGLE_HOST_IP,
                        ipv4Address: '192.168.0.1',
                        port: 1234,
                    },
                    {
                        type: CardanoPoolRelayType.SINGLE_HOST_NAME,
                        hostName: 'www.test.test',
                        port: 1234,
                    },
                    {
                        type: CardanoPoolRelayType.MULTIPLE_HOST_NAME,
                        hostName: 'www.test2.test', // max 64 characters long
                    },
                ],
                metadata: {
                    url: 'https://www.test.test', // max 64 characters long
                    hash: '914c57c1f12bbf4a82b12d977d4f274674856a11ed4b9b95bd70f5d41c5064a6',
                },
            },
        },
    ],
});

CIP-36 vote key registration

TrezorConnect.cardanoSignTransaction({
    signingMode: CardanoTxSigningMode.ORDINARY_TRANSACTION,
    inputs: [
        {
            path: "m/1852'/1815'/0'/0/0",
            prev_hash: '3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7',
            prev_index: 0,
        },
    ],
    outputs: [
        {
            address:
                'addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r',
            amount: '3003112',
        },
    ],
    fee: '42',
    ttl: '10',
    auxiliaryData: {
        cVoteRegistrationParameters: {
            stakingPath: "m/1852'/1815'/0'/2/0",
            paymentAddressParameters: {
                addressType: CardanoAddressType.BASE,
                path: "m/1852'/1815'/0'/0/0",
                stakingPath: "m/1852'/1815'/0'/2/0",
            },
            nonce: '22634813',
            format: CardanoCVoteRegistrationFormat.CIP36,
            delegations: [
                {
                    votePublicKey:
                        '1af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc',
                    weight: 1,
                },
            ],
        },
    },
    protocolMagic: 764824073,
    networkId: 1,
    includeNetworkId: false,
});

Multisig transaction

TrezorConnect.cardanoSignTransaction({
    signingMode: CardanoTxSigningMode.MULTISIG_TRANSACTION,
    inputs: [
        {
            prev_hash: '1af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc',
            prev_index: 0,
        },
    ],
    outputs: [
        {
            address: 'Ae2tdPwUPEZCanmBz5g2GEwFqKTKpNJcGYPKfDxoNeKZ8bRHr8366kseiK2',
            amount: '3003112',
        },
        {
            address:
                'addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r',
            amount: '2000000',
            tokenBundle: [
                {
                    policyId: '95a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39',
                    tokenAmounts: [
                        {
                            assetNameBytes: '74652474436f696e',
                            amount: '7878754',
                        },
                    ],
                },
            ],
        },
    ],
    fee: '42',
    ttl: '10',
    validityIntervalStart: '20',
    certificates: [
        {
            type: CardanoCertificateType.STAKE_REGISTRATION,
            scriptHash: '29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd',
        },
        {
            type: CardanoCertificateType.STAKE_DEREGISTRATION,
            scriptHash: '29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd',
        },
        {
            type: CardanoCertificateType.STAKE_DELEGATION,
            scriptHash: '29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd',
            pool: 'f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973',
        },
    ],
    withdrawals: [
        {
            scriptHash: '29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd',
            amount: '1000',
        },
    ],
    auxiliaryData: {
        hash: 'ea4c91860dd5ec5449f8f985d227946ff39086b17f10b5afb93d12ee87050b6a',
    },
    mint: [
        {
            policyId: '95a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39',
            tokenAmounts: [
                {
                    assetNameBytes: '74652474436f696e',
                    mintAmount: '7878754',
                },
            ],
        },
    ],
    additionalWitnessRequests: ["m/1854'/1815'/0'/0/0", "m/1855'/1815'/0'"],
    protocolMagic: 764824073,
    networkId: 1,
    includeNetworkId: false,
});

Plutus transaction

TrezorConnect.cardanoSignTransaction({
    signingMode: CardanoTxSigningMode.PLUTUS_TRANSACTION,
    inputs: [
        {
            path: "m/1852'/1815'/0'/0/0",
            prev_hash: '1af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc',
            prev_index: 0,
        },
        {
            prev_hash: '3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7',
            prev_index: 0,
        },
    ],
    outputs: [
        {
            address: 'Ae2tdPwUPEZCanmBz5g2GEwFqKTKpNJcGYPKfDxoNeKZ8bRHr8366kseiK2',
            amount: '3003112',
        },
        {
            address:
                'addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r',
            amount: '2000000',
            tokenBundle: [
                {
                    policyId: '95a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39',
                    tokenAmounts: [
                        {
                            assetNameBytes: '74652474436f696e',
                            amount: '7878754',
                        },
                    ],
                },
            ],
        },
    ],
    fee: '42',
    ttl: '10',
    validityIntervalStart: '20',
    certificates: [
        {
            type: CardanoCertificateType.STAKE_REGISTRATION,
            path: "m/1852'/1815'/0'/2/0",
        },
        {
            type: CardanoCertificateType.STAKE_DEREGISTRATION,
            keyHash: '3a7f09d3df4cf66a7399c2b05bfa234d5a29560c311fc5db4c490711',
        },
        {
            type: CardanoCertificateType.STAKE_DELEGATION,
            scriptHash: '29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd',
            pool: 'f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973',
        },
    ],
    withdrawals: [
        {
            path: "m/1852'/1815'/0'/2/0",
            amount: '1000',
        },
        {
            keyHash: '3a7f09d3df4cf66a7399c2b05bfa234d5a29560c311fc5db4c490711',
            amount: '1000',
        },
        {
            scriptHash: '29fb5fd4aa8cadd6705acc8263cee0fc62edca5ac38db593fec2f9fd',
            amount: '1000',
        },
    ],
    auxiliaryData: {
        hash: 'ea4c91860dd5ec5449f8f985d227946ff39086b17f10b5afb93d12ee87050b6a',
    },
    mint: [
        {
            policyId: '95a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39',
            tokenAmounts: [
                {
                    assetNameBytes: '74652474436f696e',
                    mintAmount: '7878754',
                },
            ],
        },
    ],
    scriptDataHash: 'd593fd793c377ac50a3169bb8378ffc257c944da31aa8f355dfa5a4f6ff89e02',
    collateralInputs: [
        {
            path: "m/1852'/1815'/0'/0/0",
            prev_hash: '1af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc',
            prev_index: 0,
        },
    ],
    collateralReturn: {
        format: CardanoTxOutputSerializationFormat.ARRAY_LEGACY,
        address:
            'addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r',
        amount: '1000',
        tokenBundle: [
            {
                policyId: '95a292ffee938be03e9bae5657982a74e9014eb4960108c9e23a5b39',
                tokenAmounts: [
                    {
                        assetNameBytes: '74652474436f696e',
                        amount: '7878754',
                    },
                ],
            },
        ],
    },
    totalCollateral: '1000',
    referenceInputs: [
        {
            path: "m/1852'/1815'/0'/0/0",
            prev_hash: '1af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc',
            prev_index: 0,
        },
    ],
    requiredSigners: [
        {
            keyPath: "m/1852'/1815'/0'/0/1",
        },
        {
            keyHash: '3a7f09d3df4cf66a7399c2b05bfa234d5a29560c311fc5db4c490711',
        },
    ],
    additionalWitnessRequests: ["m/1852'/1815'/0'/0/2", "m/1854'/1815'/0'/0/0", "m/1855'/1815'/0'"],
    protocolMagic: 764824073,
    networkId: 1,
    includeNetworkId: false,
});

Result

Since transaction streaming has been introduced to the Cardano implementation on Trezor because of memory constraints, Trezor no longer returns the whole serialized transaction as a result of the CardanoSignTransaction call. Instead the transaction hash, transaction witnesses and auxiliary data supplement are returned and the serialized transaction needs to be assembled by the client.

CardanoSignedTxData type

{
    success: true,
    payload: {
        hash: string,
        witnesses: CardanoSignedTxWitness[],
        auxiliaryDataSupplement?: CardanoAuxiliaryDataSupplement,
    }
}

Example:

{
    success: true,
    payload: {
        hash: "73e09bdebf98a9e0f17f86a2d11e0f14f4f8dae77cdf26ff1678e821f20c8db6",
        witnesses: [
            {
                type: CardanoTxWitnessType.BYRON_WITNESS,
                pubKey: '89053545a6c254b0d9b1464e48d2b5fcf91d4e25c128afb1fcfc61d0843338ea',
                signature:
                    'da07ac5246e3f20ebd1276476a4ae34a019dd4b264ffc22eea3c28cb0f1a6bb1c7764adeecf56bcb0bc6196fd1dbe080f3a7ef5b49f56980fe5b2881a4fdfa00',
                chainCode:
                    '26308151516f3b0e02bb1638142747863c520273ce9bd3e5cd91e1d46fe2a635',
            },
            {
                type: CardanoTxWitnessType.SHELLEY_WITNESS,
                pubKey: '5d010cf16fdeff40955633d6c565f3844a288a24967cf6b76acbeb271b4f13c1',
                signature:
                    '622f22d03bc9651ddc5eb2f5dc709ac4240a64d2b78c70355dd62106543c407d56e8134c4df7884ba67c8a1b5c706fc021df5c4d0ff37385c30572e73c727d00',
                chainCode: null,
            },
        ],
        auxiliaryDataSupplement: {
            type: 1,
            auxiliaryDataHash:
                'a943e9166f1bb6d767b175384d3bd7d23645170df36fc1861fbf344135d8e120',
            cVoteRegistrationSignature:
                '74f27d877bbb4a5fc4f7c56869905c11f70bad0af3de24b23afaa1d024e750930f434ecc4b73e5d1723c2cb8548e8bf6098ac876487b3a6ed0891cb76994d409',
        },
    }
}

Error

{
    success: false,
    payload: {
        error: string // error message
    }
}

Ripple: get address

Display requested address on device and returns it to caller. User is presented with a description of the requested key and asked to confirm the export.

const result = await TrezorConnect.rippleGetAddress(params);

Params

Optional common params

Exporting single address

  • pathrequired string | Array<number> minimum length is 5. read more
  • addressoptional string address for validation (read Handle button request section below)
  • showOnTrezoroptional boolean determines if address will be displayed on device. Default is set to true
  • chunkifyoptional boolean determines if address will be displayed in chunks of 4 characters. Default is set to false

Exporting bundle of addresses

  • bundle - Array of Objects with path and showOnTrezor fields

Handle button request

Since [email protected] there is a possibility to handle UI.ADDRESS_VALIDATION event which will be triggered once the address is displayed on the device. You can handle this event and display custom UI inside of your application.

If certain conditions are fulfilled popup will not be used at all:

  • the user gave permissions to communicate with Trezor
  • device is authenticated by pin/passphrase
  • application has TrezorConnect.on(UI.ADDRESS_VALIDATION, () => {}); listener registered
  • parameter address is set
  • parameter showOnTrezor is set to true (or not set at all)
  • application is requesting ONLY ONE(!) address

Example

Display first address of second ripple account:

TrezorConnect.rippleGetAddress({
    path: "m/44'/144'/1'/0/0",
});

Return a bundle of ripple addresses without displaying them on device:

TrezorConnect.rippleGetAddress({
    bundle: [
        { path: "m/44'/144'/0'/0/0", showOnTrezor: false }, // account 1
        { path: "m/44'/144'/1'/0/1", showOnTrezor: false }, // account 2
        { path: "m/44'/144'/2'/0/2", showOnTrezor: false }, // account 3
    ],
});

Validate address using custom UI inside of your application:

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

TrezorConnect.on(UI.ADDRESS_VALIDATION, data => {
    console.log('Handle button request', data.address, data.serializedPath);
    // here you can display custom UI inside of your app
});

const result = await TrezorConnect.rippleGetAddress({
    path: "m/44'/144'/0'/0/0",
    address: 'rNaqKtKrMSwpwZSzRckPf7S96DkimjkF4H',
});
// dont forget to hide your custom UI after you get the result!

Result

Address type

Result with only one address

{
    success: true,
    payload: {
        address: string,
        path: Array<number>,
        serializedPath: string,
    }
}

Result with bundle of addresses

{
    success: true,
    payload: [
        { address: string, path: Array<number>, serializedPath: string }, // account 1, address 1
        { address: string, path: Array<number>, serializedPath: string }, // account 2, address 2
        { address: string, path: Array<number>, serializedPath: string }, // account 3, address 3
    ]
}

Error

{
    success: false,
    payload: {
        error: string // error message
    }
}

Ripple: Sign transaction

Asks device to sign given transaction. User is asked to confirm all transaction details on Trezor.

const result = await TrezorConnect.rippleSignTransaction(params);

Params

Optional common params

RippleSignTransaction type

  • pathrequired string | Array<number> minimum length is 3. read more
  • transaction - required Object type of RippleTransaction
  • chunkifyoptional boolean determines if recipient address will be displayed in chunks of 4 characters. Default is set to false

Example

TrezorConnect.rippleSignTransaction(
    path: "m/44'/144'/0'/0/0",
    transaction: {
        fee: '100000',
        flags: 0x80000000,
        sequence: 25,
        payment: {
            amount: '100000000',
            destination: 'rBKz5MC2iXdoS3XgnNSYmF69K1Yo4NS3Ws'
        }
    }
});

Result

RippleSignedTx type

{
    success: true,
    payload: {
        serializedTx: string,
        signature: string,
    }
}

Error

{
    success: false,
    payload: {
        error: string // error message
    }
}

Tezos: get address

Display requested address on device and returns it to caller. User is presented with a description of the requested key and asked to confirm the export.

const result = await TrezorConnect.tezosGetAddress(params);

Params

Optional common params

Exporting single address

  • pathrequired string | Array<number> minimum length is 3. read more
  • addressoptional string address for validation (read Handle button request section below)
  • showOnTrezoroptional boolean determines if address will be displayed on device. Default is set to true
  • chunkifyoptional boolean determines if address will be displayed in chunks of 4 characters. Default is set to false

Exporting bundle of addresses

  • bundle - Array of Objects with path and showOnTrezor fields

Handle button request

Since [email protected] there is a possibility to handle UI.ADDRESS_VALIDATION event which will be triggered once the address is displayed on the device. You can handle this event and display custom UI inside of your application.

If certain conditions are fulfilled popup will not be used at all:

  • the user gave permissions to communicate with Trezor
  • device is authenticated by pin/passphrase
  • application has TrezorConnect.on(UI.ADDRESS_VALIDATION, () => {}); listener registered
  • parameter address is set
  • parameter showOnTrezor is set to true (or not set at all)
  • application is requesting ONLY ONE(!) address

Example

Display address of first tezos account:

TrezorConnect.tezosGetAddress({
    path: "m/44'/1729'/0'",
});

Return a bundle of tezos addresses without displaying them on device:

TrezorConnect.tezosGetAddress({
    bundle: [
        { path: "m/44'/1729'/0'", showOnTrezor: false }, // account 1
        { path: "m/44'/1729'/1'", showOnTrezor: false }, // account 2
        { path: "m/44'/1729'/2'", showOnTrezor: false }, // account 3
    ],
});

Validate address using custom UI inside of your application:

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

TrezorConnect.on(UI.ADDRESS_VALIDATION, data => {
    console.log('Handle button request', data.address, data.serializedPath);
    // here you can display custom UI inside of your app
});

const result = await TrezorConnect.tezosGetAddress({
    path: "m/44'/1729'/0'",
    address: 'tz1Kef7BSg6fo75jk37WkKRYSnJDs69KVqt9',
});
// dont forget to hide your custom UI after you get the result!

Result

Address type

Result with only one address

{
    success: true,
    payload: {
        address: string,
        path: Array<number>,
        serializedPath: string,
    }
}

Result with bundle of addresses

{
    success: true,
    payload: [
        { address: string, path: Array<number>, serializedPath: string }, // account 1
        { address: string, path: Array<number>, serializedPath: string }, // account 2
        { address: string, path: Array<number>, serializedPath: string }, // account 3
    ]
}

Error

{
    success: false,
    payload: {
        error: string // error message
    }
}

Tezos: get public key

Display requested public key on device and returns it to caller. User is presented with a description of the requested public key and asked to confirm the export.

const result = await TrezorConnect.tezosGetPublicKey(params);

Params

Optional common params

Exporting single public key

  • pathrequired string | Array<number> minimum length is 3. read more
  • showOnTrezoroptional boolean determines if public key will be displayed on device.
  • chunkifyoptional boolean determines if address will be displayed in chunks of 4 characters. Default is set to false

Exporting bundle of public keys

  • bundle - Array of Objects with path and showOnTrezor fields

Example

Result with only one public key

TrezorConnect.tezosGetPublicKey({
    path: "m/49'/1729'/0'",
});

Result with bundle of public keys

TrezorConnect.tezosGetPublicKey({
    bundle: [
        { path: "m/49'/1729'/0'" }, // account 1
        { path: "m/49'/1729'/1'" }, // account 2
        { path: "m/49'/1729'/2'" }, // account 3
    ],
});

Result

PublicKey type

Result with only one public key

{
    success: true,
    payload: {
        publicKey: string,
        path: Array<number>,
        serializedPath: string,
    }
}

Result with bundle of public keys

{
    success: true,
    payload: [
        { path, serializedPath, publicKey }, // account 1
        { path, serializedPath, publicKey }, // account 2
        { path, serializedPath, publicKey }, // account 3
    ]
}

Error

{
    success: false,
    payload: {
        error: string // error message
    }
}

Tezos: Sign transaction

Asks device to sign given transaction. User is asked to confirm all transaction details on Trezor.

const result = await TrezorConnect.tezosSignTransaction(params);

Params

Optional common params

TezosSignTransaction type

  • path - required string | Array<number>
  • branch - required string
  • operation - required Object type of TezosOperation
  • chunkifyoptional boolean determines if recipient address will be displayed in chunks of 4 characters. Default is set to false

Example

Sign transaction operation

TrezorConnect.tezosSignTransaction({
    path: "m/44'/1729'/10'",
    branch: 'BLGUkzwvguFu8ei8eLW3KgCbdtrMmv1UCqMvUpHHTGq1UPxypHS',
    operation: {
        transaction: {
            source: 'tz1UKmZhi8dhUX5a5QTfCrsH9pK4dt1dVfJo',
            destination: 'tz1Kef7BSg6fo75jk37WkKRYSnJDs69KVqt9',
            counter: 297,
            amount: 200000,
            fee: 10000,
            gas_limit: 11000,
            storage_limit: 0,
        },
    },
});

Sign the first transaction of the account with reveal operation

TrezorConnect.tezosSignTransaction({
    path: "m/44'/1729'/10'",
    branch: 'BLGUkzwvguFu8ei8eLW3KgCbdtrMmv1UCqMvUpHHTGq1UPxypHS',
    operation: {
        reveal: {
            source: 'tz1ekQapZCX4AXxTJhJZhroDKDYLHDHegvm1',
            counter: 575424,
            fee: 10000,
            gas_limit: 20000,
            storage_limit: 0,
            public_key: 'edpkuTPqWjcApwyD3VdJhviKM5C13zGk8c4m87crgFarQboF3Mp56f',
        },
        transaction: {
            source: 'tz1ekQapZCX4AXxTJhJZhroDKDYLHDHegvm1',
            destination: 'tz1UKmZhi8dhUX5a5QTfCrsH9pK4dt1dVfJo',
            counter: 575425,
            amount: 100000,
            fee: 10000,
            gas_limit: 20000,
            storage_limit: 0,
        },
    },
});

Sign origination operation

TrezorConnect.tezosSignTransaction({
    path: "m/44'/1729'/0'",
    branch: 'BLHRTdZ5vUKSDbkp5vcG1m6ZTST4SRiHWUhGodysLTbvACwi77d',
    operation: {
        origination: {
            source: 'tz1ckrgqGGGBt4jGDmwFhtXc1LNpZJUnA9F2',
            manager_pubkey: 'tz1ckrgqGGGBt4jGDmwFhtXc1LNpZJUnA9F2',
            delegate: 'tz1boot1pK9h2BVGXdyvfQSv8kd1LQM6H889',
            balance: 100000000,
            fee: 10000,
            counter: 20450,
            gas_limit: 10100,
            storage_limit: 277,
            script: '0000001c02000000170500036805010368050202000000080316053d036d03420000000a010000000568656c6c6f',
        },
    },
});

Sign delegation operation

TrezorConnect.tezosSignTransaction({
    path: "m/44'/1729'/0'",
    branch: 'BMXAKyvzcH1sGQMqpvqXsZGskYU4GuY9Y14c9g3LcNzMRtfLzFa',
    operation: {
        delegation: {
            source: 'tz1Kef7BSg6fo75jk37WkKRYSnJDs69KVqt9',
            delegate: 'tz1UKmZhi8dhUX5a5QTfCrsH9pK4dt1dVfJo',
            fee: 20000,
            counter: 564565,
            gas_limit: 20000,
            storage_limit: 0,
        },
    },
});

Sign delegation from a KT account (smart contract with manager.tz script)

TrezorConnect.tezosSignTransaction({
    path: "m/44'/1729'/0'",
    branch: 'BMdPMLXNyMTDp4vR6g7y8mWPk7KZbjoXH3gyWD1Tze43UE3BaPm',
    operation: {
        transaction: {
            source: 'tz1UKmZhi8dhUX5a5QTfCrsH9pK4dt1dVfJo',
            destination: 'KT1SBj7e8ZhV2VvJtoc73dNRDLRJ9P6VjuVN',
            counter: 292,
            amount: 0,
            fee: 10000,
            gas_limit: 36283,
            storage_limit: 0,
            parameters_manager: {
                set_delegate: 'tz1UKmZhi8dhUX5a5QTfCrsH9pK4dt1dVfJo',
            },
        },
    },
});

Sign cancel delegation from a KT account (smart contract with manager.tz script)

TrezorConnect.tezosSignTransaction({
    path: "m/44'/1729'/0'",
    branch: 'BL6oaFJeEjtYxafJqEL8hXvSCZmM5d4quyAqjzkBhXvrX97JbQs',
    operation: {
        transaction: {
            source: 'tz1UKmZhi8dhUX5a5QTfCrsH9pK4dt1dVfJo',
            destination: 'KT1SBj7e8ZhV2VvJtoc73dNRDLRJ9P6VjuVN',
            counter: 293,
            amount: 0,
            fee: 10000,
            gas_limit: 36283,
            storage_limit: 0,
            parameters_manager: {
                cancel_delegate: true,
            },
        },
    },
});

Sign transaction operation from a KT account (smart contract with manager.tz script) to a tz account (implicit account)

TrezorConnect.tezosSignTransaction({
    path: "m/44'/1729'/0'",
    branch: 'BMCKRpEsFYQTdZy8BSLuFqkHmxwXrnRpKncdoVMbeGoggLG3bND',
    operation: {
        transaction: {
            source: 'tz1UKmZhi8dhUX5a5QTfCrsH9pK4dt1dVfJo',
            destination: 'KT1SBj7e8ZhV2VvJtoc73dNRDLRJ9P6VjuVN',
            counter: 294,
            amount: 0,
            fee: 10000,
            gas_limit: 36283,
            storage_limit: 0,
            parameters_manager: {
                transfer: {
                    amount: 200,
                    destination: 'tz1UKmZhi8dhUX5a5QTfCrsH9pK4dt1dVfJo',
                },
            },
        },
    },
});

Sign transaction operation from a KT account (smart contract with manager.tz script) to another KT account (smart contract with manager.tz script)

TrezorConnect.tezosSignTransaction({
    path: "m/44'/1729'/0'",
    branch: 'BMCKRpEsFYQTdZy8BSLuFqkHmxwXrnRpKncdoVMbeGoggLG3bND',
    operation: {
        transaction: {
            source: 'tz1UKmZhi8dhUX5a5QTfCrsH9pK4dt1dVfJo',
            destination: 'KT1SBj7e8ZhV2VvJtoc73dNRDLRJ9P6VjuVN',
            counter: 294,
            amount: 0,
            fee: 10000,
            gas_limit: 36283,
            storage_limit: 0,
            parameters_manager: {
                transfer: {
                    amount: 200,
                    destination: 'tz1UKmZhi8dhUX5a5QTfCrsH9pK4dt1dVfJo',
                },
            },
        },
    },
});

Result

TezosSignedTx type

{
    success: true,
    payload: {
        signature: string,
        sig_op_contents: string,
        operation_hash: string,
    }
}

Error

{
    success: false,
    payload: {
        error: string // error message
    }
}

Binance: get address

Display requested address derived by given BIP44 path on device and returns it to caller. User is presented with a description of the requested key and asked to confirm the export on Trezor.

const result = await TrezorConnect.binanceGetAddress(params);

Params

Optional common params

Exporting single address

  • pathrequired string | Array<number> minimum length is 5. read more
  • addressoptional string address for validation (read Handle button request section below)
  • showOnTrezoroptional boolean determines if address will be displayed on device. Default is set to true
  • chunkifyoptional boolean determines if address will be displayed in chunks of 4 characters. Default is set to false

Exporting bundle of addresses

  • bundle - Array of Objects with path and showOnTrezor fields

Handle button request

Since [email protected] there is a possibility to handle UI.ADDRESS_VALIDATION event which will be triggered once the address is displayed on the device. You can handle this event and display custom UI inside of your application.

If certain conditions are fulfilled popup will not be used at all:

  • the user gave permissions to communicate with Trezor
  • device is authenticated by pin/passphrase
  • application has TrezorConnect.on(UI.ADDRESS_VALIDATION, () => {}); listener registered
  • parameter address is set
  • parameter showOnTrezor is set to true (or not set at all)
  • application is requesting ONLY ONE(!) address

Example

Display address of first Binance account:

TrezorConnect.binanceGetAddress({
    path: "m/44'/714'/0'/0/0",
});

Return a bundle of Binance addresses without displaying them on device:

TrezorConnect.binanceGetAddress({
    bundle: [
        { path: "m/44'/714'/0'/0/0", showOnTrezor: false }, // account 1, address 1
        { path: "m/44'/714'/1'/0/1", showOnTrezor: false }, // account 2, address 2
        { path: "m/44'/714'/2'/0/2", showOnTrezor: false }, // account 3, address 3
    ],
});

Validate address using custom UI inside of your application:

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

TrezorConnect.on(UI.ADDRESS_VALIDATION, data => {
    console.log('Handle button request', data.address, data.serializedPath);
    // here you can display custom UI inside of your app
});

const result = await TrezorConnect.binanceGetAddress({
    path: "m/44'/714'/0'/0/0",
    address: 'bnb1afwh46v6nn30nkmugw5swdmsyjmlxslgjfugre',
});
// don't forget to hide your custom UI after you get the result!

Result

Address type

Result with only one address

{
    success: true,
    payload: {
        path: Array<number>, // hardended path
        serializedPath: string,
        address: string,
    }
}

Result with bundle of addresses

{
    success: true,
    payload: [
        { path: Array<number>, serializedPath: string, address: string }, // account 1, address 1
        { path: Array<number>, serializedPath: string, address: string }, // account 2, address 2
        { path: Array<number>, serializedPath: string, address: string }  // account 3, address 3
    ]
}

Error

{
    success: false,
    payload: {
        error: string // error message
    }
}

Binance: get public key

Display requested public key derived by given BIP44 path on device and returns it to caller. User is presented with a description of the requested public key and asked to confirm the export.

const result = await TrezorConnect.binanceGetPublicKey(params);

Params

Optional common params

Exporting single address

  • pathrequired string | Array<number> minimum length is 5. read more
  • showOnTrezoroptional boolean determines if address will be displayed on device. Default is set to true

Exporting bundle of addresses

  • bundle - Array of Objects with path and showOnTrezor fields

Example

Displays public key derived from BIP44 path:

TrezorConnect.binanceGetPublicKey({
    path: "m/44'/714'/0'/0/0",
});

Return a bundle of public keys without displaying them on device:

TrezorConnect.binanceGetPublicKey({
    bundle: [
        { path: "m/44'/714'/0'/0/0", showOnTrezor: false }, // public key 1
        { path: "m/44'/714'/1'/0/0", showOnTrezor: false }, // public key 2
        { path: "m/44'/714'/2'/0/0", showOnTrezor: false }, // public key 3
    ],
});

Result

PublicKey type

Result with only one public key

{
    success: true,
    payload: {
        path: Array<number>,
        serializedPath: string,
        publicKey: string,
    }
}

Result with bundle of public keys sorted by FIFO

{
    success: true,
    payload: [
        { path: Array<number>, serializedPath: string, publicKey: string }, // public key 1
        { path: Array<number>, serializedPath: string, publicKey: string }, // public key 2
        { path: Array<number>, serializedPath: string, publicKey: string }  // public key 3
    ]
}

Error

{
    success: false,
    payload: {
        error: string // error message
    }
}

Binance: sign transaction

Asks device to sign given transaction using the private key derived by given BIP44 path. User is asked to confirm all transaction details on Trezor.

const result = await TrezorConnect.binanceSignTransaction(params);

Params

Optional common params

  • pathrequired string | Array<number> minimum length is 5. read more
  • transaction - required Object type of BinanceSDKTransaction
  • chunkifyoptional boolean determines if recipient address will be displayed in chunks of 4 characters. Default is set to false

Transfer example

TrezorConnect.binanceSignTransaction({
    path: "m/44'/714'/0'/0/0",
    transaction: {
        chain_id: 'Binance-Chain-Nile',
        account_number: 34,
        memo: 'test',
        sequence: 31,
        source: 1,
        transfer: {
            inputs: [
                {
                    address: 'tbnb1hgm0p7khfk85zpz5v0j8wnej3a90w709zzlffd',
                    coins: [{ amount: 1000000000, denom: 'BNB' }],
                },
            ],
            outputs: [
                {
                    address: 'tbnb1ss57e8sa7xnwq030k2ctr775uac9gjzglqhvpy',
                    coins: [{ amount: 1000000000, denom: 'BNB' }],
                },
            ],
        },
    },
});

Place order example

TrezorConnect.binanceSignTransaction({
    path: "m/44'/714'/0'/0/0",
    transaction: {
        chain_id: 'Binance-Chain-Nile',
        account_number: 34,
        memo: '',
        sequence: 32,
        source: 1,
        placeOrder: {
            id: 'BA36F0FAD74D8F41045463E4774F328F4AF779E5-33',
            ordertype: 2,
            price: 100000000,
            quantity: 100000000,
            sender: 'tbnb1hgm0p7khfk85zpz5v0j8wnej3a90w709zzlffd',
            side: 1,
            symbol: 'ADA.B-B63_BNB',
            timeinforce: 1,
        },
    },
});

Cancel order example

TrezorConnect.binanceSignTransaction({
    path: "m/44'/714'/0'/0/0",
    transaction: {
        chain_id: 'Binance-Chain-Nile',
        account_number: 34,
        memo: '',
        sequence: 33,
        source: 1,
        cancelOrder: {
            refid: 'BA36F0FAD74D8F41045463E4774F328F4AF779E5-29',
            sender: 'tbnb1hgm0p7khfk85zpz5v0j8wnej3a90w709zzlffd',
            symbol: 'BCHSV.B-10F_BNB',
        },
    },
});

Result

BinanceSignedTx type

{
    success: true,
    payload: {
        signature: string,
        public_key: string,
    }
}

Error

{
    success: false,
    payload: {
        error: string // error message
    }
}

firmwareUpdate

Installs a new firmware

const result = await TrezorConnect.firmwareUpdate(params);

Params

Optional common params

You either provide binary

  • binaryrequired bytes

Or params

  • version: required number[] version of firmware to be installed
  • btcOnly: boolean should install bitcoin only or regular firmware
  • baseUrl: string url to look for releases.json
  • intermediary: boolean should install intermediary firmware

Notable firmware ranges

It is not possible to install directly whatever version of a new firmware in all cases. Some specific firmware versions might be installed only on device which already run a version which is not lower then x.y.z. These rules are generally expressed by bootloader_version and min_bootloader_version in releases.json document

Here is a list of notable firmware ranges. 1.11.1 was the latest firmware at the time of writing this docs.

Firmware versions latest - 1.7.1

  • can be installed only on devices with firmware 1.6.2 and higher

Firmware versions 1.6.3 - 1.0.0

  • can not be updated to the latest firmware using single TrezorConnect.firmwareUpdate call
  • if device has one of these firmwares, TrezorConnect.firmwareUpdate should be called with intermediary: true which would install a special intermediary firmware first and automatically switch device into bootloader mode making it ready to accept another firmware update
  • alternatively, you may call TrezorConnect.firmwareUpdate with version: '1.6.3' and after succeeding retry this call with version: '1.11.1'

Bootloader versions latest - 1.8.0

  • the first 256 byte (containing old firmware header) must sliced off the when installing a new firmware on bootloader versions in this range.
  • TrezorConnect takes care of this automatically

Firmwares 1.7.2 - 1.6.2

  • These can be updated to the latest firmware in one TrezorConnect.firmwareUpdate call (this is apparent from bullets above).
  • Old firmware headers MUST NOT be sliced off when installing new firmwares onto these versions as these versions have lower bootloader than 1.8.0.
  • For the purpose of computing firmware hash of a newly installed firmware, we MUST slice off old firmware headers.

Example

TrezorConnect.firmwareUpdate({
    version: '2.5.1',
});

Result

FirmwareUpdateResponse type

{
    success: true,
    payload: {
        // challenge used to compute expected firmware hash. only with firmware 1.11.1 and 2.5.1 or higher
        challenge: string,
        // expected firmware hash computed from the installed binary. only with firmware 1.11.1 and 2.5.1 or higher
        hash: string,

    }
}

Error

{
    success: false,
    payload: {
        error: string // error message
    }
}

getFirmwareHash

:note: Supported only by firmwares 1.11.1 and 2.5.1 or higher!

Params

Optional common params

Example

Get hash of firmware installed in trezor device. It can be used to verify authenticity of firmware binaries intended to be installed.

TrezorConnect.getFirmwareHash({
    challenge: '430d1ca5302edb40ac605e0ba61dc50928779336fdd02b688a833564c178307c',
});

Result

FirmwareHash type

{
    success: true,
    payload: {
        hash: string,
    }
}

Error

{
    success: false,
    payload: {
        error: string // error message
    }
}

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)

Fiat:

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

Fiat errors:

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

Fee:

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

Fee errors (custom level):

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

(BTC only) OP_RETURN output:

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

OP_RETURN output errors:

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

(BTC only) Locktime:

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

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

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

Locktime errors:

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

(BTC only) RBF:

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


(ETH only) Data:

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

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

Data errors:

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

(XRP only) Destination tag:

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

Destination tag errors:

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

Broadcast:

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

Drafts:

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

Send RAW:

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

Precomposed transaction ("Total Sent" field)

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

Review modal

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

Send component architecture

Send component is a mix of hooks and redux.

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

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

@wallet-views/send

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

@wallet-hooks/useSendForm

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

@wallet-actions/sendFormActions

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

@wallet-reducers/sendFormReducer

Storing transaction drafts and temporary data used in TransactionReviewModal

Transaction compose process

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

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

Every networkType has own sendFormActions.composeTransaction method

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

img

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

Transaction signing process

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

img

Suite Desktop

Main differences between suite-web and suite-desktop builds

- @trezor/connect API

  • suite-web

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

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

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

  • suite-desktop

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

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

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

- Firmware binaries

  • suite-web

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

  • suite-desktop

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

- Trezor Bridge (trezord)

- Tor

App ID and name by environment

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

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

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

Debugging main process (Chrome dev tools)

Source

Open chrome and go to chrome://inspect

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

dev mode

modify packages/suite-desktop/package.json

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

prod mode

Run production build with --inspect=5858 runtime flag

Logging

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

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

Shortcuts

Available shortcuts:

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

Runtime flags

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

Available flags:

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

Debugging build

Linux

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

MacOS

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

NixOS

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

Extract application

MacOS

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

NixOS

Run application to get mount-id like:

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

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

How to create new package?

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

How to use this new package?

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

Features

This directory contains description of various Trezor Suite features.

Coin Protocol Handler

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

Behavior

Behavior on web

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

Chrome Prompt Firefox Prompt

Behavior on desktop

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

More apps with same handler (macOS)

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

Structure

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

Example

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

UI

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

Bitcoin Notification Bitcoin Notification Form

Figma

Notion

GitHub #3294 GitHub #5266 GitHub #5508

Handlers manipulation

Chrome

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

Firefox

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

Desktop app - macOS

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

Transactions - Export

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

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

Transactions - Search

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

You can search for the following information:

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

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

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

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

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

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

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

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

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

Metadata (labeling)

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

For non-technical introduction, see Trezor Learn.

Data stores

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

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

Google Drive specifics

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

Data structure in store

version 1.0.0 (current)

device metadata example

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

account metadata example

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

version 2.0.0 (future)

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

Data encryption

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

Where data lives in the App

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

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

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

Device has metadata property:

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

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

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

Where metadata is set and displayed

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

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

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

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

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

User stories

First time user:

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

Metadata enabled during discovery process:

Controlled by discoveryActions and metadataMiddleware

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

How to turn metadata off

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

Fiat rates

Suite provides several types of fiat rates:

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

Providers

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

First fetch

Current fiat rates

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

Current fiat rates for ERC-20 tokens

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

Weekly fiat rates

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

Historical fiat rates (for transactions)

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

Update intervals

Current fiat rates

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

Current fiat rates for ERC-20 tokens

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

Weekly fiat rates

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

Historical fiat rates (for transactions)

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

Usage

To make your life easier use FiatValue component.

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

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

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

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

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

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

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

Suite Guide

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

Content

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

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

How to update

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

Localization

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

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

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

Message definitions

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

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

Structure

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

Example:

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

Usage in Suite

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

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

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

<Translation id="TR_CONTINUE" />

or for string variant using hook:

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

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

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

or for string variant using hook:

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

Definition for TR_ENTERED_PIN_NOT_CORRECT:

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

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

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

Definition for TR_TRANSACTIONS_SEARCH_TIP_2:

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

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

Translation mode

Section shamelessly stolen from Crowdin contributions.

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

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

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

To join the ranks of translators follow Crowdin contributions guide.

Synchronization with Crowdin

With the automated CI job from GitHub.

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

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

Locally

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

'api_token': xxxx

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

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

Extract

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

yarn workspace @trezor/suite translations:extract

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

Upload

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

yarn workspace @trezor/suite translations:upload

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

Download

To download new translations from Crowdin run:

yarn workspace @trezor/suite translations:download

and then open a PR with updated language files.

Workflow for regular Crowdin Synchronization

BRANCH_NAME=feat/crowdin-sync

git checkout develop
git pull
git checkout -b $BRANCH_NAME

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

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

Plus creating, reviewing and merging the PR.

Message System

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

Example messages

Issue on Github

Types of in-app messages

There are multiple ways of displaying message to a user:

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

Implementation

Config

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

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

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

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

Schema

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

We use JSON schema for 2 reasons:

  • generating TypeScript types
  • validating configuration file

Types

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

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

Signing

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

CI job

Validation

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

Signing

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

Versioning of implementation

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

Config Structure

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

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

How to update

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

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

Priorities of messages

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

Targeting Linux version

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

Application steps

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

Followup

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

Feature Flags

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

Workflow

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

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

Example

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

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

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

Future evolutions

  • Control feature flags at runtime.

Desktop Logger

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

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

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

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

How to enable logging

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

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

API

Exported Types

LogLevel

Any of the following values:

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

Options (all optional)

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

String formatters

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

Global

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

logFormat

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

Constructor

The constructor has the following parameters:

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

Log methods

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

The following methods are available:

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

Parameters:

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

Exit method

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

Example

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

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

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

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

Q&A

How to format a string?

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

How to output an object?

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

How can I write the log in JSON format?

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

Onboarding

Incomplete developers guide to Onboarding in Trezor Suite

There are few different ways to trigger the onboarding process:

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

Prerequisites

Button Requests

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

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

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

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

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

Steps

Welcome

Before a user connects a device

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

List of valid states

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

List of invalid states

Invalid device states:

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

Invalid transport states:

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

Data analytics (only in initial run)

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

data analytics

Device security (genuinity) check

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

security check

Device without seed, no firmware installed

This should be the most common case in onboarding.

Scenario:

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

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

Primary action: Setup device (starts onboarding)

Secondary action: Contact support

Device without seed, firmware already installed

Scenario:

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

Security questions: Have the user used the device before?

Primary action: Setup device (starts onboarding)

Secondary action: Contact support

Device with seed (thus also a firmware)

Scenario:

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

Security questions: Have the user used the device before?

Primary action: Setup device (starts onboarding)

Secondary action: Contact support

Firmware

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

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

Note about normal and bootloader mode

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

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

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

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

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

Firmware installation

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

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

Possible error states:

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

Device with older firmware installed

firmware update

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

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

firmware confirm

Device with no firmware installed

User proceeds by clicking "Install firmware" CTA. Since the device without firmware is always in bootloader mode we don't need any cooperation from the user. Device doesn't ask for confirming the installation of the fw. When firmware installation is completed firmware.status is set to unplug in case of T1B1, for T2T1/T2B1 it it set to wait-for-reboot. User of T1B1 is prompted to disconnect the device and reconnect it in normal mode. T2T1/T2B1 automatically restarts itself. Then we continue to the next step (Generating seed)

reconnect in normal T1B1 firmware completed

State of currently shipped devices

Intermediary firmware

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

WebUSB

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

Caveats

UI.FIRMWARE_PROGRESS
  • Devices won’t dispatch any event after the user confirms the installation on a device. We only detect that the installation has started when we receive UI.FIRMWARE_PROGRESS which is triggered about 10 seconds too late.
  • T1B1 sends UI.FIRMWARE_PROGRESS only twice, at 0% and then at 100%. However progress bar runs smoothly, that is because we are faking a progress. There are carefully set durations of fake progress bar. When fake progress reaches certain barrier (eg. 90%) it will stop and wait for progress report from UI.FIRMWARE_PROGRESS. Also when this event reports greater progress than the fake one, it will take the precedence.
  • Faking a progress is also used on T2T1/T2B1 because, on device without any firmware installed, first UI.FIRMWARE_PROGRESS is received too late. (Only variant where we rely completely on a real progress is when we are upgrading T2T1 from older firmware).
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 (T2T1/T2B1). 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 (only available on T2T1/T2B1)

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/T2B1

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

T1B1

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

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

Backup seed

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

T2T1/T2B1

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 (T2T1/T2B1). 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/T2B1

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

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

Suite Settings

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

Final step

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

Tests

This chapter contains information about tests.

@trezor/suite-web e2e tests

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

Run it locally

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

On Linux

Prerequisites

Steps

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

Troubleshooting

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

On MacOS (Intel)

Prerequisites

Steps

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

Troubleshooting

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

On MacOS (ARM)

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

Prerequisites

Steps:

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

Troubleshooting

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

Notes

Image snapshots

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

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

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

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

run_tests script

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

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

tags

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

At the moment, there are the following tags:

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

@group

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

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

@retry

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

Results

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

@trezor/connect-popup

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

Playwright + NixOS

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

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

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

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

Link your system browser instead of downloading:

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

Fixtures

Fixtures are located in packages/connect-popup

Test results

Checkout latest screenshots

Regtest

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

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

bash docker/docker-regtest-electrum.sh

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

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

localhost:50001:t

Assorted knowledge

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

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

Front-End Build

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

The folder structure is as follows:

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

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

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

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

Aliases

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

Development on Windows

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

Prerequisites

On Windows:

In WSL:

Setup

Proceed with the general readme instructions.

Connecting USB device

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

Then run:

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

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

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

# Trezor device naming in codebase

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

  • product_class - 'T' for Trezor hardware wallet
  • platform - '1' for STM32F207, '2' for STM32F42x
  • feature_class - 'B' for Buttons, 'T' for Touch
  • generation
Official nameDevelopment name
Trezor Model 1T1B1
Trezor Model TT2T1
Trezor Safe 3T2B1

Review Process

Same as for Trezor Firmware. Please see here.

Videos in Suite

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

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

The following process is tested on MacOS:

Prerequisites

Install encoder

brew install ffmpeg

WebM - currently supported by Chrome/Firefox

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

Encode videos using desktop application

Shutterencoder

Example usage

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

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

References

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

Alpha transparency in Chrome video