@onekeyfe/hd-core 1.2.8 → 1.2.9

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.
Files changed (167) hide show
  1. package/README.md +27 -0
  2. package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +13 -4
  3. package/__tests__/DeviceCommands.test.ts +91 -13
  4. package/__tests__/allNetworkGetAddress-privacy-networks.test.ts +77 -0
  5. package/__tests__/allNetworkGetAddress-skippable.test.ts +49 -0
  6. package/__tests__/btc-derive-context-hash.test.ts +200 -0
  7. package/__tests__/core-dispose.test.ts +114 -1
  8. package/__tests__/core-error-output.test.ts +55 -3
  9. package/__tests__/device-connector-protocol.test.ts +5 -2
  10. package/__tests__/device-lifecycle-events.test.ts +191 -27
  11. package/__tests__/device-pool-state.test.ts +50 -0
  12. package/__tests__/firmware-update/firmware-artifact-source.test.ts +11 -0
  13. package/__tests__/logBlockEvent.test.ts +331 -9
  14. package/__tests__/logger.test.ts +18 -0
  15. package/__tests__/monero-get-address.test.ts +147 -0
  16. package/__tests__/monero-key-image-sync.test.ts +272 -0
  17. package/__tests__/monero-sign-transaction.test.ts +436 -0
  18. package/__tests__/protocol-v2-bootloader-mode.test.ts +43 -2
  19. package/__tests__/protocol-v2-key-export-confirmation.test.ts +75 -0
  20. package/__tests__/protocol-v2-resources.test.ts +14 -0
  21. package/__tests__/protocol-v2.test.ts +841 -46
  22. package/__tests__/protocolV2FileWrite.test.ts +454 -7
  23. package/__tests__/refresh-device-state.test.ts +19 -17
  24. package/__tests__/search-devices.test.ts +36 -0
  25. package/__tests__/sol-sign-offchain-message.test.ts +8 -0
  26. package/__tests__/usb-identity-probe.test.ts +358 -0
  27. package/__tests__/zcash-protobuf.test.ts +152 -0
  28. package/__tests__/zcash-sign-pczt.test.ts +525 -0
  29. package/dist/api/BaseMethod.d.ts.map +1 -1
  30. package/dist/api/FileWrite.d.ts.map +1 -1
  31. package/dist/api/FirmwareUpdateV4.d.ts +5 -0
  32. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  33. package/dist/api/GetDeviceState.d.ts.map +1 -1
  34. package/dist/api/SearchDevices.d.ts +21 -1
  35. package/dist/api/SearchDevices.d.ts.map +1 -1
  36. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts +3 -0
  37. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  38. package/dist/api/btc/BTCDeriveContextHash.d.ts +16 -0
  39. package/dist/api/btc/BTCDeriveContextHash.d.ts.map +1 -0
  40. package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +1 -1
  41. package/dist/api/helpers/pathUtils.d.ts +1 -0
  42. package/dist/api/helpers/pathUtils.d.ts.map +1 -1
  43. package/dist/api/helpers/protocolV2FileWrite.d.ts +5 -1
  44. package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
  45. package/dist/api/index.d.ts +8 -0
  46. package/dist/api/index.d.ts.map +1 -1
  47. package/dist/api/monero/MoneroGetAddress.d.ts +11 -0
  48. package/dist/api/monero/MoneroGetAddress.d.ts.map +1 -0
  49. package/dist/api/monero/MoneroGetWatchKey.d.ts +11 -0
  50. package/dist/api/monero/MoneroGetWatchKey.d.ts.map +1 -0
  51. package/dist/api/monero/MoneroKeyImageSync.d.ts +20 -0
  52. package/dist/api/monero/MoneroKeyImageSync.d.ts.map +1 -0
  53. package/dist/api/monero/MoneroSignTransaction.d.ts +17 -0
  54. package/dist/api/monero/MoneroSignTransaction.d.ts.map +1 -0
  55. package/dist/api/monero/helpers.d.ts +5 -0
  56. package/dist/api/monero/helpers.d.ts.map +1 -0
  57. package/dist/api/protocol-v2/DeviceReboot.d.ts.map +1 -1
  58. package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -1
  59. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
  60. package/dist/api/zcash/ZcashGetAddress.d.ts +12 -0
  61. package/dist/api/zcash/ZcashGetAddress.d.ts.map +1 -0
  62. package/dist/api/zcash/ZcashGetFullViewingKey.d.ts +12 -0
  63. package/dist/api/zcash/ZcashGetFullViewingKey.d.ts.map +1 -0
  64. package/dist/api/zcash/ZcashSignPczt.d.ts +16 -0
  65. package/dist/api/zcash/ZcashSignPczt.d.ts.map +1 -0
  66. package/dist/api/zcash/helpers.d.ts +2 -0
  67. package/dist/api/zcash/helpers.d.ts.map +1 -0
  68. package/dist/core/index.d.ts +3 -2
  69. package/dist/core/index.d.ts.map +1 -1
  70. package/dist/device/Device.d.ts +3 -0
  71. package/dist/device/Device.d.ts.map +1 -1
  72. package/dist/device/DeviceCommands.d.ts +4 -4
  73. package/dist/device/DeviceCommands.d.ts.map +1 -1
  74. package/dist/device/DevicePool.d.ts.map +1 -1
  75. package/dist/events/logBlockEvent.d.ts +10 -0
  76. package/dist/events/logBlockEvent.d.ts.map +1 -1
  77. package/dist/events/ui-request.d.ts +2 -0
  78. package/dist/events/ui-request.d.ts.map +1 -1
  79. package/dist/index.d.ts +206 -7
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +4456 -2801
  82. package/dist/inject.d.ts.map +1 -1
  83. package/dist/protocols/protocol-v2/resources.d.ts +1 -0
  84. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
  85. package/dist/protocols/protocol-v2/uiInteraction.d.ts +1 -0
  86. package/dist/protocols/protocol-v2/uiInteraction.d.ts.map +1 -1
  87. package/dist/types/api/allNetworkGetAddress.d.ts +4 -1
  88. package/dist/types/api/allNetworkGetAddress.d.ts.map +1 -1
  89. package/dist/types/api/btcDeriveContextHash.d.ts +10 -0
  90. package/dist/types/api/btcDeriveContextHash.d.ts.map +1 -0
  91. package/dist/types/api/export.d.ts +8 -0
  92. package/dist/types/api/export.d.ts.map +1 -1
  93. package/dist/types/api/index.d.ts +16 -0
  94. package/dist/types/api/index.d.ts.map +1 -1
  95. package/dist/types/api/moneroGetAddress.d.ts +15 -0
  96. package/dist/types/api/moneroGetAddress.d.ts.map +1 -0
  97. package/dist/types/api/moneroGetWatchKey.d.ts +15 -0
  98. package/dist/types/api/moneroGetWatchKey.d.ts.map +1 -0
  99. package/dist/types/api/moneroKeyImageSync.d.ts +24 -0
  100. package/dist/types/api/moneroKeyImageSync.d.ts.map +1 -0
  101. package/dist/types/api/moneroSignTransaction.d.ts +32 -0
  102. package/dist/types/api/moneroSignTransaction.d.ts.map +1 -0
  103. package/dist/types/api/zcashGetAddress.d.ts +20 -0
  104. package/dist/types/api/zcashGetAddress.d.ts.map +1 -0
  105. package/dist/types/api/zcashGetFullViewingKey.d.ts +13 -0
  106. package/dist/types/api/zcashGetFullViewingKey.d.ts.map +1 -0
  107. package/dist/types/api/zcashSignPczt.d.ts +9 -0
  108. package/dist/types/api/zcashSignPczt.d.ts.map +1 -0
  109. package/dist/types/device.d.ts +10 -0
  110. package/dist/types/device.d.ts.map +1 -1
  111. package/dist/types/params.d.ts +1 -0
  112. package/dist/types/params.d.ts.map +1 -1
  113. package/dist/utils/logger.d.ts.map +1 -1
  114. package/dist/utils/patch.d.ts +1 -1
  115. package/dist/utils/patch.d.ts.map +1 -1
  116. package/package.json +4 -4
  117. package/src/api/BaseMethod.ts +2 -0
  118. package/src/api/FileWrite.ts +7 -1
  119. package/src/api/FirmwareUpdateV4.ts +366 -99
  120. package/src/api/GetDeviceState.ts +2 -5
  121. package/src/api/SearchDevices.ts +172 -7
  122. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +55 -15
  123. package/src/api/btc/BTCDeriveContextHash.ts +90 -0
  124. package/src/api/firmware/FirmwareArtifactSource.ts +12 -3
  125. package/src/api/helpers/pathUtils.ts +23 -0
  126. package/src/api/helpers/protocolV2FileWrite.ts +382 -111
  127. package/src/api/index.ts +9 -0
  128. package/src/api/monero/MoneroGetAddress.ts +71 -0
  129. package/src/api/monero/MoneroGetWatchKey.ts +75 -0
  130. package/src/api/monero/MoneroKeyImageSync.ts +170 -0
  131. package/src/api/monero/MoneroSignTransaction.ts +319 -0
  132. package/src/api/monero/helpers.ts +28 -0
  133. package/src/api/protocol-v2/DeviceReboot.ts +2 -0
  134. package/src/api/protocol-v2/DeviceUploadNft.ts +6 -1
  135. package/src/api/protocol-v2/DeviceUploadWallpaper.ts +15 -1
  136. package/src/api/zcash/ZcashGetAddress.ts +108 -0
  137. package/src/api/zcash/ZcashGetFullViewingKey.ts +78 -0
  138. package/src/api/zcash/ZcashSignPczt.ts +152 -0
  139. package/src/api/zcash/helpers.ts +16 -0
  140. package/src/core/index.ts +52 -24
  141. package/src/data/messages/messages-protocol-v2.json +152 -0
  142. package/src/data/messages/messages.json +51 -0
  143. package/src/device/Device.ts +32 -4
  144. package/src/device/DeviceCommands.ts +32 -1
  145. package/src/device/DeviceConnector.ts +4 -4
  146. package/src/device/DevicePool.ts +12 -2
  147. package/src/events/core.ts +2 -2
  148. package/src/events/logBlockEvent.ts +58 -3
  149. package/src/events/ui-request.ts +2 -0
  150. package/src/index.ts +1 -0
  151. package/src/inject.ts +17 -0
  152. package/src/protocols/protocol-v2/resources.ts +6 -1
  153. package/src/protocols/protocol-v2/uiInteraction.ts +1 -1
  154. package/src/types/api/allNetworkGetAddress.ts +8 -1
  155. package/src/types/api/btcDeriveContextHash.ts +15 -0
  156. package/src/types/api/export.ts +17 -0
  157. package/src/types/api/index.ts +24 -0
  158. package/src/types/api/moneroGetAddress.ts +27 -0
  159. package/src/types/api/moneroGetWatchKey.ts +28 -0
  160. package/src/types/api/moneroKeyImageSync.ts +45 -0
  161. package/src/types/api/moneroSignTransaction.ts +54 -0
  162. package/src/types/api/zcashGetAddress.ts +32 -0
  163. package/src/types/api/zcashGetFullViewingKey.ts +24 -0
  164. package/src/types/api/zcashSignPczt.ts +18 -0
  165. package/src/types/device.ts +18 -0
  166. package/src/types/params.ts +1 -1
  167. package/src/utils/logger.ts +4 -0
