@onekeyfe/hd-core 1.2.2-alpha.100 → 1.2.2-alpha.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +20 -4
- package/__tests__/DeviceCommands.test.ts +13 -0
- package/__tests__/check-all-firmware-release-protocol-v2.test.ts +0 -2
- package/__tests__/check-firmware-release-protocol-v2.test.ts +0 -2
- package/__tests__/device-lifecycle-events.test.ts +86 -0
- package/__tests__/deviceUploadNft.test.ts +68 -0
- package/__tests__/evmSignTypedData.test.ts +42 -0
- package/__tests__/firmware-update/firmware-update-v4-install-poll.test.ts +825 -12
- package/__tests__/logBlockEvent.test.ts +45 -122
- package/__tests__/open-wallet-session.test.ts +781 -69
- package/__tests__/pro2HostAssetPackage.test.ts +165 -0
- package/__tests__/protocol-v2-resources.test.ts +7 -4
- package/__tests__/protocol-v2-unlock-policy.test.ts +35 -1
- package/__tests__/protocol-v2.test.ts +925 -97
- package/__tests__/protocolV2FileWrite.test.ts +40 -0
- package/dist/api/FirmwareUpdateV4.d.ts +5 -0
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/OpenWalletSession.d.ts.map +1 -1
- package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
- package/dist/api/evm/EVMSignTypedData.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +2 -2
- package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
- package/dist/api/firmware/getBinary.d.ts +2 -2
- package/dist/api/helpers/protocolV2FileWrite.d.ts +1 -0
- package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceGetFirmwareUpdateStatus.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
- package/dist/api/protocol-v2/helpers.d.ts.map +1 -1
- package/dist/core/RequestQueue.d.ts +2 -0
- package/dist/core/RequestQueue.d.ts.map +1 -1
- package/dist/core/index.d.ts +3 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/device/Device.d.ts +1 -1
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/device/DeviceConnector.d.ts +1 -1
- package/dist/device/DeviceConnector.d.ts.map +1 -1
- package/dist/events/logBlockEvent.d.ts.map +1 -1
- package/dist/events/ui-request.d.ts +3 -0
- package/dist/events/ui-request.d.ts.map +1 -1
- package/dist/index.d.ts +21 -5
- package/dist/index.js +2511 -1538
- package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts +2 -0
- package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/walletSession.d.ts +3 -0
- package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
- package/dist/types/settings.d.ts +2 -2
- package/dist/types/settings.d.ts.map +1 -1
- package/dist/utils/deviceFeaturesUtils.d.ts +3 -0
- package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
- package/dist/utils/patch.d.ts +1 -1
- package/dist/utils/patch.d.ts.map +1 -1
- package/dist/utils/pro2HostAssetPackage.d.ts +8 -0
- package/dist/utils/pro2HostAssetPackage.d.ts.map +1 -0
- package/package.json +4 -4
- package/src/api/FirmwareUpdateV4.ts +474 -83
- package/src/api/OpenWalletSession.ts +39 -12
- package/src/api/allnetwork/AllNetworkGetAddressBase.ts +5 -2
- package/src/api/evm/EVMSignTypedData.ts +1 -0
- package/src/api/firmware/FirmwareUpdateBaseMethod.ts +9 -3
- package/src/api/helpers/protocolV2FileWrite.ts +11 -5
- package/src/api/protocol-v2/DeviceGetFirmwareUpdateStatus.ts +7 -1
- package/src/api/protocol-v2/DeviceUploadNft.ts +31 -8
- package/src/api/protocol-v2/DeviceUploadWallpaper.ts +33 -8
- package/src/api/protocol-v2/helpers.ts +8 -0
- package/src/core/RequestQueue.ts +16 -1
- package/src/core/index.ts +164 -26
- package/src/data/messages/messages-protocol-v2.json +1589 -1375
- package/src/device/Device.ts +3 -1
- package/src/device/DeviceConnector.ts +7 -3
- package/src/events/logBlockEvent.ts +6 -75
- package/src/events/ui-request.ts +3 -0
- package/src/protocols/protocol-v2/resources.ts +9 -1
- package/src/protocols/protocol-v2/unlockPolicyRunner.ts +8 -1
- package/src/protocols/protocol-v2/walletSession.ts +223 -56
- package/src/types/settings.ts +4 -2
- package/src/utils/deviceFeaturesUtils.ts +4 -0
- package/src/utils/pro2HostAssetPackage.ts +400 -0
package/src/device/Device.ts
CHANGED
|
@@ -2005,7 +2005,8 @@ export class Device extends EventEmitter {
|
|
|
2005
2005
|
passphraseState?: string,
|
|
2006
2006
|
useEmptyPassphrase?: boolean,
|
|
2007
2007
|
skipPassphraseCheck?: boolean,
|
|
2008
|
-
deriveCardano?: boolean
|
|
2008
|
+
deriveCardano?: boolean,
|
|
2009
|
+
mainPinSelected?: boolean
|
|
2009
2010
|
) {
|
|
2010
2011
|
if (this.isUnacquired()) return false;
|
|
2011
2012
|
|
|
@@ -2016,6 +2017,7 @@ export class Device extends EventEmitter {
|
|
|
2016
2017
|
onlyMainPin: useEmptyPassphrase,
|
|
2017
2018
|
deriveCardano,
|
|
2018
2019
|
rejectAttachPinForMainWallet: useEmptyPassphrase === true,
|
|
2020
|
+
mainPinSelected,
|
|
2019
2021
|
});
|
|
2020
2022
|
|
|
2021
2023
|
// Main wallet and unlock Attach Pin, throw safe error
|
|
@@ -96,7 +96,8 @@ export default class DeviceConnector {
|
|
|
96
96
|
forceCleanRunPromise?: boolean,
|
|
97
97
|
expectedProtocol?: HardwareConnectProtocol,
|
|
98
98
|
protocolHint?: HardwareConnectProtocol,
|
|
99
|
-
forceProtocolDetection?: boolean
|
|
99
|
+
forceProtocolDetection?: boolean,
|
|
100
|
+
skipProtocolProbe?: boolean
|
|
100
101
|
) {
|
|
101
102
|
Log.debug('acquire', path, session, expectedProtocol, protocolHint);
|
|
102
103
|
const env = DataManager.getSettings('env');
|
|
@@ -104,13 +105,15 @@ export default class DeviceConnector {
|
|
|
104
105
|
const transport = this.getActiveTransport();
|
|
105
106
|
let res;
|
|
106
107
|
if (DataManager.isBleConnect(env)) {
|
|
107
|
-
|
|
108
|
+
const acquireInput: Parameters<Transport['acquire']>[0] = {
|
|
108
109
|
uuid: path,
|
|
109
110
|
forceCleanRunPromise,
|
|
110
111
|
expectedProtocol,
|
|
111
112
|
protocolHint,
|
|
112
113
|
forceProtocolDetection,
|
|
113
|
-
|
|
114
|
+
skipProtocolProbe,
|
|
115
|
+
};
|
|
116
|
+
res = await transport.acquire(acquireInput);
|
|
114
117
|
} else {
|
|
115
118
|
res = await transport.acquire({
|
|
116
119
|
path,
|
|
@@ -118,6 +121,7 @@ export default class DeviceConnector {
|
|
|
118
121
|
expectedProtocol,
|
|
119
122
|
protocolHint,
|
|
120
123
|
forceProtocolDetection,
|
|
124
|
+
skipProtocolProbe,
|
|
121
125
|
});
|
|
122
126
|
}
|
|
123
127
|
if (expectedProtocol) {
|
|
@@ -8,8 +8,9 @@ export const LogBlockEvent: Set<string> = new Set([
|
|
|
8
8
|
UI_RESPONSE.RECEIVE_PASSPHRASE,
|
|
9
9
|
]);
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
// These resource/file APIs did not exist in 1.1.32. Skip the payload so
|
|
12
|
+
// the log layer does not copy huge Base64 or binary data.
|
|
13
|
+
const LogPayloadBlockMethod: Set<string> = new Set([
|
|
13
14
|
'deviceUploadNft',
|
|
14
15
|
'deviceUploadWallpaper',
|
|
15
16
|
'uploadPortfolio',
|
|
@@ -17,61 +18,6 @@ const LogLabelMethod: Set<string> = new Set([
|
|
|
17
18
|
'fileRead',
|
|
18
19
|
]);
|
|
19
20
|
|
|
20
|
-
// 资源上传参数可能包含很大的 Base64 字符串。这里按方法整段跳过,避免日志层
|
|
21
|
-
// 递归复制和序列化这些数据;资源 API 与传输内容本身保持不变。
|
|
22
|
-
const LogPayloadBlockMethod: Set<string> = new Set([
|
|
23
|
-
'deviceUploadNft',
|
|
24
|
-
'deviceUploadWallpaper',
|
|
25
|
-
'uploadPortfolio',
|
|
26
|
-
]);
|
|
27
|
-
|
|
28
|
-
const SensitiveLogKeys: Set<string> = new Set([
|
|
29
|
-
'devicestate',
|
|
30
|
-
'entropy',
|
|
31
|
-
'expectedpassphrasestate',
|
|
32
|
-
'mnemonic',
|
|
33
|
-
'passphrase',
|
|
34
|
-
'passphrasestate',
|
|
35
|
-
'password',
|
|
36
|
-
'pin',
|
|
37
|
-
'privatekey',
|
|
38
|
-
'seed',
|
|
39
|
-
'session',
|
|
40
|
-
'sessionid',
|
|
41
|
-
'walletsessionid',
|
|
42
|
-
'xprv',
|
|
43
|
-
]);
|
|
44
|
-
|
|
45
|
-
const normalizeLogKey = (key: string) => key.replace(/[_-]/g, '').toLowerCase();
|
|
46
|
-
|
|
47
|
-
const isSigningMethod = (methodName: string) => /sign/i.test(methodName);
|
|
48
|
-
|
|
49
|
-
const redactLogValue = (value: unknown, seen: WeakSet<object>): unknown => {
|
|
50
|
-
if (ArrayBuffer.isView(value)) {
|
|
51
|
-
return `[BINARY:${value.byteLength}]`;
|
|
52
|
-
}
|
|
53
|
-
if (value instanceof ArrayBuffer) {
|
|
54
|
-
return `[BINARY:${value.byteLength}]`;
|
|
55
|
-
}
|
|
56
|
-
if (Array.isArray(value)) {
|
|
57
|
-
return value.map(item => redactLogValue(item, seen));
|
|
58
|
-
}
|
|
59
|
-
if (!value || typeof value !== 'object') return value;
|
|
60
|
-
if (seen.has(value)) return '[CIRCULAR]';
|
|
61
|
-
|
|
62
|
-
seen.add(value);
|
|
63
|
-
const redacted = Object.fromEntries(
|
|
64
|
-
Object.entries(value as Record<string, unknown>).map(([key, item]) => [
|
|
65
|
-
key,
|
|
66
|
-
SensitiveLogKeys.has(normalizeLogKey(key)) && item !== null && item !== undefined
|
|
67
|
-
? '[REDACTED]'
|
|
68
|
-
: redactLogValue(item, seen),
|
|
69
|
-
])
|
|
70
|
-
);
|
|
71
|
-
seen.delete(value);
|
|
72
|
-
return redacted;
|
|
73
|
-
};
|
|
74
|
-
|
|
75
21
|
export function getLogBlockLabel(message: unknown): string | undefined {
|
|
76
22
|
if (!message || typeof message !== 'object') return undefined;
|
|
77
23
|
|
|
@@ -86,7 +32,7 @@ export function getLogBlockLabel(message: unknown): string | undefined {
|
|
|
86
32
|
}
|
|
87
33
|
|
|
88
34
|
const methodName = method ?? payload?.method;
|
|
89
|
-
if (methodName &&
|
|
35
|
+
if (methodName && LogPayloadBlockMethod.has(methodName)) {
|
|
90
36
|
return methodName;
|
|
91
37
|
}
|
|
92
38
|
|
|
@@ -94,25 +40,10 @@ export function getLogBlockLabel(message: unknown): string | undefined {
|
|
|
94
40
|
}
|
|
95
41
|
|
|
96
42
|
export function getSafeLogPayload(value: unknown, blockLabel?: string): unknown {
|
|
97
|
-
if (
|
|
98
|
-
blockLabel &&
|
|
99
|
-
(LogBlockEvent.has(blockLabel) ||
|
|
100
|
-
LogPayloadBlockMethod.has(blockLabel) ||
|
|
101
|
-
isSigningMethod(blockLabel))
|
|
102
|
-
) {
|
|
43
|
+
if (blockLabel) {
|
|
103
44
|
return { method: blockLabel, payload: '[REDACTED]' };
|
|
104
45
|
}
|
|
105
|
-
|
|
106
|
-
const redactedValue = redactLogValue(value, new WeakSet());
|
|
107
|
-
if (
|
|
108
|
-
blockLabel &&
|
|
109
|
-
redactedValue &&
|
|
110
|
-
typeof redactedValue === 'object' &&
|
|
111
|
-
!Array.isArray(redactedValue)
|
|
112
|
-
) {
|
|
113
|
-
return { ...redactedValue, method: blockLabel };
|
|
114
|
-
}
|
|
115
|
-
return redactedValue;
|
|
46
|
+
return value;
|
|
116
47
|
}
|
|
117
48
|
|
|
118
49
|
export function formatLogMethodLabel(label: string, methodName?: string): string {
|
package/src/events/ui-request.ts
CHANGED
|
@@ -189,6 +189,9 @@ export interface FirmwareProgress {
|
|
|
189
189
|
totalBytes?: number;
|
|
190
190
|
rateBytesPerSecond?: number;
|
|
191
191
|
elapsedMs?: number;
|
|
192
|
+
installTargetId?: number;
|
|
193
|
+
installPhase?: 'prepare' | 'install' | 'verify';
|
|
194
|
+
installPhaseProgress?: number;
|
|
192
195
|
};
|
|
193
196
|
}
|
|
194
197
|
|
|
@@ -59,7 +59,15 @@ export function isProtocolV2ResourceArchiveEntryName(entryName: string): boolean
|
|
|
59
59
|
return (
|
|
60
60
|
fileName.length > 0 &&
|
|
61
61
|
!fileName.startsWith('.') &&
|
|
62
|
-
!parts.some(
|
|
62
|
+
!parts.some(
|
|
63
|
+
part =>
|
|
64
|
+
!part ||
|
|
65
|
+
part === '.' ||
|
|
66
|
+
part === '..' ||
|
|
67
|
+
part === '.DS_Store' ||
|
|
68
|
+
part.startsWith('._') ||
|
|
69
|
+
part.toUpperCase() === '__MACOSX'
|
|
70
|
+
)
|
|
63
71
|
);
|
|
64
72
|
}
|
|
65
73
|
|
|
@@ -14,10 +14,14 @@ const Log = getLogger(LoggerNames.Core);
|
|
|
14
14
|
|
|
15
15
|
export type ProtocolV2UnlockContext = {
|
|
16
16
|
preflightCompleted: boolean;
|
|
17
|
+
preflightStatusRefreshed?: boolean;
|
|
18
|
+
preflightMainPinSelected?: boolean;
|
|
17
19
|
};
|
|
18
20
|
|
|
19
21
|
export const createProtocolV2UnlockContext = (): ProtocolV2UnlockContext => ({
|
|
20
22
|
preflightCompleted: false,
|
|
23
|
+
preflightStatusRefreshed: false,
|
|
24
|
+
preflightMainPinSelected: false,
|
|
21
25
|
});
|
|
22
26
|
|
|
23
27
|
type RunnableMethod = Pick<
|
|
@@ -100,11 +104,12 @@ export async function runMethodWithUnlockPolicy<T = unknown>(
|
|
|
100
104
|
await afterStatusBeforeUnlock?.();
|
|
101
105
|
|
|
102
106
|
if (!status.unlocked) {
|
|
107
|
+
const preUnlockPinType = resolvePreUnlockPinType(method);
|
|
103
108
|
const unlockInteraction: HardwareUiInteractionMeta | undefined = shouldCoordinateUi
|
|
104
109
|
? uiCoordinator.enterUnlockInteraction(method.name)
|
|
105
110
|
: undefined;
|
|
106
111
|
const unlockedStatus = await device.unlockDevice(
|
|
107
|
-
|
|
112
|
+
preUnlockPinType,
|
|
108
113
|
shouldCoordinateUi
|
|
109
114
|
? { emitUiEvent: false, interaction: unlockInteraction }
|
|
110
115
|
: {
|
|
@@ -120,9 +125,11 @@ export async function runMethodWithUnlockPolicy<T = unknown>(
|
|
|
120
125
|
'Protocol V2 device remained locked after the unlock flow.'
|
|
121
126
|
);
|
|
122
127
|
}
|
|
128
|
+
context.preflightMainPinSelected = preUnlockPinType === DeviceSessionPinType.Main;
|
|
123
129
|
Log.debug('Protocol V2 pre-unlock completed', { method: method.name });
|
|
124
130
|
}
|
|
125
131
|
|
|
132
|
+
context.preflightStatusRefreshed = true;
|
|
126
133
|
context.preflightCompleted = true;
|
|
127
134
|
}
|
|
128
135
|
|
|
@@ -63,35 +63,50 @@ const negotiateEventlessWalletSession = async (device: Device) => {
|
|
|
63
63
|
const getDeviceSession = async (device: Device, request: DeviceSessionGet) =>
|
|
64
64
|
device.commands.typedCall('DeviceSessionGet', 'DeviceSession', request);
|
|
65
65
|
|
|
66
|
+
const STANDARD_SEED_DOMAINS = [DeviceSessionSeedDomain.SeedDomain_Standard];
|
|
67
|
+
const CARDANO_SEED_DOMAINS = [
|
|
68
|
+
DeviceSessionSeedDomain.SeedDomain_Standard,
|
|
69
|
+
DeviceSessionSeedDomain.SeedDomain_Cardano,
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
const buildDeviceSessionSeedDomains = (deriveCardano?: boolean): DeviceSessionSeedDomain[] =>
|
|
73
|
+
deriveCardano === true ? CARDANO_SEED_DOMAINS : STANDARD_SEED_DOMAINS;
|
|
74
|
+
|
|
75
|
+
const deviceSessionHasCardano = (message: { seed_domains?: DeviceSessionSeedDomain[] }) =>
|
|
76
|
+
Array.isArray(message.seed_domains) &&
|
|
77
|
+
message.seed_domains.includes(DeviceSessionSeedDomain.SeedDomain_Cardano);
|
|
78
|
+
|
|
79
|
+
// origin/dev Get is read/resume only. Seed generation lives on AskPassphrase.
|
|
66
80
|
const buildDeviceSessionGetRequest = ({
|
|
67
81
|
sessionId,
|
|
68
82
|
expectedPassphraseState,
|
|
69
|
-
deriveCardano,
|
|
70
83
|
}: {
|
|
71
84
|
sessionId?: string;
|
|
72
85
|
expectedPassphraseState?: string;
|
|
73
|
-
deriveCardano?: boolean;
|
|
74
86
|
} = {}): DeviceSessionGet => ({
|
|
75
87
|
...(sessionId ? { session_id: sessionId } : {}),
|
|
76
88
|
...(expectedPassphraseState ? { btc_test_address: expectedPassphraseState } : {}),
|
|
77
|
-
seed_domains:
|
|
78
|
-
deriveCardano === undefined
|
|
79
|
-
? []
|
|
80
|
-
: [
|
|
81
|
-
DeviceSessionSeedDomain.SeedDomain_Standard,
|
|
82
|
-
...(deriveCardano ? [DeviceSessionSeedDomain.SeedDomain_Cardano] : []),
|
|
83
|
-
],
|
|
84
89
|
});
|
|
85
90
|
|
|
86
|
-
const askDevicePassphrase = async (
|
|
87
|
-
|
|
91
|
+
const askDevicePassphrase = async (
|
|
92
|
+
device: Device,
|
|
93
|
+
requestPayload: Omit<DeviceSessionAskPassphrase, 'seed_domains'>,
|
|
94
|
+
deriveCardano?: boolean,
|
|
95
|
+
onStatusRefreshed?: () => void
|
|
96
|
+
) => {
|
|
97
|
+
await device.commands.typedCall('DeviceSessionAskPassphrase', 'Success', {
|
|
98
|
+
...requestPayload,
|
|
99
|
+
seed_domains: buildDeviceSessionSeedDomains(deriveCardano),
|
|
100
|
+
});
|
|
88
101
|
await refreshProtocolV2DeviceStatus(device);
|
|
102
|
+
onStatusRefreshed?.();
|
|
89
103
|
};
|
|
90
104
|
|
|
91
105
|
const selectDeviceSession = async (
|
|
92
106
|
device: Device,
|
|
93
107
|
expectedPassphraseState?: string,
|
|
94
|
-
deriveCardano?: boolean
|
|
108
|
+
deriveCardano?: boolean,
|
|
109
|
+
onStatusRefreshed?: () => void
|
|
95
110
|
) => {
|
|
96
111
|
const existsAttachPinUser = device.features?.attachToPinEnabled === true;
|
|
97
112
|
const metadata = {
|
|
@@ -153,15 +168,36 @@ const selectDeviceSession = async (
|
|
|
153
168
|
emitUiEvent: false,
|
|
154
169
|
interaction: attachPinInteraction,
|
|
155
170
|
});
|
|
156
|
-
|
|
171
|
+
onStatusRefreshed?.();
|
|
172
|
+
if (deriveCardano === true) {
|
|
173
|
+
await askDevicePassphrase(
|
|
174
|
+
device,
|
|
175
|
+
{ passphrase: '', on_device: false },
|
|
176
|
+
true,
|
|
177
|
+
onStatusRefreshed
|
|
178
|
+
);
|
|
179
|
+
// Firmware AskPassphrase Success clears unlocked_by_attach_to_pin.
|
|
180
|
+
// Identity is still the Attach PIN wallet; keep the SDK flag so a later
|
|
181
|
+
// passphrase picker still locks instead of prompting.
|
|
182
|
+
if (device.features) {
|
|
183
|
+
device.features.unlockedAttachPin = true;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const attachPinSession = await getDeviceSession(device, buildDeviceSessionGetRequest());
|
|
187
|
+
return Object.assign(attachPinSession, { viaAttachPin: true as const });
|
|
157
188
|
}
|
|
158
189
|
|
|
159
190
|
if (hasHostPassphrase) {
|
|
160
|
-
await askDevicePassphrase(
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
191
|
+
await askDevicePassphrase(
|
|
192
|
+
device,
|
|
193
|
+
{
|
|
194
|
+
passphrase: hostPassphrase,
|
|
195
|
+
on_device: false,
|
|
196
|
+
},
|
|
197
|
+
deriveCardano,
|
|
198
|
+
onStatusRefreshed
|
|
199
|
+
);
|
|
200
|
+
return getDeviceSession(device, buildDeviceSessionGetRequest());
|
|
165
201
|
}
|
|
166
202
|
|
|
167
203
|
const passphraseOnDeviceInteraction = device.createProtocolV2UiPhaseMetadata?.(
|
|
@@ -172,8 +208,8 @@ const selectDeviceSession = async (
|
|
|
172
208
|
...metadata,
|
|
173
209
|
...(passphraseOnDeviceInteraction ? { interaction: passphraseOnDeviceInteraction } : {}),
|
|
174
210
|
});
|
|
175
|
-
await askDevicePassphrase(device, { on_device: true });
|
|
176
|
-
return getDeviceSession(device, buildDeviceSessionGetRequest(
|
|
211
|
+
await askDevicePassphrase(device, { on_device: true }, deriveCardano, onStatusRefreshed);
|
|
212
|
+
return getDeviceSession(device, buildDeviceSessionGetRequest());
|
|
177
213
|
};
|
|
178
214
|
|
|
179
215
|
export async function getProtocolV2WalletSession(
|
|
@@ -191,11 +227,17 @@ export async function getProtocolV2WalletSession(
|
|
|
191
227
|
deriveCardano?: boolean;
|
|
192
228
|
/** Read the wallet already selected by Attach PIN without reopening wallet selection. */
|
|
193
229
|
readCurrentAttachPinSession?: boolean;
|
|
230
|
+
/** The current call already selected the Main PIN during its unlock preflight. */
|
|
231
|
+
mainPinSelected?: boolean;
|
|
194
232
|
}
|
|
195
233
|
) {
|
|
196
234
|
const forceWalletSelection =
|
|
197
235
|
options?.forceWalletSelection === true || options?.initSession === true;
|
|
198
236
|
const readCurrentAttachPinSession = options?.readCurrentAttachPinSession === true;
|
|
237
|
+
const sessionIsAttachPinWallet = (session?: unknown) =>
|
|
238
|
+
readCurrentAttachPinSession ||
|
|
239
|
+
(session as { viaAttachPin?: boolean } | undefined)?.viaAttachPin === true ||
|
|
240
|
+
device.features?.unlockedAttachPin === true;
|
|
199
241
|
|
|
200
242
|
if (forceWalletSelection) {
|
|
201
243
|
if (options.onlyMainPin) {
|
|
@@ -227,10 +269,20 @@ export async function getProtocolV2WalletSession(
|
|
|
227
269
|
: undefined;
|
|
228
270
|
let response;
|
|
229
271
|
let resumed = false;
|
|
230
|
-
let
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
272
|
+
let walletStatusRefreshed = false;
|
|
273
|
+
const markWalletStatusRefreshed = () => {
|
|
274
|
+
walletStatusRefreshed = true;
|
|
275
|
+
};
|
|
276
|
+
if (options?.onlyMainPin && options.mainPinSelected !== true) {
|
|
277
|
+
await refreshProtocolV2DeviceStatus(device);
|
|
278
|
+
markWalletStatusRefreshed();
|
|
279
|
+
}
|
|
280
|
+
let mainPinAuthenticated =
|
|
281
|
+
options?.mainPinSelected === true ||
|
|
282
|
+
(options?.onlyMainPin === true &&
|
|
283
|
+
device.features?.unlocked === true &&
|
|
284
|
+
device.features?.unlockedAttachPin === false);
|
|
285
|
+
let standardWalletSelected = options?.mainPinSelected === true;
|
|
234
286
|
|
|
235
287
|
const clearCurrentWalletSession = () => {
|
|
236
288
|
if (options?.onlyMainPin) {
|
|
@@ -240,8 +292,41 @@ export async function getProtocolV2WalletSession(
|
|
|
240
292
|
}
|
|
241
293
|
};
|
|
242
294
|
|
|
295
|
+
const sessionGetRequest = ({
|
|
296
|
+
sessionId,
|
|
297
|
+
expectedPassphraseState: passphraseState,
|
|
298
|
+
}: {
|
|
299
|
+
sessionId?: string;
|
|
300
|
+
expectedPassphraseState?: string;
|
|
301
|
+
} = {}) =>
|
|
302
|
+
buildDeviceSessionGetRequest({
|
|
303
|
+
sessionId,
|
|
304
|
+
expectedPassphraseState: passphraseState,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const askEmptyPassphraseAndGet = async ({
|
|
308
|
+
deriveCardano,
|
|
309
|
+
keepAttachPin,
|
|
310
|
+
}: {
|
|
311
|
+
deriveCardano?: boolean;
|
|
312
|
+
keepAttachPin?: boolean;
|
|
313
|
+
} = {}) => {
|
|
314
|
+
await askDevicePassphrase(
|
|
315
|
+
device,
|
|
316
|
+
{ passphrase: '', on_device: false },
|
|
317
|
+
deriveCardano,
|
|
318
|
+
markWalletStatusRefreshed
|
|
319
|
+
);
|
|
320
|
+
if (keepAttachPin && device.features) {
|
|
321
|
+
device.features.unlockedAttachPin = true;
|
|
322
|
+
}
|
|
323
|
+
const session = await getDeviceSession(device, buildDeviceSessionGetRequest());
|
|
324
|
+
return keepAttachPin ? Object.assign(session, { viaAttachPin: true as const }) : session;
|
|
325
|
+
};
|
|
326
|
+
|
|
243
327
|
const rejectMismatchedAttachPinWallet = async () => {
|
|
244
328
|
const features = await refreshProtocolV2DeviceStatus(device);
|
|
329
|
+
markWalletStatusRefreshed();
|
|
245
330
|
if (features.unlockedAttachPin !== true) {
|
|
246
331
|
return;
|
|
247
332
|
}
|
|
@@ -260,35 +345,63 @@ export async function getProtocolV2WalletSession(
|
|
|
260
345
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckUnlockTypeError);
|
|
261
346
|
};
|
|
262
347
|
|
|
348
|
+
// The passphrase picker would prompt. Empty host AskPassphrase does not;
|
|
349
|
+
// that path is Attach PIN / standard Cardano. Switching to a different
|
|
350
|
+
// passphrase wallet still locks first.
|
|
351
|
+
const lockAttachPinBeforePassphraseSelection = async () => {
|
|
352
|
+
if (readCurrentAttachPinSession || options?.onlyMainPin) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (device.features?.unlockedAttachPin !== true) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
await rejectMismatchedAttachPinWallet();
|
|
359
|
+
};
|
|
360
|
+
|
|
263
361
|
if (options?.onlyMainPin && options.rejectAttachPinForMainWallet) {
|
|
264
362
|
await rejectMismatchedAttachPinWallet();
|
|
265
363
|
}
|
|
266
364
|
|
|
267
365
|
const selectMainPin = async (force = false) => {
|
|
268
|
-
if (force || !
|
|
366
|
+
if (force || !mainPinAuthenticated) {
|
|
269
367
|
await device.unlockDevice(DeviceSessionPinType.Main, {
|
|
270
368
|
source: 'wallet-session-coordinator',
|
|
271
369
|
reason: expectedPassphraseState ? 'session-recovery' : 'open-wallet',
|
|
272
370
|
deviceOnly: true,
|
|
273
371
|
});
|
|
274
|
-
|
|
372
|
+
markWalletStatusRefreshed();
|
|
373
|
+
mainPinAuthenticated = true;
|
|
374
|
+
standardWalletSelected = true;
|
|
275
375
|
}
|
|
276
376
|
};
|
|
277
377
|
|
|
278
|
-
const selectStandardWallet = async () => {
|
|
279
|
-
// Main PIN authenticates the device; an empty host passphrase selects the standard derivation.
|
|
280
|
-
await selectMainPin();
|
|
378
|
+
const selectStandardWallet = async (forceMainPin = false) => {
|
|
281
379
|
if (device.features?.passphraseProtection === true) {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
380
|
+
// Main PIN authenticates the device; an empty host passphrase selects the standard derivation.
|
|
381
|
+
await selectMainPin();
|
|
382
|
+
await askDevicePassphrase(
|
|
383
|
+
device,
|
|
384
|
+
{
|
|
385
|
+
passphrase: '',
|
|
386
|
+
on_device: false,
|
|
387
|
+
},
|
|
388
|
+
options?.deriveCardano,
|
|
389
|
+
markWalletStatusRefreshed
|
|
390
|
+
);
|
|
391
|
+
standardWalletSelected = true;
|
|
392
|
+
} else if (!standardWalletSelected) {
|
|
393
|
+
// Without passphrase protection there is no empty-passphrase selector.
|
|
394
|
+
// An unlocked non-Attach-PIN device is already in the only available wallet context.
|
|
395
|
+
// Force Main PIN only when recovering from a mismatched cached standard session.
|
|
396
|
+
await selectMainPin(forceMainPin);
|
|
397
|
+
standardWalletSelected = true;
|
|
286
398
|
}
|
|
287
399
|
};
|
|
288
400
|
|
|
289
401
|
if (readCurrentAttachPinSession) {
|
|
290
402
|
const status = await requestProtocolV2DeviceStatus(device);
|
|
291
403
|
device.updateProtocolV2Status(status);
|
|
404
|
+
markWalletStatusRefreshed();
|
|
292
405
|
if (
|
|
293
406
|
status.unlocked !== true ||
|
|
294
407
|
status.passphrase_enabled !== true ||
|
|
@@ -297,10 +410,7 @@ export async function getProtocolV2WalletSession(
|
|
|
297
410
|
device.clearInternalState();
|
|
298
411
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckUnlockTypeError);
|
|
299
412
|
}
|
|
300
|
-
response = await getDeviceSession(
|
|
301
|
-
device,
|
|
302
|
-
buildDeviceSessionGetRequest({ deriveCardano: options?.deriveCardano })
|
|
303
|
-
);
|
|
413
|
+
response = await getDeviceSession(device, sessionGetRequest());
|
|
304
414
|
} else if (options?.onlyMainPin) {
|
|
305
415
|
expectedPassphraseState = cachedStandardSession?.passphraseState;
|
|
306
416
|
if (cachedStandardSession) {
|
|
@@ -310,10 +420,9 @@ export async function getProtocolV2WalletSession(
|
|
|
310
420
|
}
|
|
311
421
|
response = await getDeviceSession(
|
|
312
422
|
device,
|
|
313
|
-
|
|
423
|
+
sessionGetRequest({
|
|
314
424
|
sessionId: cachedStandardSession.sessionId,
|
|
315
425
|
expectedPassphraseState,
|
|
316
|
-
deriveCardano: options?.deriveCardano,
|
|
317
426
|
})
|
|
318
427
|
);
|
|
319
428
|
resumed = true;
|
|
@@ -328,19 +437,15 @@ export async function getProtocolV2WalletSession(
|
|
|
328
437
|
|
|
329
438
|
if (!response) {
|
|
330
439
|
await selectStandardWallet();
|
|
331
|
-
response = await getDeviceSession(
|
|
332
|
-
device,
|
|
333
|
-
buildDeviceSessionGetRequest({ deriveCardano: options?.deriveCardano })
|
|
334
|
-
);
|
|
440
|
+
response = await getDeviceSession(device, sessionGetRequest());
|
|
335
441
|
}
|
|
336
442
|
} else if (cachedSessionId && expectedPassphraseState) {
|
|
337
443
|
try {
|
|
338
444
|
response = await getDeviceSession(
|
|
339
445
|
device,
|
|
340
|
-
|
|
446
|
+
sessionGetRequest({
|
|
341
447
|
sessionId: cachedSessionId,
|
|
342
448
|
expectedPassphraseState,
|
|
343
|
-
deriveCardano: options?.deriveCardano,
|
|
344
449
|
})
|
|
345
450
|
);
|
|
346
451
|
resumed = true;
|
|
@@ -350,14 +455,18 @@ export async function getProtocolV2WalletSession(
|
|
|
350
455
|
throw error;
|
|
351
456
|
}
|
|
352
457
|
resumed = false;
|
|
458
|
+
if (device.features?.unlockedAttachPin === true) {
|
|
459
|
+
// Locking invalidates the old handle, but Attach PIN already selected the wallet.
|
|
460
|
+
// Read the current session and validate its passphrase state below.
|
|
461
|
+
response = await getDeviceSession(device, sessionGetRequest());
|
|
462
|
+
}
|
|
353
463
|
}
|
|
354
464
|
} else if (expectedPassphraseState) {
|
|
355
465
|
try {
|
|
356
466
|
response = await getDeviceSession(
|
|
357
467
|
device,
|
|
358
|
-
|
|
468
|
+
sessionGetRequest({
|
|
359
469
|
expectedPassphraseState,
|
|
360
|
-
deriveCardano: options?.deriveCardano,
|
|
361
470
|
})
|
|
362
471
|
);
|
|
363
472
|
} catch (error) {
|
|
@@ -372,7 +481,13 @@ export async function getProtocolV2WalletSession(
|
|
|
372
481
|
device.clearInternalState();
|
|
373
482
|
throw ERRORS.TypedError(HardwareErrorCode.WalletSessionInvalid);
|
|
374
483
|
}
|
|
375
|
-
|
|
484
|
+
await lockAttachPinBeforePassphraseSelection();
|
|
485
|
+
response = await selectDeviceSession(
|
|
486
|
+
device,
|
|
487
|
+
expectedPassphraseState,
|
|
488
|
+
options?.deriveCardano,
|
|
489
|
+
markWalletStatusRefreshed
|
|
490
|
+
);
|
|
376
491
|
}
|
|
377
492
|
|
|
378
493
|
let { message } = response;
|
|
@@ -395,14 +510,17 @@ export async function getProtocolV2WalletSession(
|
|
|
395
510
|
}
|
|
396
511
|
if (options?.onlyMainPin) {
|
|
397
512
|
device.clearStandardInternalState?.();
|
|
398
|
-
await selectStandardWallet();
|
|
399
|
-
response = await getDeviceSession(
|
|
400
|
-
device,
|
|
401
|
-
buildDeviceSessionGetRequest({ deriveCardano: options?.deriveCardano })
|
|
402
|
-
);
|
|
513
|
+
await selectStandardWallet(true);
|
|
514
|
+
response = await getDeviceSession(device, sessionGetRequest());
|
|
403
515
|
} else {
|
|
404
516
|
device.clearInternalState();
|
|
405
|
-
|
|
517
|
+
await lockAttachPinBeforePassphraseSelection();
|
|
518
|
+
response = await selectDeviceSession(
|
|
519
|
+
device,
|
|
520
|
+
expectedPassphraseState,
|
|
521
|
+
options?.deriveCardano,
|
|
522
|
+
markWalletStatusRefreshed
|
|
523
|
+
);
|
|
406
524
|
}
|
|
407
525
|
message = response.message;
|
|
408
526
|
try {
|
|
@@ -422,6 +540,54 @@ export async function getProtocolV2WalletSession(
|
|
|
422
540
|
}
|
|
423
541
|
}
|
|
424
542
|
|
|
543
|
+
// origin/dev generates Cardano on AskPassphrase, not Get. Empty host
|
|
544
|
+
// passphrase is the Attach PIN / standard-wallet secret. Hidden wallets
|
|
545
|
+
// still need a real passphrase Ask. Passphrase-off Get auto-requests Cardano.
|
|
546
|
+
if (options?.deriveCardano === true && !deviceSessionHasCardano(message)) {
|
|
547
|
+
if (options?.resumeOnly) {
|
|
548
|
+
device.clearInternalState();
|
|
549
|
+
throw ERRORS.TypedError(HardwareErrorCode.WalletSessionInvalid);
|
|
550
|
+
}
|
|
551
|
+
resumed = false;
|
|
552
|
+
const previousAddress = message.btc_test_address;
|
|
553
|
+
if (sessionIsAttachPinWallet(response)) {
|
|
554
|
+
response = await askEmptyPassphraseAndGet({ deriveCardano: true, keepAttachPin: true });
|
|
555
|
+
} else if (device.features?.passphraseProtection === false) {
|
|
556
|
+
response = await getDeviceSession(device, buildDeviceSessionGetRequest());
|
|
557
|
+
} else if (options?.onlyMainPin) {
|
|
558
|
+
await selectMainPin();
|
|
559
|
+
response = await askEmptyPassphraseAndGet({ deriveCardano: true });
|
|
560
|
+
} else {
|
|
561
|
+
await lockAttachPinBeforePassphraseSelection();
|
|
562
|
+
response = await selectDeviceSession(
|
|
563
|
+
device,
|
|
564
|
+
expectedPassphraseState,
|
|
565
|
+
true,
|
|
566
|
+
markWalletStatusRefreshed
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
message = response.message;
|
|
570
|
+
try {
|
|
571
|
+
assertCompleteDeviceSession(message);
|
|
572
|
+
} catch (error) {
|
|
573
|
+
if (options?.onlyMainPin) {
|
|
574
|
+
device.clearStandardInternalState?.();
|
|
575
|
+
} else {
|
|
576
|
+
device.clearInternalState();
|
|
577
|
+
}
|
|
578
|
+
throw error;
|
|
579
|
+
}
|
|
580
|
+
if (previousAddress && previousAddress !== message.btc_test_address) {
|
|
581
|
+
await rejectMismatchedAttachPinWallet();
|
|
582
|
+
clearCurrentWalletSession();
|
|
583
|
+
throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckPassphraseStateError);
|
|
584
|
+
}
|
|
585
|
+
if (!deviceSessionHasCardano(message)) {
|
|
586
|
+
clearCurrentWalletSession();
|
|
587
|
+
throw ERRORS.TypedError(HardwareErrorCode.WalletSessionInvalid);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
425
591
|
const internalStateArgs = [
|
|
426
592
|
true,
|
|
427
593
|
message.btc_test_address,
|
|
@@ -436,9 +602,9 @@ export async function getProtocolV2WalletSession(
|
|
|
436
602
|
}
|
|
437
603
|
|
|
438
604
|
let unlockedAttachPin: boolean | undefined;
|
|
439
|
-
if (readCurrentAttachPinSession) {
|
|
605
|
+
if (readCurrentAttachPinSession || sessionIsAttachPinWallet(response)) {
|
|
440
606
|
unlockedAttachPin = true;
|
|
441
|
-
} else if (
|
|
607
|
+
} else if (mainPinAuthenticated) {
|
|
442
608
|
unlockedAttachPin = false;
|
|
443
609
|
}
|
|
444
610
|
|
|
@@ -447,6 +613,7 @@ export async function getProtocolV2WalletSession(
|
|
|
447
613
|
newSession: message.session_id,
|
|
448
614
|
unlockedAttachPin,
|
|
449
615
|
resumed,
|
|
616
|
+
walletStatusRefreshed,
|
|
450
617
|
};
|
|
451
618
|
}
|
|
452
619
|
|