trezor_flutter

Connect Flutter apps to Trezor hardware wallets over native transports — USB and Bluetooth LE on Android and macOS, Bluetooth LE on iOS, USB on Windows and on the web (WebUSB) — with no bridge, no Trezor Suite and no web view.

The native side is a thin packet pipe. Everything else — framing, encryption, pairing, protobuf and the chain helpers — runs in Dart and is shared by every platform.

Built and maintained by Macromodule Technologies — AI, blockchain and software development.

Contents

Features

Transports

  • USB on Android, macOS, Windows and the web (WebUSB in Chromium browsers), through the Trezor WebUSB interface, with all I/O on background threads (the UI never waits on the device).
  • USB hotplug events (attached / detached) and, on Android, runtime USB permission prompts.
  • Bluetooth LE on Android, iOS and macOS: scanning filtered to Trezor devices, MTU negotiation, OS-level bonding, notifications and an ordered write queue.
  • Bluetooth adapter state and scan failures reported as events.
  • Disconnects (unplug, out of range, power-off) surface immediately as events and fail any pending request.

Protocols

  • Codec v1 — Model One, Model T, Safe 3, Safe 5.
  • THP (Trezor-Host Protocol) — Bluetooth-capable Trezors (Safe 7):
    • channel allocation, packet segmentation and CRC-32;
    • alternating-bit acknowledgements with retransmission and busy back-off;
    • Noise XX handshake (X25519, AES-256-GCM, SHA-256) with unlock-on-connect;
    • code-entry pairing (CPace over Curve25519 / Elligator 2);
    • pairing credentials, so a paired phone reconnects without a code;
    • encrypted, multiplexed sessions (seedless and passphrase wallets).
  • Automatic protocol detection over USB; Bluetooth always uses THP.

Device interaction

  • Device information (Features): model, firmware, label, lock and passphrase state, capabilities.
  • Callbacks for everything that needs the user: on-device confirmation, PIN matrix (Model One), passphrase (typed in the app or on the device) and the 6-digit pairing code.
  • Cancel an operation from the app; the device leaves its confirmation screen.

Chains

  • Ethereum / EVM (any chain id): address, EIP-1559 and legacy transaction signing with streamed calldata of any length, EIP-191 message signing, EIP-712 by-hash signing.
  • Solana: address, public key, transaction signing.
  • Tron: address and transaction signing straight from TronGrid raw_data (TRX, TRC-20, staking, voting, delegation), verified to be byte-identical to what the device signs.
  • Every other Trezor message (Bitcoin addresses and public keys, management calls, …) through typed protobuf classes and a generic call.

Packaging

  • Android, iOS, macOS, Windows and web; Swift Package Manager and CocoaPods, Apple privacy manifest included.
  • No required native setup beyond the Bluetooth permissions; pure-Dart protocol layer that can be tested without hardware (see TrezorLink).

Status

Path Status
Android · USB · Codec v1 Tested on real devices
Android · Bluetooth · THP Tested on real devices
iOS · Bluetooth · THP Tested on real devices
macOS · USB · Codec v1 Tested on a real device (Safe 3)
macOS · Bluetooth · THP Builds; not yet tested on device
Windows · USB · Codec v1 Tested on a real device (Safe 3)
Web · USB · Codec v1 Builds; not yet tested on device

In addition, the protocol layer has unit tests, including known-answer vectors for CRC-32, Elligator 2 and CPace, a full THP handshake + pairing + credential reuse against a simulated device built from the device-side state machines of the THP specification, retransmission after packet loss, and Ethereum calldata streaming.

Supported devices

Model USB (Android, macOS, Windows, web) Bluetooth (Android, iOS, macOS) Protocol Solana Tron
Model One ✓† — Codec v1 — —
Model T, Safe 3, Safe 5 ✓ — Codec v1 ✓ ✓*
Safe 7 ✓ ✓ THP ✓ ✓*

* Tron needs firmware with Tron support; check features.hasCapability(TrezorCapability.tron).

† On macOS, Windows and the web, Model One needs firmware 1.7 or later (WebUSB); older firmware only has a HID interface, which only Android supports.

iOS apps cannot talk to a Trezor over USB, so iOS supports Bluetooth only. Windows and the web support USB only.

Installation

