@onekeyfe/hd-core 1.2.8-alpha.125 → 1.2.8-alpha.127
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__/allNetworkGetAddress-privacy-networks.test.ts +78 -0
- package/__tests__/logBlockEvent.test.ts +292 -0
- package/__tests__/monero-get-address.test.ts +147 -0
- package/__tests__/monero-key-image-sync.test.ts +272 -0
- package/__tests__/monero-sign-transaction.test.ts +436 -0
- package/__tests__/zcash-protobuf.test.ts +141 -0
- package/__tests__/zcash-sign-pczt.test.ts +446 -0
- package/dist/api/BaseMethod.d.ts.map +1 -1
- package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
- package/dist/api/helpers/pathUtils.d.ts +1 -0
- package/dist/api/helpers/pathUtils.d.ts.map +1 -1
- package/dist/api/index.d.ts +6 -0
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/monero/MoneroGetAddress.d.ts +11 -0
- package/dist/api/monero/MoneroGetAddress.d.ts.map +1 -0
- package/dist/api/monero/MoneroGetWatchKey.d.ts +11 -0
- package/dist/api/monero/MoneroGetWatchKey.d.ts.map +1 -0
- package/dist/api/monero/MoneroKeyImageSync.d.ts +20 -0
- package/dist/api/monero/MoneroKeyImageSync.d.ts.map +1 -0
- package/dist/api/monero/MoneroSignTransaction.d.ts +17 -0
- package/dist/api/monero/MoneroSignTransaction.d.ts.map +1 -0
- package/dist/api/monero/helpers.d.ts +5 -0
- package/dist/api/monero/helpers.d.ts.map +1 -0
- package/dist/api/zcash/ZcashGetAddress.d.ts +12 -0
- package/dist/api/zcash/ZcashGetAddress.d.ts.map +1 -0
- package/dist/api/zcash/ZcashSignPczt.d.ts +16 -0
- package/dist/api/zcash/ZcashSignPczt.d.ts.map +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/events/logBlockEvent.d.ts +10 -0
- package/dist/events/logBlockEvent.d.ts.map +1 -1
- package/dist/events/ui-request.d.ts +1 -0
- package/dist/events/ui-request.d.ts.map +1 -1
- package/dist/index.d.ts +140 -4
- package/dist/index.js +866 -11
- package/dist/inject.d.ts.map +1 -1
- package/dist/types/api/allNetworkGetAddress.d.ts +5 -1
- package/dist/types/api/allNetworkGetAddress.d.ts.map +1 -1
- package/dist/types/api/export.d.ts +6 -0
- package/dist/types/api/export.d.ts.map +1 -1
- package/dist/types/api/index.d.ts +12 -0
- package/dist/types/api/index.d.ts.map +1 -1
- package/dist/types/api/moneroGetAddress.d.ts +15 -0
- package/dist/types/api/moneroGetAddress.d.ts.map +1 -0
- package/dist/types/api/moneroGetWatchKey.d.ts +15 -0
- package/dist/types/api/moneroGetWatchKey.d.ts.map +1 -0
- package/dist/types/api/moneroKeyImageSync.d.ts +24 -0
- package/dist/types/api/moneroKeyImageSync.d.ts.map +1 -0
- package/dist/types/api/moneroSignTransaction.d.ts +32 -0
- package/dist/types/api/moneroSignTransaction.d.ts.map +1 -0
- package/dist/types/api/zcashGetAddress.d.ts +22 -0
- package/dist/types/api/zcashGetAddress.d.ts.map +1 -0
- package/dist/types/api/zcashSignPczt.d.ts +9 -0
- package/dist/types/api/zcashSignPczt.d.ts.map +1 -0
- package/dist/utils/patch.d.ts +1 -1
- package/dist/utils/patch.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/api/BaseMethod.ts +2 -0
- package/src/api/allnetwork/AllNetworkGetAddressBase.ts +38 -1
- package/src/api/helpers/pathUtils.ts +23 -0
- package/src/api/index.ts +7 -0
- package/src/api/monero/MoneroGetAddress.ts +71 -0
- package/src/api/monero/MoneroGetWatchKey.ts +69 -0
- package/src/api/monero/MoneroKeyImageSync.ts +164 -0
- package/src/api/monero/MoneroSignTransaction.ts +319 -0
- package/src/api/monero/helpers.ts +28 -0
- package/src/api/zcash/ZcashGetAddress.ts +119 -0
- package/src/api/zcash/ZcashSignPczt.ts +152 -0
- package/src/core/index.ts +4 -3
- package/src/data/messages/messages-protocol-v2.json +138 -1
- package/src/events/logBlockEvent.ts +55 -3
- package/src/events/ui-request.ts +1 -0
- package/src/inject.ts +13 -0
- package/src/types/api/allNetworkGetAddress.ts +9 -1
- package/src/types/api/export.ts +15 -0
- package/src/types/api/index.ts +20 -0
- package/src/types/api/moneroGetAddress.ts +27 -0
- package/src/types/api/moneroGetWatchKey.ts +28 -0
- package/src/types/api/moneroKeyImageSync.ts +45 -0
- package/src/types/api/moneroSignTransaction.ts +54 -0
- package/src/types/api/zcashGetAddress.ts +35 -0
- package/src/types/api/zcashSignPczt.ts +18 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import MoneroSignTransaction from '../src/api/monero/MoneroSignTransaction';
|
|
2
|
+
import { getMoneroVersionRange } from '../src/api/monero/helpers';
|
|
3
|
+
import { toHardened } from '../src/api/helpers/pathUtils';
|
|
4
|
+
|
|
5
|
+
import type { Device } from '../src/device/Device';
|
|
6
|
+
import type {
|
|
7
|
+
MoneroTransactionDestinationEntry,
|
|
8
|
+
MoneroTransactionSourceEntry,
|
|
9
|
+
} from '@onekeyfe/hd-transport';
|
|
10
|
+
import type { MoneroSignTransactionResult } from '../src/types/api/moneroSignTransaction';
|
|
11
|
+
|
|
12
|
+
jest.mock('../src/data/config', () => ({
|
|
13
|
+
getSDKVersion: jest.fn(() => '1.0.0'),
|
|
14
|
+
DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
const ACCOUNT_0 = [toHardened(44), toHardened(128), toHardened(0)];
|
|
18
|
+
const key = (byte: number) => byte.toString(16).padStart(2, '0').repeat(32);
|
|
19
|
+
|
|
20
|
+
// Original order. Sorted descending: input 1 (f0..), input 2 (0aff..ff), input 0 (0aff..fe).
|
|
21
|
+
// Inputs 0 and 2 differ only in the last byte.
|
|
22
|
+
const KEY_IMAGES = [
|
|
23
|
+
'0a'.concat('ff'.repeat(30), 'fe'),
|
|
24
|
+
'f0'.concat('00'.repeat(31)),
|
|
25
|
+
'0a'.concat('ff'.repeat(31)),
|
|
26
|
+
];
|
|
27
|
+
const SORTED_ORIG_INDICES = [1, 2, 0];
|
|
28
|
+
// Prefix bytes ordered against the key images, so sorting whole vinis would differ.
|
|
29
|
+
const VINI_PREFIXES = ['0210ff', '021000', '021080'];
|
|
30
|
+
const SE_SPEND_TOKEN = 'f1'.repeat(107);
|
|
31
|
+
const OPENING_KEY = '0e'.repeat(32);
|
|
32
|
+
|
|
33
|
+
const ring = (base: number) =>
|
|
34
|
+
Array.from({ length: 16 }, (_, i) => ({
|
|
35
|
+
idx: base + i,
|
|
36
|
+
key: { dest: key(i + 1), commitment: key(i + 0x40) },
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
const sourceEntry = (index: number): MoneroTransactionSourceEntry => ({
|
|
40
|
+
outputs: ring(1000 * (index + 1)),
|
|
41
|
+
real_output: 3,
|
|
42
|
+
real_out_tx_key: key(0x91),
|
|
43
|
+
real_out_additional_tx_keys: [],
|
|
44
|
+
real_output_in_tx_index: index,
|
|
45
|
+
amount: `${index + 1}000000000000`,
|
|
46
|
+
rct: true,
|
|
47
|
+
// The fake device keys its replies on the mask.
|
|
48
|
+
mask: key(0xa0 + index),
|
|
49
|
+
subaddr_minor: 0,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const destination = (byte: number, amount: string): MoneroTransactionDestinationEntry => ({
|
|
53
|
+
amount,
|
|
54
|
+
addr: { spend_public_key: key(byte), view_public_key: key(byte + 1) },
|
|
55
|
+
is_subaddress: false,
|
|
56
|
+
is_integrated: false,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const RECIPIENT = destination(0x31, '2500000000000');
|
|
60
|
+
const CHANGE = destination(0x41, '3400000000000');
|
|
61
|
+
|
|
62
|
+
const tsxData = (overrides: Record<string, unknown> = {}) => ({
|
|
63
|
+
version: 1,
|
|
64
|
+
client_version: 3,
|
|
65
|
+
hard_fork: 16,
|
|
66
|
+
unlock_time: 0,
|
|
67
|
+
num_inputs: 3,
|
|
68
|
+
mixin: 15,
|
|
69
|
+
fee: '100000000',
|
|
70
|
+
account: 0,
|
|
71
|
+
minor_indices: [0],
|
|
72
|
+
outputs: [CHANGE, RECIPIENT],
|
|
73
|
+
change_dts: CHANGE,
|
|
74
|
+
rsig_data: { rsig_type: 3, bp_version: 4, grouping: [2] },
|
|
75
|
+
integrated_indices: [],
|
|
76
|
+
...overrides,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const INPUTS = [0, 1, 2].map(sourceEntry);
|
|
80
|
+
|
|
81
|
+
const inputIndexOf = (src?: MoneroTransactionSourceEntry) =>
|
|
82
|
+
[0, 1, 2].find(i => src?.mask === key(0xa0 + i)) as number;
|
|
83
|
+
|
|
84
|
+
type FakeOptions = {
|
|
85
|
+
offloadType?: number;
|
|
86
|
+
keyImages?: string[];
|
|
87
|
+
rsigOnOutputs?: number[];
|
|
88
|
+
openingKey?: string | null;
|
|
89
|
+
finalAck?: Record<string, unknown>;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
type FakeMessage = {
|
|
93
|
+
src_entr?: MoneroTransactionSourceEntry;
|
|
94
|
+
orig_idx?: number;
|
|
95
|
+
dst_entr?: MoneroTransactionDestinationEntry;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const createFakeDevice = (options: FakeOptions = {}) => {
|
|
99
|
+
const keyImages = options.keyImages ?? KEY_IMAGES;
|
|
100
|
+
let outputIndex = 0;
|
|
101
|
+
return (type: string, _resType: string, msg?: FakeMessage) => {
|
|
102
|
+
const reply = (message: Record<string, unknown>) =>
|
|
103
|
+
Promise.resolve({ type: type.replace(/Request$/, 'Ack'), message });
|
|
104
|
+
switch (type) {
|
|
105
|
+
case 'MoneroTransactionInitRequest':
|
|
106
|
+
return reply({
|
|
107
|
+
hmacs: ['c1'.repeat(32), 'c2'.repeat(32)],
|
|
108
|
+
rsig_data: { offload_type: options.offloadType ?? 0, grouping: [] },
|
|
109
|
+
});
|
|
110
|
+
case 'MoneroTransactionSetInputRequest': {
|
|
111
|
+
const i = inputIndexOf(msg?.src_entr);
|
|
112
|
+
return reply({
|
|
113
|
+
vini: `${VINI_PREFIXES[i]}${keyImages[i]}`,
|
|
114
|
+
vini_hmac: key(0xb0 + i),
|
|
115
|
+
pseudo_out: key(0xc0 + i),
|
|
116
|
+
pseudo_out_hmac: key(0xd0 + i),
|
|
117
|
+
pseudo_out_alpha: key(0xe0 + i),
|
|
118
|
+
spend_key: `${SE_SPEND_TOKEN}0${i}`,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
case 'MoneroTransactionInputViniRequest':
|
|
122
|
+
case 'MoneroTransactionAllInputsSetRequest':
|
|
123
|
+
return reply({});
|
|
124
|
+
case 'MoneroTransactionSetOutputRequest': {
|
|
125
|
+
const i = outputIndex++;
|
|
126
|
+
const withRsig = (options.rsigOnOutputs ?? [1]).includes(i);
|
|
127
|
+
return reply({
|
|
128
|
+
tx_out: `0${i}`.repeat(34),
|
|
129
|
+
vouti_hmac: key(0x70 + i),
|
|
130
|
+
out_pk: `${key(0x80 + i)}${key(0x90 + i)}`,
|
|
131
|
+
ecdh_info: `e${i}`.repeat(8),
|
|
132
|
+
rsig_data: withRsig
|
|
133
|
+
? { rsig: 'bb'.repeat(32 * 6 + 2), grouping: [], rsig_parts: [] }
|
|
134
|
+
: null,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
case 'MoneroTransactionAllOutSetRequest':
|
|
138
|
+
return reply({
|
|
139
|
+
extra: `01${key(0x55)}020901${'00'.repeat(8)}`,
|
|
140
|
+
tx_prefix_hash: key(0x56),
|
|
141
|
+
rv: { txn_fee: 100000000, message: key(0x56), rv_type: 6 },
|
|
142
|
+
full_message_hash: key(0x57),
|
|
143
|
+
});
|
|
144
|
+
case 'MoneroTransactionSignInputRequest':
|
|
145
|
+
return reply({
|
|
146
|
+
signature: `5${msg?.orig_idx}`.repeat(40),
|
|
147
|
+
pseudo_out: key(0x60 + (msg?.orig_idx ?? 0)),
|
|
148
|
+
});
|
|
149
|
+
case 'MoneroTransactionFinalRequest':
|
|
150
|
+
return reply({
|
|
151
|
+
opening_key: options.openingKey === undefined ? OPENING_KEY : options.openingKey,
|
|
152
|
+
salt: key(0x11),
|
|
153
|
+
rand_mult: key(0x12),
|
|
154
|
+
tx_enc_keys: 'ab'.repeat(48),
|
|
155
|
+
...options.finalAck,
|
|
156
|
+
});
|
|
157
|
+
default:
|
|
158
|
+
return Promise.reject(new Error(`unexpected ${type}`));
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const attachDevice = (method: MoneroSignTransaction, typedCall: jest.Mock) => {
|
|
164
|
+
const commands = { typedCall };
|
|
165
|
+
method.device = {
|
|
166
|
+
commands,
|
|
167
|
+
getCommands: () => commands,
|
|
168
|
+
toMessageObject: () => ({}),
|
|
169
|
+
} as unknown as Device;
|
|
170
|
+
method.postMessage = jest.fn();
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const createMethod = (payload: Record<string, unknown>) =>
|
|
174
|
+
new MoneroSignTransaction({
|
|
175
|
+
id: 1,
|
|
176
|
+
payload: {
|
|
177
|
+
method: 'moneroSignTransaction',
|
|
178
|
+
path: "m/44'/128'/0'",
|
|
179
|
+
tsxData: tsxData(),
|
|
180
|
+
inputs: INPUTS,
|
|
181
|
+
...payload,
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const runWith = (options: FakeOptions = {}, payload: Record<string, unknown> = {}) => {
|
|
186
|
+
const method = createMethod(payload);
|
|
187
|
+
method.init();
|
|
188
|
+
const typedCall = jest.fn(createFakeDevice(options));
|
|
189
|
+
attachDevice(method, typedCall);
|
|
190
|
+
const run = method.run();
|
|
191
|
+
return { method, typedCall, run };
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const callsOf = (typedCall: jest.Mock, type: string) =>
|
|
195
|
+
typedCall.mock.calls.filter(call => call[0] === type).map(call => call[2]);
|
|
196
|
+
|
|
197
|
+
describe('MoneroSignTransaction', () => {
|
|
198
|
+
test('uses the Monero device rules of the other Monero methods', () => {
|
|
199
|
+
const method = createMethod({});
|
|
200
|
+
method.init();
|
|
201
|
+
|
|
202
|
+
expect(method.getSupportedProtocols()).toEqual(['V1', 'V2']);
|
|
203
|
+
expect(method.strictCheckDeviceSupport).toBe(true);
|
|
204
|
+
expect(method.checkDeviceId).toBe(true);
|
|
205
|
+
expect(method.getVersionRange()).toEqual(getMoneroVersionRange());
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test('runs the whole protocol in order and returns the raw parts', async () => {
|
|
209
|
+
const { typedCall, run } = runWith({}, { networkType: 1 });
|
|
210
|
+
const result = await run;
|
|
211
|
+
|
|
212
|
+
expect(typedCall.mock.calls.map(call => [call[0], call[1]])).toEqual([
|
|
213
|
+
['MoneroTransactionInitRequest', 'MoneroTransactionInitAck'],
|
|
214
|
+
['MoneroTransactionSetInputRequest', 'MoneroTransactionSetInputAck'],
|
|
215
|
+
['MoneroTransactionSetInputRequest', 'MoneroTransactionSetInputAck'],
|
|
216
|
+
['MoneroTransactionSetInputRequest', 'MoneroTransactionSetInputAck'],
|
|
217
|
+
['MoneroTransactionInputViniRequest', 'MoneroTransactionInputViniAck'],
|
|
218
|
+
['MoneroTransactionInputViniRequest', 'MoneroTransactionInputViniAck'],
|
|
219
|
+
['MoneroTransactionInputViniRequest', 'MoneroTransactionInputViniAck'],
|
|
220
|
+
['MoneroTransactionAllInputsSetRequest', 'MoneroTransactionAllInputsSetAck'],
|
|
221
|
+
['MoneroTransactionSetOutputRequest', 'MoneroTransactionSetOutputAck'],
|
|
222
|
+
['MoneroTransactionSetOutputRequest', 'MoneroTransactionSetOutputAck'],
|
|
223
|
+
['MoneroTransactionAllOutSetRequest', 'MoneroTransactionAllOutSetAck'],
|
|
224
|
+
['MoneroTransactionSignInputRequest', 'MoneroTransactionSignInputAck'],
|
|
225
|
+
['MoneroTransactionSignInputRequest', 'MoneroTransactionSignInputAck'],
|
|
226
|
+
['MoneroTransactionSignInputRequest', 'MoneroTransactionSignInputAck'],
|
|
227
|
+
['MoneroTransactionFinalRequest', 'MoneroTransactionFinalAck'],
|
|
228
|
+
]);
|
|
229
|
+
expect(typedCall.mock.calls[0][2]).toEqual({
|
|
230
|
+
version: 0,
|
|
231
|
+
address_n: ACCOUNT_0,
|
|
232
|
+
network_type: 1,
|
|
233
|
+
tsx_data: tsxData(),
|
|
234
|
+
});
|
|
235
|
+
expect(callsOf(typedCall, 'MoneroTransactionSetInputRequest')).toEqual(
|
|
236
|
+
INPUTS.map(src_entr => ({ src_entr }))
|
|
237
|
+
);
|
|
238
|
+
for (const type of [
|
|
239
|
+
'MoneroTransactionAllInputsSetRequest',
|
|
240
|
+
'MoneroTransactionAllOutSetRequest',
|
|
241
|
+
'MoneroTransactionFinalRequest',
|
|
242
|
+
]) {
|
|
243
|
+
expect(callsOf(typedCall, type)).toEqual([{}]);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
expect(result).toEqual<MoneroSignTransactionResult>({
|
|
247
|
+
origIndices: SORTED_ORIG_INDICES,
|
|
248
|
+
vinis: SORTED_ORIG_INDICES.map(i => `${VINI_PREFIXES[i]}${KEY_IMAGES[i]}`),
|
|
249
|
+
pseudoOuts: SORTED_ORIG_INDICES.map(i => key(0x60 + i)),
|
|
250
|
+
signatures: SORTED_ORIG_INDICES.map(i => `5${i}`.repeat(40)),
|
|
251
|
+
txOuts: ['00'.repeat(34), '01'.repeat(34)],
|
|
252
|
+
outPks: [`${key(0x80)}${key(0x90)}`, `${key(0x81)}${key(0x91)}`],
|
|
253
|
+
ecdhInfos: ['e0'.repeat(8), 'e1'.repeat(8)],
|
|
254
|
+
rsig: 'bb'.repeat(32 * 6 + 2),
|
|
255
|
+
extra: `01${key(0x55)}020901${'00'.repeat(8)}`,
|
|
256
|
+
txPrefixHash: key(0x56),
|
|
257
|
+
fullMessageHash: key(0x57),
|
|
258
|
+
rv: { txnFee: '100000000', message: key(0x56), rvType: 6 },
|
|
259
|
+
openingKey: OPENING_KEY,
|
|
260
|
+
salt: key(0x11),
|
|
261
|
+
randMult: key(0x12),
|
|
262
|
+
txEncKeys: 'ab'.repeat(48),
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('sorts inputs by key image, descending, and hands back what each input got', async () => {
|
|
267
|
+
const { typedCall, run } = runWith();
|
|
268
|
+
await run;
|
|
269
|
+
|
|
270
|
+
const vinis = callsOf(typedCall, 'MoneroTransactionInputViniRequest');
|
|
271
|
+
expect(vinis).toEqual(
|
|
272
|
+
SORTED_ORIG_INDICES.map(i => ({
|
|
273
|
+
src_entr: INPUTS[i],
|
|
274
|
+
vini: `${VINI_PREFIXES[i]}${KEY_IMAGES[i]}`,
|
|
275
|
+
vini_hmac: key(0xb0 + i),
|
|
276
|
+
pseudo_out: key(0xc0 + i),
|
|
277
|
+
pseudo_out_hmac: key(0xd0 + i),
|
|
278
|
+
orig_idx: i,
|
|
279
|
+
}))
|
|
280
|
+
);
|
|
281
|
+
const signs = callsOf(typedCall, 'MoneroTransactionSignInputRequest');
|
|
282
|
+
expect(signs).toEqual(
|
|
283
|
+
SORTED_ORIG_INDICES.map(i => ({
|
|
284
|
+
src_entr: INPUTS[i],
|
|
285
|
+
vini: `${VINI_PREFIXES[i]}${KEY_IMAGES[i]}`,
|
|
286
|
+
vini_hmac: key(0xb0 + i),
|
|
287
|
+
// SetInputAck values; the corrected pseudo-out only comes back in SignInputAck.
|
|
288
|
+
pseudo_out: key(0xc0 + i),
|
|
289
|
+
pseudo_out_hmac: key(0xd0 + i),
|
|
290
|
+
pseudo_out_alpha: key(0xe0 + i),
|
|
291
|
+
spend_key: `${SE_SPEND_TOKEN}0${i}`,
|
|
292
|
+
orig_idx: i,
|
|
293
|
+
}))
|
|
294
|
+
);
|
|
295
|
+
const keyImages = vinis.map(({ vini }) => vini.slice(-64));
|
|
296
|
+
expect([...keyImages].sort().reverse()).toEqual(keyImages);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test('resends the same source and destination entries in every step', async () => {
|
|
300
|
+
const { typedCall, run } = runWith();
|
|
301
|
+
await run;
|
|
302
|
+
|
|
303
|
+
const byInput = (type: string) =>
|
|
304
|
+
new Map(callsOf(typedCall, type).map(({ src_entr }) => [inputIndexOf(src_entr), src_entr]));
|
|
305
|
+
const setInputs = byInput('MoneroTransactionSetInputRequest');
|
|
306
|
+
const vinis = byInput('MoneroTransactionInputViniRequest');
|
|
307
|
+
const signs = byInput('MoneroTransactionSignInputRequest');
|
|
308
|
+
[0, 1, 2].forEach(i => {
|
|
309
|
+
expect(setInputs.get(i)).toEqual(INPUTS[i]);
|
|
310
|
+
expect(vinis.get(i)).toBe(setInputs.get(i));
|
|
311
|
+
expect(signs.get(i)).toBe(setInputs.get(i));
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const initOutputs = typedCall.mock.calls[0][2].tsx_data.outputs;
|
|
315
|
+
expect(callsOf(typedCall, 'MoneroTransactionSetOutputRequest')).toEqual([
|
|
316
|
+
{ dst_entr: CHANGE, dst_entr_hmac: 'c1'.repeat(32) },
|
|
317
|
+
{ dst_entr: RECIPIENT, dst_entr_hmac: 'c2'.repeat(32) },
|
|
318
|
+
]);
|
|
319
|
+
callsOf(typedCall, 'MoneroTransactionSetOutputRequest').forEach(({ dst_entr }, i) => {
|
|
320
|
+
expect(dst_entr).toBe(initOutputs[i]);
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
test('drops null fields once so every step encodes the same entries', async () => {
|
|
325
|
+
const input = { ...INPUTS[0], multisig_kLRki: null, real_out_tx_key: undefined };
|
|
326
|
+
const { typedCall, run } = runWith(
|
|
327
|
+
{ keyImages: KEY_IMAGES },
|
|
328
|
+
{
|
|
329
|
+
tsxData: tsxData({ num_inputs: 1, payment_id: null, change_dts: null }),
|
|
330
|
+
inputs: [input],
|
|
331
|
+
}
|
|
332
|
+
);
|
|
333
|
+
await run;
|
|
334
|
+
|
|
335
|
+
const init = typedCall.mock.calls[0][2];
|
|
336
|
+
expect(init.tsx_data).not.toHaveProperty('payment_id');
|
|
337
|
+
expect(init.tsx_data).not.toHaveProperty('change_dts');
|
|
338
|
+
const sent = [
|
|
339
|
+
'MoneroTransactionSetInputRequest',
|
|
340
|
+
'MoneroTransactionInputViniRequest',
|
|
341
|
+
'MoneroTransactionSignInputRequest',
|
|
342
|
+
].map(type => callsOf(typedCall, type)[0].src_entr);
|
|
343
|
+
expect(sent[0]).not.toHaveProperty('multisig_kLRki');
|
|
344
|
+
expect(sent[0]).not.toHaveProperty('real_out_tx_key');
|
|
345
|
+
expect(sent[1]).toBe(sent[0]);
|
|
346
|
+
expect(sent[2]).toBe(sent[0]);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test('refuses a device that offloads the range proof to the host', async () => {
|
|
350
|
+
const { typedCall, run } = runWith({ offloadType: 1 });
|
|
351
|
+
|
|
352
|
+
await expect(run).rejects.toThrow(/range proof.*not supported/);
|
|
353
|
+
expect(typedCall.mock.calls.map(call => call[0])).toEqual(['MoneroTransactionInitRequest']);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test('stops before the vini step when two inputs share a key image', async () => {
|
|
357
|
+
const { typedCall, run } = runWith({
|
|
358
|
+
keyImages: [KEY_IMAGES[0], KEY_IMAGES[1], KEY_IMAGES[0]],
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
await expect(run).rejects.toThrow(/same key image/);
|
|
362
|
+
expect(callsOf(typedCall, 'MoneroTransactionInputViniRequest')).toEqual([]);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test.each([
|
|
366
|
+
['no range proof', { rsigOnOutputs: [] }, /one aggregated range proof, got 0/],
|
|
367
|
+
['two range proofs', { rsigOnOutputs: [0, 1] }, /one aggregated range proof, got 2/],
|
|
368
|
+
['no opening key', { openingKey: null }, /opening_key/],
|
|
369
|
+
['no salt', { finalAck: { salt: null } }, /missing salt/],
|
|
370
|
+
['no rand_mult', { finalAck: { rand_mult: null } }, /missing rand_mult/],
|
|
371
|
+
['no tx_enc_keys', { finalAck: { tx_enc_keys: '' } }, /missing tx_enc_keys/],
|
|
372
|
+
])('fails when the device returns %s', async (_, options, message) => {
|
|
373
|
+
const { run } = runWith(options);
|
|
374
|
+
await expect(run).rejects.toThrow(message);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test('fails when the device returns fewer output HMACs than outputs', async () => {
|
|
378
|
+
const method = createMethod({});
|
|
379
|
+
method.init();
|
|
380
|
+
const fake = createFakeDevice();
|
|
381
|
+
const typedCall = jest.fn((type: string, resType: string, msg?: FakeMessage) =>
|
|
382
|
+
type === 'MoneroTransactionInitRequest'
|
|
383
|
+
? Promise.resolve({ type: resType, message: { hmacs: ['c1'.repeat(32)], rsig_data: null } })
|
|
384
|
+
: fake(type, resType, msg)
|
|
385
|
+
);
|
|
386
|
+
attachDevice(method, typedCall);
|
|
387
|
+
|
|
388
|
+
await expect(method.run()).rejects.toThrow(/output HMACs/);
|
|
389
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
const withRsig = (rsigData: Record<string, unknown> | null) => ({
|
|
393
|
+
tsxData: tsxData({ rsig_data: rsigData }),
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test.each([
|
|
397
|
+
['an undefined tsxData', { tsxData: undefined }, /tsxData must be an object/],
|
|
398
|
+
['a non-object tsxData', { tsxData: [] }, /tsxData must be an object/],
|
|
399
|
+
['undefined inputs', { inputs: undefined }, /inputs must be a non-empty array/],
|
|
400
|
+
['empty inputs', { inputs: [] }, /"inputs" is empty/],
|
|
401
|
+
[
|
|
402
|
+
'a non-object input',
|
|
403
|
+
{ inputs: [INPUTS[0], null, INPUTS[2]] },
|
|
404
|
+
/inputs\[1\] must be an object/,
|
|
405
|
+
],
|
|
406
|
+
['a short path', { path: "m/44'/128'" }, /Not a valid path/],
|
|
407
|
+
['an account past the hardened range', { path: "m/44'/128'/4294967296'" }, /Not a valid path/],
|
|
408
|
+
['an unknown network type', { networkType: 3 }, /network type/],
|
|
409
|
+
['num_inputs not matching the inputs', { tsxData: tsxData({ num_inputs: 2 }) }, /num_inputs/],
|
|
410
|
+
['a single output', { tsxData: tsxData({ outputs: [CHANGE] }) }, /at least 2 outputs/],
|
|
411
|
+
['a missing hard_fork', { tsxData: tsxData({ hard_fork: undefined }) }, /hard_fork/],
|
|
412
|
+
['a zero hard_fork', { tsxData: tsxData({ hard_fork: 0 }) }, /hard_fork/],
|
|
413
|
+
['missing rsig_data', withRsig(null), /bp_version must be 4/],
|
|
414
|
+
['Bulletproofs v1', withRsig({ rsig_type: 3, bp_version: 3, grouping: [2] }), /bp_version/],
|
|
415
|
+
[
|
|
416
|
+
'grouping short of the outputs',
|
|
417
|
+
withRsig({ rsig_type: 3, bp_version: 4, grouping: [1] }),
|
|
418
|
+
/grouping must add up/,
|
|
419
|
+
],
|
|
420
|
+
[
|
|
421
|
+
'an empty batch',
|
|
422
|
+
withRsig({ rsig_type: 3, bp_version: 4, grouping: [0, 2] }),
|
|
423
|
+
/grouping must add up/,
|
|
424
|
+
],
|
|
425
|
+
['missing grouping', withRsig({ rsig_type: 3, bp_version: 4 }), /grouping must add up/],
|
|
426
|
+
])('rejects %s', (_, params, message) => {
|
|
427
|
+
expect(() => createMethod(params).init()).toThrow(message);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
test('accepts the outputs split into several batches', () => {
|
|
431
|
+
const method = createMethod(withRsig({ rsig_type: 3, bp_version: 4, grouping: [1, 1] }));
|
|
432
|
+
expect(() => method.init()).not.toThrow();
|
|
433
|
+
expect(method.params.address_n).toEqual(ACCOUNT_0);
|
|
434
|
+
expect(method.params.network_type).toBe(0);
|
|
435
|
+
});
|
|
436
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import * as protobuf from 'protobufjs/light';
|
|
2
|
+
import transport from '@onekeyfe/hd-transport';
|
|
3
|
+
|
|
4
|
+
import messagesJson from '../src/data/messages/messages-protocol-v2.json';
|
|
5
|
+
|
|
6
|
+
// Round-trips the Zcash messages through the SDK's real protobuf encoder and
|
|
7
|
+
// decoder against the shipped protocol V2 schema, pinning wire ids and defaults.
|
|
8
|
+
const {
|
|
9
|
+
createMessageFromName,
|
|
10
|
+
decodeProtobuf: decode,
|
|
11
|
+
encodeProtobuf: encode,
|
|
12
|
+
parseConfigure,
|
|
13
|
+
} = transport;
|
|
14
|
+
const messages = parseConfigure(messagesJson as protobuf.INamespace);
|
|
15
|
+
|
|
16
|
+
const wireFieldIds = (buf: Uint8Array) => {
|
|
17
|
+
const reader = protobuf.Reader.create(buf);
|
|
18
|
+
const ids: number[] = [];
|
|
19
|
+
while (reader.pos < reader.len) {
|
|
20
|
+
const tag = reader.uint32();
|
|
21
|
+
// eslint-disable-next-line no-bitwise
|
|
22
|
+
ids.push(tag >>> 3);
|
|
23
|
+
// eslint-disable-next-line no-bitwise
|
|
24
|
+
reader.skipType(tag & 7);
|
|
25
|
+
}
|
|
26
|
+
return ids;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const roundTrip = (name: string, data: Record<string, unknown>) => {
|
|
30
|
+
const { Message, messageTypeId } = createMessageFromName(messages, name);
|
|
31
|
+
const encoded = encode(Message, data);
|
|
32
|
+
const bytes = new Uint8Array(encoded.toBuffer());
|
|
33
|
+
return { messageTypeId, ids: wireFieldIds(bytes), decoded: decode(Message, encoded) };
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
describe('Zcash protobuf messages (protocol V2)', () => {
|
|
37
|
+
test.each([
|
|
38
|
+
['ZcashGetAddress', 12400],
|
|
39
|
+
['ZcashAddress', 12401],
|
|
40
|
+
['ZcashSignPczt', 12402],
|
|
41
|
+
['ZcashSignedPczt', 12403],
|
|
42
|
+
['ZcashPcztChunkRequest', 12404],
|
|
43
|
+
['ZcashPcztChunkAck', 12405],
|
|
44
|
+
['ZcashSignedPcztChunkRequest', 12406],
|
|
45
|
+
])('%s has wire type %d', (name, typeId) => {
|
|
46
|
+
expect(createMessageFromName(messages, name).messageTypeId).toBe(typeId);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test.each(['ZcashSignPczt', 'ZcashSignedPczt'])('%s round trips length and bytes', name => {
|
|
50
|
+
const chunk = 'ab'.repeat(1024);
|
|
51
|
+
const { ids, decoded } = roundTrip(name, { pczt_length: 4096, pczt_initial_chunk: chunk });
|
|
52
|
+
expect(ids).toEqual([1, 2]);
|
|
53
|
+
expect(decoded).toEqual({ pczt_length: 4096, pczt_initial_chunk: chunk });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('ZcashPcztChunkAck round trips the data chunk', () => {
|
|
57
|
+
const { ids, decoded } = roundTrip('ZcashPcztChunkAck', { data_chunk: '00ff10' });
|
|
58
|
+
expect(ids).toEqual([1]);
|
|
59
|
+
expect(decoded).toEqual({ data_chunk: '00ff10' });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test.each(['ZcashPcztChunkRequest', 'ZcashSignedPcztChunkRequest'])(
|
|
63
|
+
'%s round trips the chunk length',
|
|
64
|
+
name => {
|
|
65
|
+
const { ids, decoded } = roundTrip(name, { chunk_length: 1024 });
|
|
66
|
+
expect(ids).toEqual([1]);
|
|
67
|
+
expect(decoded).toEqual({ chunk_length: 1024 });
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
test('ZcashGetAddress encodes every field with its schema id', () => {
|
|
72
|
+
const { ids, decoded } = roundTrip('ZcashGetAddress', {
|
|
73
|
+
address_n: [2147483680, 2147483781, 2147483648],
|
|
74
|
+
show_display: true,
|
|
75
|
+
include_seed_fingerprint: true,
|
|
76
|
+
include_ufvk: true,
|
|
77
|
+
diversifier_index: 0xffffffff,
|
|
78
|
+
address_type: 2,
|
|
79
|
+
scope: 1,
|
|
80
|
+
});
|
|
81
|
+
// address_n is unpacked: one tag per element.
|
|
82
|
+
expect(ids).toEqual([1, 1, 1, 2, 3, 4, 5, 6, 7]);
|
|
83
|
+
expect(decoded).toEqual({
|
|
84
|
+
address_n: [2147483680, 2147483781, 2147483648],
|
|
85
|
+
show_display: true,
|
|
86
|
+
include_seed_fingerprint: true,
|
|
87
|
+
include_ufvk: true,
|
|
88
|
+
diversifier_index: 0xffffffff,
|
|
89
|
+
address_type: 'UNIFIED_ORCHARD',
|
|
90
|
+
scope: 'INTERNAL',
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('ZcashGetAddress enum values match the schema', () => {
|
|
95
|
+
const addressType = messages.lookupEnum('ZcashAddressType').values;
|
|
96
|
+
const scope = messages.lookupEnum('ZcashAddressScope').values;
|
|
97
|
+
expect(addressType).toEqual({
|
|
98
|
+
UNIFIED_ORCHARD_P2PKH: 0,
|
|
99
|
+
TRANSPARENT_P2PKH: 1,
|
|
100
|
+
UNIFIED_ORCHARD: 2,
|
|
101
|
+
});
|
|
102
|
+
expect(scope).toEqual({ EXTERNAL: 0, INTERNAL: 1 });
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('ZcashGetAddress omits unset optional fields and keeps schema defaults', () => {
|
|
106
|
+
const { Message } = createMessageFromName(messages, 'ZcashGetAddress');
|
|
107
|
+
const { ids } = roundTrip('ZcashGetAddress', { address_n: [2147483680] });
|
|
108
|
+
expect(ids).toEqual([1]);
|
|
109
|
+
expect(Message.fields.diversifier_index.defaultValue).toBe(0);
|
|
110
|
+
expect(Message.fields.address_type.defaultValue).toBe(0);
|
|
111
|
+
expect(Message.fields.scope.defaultValue).toBe(0);
|
|
112
|
+
const empty = Message.decode(new Uint8Array(0)) as unknown as Record<string, unknown>;
|
|
113
|
+
expect(empty.diversifier_index).toBe(0);
|
|
114
|
+
expect(empty.address_type).toBe(0);
|
|
115
|
+
expect(empty.scope).toBe(0);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('ZcashAddress decodes optional ufvk and seed fingerprint', () => {
|
|
119
|
+
const fingerprint = '0f'.repeat(32);
|
|
120
|
+
const full = roundTrip('ZcashAddress', {
|
|
121
|
+
address: 'u1abc',
|
|
122
|
+
ufvk: 'uview1abc',
|
|
123
|
+
seed_fingerprint: fingerprint,
|
|
124
|
+
});
|
|
125
|
+
expect(full.ids).toEqual([1, 2, 3]);
|
|
126
|
+
expect(full.decoded).toEqual({
|
|
127
|
+
address: 'u1abc',
|
|
128
|
+
ufvk: 'uview1abc',
|
|
129
|
+
seed_fingerprint: fingerprint,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const minimal = roundTrip('ZcashAddress', { address: 't1abc' });
|
|
133
|
+
expect(minimal.ids).toEqual([1]);
|
|
134
|
+
expect(minimal.decoded).toEqual({ address: 't1abc', ufvk: null, seed_fingerprint: null });
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('required fields are enforced on decode', () => {
|
|
138
|
+
const { Message } = createMessageFromName(messages, 'ZcashSignedPczt');
|
|
139
|
+
expect(() => Message.decode(new Uint8Array(0))).toThrow(/missing required/);
|
|
140
|
+
});
|
|
141
|
+
});
|