feat(connect): cancel THP workflow

This commit is contained in:
Szymon Lesisz
2025-07-11 18:38:45 +02:00
committed by Szymon Lesisz
parent 1a7e054e21
commit de01a0302e
7 changed files with 70 additions and 4 deletions

View File

@@ -16,7 +16,7 @@ import { ERRORS, FIRMWARE, PROTO } from '../constants';
import { DeviceCurrentSession, TypedCallProvider } from './DeviceCurrentSession';
import { IStateStorage } from './StateStorage';
import { checkFirmwareRevision } from './checkFirmwareRevision';
import { getThpChannel } from './thp';
import { abortThpWorkflow, getThpChannel } from './thp';
import { checkFirmwareHashWithRetries } from './workflow/checkFirmwareHashWithRetries';
import { getAllNetworks } from '../data/coinInfo';
import { getFirmwareReleaseConfigInfo, getFirmwareStatus, getLanguage } from '../data/firmwareInfo';
@@ -439,6 +439,7 @@ export class Device extends TypedEmitter<DeviceEvents> {
}
async interrupt(reason: Error) {
await abortThpWorkflow(this);
await this.currentSession?.abort(reason);
// reject inner defer

View File

@@ -209,6 +209,7 @@ export class DeviceCurrentSession implements TypedCallProvider {
// ignore whatever happens
}
} else {
this.device.getThpState()?.sync('send', 'Cancel');
await this.transport.send({
name: 'Cancel',
data: {},

View File

@@ -141,7 +141,7 @@ const waitForPairingTag = async (device: Device) => {
// start listening for the Cancel message from Trezor
const { readAbort, readCancel } = waitForPairingCancel(device);
readCancel
const cancelResult = readCancel
.then(readResult => {
if (readResult.success) {
let error: string;
@@ -158,6 +158,13 @@ const waitForPairingTag = async (device: Device) => {
// silent
});
thpState.setPairingTagPromise({
abort: async () => {
readAbort.abort();
await cancelResult;
},
});
// start listening for the UI response
const payload = {
availableMethods: thpState.handshakeCredentials.pairingMethods,
@@ -180,6 +187,8 @@ const waitForPairingTag = async (device: Device) => {
readAbort.abort();
await readCancel;
thpState.setPairingTagPromise(undefined);
if ('error' in pairingResponse) {
throw new Error(pairingResponse.error);
}

View File

@@ -61,7 +61,11 @@ export const thpCall = async <T extends MessageKey>(
throw ERRORS.serializeError({ code: result.error, message: result.error.message });
}
thpState.setCancelablePromise(false);
if (result.payload.type === 'ButtonRequest') {
thpState.setCancelablePromise(true);
if (result.payload.message.code === 'ButtonRequest_PassphraseEntry') {
device.emit(DEVICE.PASSPHRASE_ON_DEVICE);
} else {
@@ -81,3 +85,25 @@ export const thpCall = async <T extends MessageKey>(
return result.payload as ThpCallResponse[T];
};
export const abortThpWorkflow = async (device: Device) => {
const thpState = device.getThpState();
if (!thpState || !device.currentRun) {
return Promise.resolve(); // not a THP device
}
// check that current workflow is awaiting for Cancel (see ./pairing waitForPairingCancel)
// - transport is in read state (read Cancel from the device)
// - @trezor/connect is waiting for UI response (pairing tag)
// in that case we don't need to update THP sync values because thpState is synchronized
// in any other case we need to update sync values before current transport.call process is resolved
if (thpState.pairingTagPromise) {
await thpState.pairingTagPromise.abort();
await device.getCurrentSession().cancelCall();
thpState.resetState();
} else if (thpState.cancelablePromise) {
thpState.sync('send', 'Cancel');
await device.getCurrentSession().send('Cancel', {});
await device.currentRun;
}
};

View File

@@ -24,6 +24,8 @@ export class ThpState {
private _pairingCredentials: ThpCredentials[] = [];
private _phase: ThpPhase = 'handshake';
private _isPaired: boolean = false;
private _pairingTagPromise: { abort: () => Promise<void> } | undefined;
private _cancelablePromise: boolean = false;
private _handshakeCredentials?: ThpHandshakeCredentials;
private _channel: Buffer = Buffer.alloc(0);
private _sendBit: ThpMessageSyncBit = 0;
@@ -34,6 +36,22 @@ export class ThpState {
private _selectedMethod?: ThpPairingMethod;
private _nfcSecret?: Buffer;
get pairingTagPromise() {
return this._pairingTagPromise;
}
setPairingTagPromise(p?: { abort: () => Promise<void> }) {
this._pairingTagPromise = p;
}
get cancelablePromise() {
return this._cancelablePromise;
}
setCancelablePromise(p: boolean) {
this._cancelablePromise = p;
}
get properties() {
return this._properties;
}
@@ -245,6 +263,9 @@ export class ThpState {
resetState() {
this._phase = 'handshake';
this._isPaired = false;
this._properties = undefined;
this._pairingTagPromise = undefined;
this._cancelablePromise = false;
this._handshakeCredentials = undefined;
this._channel = Buffer.alloc(0);
this._sendBit = 0;

View File

@@ -220,6 +220,7 @@ export abstract class AbstractApiTransport extends AbstractTransport {
this.api.read(path, attemptSignal || signal);
if (protocol.name === 'v2') {
const prevNonce = thpState?.sendNonce;
const callResult = await callThpMessage({
thpState,
chunks,
@@ -234,7 +235,10 @@ export abstract class AbstractApiTransport extends AbstractTransport {
return callResult;
}
thpState?.sync('send', name);
// sync bit and nonce updated by Cancel
if (prevNonce === thpState?.sendNonce) {
thpState?.sync('send', name);
}
const message = parseThpMessage({
messages: this.messages,
decoded: callResult.payload,

View File

@@ -236,6 +236,7 @@ export class BridgeTransport extends AbstractTransport {
thpState,
});
const prevNonce = thpState?.sendNonce;
const response = await this.post(`/call`, {
params: session,
body: this.getRequestBody(bytes, protocol, thpState),
@@ -249,7 +250,10 @@ export class BridgeTransport extends AbstractTransport {
const respBytes = Buffer.from(response.payload.data, 'hex');
if (protocol.name === 'v2') {
// see callThpMessage in @trezor/transport-bridge
thpState?.sync('send', name);
// sync bit and nonce updated by Cancel
if (prevNonce === thpState?.sendNonce) {
thpState?.sync('send', name);
}
const message = parseThpMessage({
decoded: protocol.decode(respBytes),
messages: this.messages,