dependencies:
  trezor_flutter: ^1.3.0

Android

  • minSdkVersion 21.
  • The plugin's manifest declares BLUETOOTH_SCAN (with neverForLocation), BLUETOOTH_CONNECT, the pre-Android-12 BLUETOOTH / BLUETOOTH_ADMIN, and the optional usb.host / bluetooth_le features. They merge into your app.
  • Your app must request the Bluetooth runtime permissions before scanning or connecting (BLUETOOTH_SCAN + BLUETOOTH_CONNECT on Android 12+). On Android 11 and below a scan also needs ACCESS_FINE_LOCATION, which the plugin deliberately does not declare — add it to your manifest with android:maxSdkVersion="30" if you support those versions.
  • USB needs no manifest entry: access is asked per device at runtime.

macOS

  • macOS 10.15 or later.

  • USB needs no permission prompt. A sandboxed app (the Flutter default) needs the USB and Bluetooth entitlements in both macos/Runner/DebugProfile.entitlements and Release.entitlements:

    <key>com.apple.security.device.usb</key>
    <true/>
    <key>com.apple.security.device.bluetooth</key>
    <true/>
    
  • Add a Bluetooth usage description to macos/Runner/Info.plist:

    <key>NSBluetoothAlwaysUsageDescription</key>
    <string>Used to connect to your Trezor hardware wallet.</string>
    
  • Only one app can hold a Trezor's USB interface: close Trezor Suite (and stop trezord) while your app is connected.

Windows

  • Windows 10 or later. Trezor firmware carries Microsoft OS descriptors, so Windows binds the Trezor to its built-in WinUSB driver: no driver install and no permission prompt.
  • USB only: capabilities() reports ble: false, and the Bluetooth methods throw TrezorPlatformException with code unsupported.
  • Only one app can hold a Trezor's USB interface: close Trezor Suite (and stop trezord) while your app is connected.

Web

  • Chromium-based browsers (Chrome, Edge, Opera) over WebUSB, on a secure page: https://, or http://localhost / 127.0.0.1 while developing. Over plain http from any other address (such as http://192.168.x.x:8080 from another computer), the browser hides WebUSB and capabilities() reports usb: false, exactly as in an unsupported browser.
  • Safari and Firefox are not supported: they do not implement WebUSB, so a page cannot reach USB devices at all. capabilities() reports usb: false there; check it and tell users to open the page in Chrome or Edge.
  • A page sees only Trezors the user picked in the browser's device chooser. Show it with platform.usbRequestDevice() from a button handler (browsers open it only right after a click or tap); the browser remembers the choice, so usbListDevices() lists the Trezor on later visits.
  • USB only: the Bluetooth methods throw unsupported.
  • On Linux, the browser needs Trezor's udev rules to access the device.
  • Only one program can hold a Trezor's USB interface: close Trezor Suite (and stop trezord), and other tabs using the Trezor, while your app is connected.

iOS

  • iOS 14 or later.

  • Add a Bluetooth usage description to ios/Runner/Info.plist:

    <key>NSBluetoothAlwaysUsageDescription</key>
    <string>Used to connect to your Trezor hardware wallet.</string>
    

Usage

1. Find a device

import 'package:trezor_flutter/trezor_flutter.dart';

final platform = TrezorPlatform.instance;

final caps = await platform.capabilities(); // (usb: true, ble: true) on Android and macOS; ble: false on Windows and web

// USB (Android, macOS, Windows, web): list what is plugged in, and follow
// plug / unplug.
final usbDevices = await platform.usbListDevices();

// Web only: let the user pick a Trezor in the browser's chooser. Call it from a
// button handler; afterwards the device is in usbListDevices().
final picked = await platform.usbRequestDevice(); // null if dismissed

// Bluetooth: results arrive as events.
platform.events.listen((event) {
  switch (event) {
    case TrezorBleScanResult(:final device):
      print('found ${device.name} (${device.rssi} dBm)');
    case TrezorUsbAttached(:final device):
      print('plugged in: $device');
    case TrezorLinkDisconnected(:final deviceId):
      print('lost $deviceId');
    default:
      break;
  }
});
if (await platform.bluetoothState() == TrezorBluetoothState.on) {
  await platform.bleStartScan();
}

2. Connect