@@ -0,0 +1,71 @@
1
+ import { BaseMethod } from '../BaseMethod';
2
+ import { serializedPath, validateStrictPath } from '../helpers/pathUtils';
3
+ import { validateParams, validateResult } from '../helpers/paramsValidator';
4
+ import { UI_REQUEST } from '../../constants/ui-request';
5
+ import { decodeMoneroAddress, getMoneroVersionRange, validateMoneroNetworkType } from './helpers';
6
+
7
+ import type { MoneroGetAddress as HardwareMoneroGetAddress } from '@onekeyfe/hd-transport';
8
+ import type { MoneroAddress, MoneroGetAddressParams } from '../../types/api/moneroGetAddress';
9
+
10
+ export default class MoneroGetAddress extends BaseMethod<HardwareMoneroGetAddress[]> {
11
+ hasBundle = false;
12
+
13
+ getSupportedProtocols() {
14
+ return ['V1', 'V2'] as const;
15
+ }
16
+
17
+ init() {
18
+ this.checkDeviceId = true;
19
+ this.allowDeviceMode = [...this.allowDeviceMode, UI_REQUEST.NOT_INITIALIZE];
20
+ this.strictCheckDeviceSupport = true;
21
+
22
+ this.hasBundle = !!this.payload?.bundle;
23
+ const payload = this.hasBundle ? this.payload : { bundle: [this.payload] };
24
+
25
+ validateParams(payload, [{ name: 'bundle', type: 'array' }]);
26
+
27
+ this.params = [];
28
+ payload.bundle.forEach((batch: MoneroGetAddressParams) => {
29
+ validateParams(batch, [
30
+ { name: 'path', required: true },
31
+ { name: 'showOnOneKey', type: 'boolean' },
32
+ { name: 'networkType', type: 'number' },
33
+ ]);
34
+
35
+ this.params.push({
36
+ address_n: validateStrictPath(batch.path, 3),
37
+ network_type: validateMoneroNetworkType(batch.networkType),
38
+ show_display: batch.showOnOneKey ?? true,
39
+ });
40
+ });
41
+ }
42
+
43
+ getVersionRange() {
44
+ return getMoneroVersionRange();
45
+ }
46
+
47
+ async run() {
48
+ // Missing fields stay undefined so validateResult rejects them.
49
+ const responses: Partial<MoneroAddress>[] = [];
50
+
51
+ for (let i = 0; i < this.params.length; i++) {
52
+ const param = this.params[i];
53
+
54
+ const res = await this.device.commands.typedCall('MoneroGetAddress', 'MoneroAddress', {
55
+ ...param,
56
+ });
57
+
58
+ const path = serializedPath(param.address_n);
59
+ const address = decodeMoneroAddress(res.message.address);
60
+ responses.push({ path, address });
61
+
62
+ this.postPreviousAddressMessage({ address, path });
63
+ }
64
+
65
+ validateResult(responses, ['address'], {
66
+ expectedLength: this.params.length,
67
+ });
68
+
69
+ return Promise.resolve(this.hasBundle ? responses : responses[0]);
70
+ }
71
+ }
@@ -0,0 +1,75 @@
1
+ import { BaseMethod } from '../BaseMethod';
2
+ import { serializedPath, validateStrictPath } from '../helpers/pathUtils';
3
+ import { validateParams, validateResult } from '../helpers/paramsValidator';
4
+ import { UI_REQUEST } from '../../constants/ui-request';
5
+ import { createMethodInteraction } from '../../protocols/protocol-v2/uiInteraction';
6
+ import { decodeMoneroAddress, getMoneroVersionRange, validateMoneroNetworkType } from './helpers';
7
+
8
+ import type { MoneroGetWatchKey as HardwareMoneroGetWatchKey } from '@onekeyfe/hd-transport';
9
+ import type { MoneroGetWatchKeyParams, MoneroWatchKey } from '../../types/api/moneroGetWatchKey';
10
+
11
+ export default class MoneroGetWatchKey extends BaseMethod<HardwareMoneroGetWatchKey[]> {
12
+ hasBundle = false;
13
+
14
+ getSupportedProtocols() {
15
+ return ['V1', 'V2'] as const;
16
+ }
17
+
18
+ init() {
19
+ this.checkDeviceId = true;
20
+ this.allowDeviceMode = [...this.allowDeviceMode, UI_REQUEST.NOT_INITIALIZE];
21
+ this.strictCheckDeviceSupport = true;
22
+ // The name matches no Protocol V2 confirmation rule, so the export prompt is declared here.
23
+ this.protocolV2UiInteraction = createMethodInteraction(
24
+ 'public-key-confirmation',
25
+ 'moneroGetWatchKey'
26
+ );
27
+
28
+ this.hasBundle = !!this.payload?.bundle;
29
+ const payload = this.hasBundle ? this.payload : { bundle: [this.payload] };
30
+
31
+ validateParams(payload, [{ name: 'bundle', type: 'array' }]);
32
+
33
+ this.params = [];
34
+ payload.bundle.forEach((batch: MoneroGetWatchKeyParams) => {
35
+ validateParams(batch, [
36
+ { name: 'path', required: true },
37
+ { name: 'networkType', type: 'number' },
38
+ ]);
39
+
40
+ this.params.push({
41
+ address_n: validateStrictPath(batch.path, 3),
42
+ network_type: validateMoneroNetworkType(batch.networkType),
43
+ });
44
+ });
45
+ }
46
+
47
+ getVersionRange() {
48
+ return getMoneroVersionRange();
49
+ }
50
+
51
+ async run() {
52
+ // Missing fields stay undefined so validateResult rejects them.
53
+ const responses: Partial<MoneroWatchKey>[] = [];
54
+
55
+ for (let i = 0; i < this.params.length; i++) {
56
+ const param = this.params[i];
57
+
58
+ const res = await this.device.commands.typedCall('MoneroGetWatchKey', 'MoneroWatchKey', {
59
+ ...param,
60
+ });
61
+
62
+ responses.push({
63
+ path: serializedPath(param.address_n),
64
+ watchKey: res.message.watch_key,
65
+ address: decodeMoneroAddress(res.message.address),
66
+ });
67
+ }
68
+
69
+ validateResult(responses, ['watchKey', 'address'], {
70
+ expectedLength: this.params.length,
71
+ });
72
+
73
+ return Promise.resolve(this.hasBundle ? responses : responses[0]);
74
+ }
75
+ }
@@ -0,0 +1,170 @@
1
+ import { keccak_256 } from '@noble/hashes/sha3';
2
+ import { bytesToHex } from '@noble/hashes/utils';
3
+
4
+ import { BaseMethod } from '../BaseMethod';
5
+ import { hexToBytes, stripHexPrefix } from '../helpers/hexUtils';
6
+ import { validateStrictPath } from '../helpers/pathUtils';
7
+ import { invalidParameter, validateParams, validateResult } from '../helpers/paramsValidator';
8
+ import { UI_REQUEST } from '../../constants/ui-request';
9
+ import { createMethodInteraction } from '../../protocols/protocol-v2/uiInteraction';
10
+ import { getMoneroVersionRange, validateMoneroNetworkType } from './helpers';
11
+
12
+ import type { MoneroTransferDetails } from '@onekeyfe/hd-transport';
13
+ import type {
14
+ MoneroEncryptedKeyImage,
15
+ MoneroKeyImageSyncParams,
16
+ MoneroKeyImageTransfer,
17
+ } from '../../types/api/moneroKeyImageSync';
18
+
19
+ // wallet2 (device_trezor::ki_sync) sends 10 transfers per step.
20
+ const STEP_SIZE = 10;
21
+ const UINT32_MAX = 0xffffffff;
22
+
23
+ type TransferDetails = Required<MoneroTransferDetails>;
24
+
25
+ type KeyImageSyncParams = {
26
+ address_n: number[];
27
+ network_type: number;
28
+ tdis: TransferDetails[];
29
+ };
30
+
31
+ const parseKey = (value: unknown, name: string) => {
32
+ const hex = typeof value === 'string' ? stripHexPrefix(value) : '';
33
+ if (!/^[0-9a-fA-F]{64}$/.test(hex)) {
34
+ throw invalidParameter(`${name} must be a 32-byte hex string`);
35
+ }
36
+ return hex.toLowerCase();
37
+ };
38
+
39
+ const parseUint = (value: unknown, name: string, max = Number.MAX_SAFE_INTEGER) => {
40
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0 || value > max) {
41
+ throw invalidParameter(`${name} must be an unsigned integer`);
42
+ }
43
+ return value;
44
+ };
45
+
46
+ const parseTransfer = (transfer: MoneroKeyImageTransfer, index: number): TransferDetails => {
47
+ const name = `transfers[${index}]`;
48
+ if (!transfer || typeof transfer !== 'object') {
49
+ throw invalidParameter(`${name} must be an object`);
50
+ }
51
+ const additional = transfer.additionalTxPubKeys ?? [];
52
+ if (!Array.isArray(additional)) {
53
+ throw invalidParameter(`${name}.additionalTxPubKeys must be an array`);
54
+ }
55
+
56
+ return {
57
+ out_key: parseKey(transfer.outKey, `${name}.outKey`),
58
+ tx_pub_key: parseKey(transfer.txPubKey, `${name}.txPubKey`),
59
+ additional_tx_pub_keys: additional.map((key, i) =>
60
+ parseKey(key, `${name}.additionalTxPubKeys[${i}]`)
61
+ ),
62
+ internal_output_index: parseUint(transfer.internalOutputIndex, `${name}.internalOutputIndex`),
63
+ sub_addr_major: parseUint(transfer.subAddrMajor ?? 0, `${name}.subAddrMajor`, UINT32_MAX),
64
+ sub_addr_minor: parseUint(transfer.subAddrMinor ?? 0, `${name}.subAddrMinor`, UINT32_MAX),
65
+ };
66
+ };
67
+
68
+ // Monero varint, as the firmware's dump_uvarint_b writes it. Arithmetic keeps
69
+ // indices above 2^31 exact.
70
+ const encodeUvarint = (value: number) => {
71
+ const bytes: number[] = [];
72
+ let n = value;
73
+ while (n >= 0x80) {
74
+ bytes.push((n % 0x80) + 0x80);
75
+ n = Math.floor(n / 0x80);
76
+ }
77
+ bytes.push(n);
78
+ return Uint8Array.from(bytes);
79
+ };
80
+
81
+ // keccak(H_0 || ... || H_n-1), H_i = keccak(out_key || tx_pub_key || additional || varint(index)).
82
+ // The device rehashes the steps and rejects Final on a mismatch.
83
+ const computeTransfersHash = (tdis: TransferDetails[]) => {
84
+ const total = keccak_256.create();
85
+ tdis.forEach(td => {
86
+ const hash = keccak_256.create();
87
+ hash.update(hexToBytes(td.out_key));
88
+ hash.update(hexToBytes(td.tx_pub_key));
89
+ td.additional_tx_pub_keys.forEach(key => hash.update(hexToBytes(key)));
90
+ hash.update(encodeUvarint(td.internal_output_index));
91
+ total.update(hash.digest());
92
+ });
93
+ return bytesToHex(total.digest());
94
+ };
95
+
96
+ export default class MoneroKeyImageSync extends BaseMethod<KeyImageSyncParams> {
97
+ getSupportedProtocols() {
98
+ return ['V1', 'V2'] as const;
99
+ }
100
+
101
+ init() {
102
+ this.checkDeviceId = true;
103
+ this.allowDeviceMode = [...this.allowDeviceMode, UI_REQUEST.NOT_INITIALIZE];
104
+ this.strictCheckDeviceSupport = true;
105
+ // The name matches no Protocol V2 confirmation rule, so the export prompt is declared here.
106
+ this.protocolV2UiInteraction = createMethodInteraction(
107
+ 'public-key-confirmation',
108
+ 'moneroKeyImageSync'
109
+ );
110
+
111
+ const payload = this.payload as MoneroKeyImageSyncParams;
112
+ validateParams(payload, [
113
+ { name: 'path', required: true },
114
+ { name: 'networkType', type: 'number' },
115
+ { name: 'transfers', type: 'array', required: true },
116
+ ]);
117
+ if (!Array.isArray(payload.transfers) || payload.transfers.length === 0) {
118
+ throw invalidParameter('transfers must be a non-empty array');
119
+ }
120
+
121
+ this.params = {
122
+ address_n: validateStrictPath(payload.path, 3),
123
+ network_type: validateMoneroNetworkType(payload.networkType),
124
+ tdis: payload.transfers.map(parseTransfer),
125
+ };
126
+ }
127
+
128
+ getVersionRange() {
129
+ return getMoneroVersionRange();
130
+ }
131
+
132
+ async run() {
133
+ const { address_n, network_type, tdis } = this.params;
134
+
135
+ await this.device.commands.typedCall(
136
+ 'MoneroKeyImageExportInitRequest',
137
+ 'MoneroKeyImageExportInitAck',
138
+ {
139
+ num: tdis.length,
140
+ hash: computeTransfersHash(tdis),
141
+ address_n,
142
+ network_type,
143
+ subs: [],
144
+ }
145
+ );
146
+
147
+ // Missing fields stay undefined so validateResult rejects them.
148
+ const keyImages: Partial<MoneroEncryptedKeyImage>[] = [];
149
+ for (let start = 0; start < tdis.length; start += STEP_SIZE) {
150
+ const res = await this.device.commands.typedCall(
151
+ 'MoneroKeyImageSyncStepRequest',
152
+ 'MoneroKeyImageSyncStepAck',
153
+ { tdis: tdis.slice(start, start + STEP_SIZE) }
154
+ );
155
+ res.message.kis.forEach(ki => keyImages.push({ iv: ki.iv, blob: ki.blob }));
156
+ }
157
+
158
+ const res = await this.device.commands.typedCall(
159
+ 'MoneroKeyImageSyncFinalRequest',
160
+ 'MoneroKeyImageSyncFinalAck',
161
+ {}
162
+ );
163
+ const encKey = res.message.enc_key;
164
+
165
+ validateResult(keyImages, ['iv', 'blob'], { expectedLength: tdis.length });
166
+ validateResult({ encKey }, ['encKey']);
167
+
168
+ return Promise.resolve({ encKey, keyImages });
169
+ }
170
+ }
@@ -0,0 +1,319 @@
1
+ import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
2
+
3
+ import { BaseMethod } from '../BaseMethod';
4
+ import { validateStrictPath } from '../helpers/pathUtils';
5
+ import { invalidParameter, validateParams } from '../helpers/paramsValidator';
6
+ import { UI_REQUEST } from '../../constants/ui-request';
7
+ import { getMoneroVersionRange, validateMoneroNetworkType } from './helpers';
8
+
9
+ import type {
10
+ MoneroTransactionAllOutSetAck,
11
+ MoneroTransactionData,
12
+ MoneroTransactionSetInputAck,
13
+ MoneroTransactionSourceEntry,
14
+ } from '@onekeyfe/hd-transport';
15
+ import type {
16
+ MoneroSignTransactionParams,
17
+ MoneroSignTransactionResult,
18
+ } from '../../types/api/moneroSignTransaction';
19
+
20
+ const BP_PLUS_VERSION = 4;
21
+ // InitAck asks the host to prove the range proof itself (more than 2 outputs).
22
+ const OFFLOAD_RANGE_PROOF = 1;
23
+ // tx.vin[i] ends with the key image (firmware offloading_keys.get_ki_from_vini).
24
+ const KEY_IMAGE_HEX_LENGTH = 64;
25
+
26
+ type SignTransactionParams = {
27
+ address_n: number[];
28
+ network_type: number;
29
+ tsx_data: MoneroTransactionData;
30
+ inputs: MoneroTransactionSourceEntry[];
31
+ };
32
+
33
+ // What SetInputAck returned for one input; sent back unchanged in later steps.
34
+ type InputState = {
35
+ origIdx: number;
36
+ keyImage: string;
37
+ vini: string;
38
+ vini_hmac: string;
39
+ pseudo_out: string;
40
+ pseudo_out_hmac: string;
41
+ pseudo_out_alpha: string;
42
+ spend_key: string;
43
+ };
44
+
45
+ const invalidResponse = (message: string) =>
46
+ ERRORS.TypedError(HardwareErrorCode.CallMethodError, message);
47
+
48
+ const isObject = (value: unknown): value is Record<string, unknown> =>
49
+ typeof value === 'object' && value !== null && !Array.isArray(value);
50
+
51
+ const requireBytes = (value: string | null | undefined, name: string) => {
52
+ if (typeof value !== 'string' || value.length === 0) {
53
+ throw invalidResponse(`Device response is missing ${name}`);
54
+ }
55
+ return value;
56
+ };
57
+
58
+ // The transport cannot encode null bytes fields. Normalize once so every step
59
+ // resends the very same entry: the device HMACs its re-encoding.
60
+ const withoutNulls = (value: unknown): unknown => {
61
+ if (Array.isArray(value)) return value.map(withoutNulls);
62
+ if (!isObject(value)) return value;
63
+ return Object.fromEntries(
64
+ Object.entries(value)
65
+ .filter(([, item]) => item !== null && item !== undefined)
66
+ .map(([key, item]) => [key, withoutNulls(item)])
67
+ );
68
+ };
69
+
70
+ const validateObjects = (values: unknown[], name: string) => {
71
+ values.forEach((value, index) => {
72
+ if (!isObject(value)) {
73
+ throw invalidParameter(`${name}[${index}] must be an object`);
74
+ }
75
+ });
76
+ };
77
+
78
+ const validateTsxData = (tsxData: MoneroTransactionData, inputCount: number) => {
79
+ if (tsxData.num_inputs !== inputCount) {
80
+ throw invalidParameter('tsxData.num_inputs must equal the number of inputs');
81
+ }
82
+ const { outputs } = tsxData;
83
+ if (!Array.isArray(outputs) || outputs.length < 2) {
84
+ throw invalidParameter('tsxData.outputs must hold at least 2 outputs');
85
+ }
86
+ validateObjects(outputs, 'tsxData.outputs');
87
+ const hardFork = tsxData.hard_fork;
88
+ if (typeof hardFork !== 'number' || !Number.isSafeInteger(hardFork) || hardFork <= 0) {
89
+ throw invalidParameter('tsxData.hard_fork is required');
90
+ }
91
+
92
+ const rsigData = tsxData.rsig_data;
93
+ if (!isObject(rsigData) || rsigData.bp_version !== BP_PLUS_VERSION) {
94
+ throw invalidParameter('tsxData.rsig_data.bp_version must be 4 (Bulletproof+)');
95
+ }
96
+ const { grouping } = rsigData;
97
+ const validBatches =
98
+ Array.isArray(grouping) &&
99
+ grouping.every(size => Number.isSafeInteger(size) && size > 0) &&
100
+ grouping.reduce((sum, size) => sum + size, 0) === outputs.length;
101
+ if (!validBatches) {
102
+ throw invalidParameter('tsxData.rsig_data.grouping must add up to the number of outputs');
103
+ }
104
+ };
105
+
106
+ const parseSetInputAck = (ack: MoneroTransactionSetInputAck, origIdx: number): InputState => {
107
+ const name = (field: string) => `${field} of input ${origIdx}`;
108
+ const vini = requireBytes(ack.vini, name('vini'));
109
+ if (vini.length < KEY_IMAGE_HEX_LENGTH) {
110
+ throw invalidResponse(`Device returned a vini without a key image for input ${origIdx}`);
111
+ }
112
+ return {
113
+ origIdx,
114
+ keyImage: vini.slice(-KEY_IMAGE_HEX_LENGTH).toLowerCase(),
115
+ vini,
116
+ vini_hmac: requireBytes(ack.vini_hmac, name('vini_hmac')),
117
+ pseudo_out: requireBytes(ack.pseudo_out, name('pseudo_out')),
118
+ pseudo_out_hmac: requireBytes(ack.pseudo_out_hmac, name('pseudo_out_hmac')),
119
+ pseudo_out_alpha: requireBytes(ack.pseudo_out_alpha, name('pseudo_out_alpha')),
120
+ spend_key: requireBytes(ack.spend_key, name('spend_key')),
121
+ };
122
+ };
123
+
124
+ const parseAllOutSetAck = (ack: MoneroTransactionAllOutSetAck) => {
125
+ const { rv } = ack;
126
+ if (!rv || rv.txn_fee == null || rv.rv_type == null) {
127
+ throw invalidResponse('Device response is missing rv');
128
+ }
129
+ return {
130
+ extra: requireBytes(ack.extra, 'extra'),
131
+ txPrefixHash: requireBytes(ack.tx_prefix_hash, 'tx_prefix_hash'),
132
+ fullMessageHash: requireBytes(ack.full_message_hash, 'full_message_hash'),
133
+ rv: {
134
+ txnFee: String(rv.txn_fee),
135
+ message: requireBytes(rv.message, 'rv.message'),
136
+ rvType: rv.rv_type,
137
+ },
138
+ };
139
+ };
140
+
141
+ // The device hashes tx.vin in this order and requires strictly descending key
142
+ // images, compared byte-wise (equal-length lowercase hex compares the same way).
143
+ const sortByKeyImage = (inputs: InputState[]) => {
144
+ const sorted = [...inputs].sort((a, b) => {
145
+ if (a.keyImage === b.keyImage) return 0;
146
+ return a.keyImage > b.keyImage ? -1 : 1;
147
+ });
148
+ sorted.forEach((input, index) => {
149
+ if (index > 0 && sorted[index - 1].keyImage === input.keyImage) {
150
+ throw invalidResponse('Two inputs have the same key image');
151
+ }
152
+ });
153
+ return sorted;
154
+ };
155
+
156
+ export default class MoneroSignTransaction extends BaseMethod<SignTransactionParams> {
157
+ getSupportedProtocols() {
158
+ return ['V1', 'V2'] as const;
159
+ }
160
+
161
+ init() {
162
+ this.checkDeviceId = true;
163
+ this.allowDeviceMode = [...this.allowDeviceMode, UI_REQUEST.NOT_INITIALIZE];
164
+ this.strictCheckDeviceSupport = true;
165
+
166
+ const payload = this.payload as MoneroSignTransactionParams;
167
+ validateParams(payload, [
168
+ { name: 'path', required: true },
169
+ { name: 'networkType', type: 'number' },
170
+ { name: 'tsxData', type: 'object', required: true },
171
+ { name: 'inputs', type: 'array', required: true },
172
+ ]);
173
+ if (!isObject(payload.tsxData)) {
174
+ throw invalidParameter('tsxData must be an object');
175
+ }
176
+ if (!Array.isArray(payload.inputs) || payload.inputs.length === 0) {
177
+ throw invalidParameter('inputs must be a non-empty array');
178
+ }
179
+ validateObjects(payload.inputs, 'inputs');
180
+
181
+ const tsxData = withoutNulls(payload.tsxData) as MoneroTransactionData;
182
+ validateTsxData(tsxData, payload.inputs.length);
183
+
184
+ this.params = {
185
+ address_n: validateStrictPath(payload.path, 3),
186
+ network_type: validateMoneroNetworkType(payload.networkType),
187
+ tsx_data: tsxData,
188
+ inputs: payload.inputs.map(input => withoutNulls(input) as MoneroTransactionSourceEntry),
189
+ };
190
+ }
191
+
192
+ getVersionRange() {
193
+ return getMoneroVersionRange();
194
+ }
195
+
196
+ async run(): Promise<MoneroSignTransactionResult> {
197
+ const { address_n, network_type, tsx_data, inputs } = this.params;
198
+ const { outputs } = tsx_data;
199
+
200
+ const init = await this.device.commands.typedCall(
201
+ 'MoneroTransactionInitRequest',
202
+ 'MoneroTransactionInitAck',
203
+ { version: 0, address_n, network_type, tsx_data }
204
+ );
205
+ if (init.message.rsig_data?.offload_type === OFFLOAD_RANGE_PROOF) {
206
+ throw invalidResponse(
207
+ 'The device asked the host to prove the range proof, which is not supported: send at most 2 outputs'
208
+ );
209
+ }
210
+ const { hmacs } = init.message;
211
+ if (!Array.isArray(hmacs) || hmacs.length !== outputs.length) {
212
+ throw invalidResponse('Device returned a wrong number of output HMACs');
213
+ }
214
+
215
+ const states: InputState[] = [];
216
+ for (let i = 0; i < inputs.length; i++) {
217
+ const res = await this.device.commands.typedCall(
218
+ 'MoneroTransactionSetInputRequest',
219
+ 'MoneroTransactionSetInputAck',
220
+ { src_entr: inputs[i] }
221
+ );
222
+ states.push(parseSetInputAck(res.message, i));
223
+ }
224
+ const sorted = sortByKeyImage(states);
225
+
226
+ for (const input of sorted) {
227
+ await this.device.commands.typedCall(
228
+ 'MoneroTransactionInputViniRequest',
229
+ 'MoneroTransactionInputViniAck',
230
+ {
231
+ src_entr: inputs[input.origIdx],
232
+ vini: input.vini,
233
+ vini_hmac: input.vini_hmac,
234
+ pseudo_out: input.pseudo_out,
235
+ pseudo_out_hmac: input.pseudo_out_hmac,
236
+ orig_idx: input.origIdx,
237
+ }
238
+ );
239
+ }
240
+
241
+ await this.device.commands.typedCall(
242
+ 'MoneroTransactionAllInputsSetRequest',
243
+ 'MoneroTransactionAllInputsSetAck',
244
+ {}
245
+ );
246
+
247
+ const txOuts: string[] = [];
248
+ const outPks: string[] = [];
249
+ const ecdhInfos: string[] = [];
250
+ const rsigs: string[] = [];
251
+ for (let i = 0; i < outputs.length; i++) {
252
+ const res = await this.device.commands.typedCall(
253
+ 'MoneroTransactionSetOutputRequest',
254
+ 'MoneroTransactionSetOutputAck',
255
+ { dst_entr: outputs[i], dst_entr_hmac: hmacs[i] }
256
+ );
257
+ txOuts.push(requireBytes(res.message.tx_out, `tx_out of output ${i}`));
258
+ outPks.push(requireBytes(res.message.out_pk, `out_pk of output ${i}`));
259
+ ecdhInfos.push(requireBytes(res.message.ecdh_info, `ecdh_info of output ${i}`));
260
+ // The last output of a batch carries the batch's range proof.
261
+ const rsig = res.message.rsig_data?.rsig;
262
+ if (rsig) rsigs.push(rsig);
263
+ }
264
+ if (rsigs.length !== 1) {
265
+ throw invalidResponse(`Expected one aggregated range proof, got ${rsigs.length}`);
266
+ }
267
+
268
+ const allOut = await this.device.commands.typedCall(
269
+ 'MoneroTransactionAllOutSetRequest',
270
+ 'MoneroTransactionAllOutSetAck',
271
+ {}
272
+ );
273
+ const prefix = parseAllOutSetAck(allOut.message);
274
+
275
+ const signatures: string[] = [];
276
+ const pseudoOuts: string[] = [];
277
+ for (const input of sorted) {
278
+ const res = await this.device.commands.typedCall(
279
+ 'MoneroTransactionSignInputRequest',
280
+ 'MoneroTransactionSignInputAck',
281
+ {
282
+ src_entr: inputs[input.origIdx],
283
+ vini: input.vini,
284
+ vini_hmac: input.vini_hmac,
285
+ pseudo_out: input.pseudo_out,
286
+ pseudo_out_hmac: input.pseudo_out_hmac,
287
+ pseudo_out_alpha: input.pseudo_out_alpha,
288
+ spend_key: input.spend_key,
289
+ orig_idx: input.origIdx,
290
+ }
291
+ );
292
+ signatures.push(requireBytes(res.message.signature, `signature of input ${input.origIdx}`));
293
+ // The device rebalances the last input's mask, so its pseudo-out changes here.
294
+ pseudoOuts.push(requireBytes(res.message.pseudo_out, `pseudo_out of input ${input.origIdx}`));
295
+ }
296
+
297
+ const final = await this.device.commands.typedCall(
298
+ 'MoneroTransactionFinalRequest',
299
+ 'MoneroTransactionFinalAck',
300
+ {}
301
+ );
302
+
303
+ return {
304
+ origIndices: sorted.map(input => input.origIdx),
305
+ vinis: sorted.map(input => input.vini),
306
+ pseudoOuts,
307
+ signatures,
308
+ txOuts,
309
+ outPks,
310
+ ecdhInfos,
311
+ rsig: rsigs[0],
312
+ ...prefix,
313
+ openingKey: requireBytes(final.message.opening_key, 'opening_key'),
314
+ salt: requireBytes(final.message.salt, 'salt'),
315
+ randMult: requireBytes(final.message.rand_mult, 'rand_mult'),
316
+ txEncKeys: requireBytes(final.message.tx_enc_keys, 'tx_enc_keys'),
317
+ };
318
+ }
319
+ }
@@ -0,0 +1,28 @@
1
+ import { hexToBytes } from '../helpers/hexUtils';
2
+ import { invalidParameter } from '../helpers/paramsValidator';
3
+
4
+ import type { DeviceFirmwareRange } from '../../types';
5
+
6
+ const NETWORK_TYPES = [0, 1, 2];
7
+
8
+ export function validateMoneroNetworkType(networkType: number | undefined) {
9
+ const value = networkType ?? 0;
10
+ if (!NETWORK_TYPES.includes(value)) {
11
+ throw invalidParameter('Invalid Monero network type');
12
+ }
13
+ return value;
14
+ }
15
+
16
+ // The device returns the base58 address as ASCII bytes.
17
+ export function decodeMoneroAddress(hex: string | undefined) {
18
+ return hex ? Buffer.from(hexToBytes(hex)).toString('utf-8') : undefined;
19
+ }
20
+
21
+ // Pro and Touch ship the Monero app; Pro 2 and the Classic family do not.
22
+ export function getMoneroVersionRange(): DeviceFirmwareRange {
23
+ return {
24
+ model_touch: {
25
+ min: '0.0.0',
26
+ },
27
+ };
28
+ }
@@ -1,5 +1,6 @@
1
1
  import { DeviceSessionPinType } from '@onekeyfe/hd-transport';
2
2
 
3
+ import { UI_REQUEST } from '../../constants/ui-request';
3
4
  import { BaseMethod } from '../BaseMethod';
4
5
  import { normalizeRebootType } from './helpers';
5
6
 
@@ -11,6 +12,7 @@ export default class DeviceReboot extends BaseMethod<DeviceRebootParams> {
11
12
  }
12
13
 
13
14
  init() {
15
+ this.allowDeviceMode.push(UI_REQUEST.BOOTLOADER);
14
16
  this.skipForceUpdateCheck = true;
15
17
  this.useDevicePassphraseState = false;
16
18
  this.unlockPolicy = 'unlock-before-run';