@onekeyfe/hd-core 1.2.0-alpha.17 → 1.2.0-alpha.19
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__/DeviceEventRegistration.test.ts +3 -0
- package/__tests__/device-features-state.test.ts +78 -0
- package/__tests__/device-identity.test.ts +30 -0
- package/__tests__/device-pool-state.test.ts +113 -0
- package/__tests__/device-settings.test.ts +108 -0
- package/__tests__/device-state-contract.test.ts +27 -0
- package/__tests__/device-state-events.test.ts +163 -0
- package/__tests__/device-state-mapper.test.ts +222 -0
- package/__tests__/device-state-projector.test.ts +94 -0
- package/__tests__/device-state-store.test.ts +115 -0
- package/__tests__/device-wallet-session-store.test.ts +46 -0
- package/__tests__/deviceFeaturesUtils.test.ts +13 -1
- package/__tests__/deviceSettings.test.ts +54 -0
- package/__tests__/get-device-state.test.ts +367 -0
- package/__tests__/kaspaSignTransaction.test.ts +591 -0
- package/__tests__/logBlockEvent.test.ts +13 -0
- package/__tests__/protocol-v2.test.ts +358 -413
- package/__tests__/public-device-state-api.test.ts +153 -0
- package/__tests__/refresh-device-state.test.ts +101 -0
- package/__tests__/request-context-logging.test.ts +16 -0
- package/__tests__/search-devices.test.ts +93 -0
- package/dist/api/ClearSessionCache.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/GetDeviceState.d.ts +7 -0
- package/dist/api/GetDeviceState.d.ts.map +1 -0
- package/dist/api/GetFeatures.d.ts +1 -1
- package/dist/api/GetFeatures.d.ts.map +1 -1
- package/dist/api/GetOnekeyFeatures.d.ts.map +1 -1
- package/dist/api/GetPassphraseState.d.ts.map +1 -1
- package/dist/api/SearchDevices.d.ts.map +1 -1
- package/dist/api/device/DeviceRebootToBoardloader.d.ts.map +1 -1
- package/dist/api/device/DeviceRebootToBootloader.d.ts.map +1 -1
- package/dist/api/device/DeviceSettings.d.ts.map +1 -1
- package/dist/api/index.d.ts +1 -4
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/internal.d.ts +4 -0
- package/dist/api/internal.d.ts.map +1 -0
- package/dist/api/kaspa/KaspaSignTransaction.d.ts +5 -1
- package/dist/api/kaspa/KaspaSignTransaction.d.ts.map +1 -1
- package/dist/api/kaspa/helpers/TransferSerialize.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceInfoGet.d.ts +0 -1
- package/dist/api/protocol-v2/DeviceInfoGet.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceReboot.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceSettingsGet.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceSettingsSet.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceStatusGet.d.ts.map +1 -1
- package/dist/api/utils.d.ts +2 -1
- package/dist/api/utils.d.ts.map +1 -1
- package/dist/constants/index.d.ts +1 -1
- package/dist/constants/index.d.ts.map +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/device/Device.d.ts +22 -3
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/device/DeviceCommands.d.ts +4 -4
- package/dist/device/DeviceFeaturesState.d.ts +4 -0
- package/dist/device/DeviceFeaturesState.d.ts.map +1 -0
- package/dist/device/DevicePool.d.ts +1 -0
- package/dist/device/DevicePool.d.ts.map +1 -1
- package/dist/device/DeviceStateMapper.d.ts +13 -0
- package/dist/device/DeviceStateMapper.d.ts.map +1 -0
- package/dist/device/DeviceStateProjector.d.ts +8 -0
- package/dist/device/DeviceStateProjector.d.ts.map +1 -0
- package/dist/device/DeviceStateStore.d.ts +23 -0
- package/dist/device/DeviceStateStore.d.ts.map +1 -0
- package/dist/device/deviceIdentity.d.ts +5 -0
- package/dist/device/deviceIdentity.d.ts.map +1 -0
- package/dist/deviceProfile/buildDeviceFeatures.d.ts.map +1 -1
- package/dist/deviceProfile/index.d.ts +0 -1
- package/dist/deviceProfile/index.d.ts.map +1 -1
- package/dist/events/device.d.ts +7 -2
- package/dist/events/device.d.ts.map +1 -1
- package/dist/index.d.ts +722 -648
- package/dist/index.js +1863 -1033
- package/dist/inject.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/features.d.ts +1 -0
- package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/index.d.ts +1 -1
- package/dist/protocols/protocol-v2/index.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
- package/dist/types/api/deviceSettings.d.ts +8 -0
- package/dist/types/api/deviceSettings.d.ts.map +1 -1
- package/dist/types/api/getDeviceState.d.ts +12 -0
- package/dist/types/api/getDeviceState.d.ts.map +1 -0
- package/dist/types/api/index.d.ts +4 -7
- package/dist/types/api/index.d.ts.map +1 -1
- package/dist/types/api/kaspaSignTransaction.d.ts +27 -2
- package/dist/types/api/kaspaSignTransaction.d.ts.map +1 -1
- package/dist/types/api/protocolV2.d.ts +1 -6
- package/dist/types/api/protocolV2.d.ts.map +1 -1
- package/dist/types/device.d.ts +121 -1
- package/dist/types/device.d.ts.map +1 -1
- package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
- package/dist/utils/deviceSettings.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/tracing.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/api/ClearSessionCache.ts +4 -0
- package/src/api/FirmwareUpdateV4.ts +15 -7
- package/src/api/GetDeviceState.ts +55 -0
- package/src/api/GetFeatures.ts +8 -3
- package/src/api/GetOnekeyFeatures.ts +3 -72
- package/src/api/GetPassphraseState.ts +3 -1
- package/src/api/SearchDevices.ts +25 -4
- package/src/api/device/DeviceRebootToBoardloader.ts +1 -0
- package/src/api/device/DeviceRebootToBootloader.ts +1 -0
- package/src/api/device/DeviceSettings.ts +28 -0
- package/src/api/index.ts +1 -4
- package/src/api/internal.ts +9 -0
- package/src/api/kaspa/KaspaSignTransaction.ts +341 -22
- package/src/api/kaspa/helpers/TransferSerialize.ts +14 -0
- package/src/api/protocol-v2/DeviceInfoGet.ts +3 -5
- package/src/api/protocol-v2/DeviceReboot.ts +3 -1
- package/src/api/protocol-v2/DeviceSettingsGet.ts +3 -1
- package/src/api/protocol-v2/DeviceSettingsSet.ts +2 -0
- package/src/api/protocol-v2/DeviceStatusGet.ts +1 -0
- package/src/api/utils.ts +9 -3
- package/src/constants/index.ts +1 -4
- package/src/core/index.ts +7 -1
- package/src/data/messages/messages-protocol-v2.json +22 -13
- package/src/data/messages/messages.json +286 -1
- package/src/device/Device.ts +256 -78
- package/src/device/DeviceFeaturesState.ts +34 -0
- package/src/device/DevicePool.ts +35 -9
- package/src/device/DeviceStateMapper.ts +364 -0
- package/src/device/DeviceStateProjector.ts +92 -0
- package/src/device/DeviceStateStore.ts +242 -0
- package/src/device/deviceIdentity.ts +18 -0
- package/src/deviceProfile/buildDeviceFeatures.ts +166 -109
- package/src/deviceProfile/index.ts +0 -1
- package/src/events/device.ts +13 -1
- package/src/events/logBlockEvent.ts +1 -1
- package/src/inject.ts +4 -5
- package/src/protocols/protocol-v2/features.ts +18 -9
- package/src/protocols/protocol-v2/index.ts +1 -0
- package/src/protocols/protocol-v2/walletSession.ts +5 -7
- package/src/types/api/deviceSettings.ts +8 -0
- package/src/types/api/getDeviceState.ts +19 -0
- package/src/types/api/index.ts +3 -20
- package/src/types/api/kaspaSignTransaction.ts +40 -2
- package/src/types/api/protocolV2.ts +0 -24
- package/src/types/device.ts +148 -1
- package/src/utils/deviceFeaturesUtils.ts +5 -1
- package/src/utils/deviceSettings.ts +41 -0
- package/src/utils/tracing.ts +3 -1
- package/dist/api/GetDeviceInfo.d.ts +0 -9
- package/dist/api/GetDeviceInfo.d.ts.map +0 -1
- package/dist/deviceProfile/buildDeviceProfile.d.ts +0 -22
- package/dist/deviceProfile/buildDeviceProfile.d.ts.map +0 -1
- package/dist/types/api/getDeviceInfo.d.ts +0 -88
- package/dist/types/api/getDeviceInfo.d.ts.map +0 -1
- package/src/api/GetDeviceInfo.ts +0 -152
- package/src/deviceProfile/buildDeviceProfile.ts +0 -370
- package/src/types/api/getDeviceInfo.ts +0 -106
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { bytesToHex } from '@noble/hashes/utils';
|
|
2
|
+
import { ERRORS, HardwareError, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
3
|
|
|
3
4
|
import { UI_REQUEST } from '../../constants/ui-request';
|
|
4
5
|
import { validatePath } from '../helpers/pathUtils';
|
|
@@ -14,11 +15,27 @@ import type {
|
|
|
14
15
|
KaspaSignTransactionParams,
|
|
15
16
|
KaspaSignature,
|
|
16
17
|
} from '../../types';
|
|
17
|
-
import type {
|
|
18
|
+
import type {
|
|
19
|
+
KaspaInputScriptType,
|
|
20
|
+
KaspaTxRequest,
|
|
21
|
+
KaspaTxRequestSignature,
|
|
22
|
+
TypedCall,
|
|
23
|
+
} from '@onekeyfe/hd-transport';
|
|
24
|
+
|
|
25
|
+
// Streaming only handles plain P2PK scripts; anything else (e.g. KRC20 P2SH)
|
|
26
|
+
// must blind-sign. Absent script means a streaming-only caller.
|
|
27
|
+
const P2PK_SCRIPT = /^(20[0-9a-f]{64}ac|21[0-9a-f]{66}ab)$/i;
|
|
28
|
+
|
|
29
|
+
const isStreamableScript = (script?: string) => !script || P2PK_SCRIPT.test(script);
|
|
18
30
|
|
|
19
31
|
export default class KaspaSignTransaction extends BaseMethod<KaspaSignTransactionParams> {
|
|
20
32
|
hasBundle = false;
|
|
21
33
|
|
|
34
|
+
// Protocols this tx can be signed with; the device picks via its first response.
|
|
35
|
+
supportsLegacy = false;
|
|
36
|
+
|
|
37
|
+
supportsStreaming = false;
|
|
38
|
+
|
|
22
39
|
init() {
|
|
23
40
|
this.checkDeviceId = true;
|
|
24
41
|
this.allowDeviceMode = [...this.allowDeviceMode, UI_REQUEST.NOT_INITIALIZE];
|
|
@@ -29,17 +46,17 @@ export default class KaspaSignTransaction extends BaseMethod<KaspaSignTransactio
|
|
|
29
46
|
// check payload
|
|
30
47
|
validateParams(payload, [
|
|
31
48
|
{ name: 'version', type: 'number' },
|
|
32
|
-
{ name: 'sigHashType', type: 'number'
|
|
49
|
+
{ name: 'sigHashType', type: 'number' },
|
|
33
50
|
{ name: 'inputs', type: 'array', required: true },
|
|
34
51
|
{ name: 'outputs', type: 'array', required: true },
|
|
35
52
|
{ name: 'lockTime', required: true },
|
|
36
53
|
{ name: 'sigOpCount', type: 'number' },
|
|
37
54
|
{ name: 'subNetworkID', type: 'string' },
|
|
55
|
+
{ name: 'payload', type: 'hexString' },
|
|
56
|
+
{ name: 'refTxs', type: 'array', allowEmpty: true },
|
|
38
57
|
{ name: 'useTweak', type: 'boolean' },
|
|
39
58
|
]);
|
|
40
59
|
|
|
41
|
-
// if(!payload.inputs.length) throw
|
|
42
|
-
|
|
43
60
|
const inputs: KaspaSignInputParams[] = payload.inputs.map(input => {
|
|
44
61
|
validateParams(input, [
|
|
45
62
|
{ name: 'path', type: 'string', required: true },
|
|
@@ -47,6 +64,7 @@ export default class KaspaSignTransaction extends BaseMethod<KaspaSignTransactio
|
|
|
47
64
|
{ name: 'outputIndex', type: 'number', required: true },
|
|
48
65
|
{ name: 'sequenceNumber', required: true },
|
|
49
66
|
]);
|
|
67
|
+
validateParams(input.output, [{ name: 'satoshis', required: true }]);
|
|
50
68
|
|
|
51
69
|
const addressN = validatePath(input.path, 3);
|
|
52
70
|
|
|
@@ -57,15 +75,28 @@ export default class KaspaSignTransaction extends BaseMethod<KaspaSignTransactio
|
|
|
57
75
|
};
|
|
58
76
|
});
|
|
59
77
|
|
|
78
|
+
payload.refTxs?.forEach(refTx => {
|
|
79
|
+
validateParams(refTx, [
|
|
80
|
+
{ name: 'txId', type: 'string', required: true },
|
|
81
|
+
{ name: 'version', type: 'number', required: true },
|
|
82
|
+
// Coinbase transactions legitimately have zero inputs.
|
|
83
|
+
{ name: 'inputs', type: 'array', required: true, allowEmpty: true },
|
|
84
|
+
{ name: 'outputs', type: 'array', required: true, allowEmpty: true },
|
|
85
|
+
{ name: 'payload', type: 'hexString' },
|
|
86
|
+
]);
|
|
87
|
+
});
|
|
88
|
+
|
|
60
89
|
const outputs: KaspaSignOutputParams[] = payload.outputs.map(output => {
|
|
61
90
|
validateParams(output, [
|
|
62
91
|
{ name: 'satoshis', required: true },
|
|
63
|
-
{ name: '
|
|
92
|
+
{ name: 'address', type: 'string' },
|
|
93
|
+
{ name: 'script', type: 'string' },
|
|
64
94
|
{ name: 'scriptVersion', type: 'number' },
|
|
65
95
|
]);
|
|
66
96
|
|
|
67
97
|
return {
|
|
68
98
|
...output,
|
|
99
|
+
addressN: output.addressN ? validatePath(output.addressN, 3) : undefined,
|
|
69
100
|
scriptVersion: output.scriptVersion ?? 0,
|
|
70
101
|
};
|
|
71
102
|
});
|
|
@@ -80,8 +111,37 @@ export default class KaspaSignTransaction extends BaseMethod<KaspaSignTransactio
|
|
|
80
111
|
sigHashType: payload.sigHashType ?? SignatureType.SIGHASH_ALL | SignatureType.SIGHASH_FORKID,
|
|
81
112
|
sigOpCount: payload.sigOpCount ?? 1,
|
|
82
113
|
subNetworkID: payload.subNetworkID ?? bytesToHex(zeroSubnetworkID()),
|
|
114
|
+
gas: payload.gas ?? 0,
|
|
83
115
|
useTweak: payload.useTweak,
|
|
84
116
|
};
|
|
117
|
+
|
|
118
|
+
// Legacy prehashes on the host with payload/gas/subnetworkID hardcoded to
|
|
119
|
+
// zero (TransferSerialize), so those rule it out.
|
|
120
|
+
this.supportsLegacy =
|
|
121
|
+
this.params.outputs.every(output => !!output.script) &&
|
|
122
|
+
this.params.inputs.every(input => !!input.output.script) &&
|
|
123
|
+
!this.params.payload &&
|
|
124
|
+
Number(this.params.gas ?? 0) === 0 &&
|
|
125
|
+
/^0*$/.test(this.params.subNetworkID ?? '');
|
|
126
|
+
|
|
127
|
+
// The streaming protocol has no sighash-type field (device signs SIGHASH_ALL).
|
|
128
|
+
const isDefaultSigHashType =
|
|
129
|
+
this.params.sigHashType === SignatureType.SIGHASH_ALL ||
|
|
130
|
+
// eslint-disable-next-line no-bitwise
|
|
131
|
+
this.params.sigHashType === (SignatureType.SIGHASH_ALL | SignatureType.SIGHASH_FORKID);
|
|
132
|
+
|
|
133
|
+
this.supportsStreaming =
|
|
134
|
+
isDefaultSigHashType &&
|
|
135
|
+
this.params.outputs.every(output => !!output.address || !!output.addressN) &&
|
|
136
|
+
this.params.inputs.every(input => isStreamableScript(input.output.script)) &&
|
|
137
|
+
this.params.outputs.every(output => isStreamableScript(output.script));
|
|
138
|
+
|
|
139
|
+
if (!this.supportsLegacy && !this.supportsStreaming) {
|
|
140
|
+
throw ERRORS.TypedError(
|
|
141
|
+
HardwareErrorCode.CallMethodInvalidParameter,
|
|
142
|
+
'KaspaSignTransaction: outputs require either address/addressN (streaming protocol) or script (legacy protocol)'
|
|
143
|
+
);
|
|
144
|
+
}
|
|
85
145
|
}
|
|
86
146
|
|
|
87
147
|
getVersionRange() {
|
|
@@ -106,6 +166,10 @@ export default class KaspaSignTransaction extends BaseMethod<KaspaSignTransactio
|
|
|
106
166
|
};
|
|
107
167
|
}
|
|
108
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Legacy blind-sign flow: run() sent input 0's prehash in raw_message; feed
|
|
171
|
+
* each next prehash via KaspaTxInputAck until KaspaSignedTx.
|
|
172
|
+
*/
|
|
109
173
|
async processTxRequest(
|
|
110
174
|
typedCall: TypedCall,
|
|
111
175
|
res: TypedResponseMessage<'KaspaTxInputRequest'> | TypedResponseMessage<'KaspaSignedTx'>,
|
|
@@ -136,7 +200,6 @@ export default class KaspaSignTransaction extends BaseMethod<KaspaSignTransactio
|
|
|
136
200
|
'KaspaTxInputAck',
|
|
137
201
|
['KaspaTxInputRequest', 'KaspaSignedTx'],
|
|
138
202
|
{
|
|
139
|
-
// params.inputs 的 path 可能是字符串或已解析数组,统一归一为 address_n 数组
|
|
140
203
|
address_n: validatePath(input.path, 3),
|
|
141
204
|
raw_message: bytesToHex(rawMessage),
|
|
142
205
|
}
|
|
@@ -148,6 +211,190 @@ export default class KaspaSignTransaction extends BaseMethod<KaspaSignTransactio
|
|
|
148
211
|
return signature;
|
|
149
212
|
}
|
|
150
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Answer a previous-transaction request (selected by prev_tx_id) from
|
|
216
|
+
* refTxs; the device uses them to verify input amounts.
|
|
217
|
+
*/
|
|
218
|
+
async ackPrevRequest(typedCall: TypedCall, request: KaspaTxRequest, requestIndex: number) {
|
|
219
|
+
const prevTxId = (request.prev_tx_id ?? '').toLowerCase();
|
|
220
|
+
const refTx = (this.params.refTxs ?? []).find(tx => tx.txId.toLowerCase() === prevTxId);
|
|
221
|
+
if (!refTx) {
|
|
222
|
+
throw ERRORS.TypedError(
|
|
223
|
+
HardwareErrorCode.CallMethodInvalidParameter,
|
|
224
|
+
`KaspaSignTransaction: device requested previous transaction ${
|
|
225
|
+
prevTxId || '(unknown)'
|
|
226
|
+
}; provide it via refTxs`
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const requestType = request.request_type;
|
|
231
|
+
|
|
232
|
+
if (requestType === 'KASPA_TX_PREV_META') {
|
|
233
|
+
return typedCall('KaspaTxAckPrevMeta', 'KaspaTxRequest', {
|
|
234
|
+
version: refTx.version,
|
|
235
|
+
input_count: refTx.inputs.length,
|
|
236
|
+
output_count: refTx.outputs.length,
|
|
237
|
+
lock_time: (refTx.lockTime ?? 0) as number,
|
|
238
|
+
subnetwork_id: refTx.subNetworkID ?? bytesToHex(zeroSubnetworkID()),
|
|
239
|
+
gas: (refTx.gas ?? 0) as number,
|
|
240
|
+
payload_length: (refTx.payload ?? '').length / 2,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (requestType === 'KASPA_TX_INPUT') {
|
|
245
|
+
const input = refTx.inputs[requestIndex];
|
|
246
|
+
if (!input) {
|
|
247
|
+
throw ERRORS.TypedError(
|
|
248
|
+
HardwareErrorCode.RuntimeError,
|
|
249
|
+
`KaspaSignTransaction: device requested input ${requestIndex} of previous tx out of range`
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
return typedCall('KaspaTxAckPrevInput', 'KaspaTxRequest', {
|
|
253
|
+
previous_outpoint: {
|
|
254
|
+
tx_id: input.prevTxId,
|
|
255
|
+
index: input.outputIndex,
|
|
256
|
+
},
|
|
257
|
+
sequence: input.sequenceNumber as number,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (requestType === 'KASPA_TX_OUTPUT') {
|
|
262
|
+
const output = refTx.outputs[requestIndex];
|
|
263
|
+
if (!output) {
|
|
264
|
+
throw ERRORS.TypedError(
|
|
265
|
+
HardwareErrorCode.RuntimeError,
|
|
266
|
+
`KaspaSignTransaction: device requested output ${requestIndex} of previous tx out of range`
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
return typedCall('KaspaTxAckPrevOutput', 'KaspaTxRequest', {
|
|
270
|
+
amount: output.satoshis,
|
|
271
|
+
script_version: output.scriptVersion ?? 0,
|
|
272
|
+
script_public_key: output.script,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (requestType === 'KASPA_TX_PAYLOAD') {
|
|
277
|
+
const payloadHex = refTx.payload ?? '';
|
|
278
|
+
const payloadLength = payloadHex.length / 2;
|
|
279
|
+
const length = request.request_payload_length ?? payloadLength - requestIndex;
|
|
280
|
+
return typedCall('KaspaTxAckPayloadChunk', 'KaspaTxRequest', {
|
|
281
|
+
payload_chunk: payloadHex.substring(requestIndex * 2, (requestIndex + length) * 2),
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
throw ERRORS.TypedError(
|
|
286
|
+
HardwareErrorCode.RuntimeError,
|
|
287
|
+
`KaspaSignTransaction: unknown previous-tx request type ${requestType ?? 'undefined'}`
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Streaming flow (device computes the sighash): the device drives via
|
|
293
|
+
* KaspaTxRequest, asking for inputs/outputs/payload chunks and carrying
|
|
294
|
+
* finished signatures back, until FINISHED. Mirrors BTC signtx.
|
|
295
|
+
*/
|
|
296
|
+
async signTxStream(
|
|
297
|
+
typedCall: TypedCall,
|
|
298
|
+
firstResponse: TypedResponseMessage<'KaspaTxRequest'>
|
|
299
|
+
): Promise<KaspaSignature[]> {
|
|
300
|
+
const { params } = this;
|
|
301
|
+
const signatures: KaspaSignature[] = [];
|
|
302
|
+
|
|
303
|
+
const inputScriptType: KaspaInputScriptType =
|
|
304
|
+
params.scheme === 'ecdsa' ? 'KASPA_SPEND_P2PK_ECDSA' : 'KASPA_SPEND_P2PK_SCHNORR';
|
|
305
|
+
|
|
306
|
+
const saveSignature = (signature?: KaspaTxRequestSignature) => {
|
|
307
|
+
if (signature && typeof signature.signature_index === 'number' && signature.signature) {
|
|
308
|
+
signatures[signature.signature_index] = {
|
|
309
|
+
index: signature.signature_index,
|
|
310
|
+
signature: signature.signature,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const payloadHex = params.payload ?? '';
|
|
316
|
+
const payloadLength = payloadHex.length / 2;
|
|
317
|
+
|
|
318
|
+
let response = firstResponse;
|
|
319
|
+
|
|
320
|
+
// eslint-disable-next-line no-constant-condition
|
|
321
|
+
while (true) {
|
|
322
|
+
const request = response.message;
|
|
323
|
+
// Save first so the last input's signature (carried on the FINISHED request) is captured.
|
|
324
|
+
saveSignature(request.signature);
|
|
325
|
+
|
|
326
|
+
const requestType = request.request_type;
|
|
327
|
+
if (requestType === 'KASPA_TX_FINISHED') {
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const requestIndex = request.request_index ?? 0;
|
|
332
|
+
|
|
333
|
+
// prev_tx_id selects a previous-transaction request; answer from refTxs.
|
|
334
|
+
if (request.prev_tx_id || requestType === 'KASPA_TX_PREV_META') {
|
|
335
|
+
response = await this.ackPrevRequest(typedCall, request, requestIndex);
|
|
336
|
+
} else if (requestType === 'KASPA_TX_INPUT') {
|
|
337
|
+
const input = params.inputs[requestIndex];
|
|
338
|
+
if (!input) {
|
|
339
|
+
throw ERRORS.TypedError(
|
|
340
|
+
HardwareErrorCode.RuntimeError,
|
|
341
|
+
`KaspaSignTransaction: device requested input ${requestIndex} out of range`
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
response = await typedCall('KaspaTxAckInput', 'KaspaTxRequest', {
|
|
345
|
+
address_n: input.path as number[],
|
|
346
|
+
previous_outpoint: {
|
|
347
|
+
tx_id: input.prevTxId,
|
|
348
|
+
index: input.outputIndex,
|
|
349
|
+
},
|
|
350
|
+
amount: input.output.satoshis,
|
|
351
|
+
sequence: input.sequenceNumber as number,
|
|
352
|
+
sig_op_count: input.sigOpCount ?? 1,
|
|
353
|
+
script_type: inputScriptType,
|
|
354
|
+
use_tweak: params.useTweak,
|
|
355
|
+
});
|
|
356
|
+
} else if (requestType === 'KASPA_TX_OUTPUT') {
|
|
357
|
+
const output = params.outputs[requestIndex];
|
|
358
|
+
if (!output) {
|
|
359
|
+
throw ERRORS.TypedError(
|
|
360
|
+
HardwareErrorCode.RuntimeError,
|
|
361
|
+
`KaspaSignTransaction: device requested output ${requestIndex} out of range`
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
const isChange = !!output.addressN;
|
|
365
|
+
response = await typedCall('KaspaTxAckOutput', 'KaspaTxRequest', {
|
|
366
|
+
script_type: isChange ? 'KASPA_PAYTOCHANGE' : 'KASPA_PAYTOADDRESS',
|
|
367
|
+
amount: output.satoshis,
|
|
368
|
+
address_n: (output.addressN as number[]) ?? [],
|
|
369
|
+
address: output.address,
|
|
370
|
+
scheme: params.scheme,
|
|
371
|
+
use_tweak: params.useTweak,
|
|
372
|
+
});
|
|
373
|
+
} else if (requestType === 'KASPA_TX_PAYLOAD') {
|
|
374
|
+
const offset = requestIndex;
|
|
375
|
+
const length = request.request_payload_length ?? payloadLength - offset;
|
|
376
|
+
const chunk = payloadHex.substring(offset * 2, (offset + length) * 2);
|
|
377
|
+
response = await typedCall('KaspaTxAckPayloadChunk', 'KaspaTxRequest', {
|
|
378
|
+
payload_chunk: chunk,
|
|
379
|
+
});
|
|
380
|
+
} else {
|
|
381
|
+
throw ERRORS.TypedError(
|
|
382
|
+
HardwareErrorCode.RuntimeError,
|
|
383
|
+
`KaspaSignTransaction: unknown request type ${requestType ?? 'undefined'}`
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const collected = signatures.filter(Boolean);
|
|
389
|
+
if (collected.length !== params.inputs.length) {
|
|
390
|
+
throw ERRORS.TypedError(
|
|
391
|
+
HardwareErrorCode.RuntimeError,
|
|
392
|
+
`KaspaSignTransaction: expected ${params.inputs.length} signatures, received ${collected.length}`
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
return collected;
|
|
396
|
+
}
|
|
397
|
+
|
|
151
398
|
async run() {
|
|
152
399
|
this.checkFeatureVersionLimit(
|
|
153
400
|
// exists use_tweak is false check firmware version
|
|
@@ -158,25 +405,97 @@ export default class KaspaSignTransaction extends BaseMethod<KaspaSignTransactio
|
|
|
158
405
|
}
|
|
159
406
|
);
|
|
160
407
|
|
|
161
|
-
const { raw: rawMessage } = serialize(this.params, 0);
|
|
162
|
-
const input = this.params.inputs[0];
|
|
163
|
-
|
|
164
408
|
const { device, params } = this;
|
|
409
|
+
const payloadHex = params.payload ?? '';
|
|
165
410
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
411
|
+
// output_count is the protocol discriminator: attach streaming fields only
|
|
412
|
+
// when the tx can stream. Streaming verification needs refTxs, so without
|
|
413
|
+
// them prefer blind signing when available; a streaming-only tx still
|
|
414
|
+
// streams and fails clearly if the device requests previous txs.
|
|
415
|
+
const hasRefTxs = (params.refTxs?.length ?? 0) > 0;
|
|
416
|
+
const streamingReady = this.supportsStreaming && (hasRefTxs || !this.supportsLegacy);
|
|
417
|
+
|
|
418
|
+
const streamingFields = streamingReady
|
|
419
|
+
? {
|
|
420
|
+
output_count: params.outputs.length,
|
|
421
|
+
version: params.version,
|
|
422
|
+
lock_time: params.lockTime as number,
|
|
423
|
+
subnetwork_id: params.subNetworkID,
|
|
424
|
+
gas: params.gas as number,
|
|
425
|
+
payload_length: payloadHex.length / 2,
|
|
426
|
+
}
|
|
427
|
+
: {};
|
|
428
|
+
|
|
429
|
+
// raw_message (input 0's prehash) is only computable/correct when legacy-signable.
|
|
430
|
+
const legacyFields = this.supportsLegacy
|
|
431
|
+
? { raw_message: bytesToHex(serialize(params, 0).raw) }
|
|
432
|
+
: {};
|
|
433
|
+
|
|
434
|
+
// Legacy firmware reads raw_message and skips unknown streaming fields;
|
|
435
|
+
// new firmware streams iff output_count is present.
|
|
436
|
+
let response;
|
|
437
|
+
try {
|
|
438
|
+
response = await device.commands.typedCall(
|
|
439
|
+
'KaspaSignTx',
|
|
440
|
+
['KaspaTxRequest', 'KaspaTxInputRequest', 'KaspaSignedTx'],
|
|
441
|
+
{
|
|
442
|
+
address_n: params.inputs[0].path as number[],
|
|
443
|
+
scheme: params.scheme,
|
|
444
|
+
prefix: params.prefix,
|
|
445
|
+
input_count: params.inputs.length,
|
|
446
|
+
use_tweak: params.useTweak,
|
|
447
|
+
...streamingFields,
|
|
448
|
+
...legacyFields,
|
|
449
|
+
}
|
|
450
|
+
);
|
|
451
|
+
} catch (error) {
|
|
452
|
+
// Old firmware cannot decode a packet without raw_message
|
|
453
|
+
// (Failure_DataError); map it to an actionable upgrade error.
|
|
454
|
+
if (
|
|
455
|
+
!this.supportsLegacy &&
|
|
456
|
+
error instanceof HardwareError &&
|
|
457
|
+
error.errorCode === HardwareErrorCode.RuntimeError &&
|
|
458
|
+
String(error.message).includes('Failure_DataError')
|
|
459
|
+
) {
|
|
460
|
+
throw ERRORS.TypedError(
|
|
461
|
+
HardwareErrorCode.CallMethodNeedUpgradeFirmware,
|
|
462
|
+
'KaspaSignTransaction: this transaction requires firmware with Kaspa streaming support'
|
|
463
|
+
);
|
|
177
464
|
}
|
|
178
|
-
|
|
465
|
+
throw error;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const typedCall = device.commands.typedCall.bind(device.commands);
|
|
469
|
+
|
|
470
|
+
if (response.type === 'KaspaTxRequest') {
|
|
471
|
+
if (!this.supportsStreaming) {
|
|
472
|
+
throw ERRORS.TypedError(
|
|
473
|
+
HardwareErrorCode.CallMethodInvalidParameter,
|
|
474
|
+
'KaspaSignTransaction: device firmware uses the streaming protocol; every output requires address or addressN'
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
try {
|
|
478
|
+
return await this.signTxStream(typedCall, response);
|
|
479
|
+
} catch (error) {
|
|
480
|
+
// Device rejected our refTxs; the caller decides whether to sign without them.
|
|
481
|
+
if (
|
|
482
|
+
error instanceof HardwareError &&
|
|
483
|
+
String(error.message).toLowerCase().includes('previous transaction id mismatch')
|
|
484
|
+
) {
|
|
485
|
+
throw ERRORS.TypedError(HardwareErrorCode.KaspaPrevTxIdMismatch, String(error.message));
|
|
486
|
+
}
|
|
487
|
+
throw error;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Legacy answer to a streaming-only packet: no prehash material exists.
|
|
492
|
+
if (!this.supportsLegacy) {
|
|
493
|
+
throw ERRORS.TypedError(
|
|
494
|
+
HardwareErrorCode.RuntimeError,
|
|
495
|
+
'KaspaSignTransaction: device chose the legacy protocol but the transaction is not legacy-signable'
|
|
496
|
+
);
|
|
497
|
+
}
|
|
179
498
|
|
|
180
|
-
return this.processTxRequest(
|
|
499
|
+
return this.processTxRequest(typedCall, response, 0, []);
|
|
181
500
|
}
|
|
182
501
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
2
|
+
import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
3
|
+
|
|
2
4
|
import { SignatureType } from './SignatureType';
|
|
3
5
|
import { HashWriter } from './HashWriter';
|
|
4
6
|
|
|
@@ -76,6 +78,12 @@ function getSigOpCountsHash(transaction: KaspaSignTransactionParams, sighashType
|
|
|
76
78
|
function hashTxOut(hashWriter: HashWriter, output: KaspaSignOutputParams) {
|
|
77
79
|
hashWriter.writeUInt64LE(output.satoshis);
|
|
78
80
|
hashWriter.writeUInt16LE(0); // TODO: USE REAL SCRIPT VERSION
|
|
81
|
+
if (output.script === undefined) {
|
|
82
|
+
throw ERRORS.TypedError(
|
|
83
|
+
HardwareErrorCode.CallMethodInvalidParameter,
|
|
84
|
+
'Kaspa legacy sighash requires output.script'
|
|
85
|
+
);
|
|
86
|
+
}
|
|
79
87
|
hashWriter.writeVarBytes(Buffer.from(output.script, 'hex'));
|
|
80
88
|
}
|
|
81
89
|
|
|
@@ -126,6 +134,12 @@ export function serialize(transaction: KaspaSignTransactionParams, inputNumber:
|
|
|
126
134
|
const input = transaction.inputs[inputNumber];
|
|
127
135
|
hashOutpoint(hashWriter, input);
|
|
128
136
|
hashWriter.writeUInt16LE(0); // TODO: USE REAL SCRIPT VERSION
|
|
137
|
+
if (input.output.script === undefined) {
|
|
138
|
+
throw ERRORS.TypedError(
|
|
139
|
+
HardwareErrorCode.CallMethodInvalidParameter,
|
|
140
|
+
'Kaspa legacy sighash requires input.output.script'
|
|
141
|
+
);
|
|
142
|
+
}
|
|
129
143
|
hashWriter.writeVarBytes(Buffer.from(input.output.script, 'hex'));
|
|
130
144
|
hashWriter.writeUInt64LE(input.output.satoshis);
|
|
131
145
|
hashWriter.writeUInt64LE(input.sequenceNumber);
|
|
@@ -13,7 +13,6 @@ export type DeviceInfoGetTargets = {
|
|
|
13
13
|
se2?: boolean;
|
|
14
14
|
se3?: boolean;
|
|
15
15
|
se4?: boolean;
|
|
16
|
-
status?: boolean;
|
|
17
16
|
};
|
|
18
17
|
|
|
19
18
|
export type DeviceInfoGetTypes = {
|
|
@@ -36,7 +35,6 @@ const TARGET_KEYS: (keyof DeviceInfoGetTargets)[] = [
|
|
|
36
35
|
'se2',
|
|
37
36
|
'se3',
|
|
38
37
|
'se4',
|
|
39
|
-
'status',
|
|
40
38
|
];
|
|
41
39
|
|
|
42
40
|
const TYPE_KEYS: (keyof DeviceInfoGetTypes)[] = ['version', 'build_id', 'hash', 'specific'];
|
|
@@ -104,9 +102,8 @@ function normalizeTypes(value: unknown): DeviceInfoGetTypes | undefined {
|
|
|
104
102
|
/**
|
|
105
103
|
* 原生 DeviceInfoGet(Protocol V2 only)。
|
|
106
104
|
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
* 用于调试固件字段上报。
|
|
105
|
+
* SDK 内部按需构造 targets/types,并返回未加工的 DeviceInfo 消息。
|
|
106
|
+
* 该 command 不属于公共 CoreApi;业务接入统一使用 getDeviceState。
|
|
110
107
|
*/
|
|
111
108
|
export default class DeviceInfoGet extends BaseMethod<{
|
|
112
109
|
targets: DeviceInfoGetTargets;
|
|
@@ -115,6 +112,7 @@ export default class DeviceInfoGet extends BaseMethod<{
|
|
|
115
112
|
init() {
|
|
116
113
|
// Protocol V2 (Pro2) 专属方法,core 调度层统一做非 V2 设备守卫
|
|
117
114
|
this.requireProtocolV2 = true;
|
|
115
|
+
this.unlockPolicy = 'none';
|
|
118
116
|
this.allowDeviceMode = [
|
|
119
117
|
...this.allowDeviceMode,
|
|
120
118
|
UI_REQUEST.NOT_INITIALIZE,
|
|
@@ -16,9 +16,11 @@ export default class DeviceReboot extends BaseMethod<DeviceRebootParams> {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
async run() {
|
|
19
|
+
const rebootType = normalizeRebootType(this.params.reboot_type ?? this.params.rebootType);
|
|
19
20
|
const res = await this.device.commands.typedCall('DeviceReboot', 'Success', {
|
|
20
|
-
reboot_type:
|
|
21
|
+
reboot_type: rebootType,
|
|
21
22
|
});
|
|
23
|
+
this.device.markProtocolV2Reboot(rebootType);
|
|
22
24
|
return Promise.resolve(res.message);
|
|
23
25
|
}
|
|
24
26
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { BaseMethod } from '../BaseMethod';
|
|
2
|
+
import { mapDeviceSettingsToState } from '../../device/DeviceStateMapper';
|
|
2
3
|
|
|
3
4
|
export default class DeviceSettingsGet extends BaseMethod {
|
|
4
5
|
init() {
|
|
5
6
|
this.requireProtocolV2 = true;
|
|
6
|
-
this.unlockPolicy = '
|
|
7
|
+
this.unlockPolicy = 'none';
|
|
7
8
|
this.skipForceUpdateCheck = true;
|
|
8
9
|
this.useDevicePassphraseState = false;
|
|
9
10
|
this.params = undefined;
|
|
@@ -11,6 +12,7 @@ export default class DeviceSettingsGet extends BaseMethod {
|
|
|
11
12
|
|
|
12
13
|
async run() {
|
|
13
14
|
const res = await this.device.commands.typedCall('DeviceSettingsGet', 'DeviceSettings', {});
|
|
15
|
+
this.device.updateState(mapDeviceSettingsToState(res.message), 'apply-settings');
|
|
14
16
|
return Promise.resolve(res.message);
|
|
15
17
|
}
|
|
16
18
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BaseMethod } from '../BaseMethod';
|
|
2
|
+
import { mapDeviceSettingsToState } from '../../device/DeviceStateMapper';
|
|
2
3
|
import { invalidParameter } from '../helpers/filesystemValidation';
|
|
3
4
|
|
|
4
5
|
import type { DeviceSettings } from '@onekeyfe/hd-transport';
|
|
@@ -44,6 +45,7 @@ export default class DeviceSettingsSet extends BaseMethod<{
|
|
|
44
45
|
|
|
45
46
|
async run() {
|
|
46
47
|
const res = await this.device.commands.typedCall('DeviceSettingsSet', 'Success', this.params);
|
|
48
|
+
this.device.updateState(mapDeviceSettingsToState(this.params.settings), 'apply-settings');
|
|
47
49
|
return Promise.resolve(res.message);
|
|
48
50
|
}
|
|
49
51
|
}
|
package/src/api/utils.ts
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
2
|
|
|
3
3
|
import * as ApiMethods from './index';
|
|
4
|
+
import * as InternalApiMethods from './internal';
|
|
4
5
|
|
|
6
|
+
import type { BaseMethod } from './BaseMethod';
|
|
5
7
|
import type { IFrameCallMessage } from '../events';
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
type MethodConstructor = new (message: IFrameCallMessage & { id?: number }) => BaseMethod<any>;
|
|
10
|
+
|
|
11
|
+
const publicMethodRegistry = ApiMethods as unknown as Record<string, MethodConstructor>;
|
|
12
|
+
const internalMethodRegistry = InternalApiMethods as unknown as Record<string, MethodConstructor>;
|
|
13
|
+
|
|
14
|
+
export function findMethod(message: IFrameCallMessage & { id?: number }): BaseMethod<any> {
|
|
8
15
|
const { method } = message.payload;
|
|
9
16
|
if (typeof method !== 'string') {
|
|
10
17
|
throw ERRORS.TypedError(HardwareErrorCode.CallMethodInvalidParameter, 'Method is not set');
|
|
11
18
|
}
|
|
12
19
|
|
|
13
|
-
|
|
14
|
-
const MethodConstructor = ApiMethods[method];
|
|
20
|
+
const MethodConstructor = publicMethodRegistry[method] ?? internalMethodRegistry[method];
|
|
15
21
|
if (MethodConstructor) {
|
|
16
22
|
return new MethodConstructor(message);
|
|
17
23
|
}
|
package/src/constants/index.ts
CHANGED
package/src/core/index.ts
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
updateRequestContext,
|
|
37
37
|
} from '../utils/tracing';
|
|
38
38
|
import { Device } from '../device/Device';
|
|
39
|
+
import { checkLiveDeviceId } from '../device/deviceIdentity';
|
|
39
40
|
import { DeviceList } from '../device/DeviceList';
|
|
40
41
|
import { DevicePool } from '../device/DevicePool';
|
|
41
42
|
import { PollingStateManager } from './PollingStateManager';
|
|
@@ -410,6 +411,7 @@ const onCallDevice = async (
|
|
|
410
411
|
attachPinOnDevice: onEnterAttachPinOnDeviceHandler,
|
|
411
412
|
});
|
|
412
413
|
device.on(DEVICE.FEATURES, onDeviceFeaturesHandler);
|
|
414
|
+
device.on(DEVICE.STATE, onDeviceStateHandler);
|
|
413
415
|
device.on(
|
|
414
416
|
DEVICE.SELECT_DEVICE_IN_BOOTLOADER_FOR_WEB_DEVICE,
|
|
415
417
|
onSelectDeviceInBootloaderForWebDeviceHandler
|
|
@@ -549,7 +551,7 @@ const onCallDevice = async (
|
|
|
549
551
|
}
|
|
550
552
|
|
|
551
553
|
if (method.deviceId && method.checkDeviceId) {
|
|
552
|
-
const isSameDeviceID = device
|
|
554
|
+
const isSameDeviceID = await checkLiveDeviceId(device, method.deviceId);
|
|
553
555
|
if (!isSameDeviceID) {
|
|
554
556
|
return Promise.reject(ERRORS.TypedError(HardwareErrorCode.DeviceCheckDeviceIdError));
|
|
555
557
|
}
|
|
@@ -1240,6 +1242,10 @@ const onDeviceFeaturesHandler = (...[_, features]: [...DeviceEvents['features']]
|
|
|
1240
1242
|
postMessage(createDeviceMessage(DEVICE.FEATURES, { ...features }));
|
|
1241
1243
|
};
|
|
1242
1244
|
|
|
1245
|
+
const onDeviceStateHandler = (...[_, stateEvent]: [...DeviceEvents['state']]) => {
|
|
1246
|
+
postMessage(createDeviceMessage(DEVICE.STATE, stateEvent));
|
|
1247
|
+
};
|
|
1248
|
+
|
|
1243
1249
|
const onDevicePassphraseHandler = async (
|
|
1244
1250
|
...[device, requestPayload, callback]: DeviceEvents['passphrase']
|
|
1245
1251
|
) => {
|