if (device.transport == TrezorTransportType.usb && !device.hasPermission) {
  await platform.usbRequestPermission(device.id); // Android's USB dialog; always granted on macOS, Windows and web
}

final link = await NativeTrezorLink.open(device);
final trezor = await TrezorClient.connect(
  link: link,
  transport: device.transport,
  app: const TrezorAppIdentity(appName: 'My Wallet', hostName: 'My phone'),
  credentialStore: myCredentialStore,
  interaction: TrezorInteraction(
    onButtonRequest: (request) => showConfirmOnDeviceHint(),
    onInteractionEnded: hideConfirmOnDeviceHint,
    onPairingCodeRequest: askUserForSixDigitCode,
    onPassphraseRequest: () async => TrezorPassphrase.onDevice,
  ),
);

final f = trezor.features;
print('${f.model} ${f.firmwareVersion} "${f.label}"');

connect picks the protocol (Bluetooth is always THP; over USB it probes), runs the THP handshake — a locked device shows its PIN screen — and, on the first Bluetooth connection, pairing: the Trezor shows a 6-digit code and onPairingCodeRequest returns what the user typed.

3. Pairing credentials

After pairing, THP issues a credential that lets the same phone reconnect without a code. Implement ThpCredentialStore on top of secure storage (Keychain / Keystore) — each credential contains the host's private key. ThpCredential serializes with toJson / fromJson. InMemoryThpCredentialStore is for tests: every restart pairs again.

4. Ethereum / EVM

const path = "m/44'/60'/0'/0/0";
final address = await trezor.ethereumGetAddress(path);
// Show it on the Trezor's screen for the user to verify:
await trezor.ethereumGetAddress(path, showOnDevice: true);

final sig = await trezor.ethereumSignEip1559(
  path: path,
  chainId: 137,
  nonce: nonce,
  maxFeePerGas: maxFee,
  maxPriorityFeePerGas: tip,
  gasLimit: gasLimit,
  to: '0x…',
  value: amountWei,
  data: calldata, // any length; streamed to the device as it asks
);
// sig.v is the y-parity; build the type-2 envelope with sig.v / sig.r / sig.s.

final legacy = await trezor.ethereumSignLegacy(/* … gasPrice … */);
// legacy.v is the full EIP-155 value.

final personal = await trezor.ethereumSignMessage(path, utf8.encode('hello'));
final typed = await trezor.ethereumSignTypedHash(
  path,
  domainSeparatorHash: domainHash,
  messageHash: messageHash,
);

5. Solana

const path = "m/44'/501'/0'/0'"; // ed25519: every level hardened
if (trezor.features.hasCapability(TrezorCapability.solana)) {
  final address = await trezor.solanaGetAddress(path);
  final pubkey = await trezor.solanaGetPublicKey(path); // 32 bytes
  final signature = await trezor.solanaSignTransaction(path, messageBytes);
}

6. Tron

const path = "m/44'/195'/0'/0/0";
if (trezor.features.hasCapability(TrezorCapability.tron)) {
  final address = await trezor.tronGetAddress(path);

  // raw_data_hex from TronGrid createtransaction / triggersmartcontract:
  final signature = await trezor.tronSignRawTransaction(path, rawDataBytes);
  // 65 bytes r ‖ s ‖ v — put its hex in the transaction's `signature` list
  // and broadcast.
}

Trezor rebuilds a Tron transaction from its fields rather than signing opaque bytes, so tronSignRawTransaction first proves the rebuild will be byte-identical to rawDataBytes and throws TrezorProtocolException if not (multi-contract, permission ids, TRC-10 call fields). Supported contracts: transfer, TRC-20 / smart-contract calls, freeze / unfreeze (v2), withdraw, vote, delegate / undelegate resources.

7. Any other message

import 'package:trezor_flutter/messages.dart' as msg;

final pong = await trezor.callExpect<msg.Success>(msg.Ping(message: 'hi'));

final btc = await trezor.callWallet<msg.Address>(
  msg.GetAddress(
    addressN: parseBip32Path("m/84'/0'/0'/0/0"),
    coinName: 'Bitcoin',
    scriptType: msg.BitcoinScriptType.spendWitness,
  ),
);

