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 — 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
- Status
- Supported devices
- Installation and platform setup
- Usage
- API reference
- Architecture
- Limitations
- About Macromodule Technologies
- Contributors
Features
Transports
- USB on Android, macOS and Windows, 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 and Windows; 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) |
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) | 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 and Windows, Model One needs firmware 1.7 or later (WebUSB); older firmware only has a HID interface, which Android supports and the desktop platforms do not.
iOS apps cannot talk to a Trezor over USB, so iOS supports Bluetooth only. Windows supports USB only.
Installation
dependencies:
trezor_flutter: ^1.2.0
Android
minSdkVersion21.- The plugin's manifest declares
BLUETOOTH_SCAN(withneverForLocation),BLUETOOTH_CONNECT, the pre-Android-12BLUETOOTH/BLUETOOTH_ADMIN, and the optionalusb.host/bluetooth_lefeatures. They merge into your app. - Your app must request the Bluetooth runtime permissions before scanning
or connecting (
BLUETOOTH_SCAN+BLUETOOTH_CONNECTon Android 12+). On Android 11 and below a scan also needsACCESS_FINE_LOCATION, which the plugin deliberately does not declare — add it to your manifest withandroid: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.entitlementsandRelease.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()reportsble: false, and the Bluetooth methods throwTrezorPlatformExceptionwith codeunsupported. - Only one app can hold a Trezor's USB interface: close Trezor Suite (and stop
trezord) 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
// USB (Android, macOS): list what is plugged in, and follow plug / unplug.
final usbDevices = await platform.usbListDevices();
// 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 and Windows
}
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 |
Native bridge (singleton). |
capabilities() |
Which transports this platform offers: (usb, ble). |
bluetoothState() |
TrezorBluetoothState: on, off, unauthorized, unavailable, unknown. |
usbListDevices() |
Trezors on USB (Android, macOS), as TrezorDevice. |
usbRequestPermission(deviceId) |
Shows Android's USB access dialog; always granted on macOS. |
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 ───┐
│ UsbManager │ CoreBluetooth │ IOKit (USB) │ WinUSB │
│ BluetoothGatt │ │ CoreBluetooth │ │
└───────────────┴───────────────┴───────────────┴───────────────┘
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.ioare 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.
About Macromodule Technologies
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 @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.dartdo not cover: - trezor_flutter
- Trezor hardware wallets for Flutter, over native transports: USB and Bluetooth LE on Android, Bluetooth LE on iOS.