call / callExpect use the seedless session (management calls); callWallet opens the seeded session first, asking for the passphrase through onPassphraseRequest when passphrase protection is on. Button, PIN and passphrase prompts in between are answered automatically through TrezorInteraction.

8. Cancel, errors and disconnect

try {
  await trezor.ethereumSignEip1559(/* … */);
} on TrezorFailureException catch (e) {
  if (e.isCancelled) print('rejected on the device or cancelled');
} on TrezorDisconnectedException {
  print('unplugged / out of range');
}

await trezor.cancel(); // backs the device out of its confirmation screen
await trezor.close();  // releases the connection

API reference

Discovery and transport

API Purpose
TrezorPlatform.instance Platform bridge (singleton).
capabilities() Which transports this platform offers: (usb, ble).
bluetoothState() TrezorBluetoothState: on, off, unauthorized, unavailable, unknown.
usbListDevices() Trezors on USB (Android, macOS, Windows, web), as TrezorDevice.
usbRequestDevice() Web: the browser's device chooser; returns the picked Trezor or null.
usbRequestPermission(deviceId) Shows Android's USB access dialog; always granted elsewhere.
bleStartScan() / bleStopScan() Bluetooth scan; results arrive on events.
events TrezorBleScanResult, TrezorBleScanFailed, TrezorUsbAttached, TrezorUsbDetached, TrezorBluetoothStateChanged, TrezorLinkDisconnected.
TrezorDevice id, transport, name, vendorId, productId, rssi, hasPermission, isBootloader.
NativeTrezorLink.open(device) Opens the device (connect, MTU, bonding and notifications for BLE).
TrezorLink The packet-pipe interface; implement it for custom transports or tests.

Client

API Purpose
TrezorClient.connect(...) Protocol detection, THP handshake and pairing, features.
features / refreshFeatures() Device information (Features).
protocol TrezorProtocol.codecV1 or TrezorProtocol.thp.
call / callExpect<T> Send any message on the seedless session.
callWallet<T> Send any message on the seeded (wallet) session.
interaction TrezorInteraction callbacks; replaceable at any time.
cancel() Abort the current operation on the device.
close() Release the protocol and close the link.
TrezorAppIdentity App and host name shown on the device when pairing.

User interaction

API Purpose
TrezorInteraction.onButtonRequest The device waits for the user to confirm on its screen.
onInteractionEnded The device answered after confirmation prompts.
onPinMatrixRequest Model One: return the PIN as scrambled-matrix positions.
onPassphraseRequest Return TrezorPassphrase.standardWallet, .onDevice or TrezorPassphraseText(...).
onPairingCodeRequest Return the 6-digit code shown on the Trezor (THP pairing).

Pairing credentials (THP)

API Purpose
ThpCredentialStore load, save, remove — implement on secure storage.
ThpCredential Stored credential; toJson / fromJson.
InMemoryThpCredentialStore Process-lifetime store for tests.

Chains

API Purpose
ethereumGetAddress(path, showOnDevice:) EIP-55 address.
ethereumSignEip1559(...) Type-2 transaction signature (EthereumSignature).
ethereumSignLegacy(...) Legacy / EIP-155 transaction signature.
ethereumSignMessage(path, message) EIP-191 personal_sign.
ethereumSignTypedHash(path, ...) EIP-712 by domain and message hash.
solanaGetAddress(path, showOnDevice:) Base58 address.
solanaGetPublicKey(path) Raw 32-byte ed25519 public key.
solanaSignTransaction(path, message) 64-byte signature over a serialized message.
tronGetAddress(path, showOnDevice:) Base58check T… address.
tronSignRawTransaction(path, rawData) 65-byte signature over TronGrid raw_data.
tronSignTransaction(tx, contract) Same, from explicit TronSignTx + contract messages.

Optional EthereumDefinitions (signed network / token definitions from data.trezor.io) can be passed to the Ethereum signing calls.

Device information

Features: model, internalModel, firmwareVersion, label, deviceId, initialized, unlocked, pinProtection, passphraseProtection, passphraseAlwaysOnDevice, bootloaderMode, capabilities and hasCapability(TrezorCapability.…).

Errors

Exception When
TrezorException Base class of everything below.
TrezorPlatformException Native error; code is a TrezorPlatformErrorCode (permissionDenied, bluetoothOff, bondingFailed, openFailed, …).
TrezorFailureException The device returned Failure; code is a TrezorFailureCode, isCancelled for rejections.
TrezorDisconnectedException The device went away.
TrezorTimeoutException No answer in time.
TrezorPairingException THP pairing failed (wrong code, declined, check mismatch).
ThpTransportException THP transport error (e.g. device locked, channel released).
TrezorProtocolException Malformed data on the wire.

Utilities

parseBip32Path("m/44'/60'/0'/0/0"), formatBip32Path(...), hardenedOffset.

Raw messages

package:trezor_flutter/messages.dart — typed protobuf classes for management (Initialize, GetFeatures, Ping, LockDevice, EndSession, …), common (Success, Failure, ButtonRequest, PinMatrixAck, PassphraseAck, …), Ethereum, Solana, Tron (TronSignTx, TronTransferContract, TronTriggerSmartContract, …), Bitcoin (GetAddress, GetPublicKey) and THP pairing messages. Import it with a prefix.

Architecture

┌──────────────────────────────── Dart (shared) ────────────────────────────────┐
│ Chain helpers   ethereum*, solana*, tron*                                     │
│ TrezorClient    prompts, sessions, cancel                                     │
│ Protocols       Codec v1 │ THP (Noise, CPace)                                 │
│ Protobuf        typed messages                                                │
│ TrezorLink      64-byte (USB) / 244-byte (BLE)                                │
└───────────────────────────────────────┬───────────────────────────────────────┘
                                        │ method + event channel
┌─── Android ───┬───── iOS ─────┬──── macOS ────┬─── Windows ───┬───── Web ─────┐
│ UsbManager    │ CoreBluetooth │ IOKit (USB)   │ WinUSB        │ WebUSB        │
│ BluetoothGatt │               │ CoreBluetooth │               │               │
└───────────────┴───────────────┴───────────────┴───────────────┴───────────────┘

On the web the platform side is Dart too (lib/trezor_flutter_web.dart, over WebUSB), behind the same channels. WebUSB exists only in Chromium-based browsers, so in Safari and Firefox the Web column has no transport at all.

Native code only discovers devices and moves fixed-size packets, which keeps it small enough to audit and makes the whole protocol stack testable with an in-memory TrezorLink.

Limitations

  • THP pairing supports code entry, the only method current firmware offers; QR code and NFC pairing are not implemented.
  • No high-level helper yet for Bitcoin signing or structured EIP-712 (by-hash signing is supported); their messages can be sent through call / callWallet.
  • Ethereum definitions from data.trezor.io are not fetched automatically; unknown chains and tokens are shown by chain id and contract address.
  • Model One PIN entry through the host needs your own matrix UI via onPinMatrixRequest; touchscreen models take the PIN on the device.
  • Web: USB only, and only in Chromium-based browsers (Safari and Firefox have no WebUSB). No Bluetooth on the web or on Windows.

About Macromodule Technologies

Macromodule Technologies — Empowering Your Passion

trezor_flutter is built and maintained by Macromodule Technologies, an AI, blockchain and software development company that helps startups, mid-market businesses and enterprises plan, build and scale secure digital products — including the hardware-wallet support in this package.

Need Trezor or other hardware-wallet integration, a crypto wallet, or a Flutter app built? Get in touch at macromodule.com.

Contributors

Asad Khan
Asad Khan

@asaddigital2809
Core contributor

Contributing

Contributions are welcome — bug reports, device test results and pull requests alike.

  • Found a bug? Open an issue with your Trezor model, firmware version, platform and transport (USB or Bluetooth).
  • Want to help? Fork MT-Public/trezor_flutter, run flutter test, and open a pull request.

Disclaimer

This is an independent, community-maintained package. It is not affiliated with, endorsed by, or supported by SatoshiLabs. "Trezor" is a trademark of SatoshiLabs s.r.o.; it is used here only to describe the devices this package works with.

License

MIT — see LICENSE.

Libraries

messages
Protobuf message classes, for sending requests the chain helpers in package:trezor_flutter/trezor_flutter.dart do not cover:
trezor_flutter
Trezor hardware wallets for Flutter, over native transports: USB and Bluetooth LE on Android, Bluetooth LE on iOS.
trezor_flutter_web
Web side of trezor_flutter: a USB packet pipe over WebUSB.