@onekeyfe/hd-core 1.2.0-alpha.102 → 1.2.0-alpha.103
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__/base64Data.test.ts +70 -0
- package/__tests__/deviceUploadNft.test.ts +33 -4
- package/__tests__/protocol-v2.test.ts +75 -16
- package/__tests__/resourceBase64Boundary.test.ts +48 -0
- package/dist/api/UploadPortfolio.d.ts +1 -1
- package/dist/api/UploadPortfolio.d.ts.map +1 -1
- package/dist/api/helpers/base64Data.d.ts +16 -0
- package/dist/api/helpers/base64Data.d.ts.map +1 -0
- package/dist/api/protocol-v2/DeviceUploadNft.d.ts +2 -3
- package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts +2 -3
- package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
- package/dist/api/utils.d.ts.map +1 -1
- package/dist/index.d.ts +4 -12
- package/dist/index.js +154 -157
- package/dist/topLevelInject.d.ts.map +1 -1
- package/dist/types/api/protocolV2.d.ts +1 -1
- package/dist/types/api/protocolV2.d.ts.map +1 -1
- package/dist/utils/pro2Nft.d.ts +7 -0
- package/dist/utils/pro2Nft.d.ts.map +1 -1
- package/package.json +6 -4
- package/src/api/UploadPortfolio.ts +9 -2
- package/src/api/helpers/base64Data.ts +85 -0
- package/src/api/protocol-v2/DeviceUploadNft.ts +56 -8
- package/src/api/protocol-v2/DeviceUploadWallpaper.ts +37 -18
- package/src/api/utils.ts +2 -5
- package/src/topLevelInject.ts +2 -4
- package/src/types/api/protocolV2.ts +1 -1
- package/src/utils/pro2Nft.ts +31 -8
- package/__tests__/bridgeBinaryPayload.test.ts +0 -173
- package/dist/utils/bridgeBinaryPayload.d.ts +0 -3
- package/dist/utils/bridgeBinaryPayload.d.ts.map +0 -1
- package/src/utils/bridgeBinaryPayload.ts +0 -137
|
@@ -7,21 +7,26 @@ import {
|
|
|
7
7
|
PRO2_NFT_DEFAULT_PACE_MS,
|
|
8
8
|
PRO2_NFT_DEFAULT_TIMEOUT_MS,
|
|
9
9
|
PRO2_NFT_DIRECTORY,
|
|
10
|
+
PRO2_NFT_IMAGE_HEIGHT,
|
|
11
|
+
PRO2_NFT_IMAGE_WIDTH,
|
|
10
12
|
PRO2_NFT_MAX_CHUNK_SIZE,
|
|
11
13
|
PRO2_NFT_MAX_ITEMS,
|
|
12
14
|
PRO2_NFT_MIN_CHUNK_SIZE,
|
|
15
|
+
PRO2_NFT_THUMBNAIL_HEIGHT,
|
|
16
|
+
PRO2_NFT_THUMBNAIL_WIDTH,
|
|
13
17
|
type Pro2NftBundle,
|
|
14
|
-
|
|
15
|
-
buildPro2NftBundle,
|
|
18
|
+
buildPro2NftBundleFromEncodedImages,
|
|
16
19
|
getCompletePro2NftBasenames,
|
|
17
20
|
} from '../../utils/pro2Nft';
|
|
21
|
+
import { encodePro2Image } from '../../utils/pro2Wallpaper';
|
|
18
22
|
import { BaseMethod } from '../BaseMethod';
|
|
23
|
+
import { decodeJpegBase64ToRgba } from '../helpers/base64Data';
|
|
19
24
|
import { invalidParameter } from '../helpers/filesystemValidation';
|
|
20
25
|
import { writeProtocolV2File } from '../helpers/protocolV2FileWrite';
|
|
21
26
|
|
|
22
27
|
export type DeviceUploadNftParams = {
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
imageJpegBase64: string;
|
|
29
|
+
thumbnailJpegBase64: string;
|
|
25
30
|
title: string;
|
|
26
31
|
subtitle: string;
|
|
27
32
|
timestampMs?: number;
|
|
@@ -54,8 +59,8 @@ export default class DeviceUploadNft extends BaseMethod<DeviceUploadNftParams> {
|
|
|
54
59
|
|
|
55
60
|
init() {
|
|
56
61
|
const {
|
|
57
|
-
|
|
58
|
-
|
|
62
|
+
imageJpegBase64,
|
|
63
|
+
thumbnailJpegBase64,
|
|
59
64
|
title,
|
|
60
65
|
subtitle,
|
|
61
66
|
timestampMs = Date.now(),
|
|
@@ -79,8 +84,51 @@ export default class DeviceUploadNft extends BaseMethod<DeviceUploadNftParams> {
|
|
|
79
84
|
throw invalidParameter('Parameter [timeoutMs] must be a positive integer.');
|
|
80
85
|
}
|
|
81
86
|
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
const encodedImage = (() => {
|
|
88
|
+
const decoded = decodeJpegBase64ToRgba({
|
|
89
|
+
jpegBase64: imageJpegBase64,
|
|
90
|
+
parameterName: 'imageJpegBase64',
|
|
91
|
+
expectedWidth: PRO2_NFT_IMAGE_WIDTH,
|
|
92
|
+
expectedHeight: PRO2_NFT_IMAGE_HEIGHT,
|
|
93
|
+
});
|
|
94
|
+
return encodePro2Image({
|
|
95
|
+
width: PRO2_NFT_IMAGE_WIDTH,
|
|
96
|
+
height: PRO2_NFT_IMAGE_HEIGHT,
|
|
97
|
+
rgba: decoded.data,
|
|
98
|
+
alphaMode: 'black-background',
|
|
99
|
+
}).data;
|
|
100
|
+
})();
|
|
101
|
+
const encodedThumbnail = (() => {
|
|
102
|
+
const decoded = decodeJpegBase64ToRgba({
|
|
103
|
+
jpegBase64: thumbnailJpegBase64,
|
|
104
|
+
parameterName: 'thumbnailJpegBase64',
|
|
105
|
+
expectedWidth: PRO2_NFT_THUMBNAIL_WIDTH,
|
|
106
|
+
expectedHeight: PRO2_NFT_THUMBNAIL_HEIGHT,
|
|
107
|
+
});
|
|
108
|
+
return encodePro2Image({
|
|
109
|
+
width: PRO2_NFT_THUMBNAIL_WIDTH,
|
|
110
|
+
height: PRO2_NFT_THUMBNAIL_HEIGHT,
|
|
111
|
+
rgba: decoded.data,
|
|
112
|
+
alphaMode: 'black-background',
|
|
113
|
+
}).data;
|
|
114
|
+
})();
|
|
115
|
+
this.bundle = buildPro2NftBundleFromEncodedImages({
|
|
116
|
+
image: encodedImage,
|
|
117
|
+
thumbnail: encodedThumbnail,
|
|
118
|
+
title,
|
|
119
|
+
subtitle,
|
|
120
|
+
timestampMs,
|
|
121
|
+
});
|
|
122
|
+
this.params = {
|
|
123
|
+
imageJpegBase64,
|
|
124
|
+
thumbnailJpegBase64,
|
|
125
|
+
title,
|
|
126
|
+
subtitle,
|
|
127
|
+
timestampMs,
|
|
128
|
+
chunkSize,
|
|
129
|
+
paceMs,
|
|
130
|
+
timeoutMs,
|
|
131
|
+
};
|
|
84
132
|
this.unlockPolicy = 'none';
|
|
85
133
|
this.skipForceUpdateCheck = true;
|
|
86
134
|
this.useDevicePassphraseState = false;
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { blake2s } from '@noble/hashes/blake2s';
|
|
2
2
|
import { bytesToHex } from '@noble/hashes/utils';
|
|
3
|
+
import { createDeviceNotSupportMethodError } from '@onekeyfe/hd-shared';
|
|
3
4
|
|
|
4
5
|
import { BaseMethod } from '../BaseMethod';
|
|
6
|
+
import { decodeJpegBase64ToRgba } from '../helpers/base64Data';
|
|
5
7
|
import { invalidParameter } from '../helpers/filesystemValidation';
|
|
6
8
|
import { writeProtocolV2File } from '../helpers/protocolV2FileWrite';
|
|
7
9
|
import { UI_REQUEST, createUiMessage } from '../../events/ui-request';
|
|
10
|
+
import { supportsProtocolV2Message } from '../../protocols/protocol-v2/features';
|
|
8
11
|
import {
|
|
9
12
|
PRO2_WALLPAPER_HEIGHT,
|
|
10
13
|
PRO2_WALLPAPER_WIDTH,
|
|
@@ -13,9 +16,7 @@ import {
|
|
|
13
16
|
} from '../../utils/pro2Wallpaper';
|
|
14
17
|
|
|
15
18
|
export type DeviceUploadWallpaperParams = {
|
|
16
|
-
|
|
17
|
-
height: number;
|
|
18
|
-
rgba: Uint8Array | ArrayBuffer;
|
|
19
|
+
jpegBase64: string;
|
|
19
20
|
fileName?: string;
|
|
20
21
|
chunkSize?: number;
|
|
21
22
|
};
|
|
@@ -29,6 +30,9 @@ export type DeviceUploadWallpaperResponse = {
|
|
|
29
30
|
|
|
30
31
|
const WALLPAPER_DIRECTORY = 'vol1:/wallpapers';
|
|
31
32
|
const SAFE_FILE_NAME = /^[A-Za-z0-9_-]+(?:\.bin)?$/;
|
|
33
|
+
const DEVICE_SETTINGS_SET_MESSAGE_TYPE = 60412;
|
|
34
|
+
const FILESYSTEM_FILE_WRITE_MESSAGE_TYPE = 60805;
|
|
35
|
+
const FILESYSTEM_DIR_MAKE_MESSAGE_TYPE = 60809;
|
|
32
36
|
|
|
33
37
|
function normalizeFileName(fileName: string | undefined, data: Uint8Array): string {
|
|
34
38
|
if (fileName !== undefined && (!fileName || !SAFE_FILE_NAME.test(fileName))) {
|
|
@@ -54,31 +58,45 @@ export default class DeviceUploadWallpaper extends BaseMethod<DeviceUploadWallpa
|
|
|
54
58
|
private path = '';
|
|
55
59
|
|
|
56
60
|
init() {
|
|
57
|
-
const {
|
|
58
|
-
if (width !== PRO2_WALLPAPER_WIDTH || height !== PRO2_WALLPAPER_HEIGHT) {
|
|
59
|
-
throw invalidParameter(
|
|
60
|
-
`Pro2 wallpaper dimensions must be ${PRO2_WALLPAPER_WIDTH}x${PRO2_WALLPAPER_HEIGHT}.`
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
if (!(rgba instanceof ArrayBuffer) && !ArrayBuffer.isView(rgba)) {
|
|
64
|
-
throw invalidParameter('Parameter [rgba] must be an ArrayBuffer or Uint8Array.');
|
|
65
|
-
}
|
|
61
|
+
const { jpegBase64, fileName, chunkSize } = this.payload;
|
|
66
62
|
if (chunkSize !== undefined && (!Number.isInteger(chunkSize) || chunkSize <= 0)) {
|
|
67
63
|
throw invalidParameter('Parameter [chunkSize] must be a positive integer.');
|
|
68
64
|
}
|
|
69
65
|
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
66
|
+
const decoded = decodeJpegBase64ToRgba({
|
|
67
|
+
jpegBase64,
|
|
68
|
+
parameterName: 'jpegBase64',
|
|
69
|
+
expectedWidth: PRO2_WALLPAPER_WIDTH,
|
|
70
|
+
expectedHeight: PRO2_WALLPAPER_HEIGHT,
|
|
71
|
+
});
|
|
72
|
+
this.encoded = encodePro2Wallpaper({
|
|
73
|
+
width: PRO2_WALLPAPER_WIDTH,
|
|
74
|
+
height: PRO2_WALLPAPER_HEIGHT,
|
|
75
|
+
rgba: decoded.data,
|
|
76
|
+
});
|
|
75
77
|
this.path = `${WALLPAPER_DIRECTORY}/${normalizeFileName(fileName, this.encoded.data)}`;
|
|
76
|
-
this.params = {
|
|
78
|
+
this.params = { jpegBase64, fileName, chunkSize };
|
|
77
79
|
this.unlockPolicy = 'none';
|
|
78
80
|
this.skipForceUpdateCheck = true;
|
|
79
81
|
this.useDevicePassphraseState = false;
|
|
80
82
|
}
|
|
81
83
|
|
|
84
|
+
private async assertCapabilities() {
|
|
85
|
+
const protocolInfo = await this.device.ensureProtocolV2RuntimeContext();
|
|
86
|
+
const requiredMessageTypes = [
|
|
87
|
+
DEVICE_SETTINGS_SET_MESSAGE_TYPE,
|
|
88
|
+
FILESYSTEM_FILE_WRITE_MESSAGE_TYPE,
|
|
89
|
+
FILESYSTEM_DIR_MAKE_MESSAGE_TYPE,
|
|
90
|
+
];
|
|
91
|
+
if (
|
|
92
|
+
requiredMessageTypes.some(
|
|
93
|
+
messageType => !supportsProtocolV2Message(protocolInfo, messageType)
|
|
94
|
+
)
|
|
95
|
+
) {
|
|
96
|
+
throw createDeviceNotSupportMethodError(this.name, this.device.getCurrentFirmwareType());
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
82
100
|
private async ensureDirectory() {
|
|
83
101
|
if (this.directoryReady) return;
|
|
84
102
|
try {
|
|
@@ -119,6 +137,7 @@ export default class DeviceUploadWallpaper extends BaseMethod<DeviceUploadWallpa
|
|
|
119
137
|
async run(): Promise<DeviceUploadWallpaperResponse> {
|
|
120
138
|
const { encoded } = this;
|
|
121
139
|
if (!encoded) throw invalidParameter('Wallpaper data has not been initialized.');
|
|
140
|
+
await this.assertCapabilities();
|
|
122
141
|
await this.ensureDirectory();
|
|
123
142
|
await this.upload();
|
|
124
143
|
const response = await this.device.commands.typedCall('DeviceSettingsSet', 'Success', {
|
package/src/api/utils.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
2
|
|
|
3
3
|
import * as ApiMethods from './index';
|
|
4
|
-
import { decodeBridgeBinaryPayload } from '../utils/bridgeBinaryPayload';
|
|
5
4
|
|
|
6
5
|
import type { BaseMethod } from './BaseMethod';
|
|
7
6
|
import type { IFrameCallMessage } from '../events';
|
|
@@ -12,16 +11,14 @@ type MethodConstructor = new (message: IFrameCallMessage & { id?: number }) => B
|
|
|
12
11
|
const publicMethodRegistry = ApiMethods as unknown as Record<string, MethodConstructor>;
|
|
13
12
|
|
|
14
13
|
export function findMethod(message: IFrameCallMessage & { id?: number }): BaseMethod<any> {
|
|
15
|
-
const
|
|
16
|
-
const normalizedMessage = payload === message.payload ? message : { ...message, payload };
|
|
17
|
-
const { method } = payload;
|
|
14
|
+
const { method } = message.payload;
|
|
18
15
|
if (typeof method !== 'string') {
|
|
19
16
|
throw ERRORS.TypedError(HardwareErrorCode.CallMethodInvalidParameter, 'Method is not set');
|
|
20
17
|
}
|
|
21
18
|
|
|
22
19
|
const MethodConstructor = publicMethodRegistry[method];
|
|
23
20
|
if (MethodConstructor) {
|
|
24
|
-
return new MethodConstructor(
|
|
21
|
+
return new MethodConstructor(message);
|
|
25
22
|
}
|
|
26
23
|
|
|
27
24
|
throw ERRORS.TypedError(
|
package/src/topLevelInject.ts
CHANGED
|
@@ -2,7 +2,6 @@ import EventEmitter from 'events';
|
|
|
2
2
|
|
|
3
3
|
import { createCoreApi, createProtocolAwareCall } from './inject';
|
|
4
4
|
import { unregisterFirmwareUpdateHostBinding } from './api/firmware/FirmwareHostBinding';
|
|
5
|
-
import { encodeBridgeBinaryPayload } from './utils/bridgeBinaryPayload';
|
|
6
5
|
|
|
7
6
|
import type { ConnectSettings } from './types/settings';
|
|
8
7
|
import type { CoreApi } from './types/api';
|
|
@@ -17,10 +16,9 @@ const eventEmitter = new EventEmitter();
|
|
|
17
16
|
|
|
18
17
|
export const topLevelInject = () => {
|
|
19
18
|
let lowLevelApi: LowLevelCoreApi | undefined;
|
|
20
|
-
const call =
|
|
19
|
+
const call = (params: any) => {
|
|
21
20
|
if (!lowLevelApi) return Promise.resolve(undefined);
|
|
22
|
-
|
|
23
|
-
return lowLevelApi.call(encodedParams as any);
|
|
21
|
+
return lowLevelApi.call(params);
|
|
24
22
|
};
|
|
25
23
|
const protocolAwareCall = createProtocolAwareCall(call);
|
|
26
24
|
const api: CoreApi = {
|
|
@@ -68,7 +68,7 @@ export declare function deviceUploadNft(
|
|
|
68
68
|
export declare function uploadPortfolio(
|
|
69
69
|
connectId: string,
|
|
70
70
|
params: {
|
|
71
|
-
|
|
71
|
+
packageBase64: string;
|
|
72
72
|
timeoutMs?: number | string;
|
|
73
73
|
}
|
|
74
74
|
): Response<FileInfo & { portfolioUpdated: true }>;
|
package/src/utils/pro2Nft.ts
CHANGED
|
@@ -109,6 +109,34 @@ export function buildPro2NftBundle(options: {
|
|
|
109
109
|
const { image, thumbnail, title, subtitle, timestampMs } = options;
|
|
110
110
|
assertImage('image', image, PRO2_NFT_IMAGE_WIDTH, PRO2_NFT_IMAGE_HEIGHT);
|
|
111
111
|
assertImage('thumbnail', thumbnail, PRO2_NFT_THUMBNAIL_WIDTH, PRO2_NFT_THUMBNAIL_HEIGHT);
|
|
112
|
+
const encodedImage = encodePro2Image({ ...image, alphaMode: 'black-background' }).data;
|
|
113
|
+
const encodedThumbnail = encodePro2Image({
|
|
114
|
+
...thumbnail,
|
|
115
|
+
alphaMode: 'black-background',
|
|
116
|
+
}).data;
|
|
117
|
+
return buildPro2NftBundleFromEncodedImages({
|
|
118
|
+
image: encodedImage,
|
|
119
|
+
thumbnail: encodedThumbnail,
|
|
120
|
+
title,
|
|
121
|
+
subtitle,
|
|
122
|
+
timestampMs,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function buildPro2NftBundleFromEncodedImages(options: {
|
|
127
|
+
image: Uint8Array;
|
|
128
|
+
thumbnail: Uint8Array;
|
|
129
|
+
title: string;
|
|
130
|
+
subtitle: string;
|
|
131
|
+
timestampMs: number;
|
|
132
|
+
}): Pro2NftBundle {
|
|
133
|
+
const { image, thumbnail, title, subtitle, timestampMs } = options;
|
|
134
|
+
if (!(image instanceof Uint8Array) || image.byteLength === 0) {
|
|
135
|
+
throw invalidParameter('Parameter [image] must contain encoded NFT image data.');
|
|
136
|
+
}
|
|
137
|
+
if (!(thumbnail instanceof Uint8Array) || thumbnail.byteLength === 0) {
|
|
138
|
+
throw invalidParameter('Parameter [thumbnail] must contain encoded NFT thumbnail data.');
|
|
139
|
+
}
|
|
112
140
|
const titleLength = typeof title === 'string' ? utf8Length(title) : 0;
|
|
113
141
|
const subtitleLength =
|
|
114
142
|
typeof subtitle === 'string' ? utf8Length(subtitle) : Number.POSITIVE_INFINITY;
|
|
@@ -122,21 +150,16 @@ export function buildPro2NftBundle(options: {
|
|
|
122
150
|
throw invalidParameter('Parameter [timestampMs] must be a positive safe integer.');
|
|
123
151
|
}
|
|
124
152
|
|
|
125
|
-
const encodedImage = encodePro2Image({ ...image, alphaMode: 'black-background' }).data;
|
|
126
|
-
const encodedThumbnail = encodePro2Image({
|
|
127
|
-
...thumbnail,
|
|
128
|
-
alphaMode: 'black-background',
|
|
129
|
-
}).data;
|
|
130
153
|
const metadata = new TextEncoder().encode(JSON.stringify({ title, subtitle }));
|
|
131
154
|
if (metadata.byteLength === 0 || metadata.byteLength > 512) {
|
|
132
155
|
throw invalidParameter('Pro2 NFT metadata must contain 1 to 512 UTF-8 bytes.');
|
|
133
156
|
}
|
|
134
157
|
|
|
135
|
-
const hash8 = bytesToHex(blake2s(
|
|
158
|
+
const hash8 = bytesToHex(blake2s(image)).slice(0, 8);
|
|
136
159
|
return {
|
|
137
160
|
basename: `nft-${hash8}-${timestampMs}`,
|
|
138
|
-
image
|
|
139
|
-
thumbnail
|
|
161
|
+
image,
|
|
162
|
+
thumbnail,
|
|
140
163
|
metadata,
|
|
141
164
|
};
|
|
142
165
|
}
|
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
import { HardwareTopLevelSdk } from '../src';
|
|
2
|
-
import { findMethod } from '../src/api/utils';
|
|
3
|
-
import {
|
|
4
|
-
decodeBridgeBinaryPayload,
|
|
5
|
-
encodeBridgeBinaryPayload,
|
|
6
|
-
} from '../src/utils/bridgeBinaryPayload';
|
|
7
|
-
|
|
8
|
-
import type { LowLevelCoreApi } from '../src';
|
|
9
|
-
|
|
10
|
-
jest.mock('../src/data/config', () => ({
|
|
11
|
-
DEFAULT_DOMAIN: 'https://example.com/',
|
|
12
|
-
getSDKVersion: () => '0.0.0-test',
|
|
13
|
-
}));
|
|
14
|
-
|
|
15
|
-
const crossJsonOnlyBridge = (value: unknown): unknown => JSON.parse(JSON.stringify(value));
|
|
16
|
-
|
|
17
|
-
describe('bridge binary payload', () => {
|
|
18
|
-
test('preserves nested ArrayBuffer and sliced Uint8Array values', async () => {
|
|
19
|
-
const arrayBuffer = new Uint8Array([1, 2, 3]).buffer;
|
|
20
|
-
const backing = new Uint8Array([90, 4, 5, 6, 91]);
|
|
21
|
-
const encoded = await encodeBridgeBinaryPayload({
|
|
22
|
-
arrayBuffer,
|
|
23
|
-
bytes: backing.subarray(1, 4),
|
|
24
|
-
});
|
|
25
|
-
const decoded = decodeBridgeBinaryPayload(crossJsonOnlyBridge(encoded)) as {
|
|
26
|
-
arrayBuffer: ArrayBuffer;
|
|
27
|
-
bytes: Uint8Array;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
expect(decoded.arrayBuffer).toBeInstanceOf(ArrayBuffer);
|
|
31
|
-
expect(Array.from(new Uint8Array(decoded.arrayBuffer))).toEqual([1, 2, 3]);
|
|
32
|
-
expect(decoded.bytes).toBeInstanceOf(Uint8Array);
|
|
33
|
-
expect(Array.from(decoded.bytes)).toEqual([4, 5, 6]);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
test('encodes Blob input as byte data', async () => {
|
|
37
|
-
const encoded = await encodeBridgeBinaryPayload(new Blob([new Uint8Array([7, 8])]));
|
|
38
|
-
const decoded = decodeBridgeBinaryPayload(crossJsonOnlyBridge(encoded));
|
|
39
|
-
|
|
40
|
-
expect(decoded).toBeInstanceOf(Uint8Array);
|
|
41
|
-
expect(Array.from(decoded as Uint8Array)).toEqual([7, 8]);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
test('routes portfolio, wallpaper and NFT bytes through the top-level boundary', async () => {
|
|
45
|
-
const restoredPayloads: Array<Record<string, any>> = [];
|
|
46
|
-
const lowLevelApi = {
|
|
47
|
-
call: jest.fn(params => {
|
|
48
|
-
const wirePayload = crossJsonOnlyBridge(params);
|
|
49
|
-
const method = findMethod({ id: 1, payload: wirePayload } as any);
|
|
50
|
-
restoredPayloads.push(method.payload);
|
|
51
|
-
return Promise.resolve({ success: true, payload: {} });
|
|
52
|
-
}),
|
|
53
|
-
init: jest.fn(() => Promise.resolve(true)),
|
|
54
|
-
} as unknown as LowLevelCoreApi;
|
|
55
|
-
const sdk = HardwareTopLevelSdk();
|
|
56
|
-
await sdk.init({}, lowLevelApi);
|
|
57
|
-
|
|
58
|
-
const portfolioBytes = new Uint8Array([1, 2, 3]).buffer;
|
|
59
|
-
const wallpaperBacking = new Uint8Array([90, 4, 5, 6, 91]);
|
|
60
|
-
const nftBacking = new Uint8Array([80, 7, 8, 9, 10, 81]);
|
|
61
|
-
await sdk.uploadPortfolio('connect-id', { packageBytes: portfolioBytes });
|
|
62
|
-
await sdk.deviceUploadWallpaper('connect-id', {
|
|
63
|
-
width: 604,
|
|
64
|
-
height: 1024,
|
|
65
|
-
rgba: wallpaperBacking.subarray(1, 4),
|
|
66
|
-
});
|
|
67
|
-
await sdk.deviceUploadNft('connect-id', {
|
|
68
|
-
image: { width: 540, height: 540, rgba: nftBacking.subarray(1, 3) },
|
|
69
|
-
thumbnail: { width: 263, height: 263, rgba: nftBacking.subarray(3, 5) },
|
|
70
|
-
title: 'NFT',
|
|
71
|
-
subtitle: '',
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
expect(Array.from(new Uint8Array(restoredPayloads[0].packageBytes))).toEqual([1, 2, 3]);
|
|
75
|
-
expect(Array.from(restoredPayloads[1].rgba)).toEqual([4, 5, 6]);
|
|
76
|
-
expect(Array.from(restoredPayloads[2].image.rgba)).toEqual([7, 8]);
|
|
77
|
-
expect(Array.from(restoredPayloads[2].thumbnail.rgba)).toEqual([9, 10]);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test('routes firmware update binaries through the top-level boundary', async () => {
|
|
81
|
-
const restoredPayloads: Array<Record<string, any>> = [];
|
|
82
|
-
const lowLevelApi = {
|
|
83
|
-
call: jest.fn(params => {
|
|
84
|
-
const wirePayload = crossJsonOnlyBridge(params);
|
|
85
|
-
const method = findMethod({ id: 1, payload: wirePayload } as any);
|
|
86
|
-
restoredPayloads.push(method.payload);
|
|
87
|
-
return Promise.resolve({ success: true, payload: {} });
|
|
88
|
-
}),
|
|
89
|
-
init: jest.fn(() => Promise.resolve(true)),
|
|
90
|
-
} as unknown as LowLevelCoreApi;
|
|
91
|
-
const sdk = HardwareTopLevelSdk();
|
|
92
|
-
await sdk.init({}, lowLevelApi);
|
|
93
|
-
const binary = (...bytes: number[]) => Uint8Array.from(bytes).buffer;
|
|
94
|
-
|
|
95
|
-
await sdk.firmwareUpdate('connect-id', {
|
|
96
|
-
binary: binary(1),
|
|
97
|
-
updateType: 'firmware',
|
|
98
|
-
});
|
|
99
|
-
await sdk.firmwareUpdateV2('connect-id', {
|
|
100
|
-
binary: binary(2),
|
|
101
|
-
updateType: 'firmware',
|
|
102
|
-
platform: 'ext',
|
|
103
|
-
});
|
|
104
|
-
await sdk.firmwareUpdateV3('connect-id', {
|
|
105
|
-
platform: 'ext',
|
|
106
|
-
bleBinary: binary(3),
|
|
107
|
-
firmwareBinary: binary(4),
|
|
108
|
-
bootloaderBinary: binary(5),
|
|
109
|
-
resourceBinary: binary(6),
|
|
110
|
-
});
|
|
111
|
-
await sdk.firmwareUpdateV4('connect-id', {
|
|
112
|
-
platform: 'ext',
|
|
113
|
-
targetsToUpdate: [
|
|
114
|
-
'boot',
|
|
115
|
-
'app_v1',
|
|
116
|
-
'app_v2',
|
|
117
|
-
'coprocessor',
|
|
118
|
-
'se01',
|
|
119
|
-
'se02',
|
|
120
|
-
'se03',
|
|
121
|
-
'se04',
|
|
122
|
-
'resource',
|
|
123
|
-
],
|
|
124
|
-
romloaderBinary: binary(7),
|
|
125
|
-
bootloaderBinary: binary(8),
|
|
126
|
-
applicationP1Binary: binary(9),
|
|
127
|
-
applicationP2Binary: binary(10),
|
|
128
|
-
coprocessorBinary: binary(11),
|
|
129
|
-
se01Binary: binary(12),
|
|
130
|
-
se02Binary: binary(13),
|
|
131
|
-
se03Binary: binary(14),
|
|
132
|
-
se04Binary: binary(15),
|
|
133
|
-
resourceArchiveBinary: binary(16),
|
|
134
|
-
});
|
|
135
|
-
await sdk.deviceUpdateBootloader('connect-id', { binary: binary(17) });
|
|
136
|
-
await sdk.deviceFullyUploadResource('connect-id', { binary: binary(18) });
|
|
137
|
-
|
|
138
|
-
expect(restoredPayloads).toHaveLength(6);
|
|
139
|
-
const restoredBinaries = [
|
|
140
|
-
restoredPayloads[0].binary,
|
|
141
|
-
restoredPayloads[1].binary,
|
|
142
|
-
restoredPayloads[2].bleBinary,
|
|
143
|
-
restoredPayloads[2].firmwareBinary,
|
|
144
|
-
restoredPayloads[2].bootloaderBinary,
|
|
145
|
-
restoredPayloads[2].resourceBinary,
|
|
146
|
-
restoredPayloads[3].romloaderBinary,
|
|
147
|
-
restoredPayloads[3].bootloaderBinary,
|
|
148
|
-
restoredPayloads[3].applicationP1Binary,
|
|
149
|
-
restoredPayloads[3].applicationP2Binary,
|
|
150
|
-
restoredPayloads[3].coprocessorBinary,
|
|
151
|
-
restoredPayloads[3].se01Binary,
|
|
152
|
-
restoredPayloads[3].se02Binary,
|
|
153
|
-
restoredPayloads[3].se03Binary,
|
|
154
|
-
restoredPayloads[3].se04Binary,
|
|
155
|
-
restoredPayloads[3].resourceArchiveBinary,
|
|
156
|
-
restoredPayloads[4].binary,
|
|
157
|
-
restoredPayloads[5].binary,
|
|
158
|
-
];
|
|
159
|
-
expect(restoredBinaries.map(value => Array.from(new Uint8Array(value)))).toEqual(
|
|
160
|
-
Array.from({ length: 18 }, (_, index) => [index + 1])
|
|
161
|
-
);
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
test('rejects malformed tagged binary data', () => {
|
|
165
|
-
expect(() =>
|
|
166
|
-
decodeBridgeBinaryPayload({
|
|
167
|
-
__onekey_hd_bridge_binary_payload__: 1,
|
|
168
|
-
data: 'not-base64',
|
|
169
|
-
type: 'uint8-array',
|
|
170
|
-
})
|
|
171
|
-
).toThrow('Invalid bridge binary payload data');
|
|
172
|
-
});
|
|
173
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"bridgeBinaryPayload.d.ts","sourceRoot":"","sources":["../../src/utils/bridgeBinaryPayload.ts"],"names":[],"mappings":"AAkIA,wBAAsB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAEhF;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAEjE"}
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import { Buffer } from 'buffer';
|
|
2
|
-
import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
3
|
-
|
|
4
|
-
// Top-level and low-level SDK instances can be separated by JSON-only hosts,
|
|
5
|
-
// including browser extension background/offscreen message bridges.
|
|
6
|
-
const BRIDGE_BINARY_PAYLOAD_MARKER = '__onekey_hd_bridge_binary_payload__';
|
|
7
|
-
|
|
8
|
-
type BridgeBinaryPayload = {
|
|
9
|
-
[BRIDGE_BINARY_PAYLOAD_MARKER]: 1;
|
|
10
|
-
data: string;
|
|
11
|
-
type: 'array-buffer' | 'uint8-array';
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
const BASE64_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
15
|
-
|
|
16
|
-
const invalidBinaryPayload = (message: string) =>
|
|
17
|
-
ERRORS.TypedError(HardwareErrorCode.CallMethodInvalidParameter, message);
|
|
18
|
-
|
|
19
|
-
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
20
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
21
|
-
const prototype = Object.getPrototypeOf(value);
|
|
22
|
-
return prototype === Object.prototype || prototype === null;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function isArrayBufferValue(value: unknown): value is ArrayBuffer {
|
|
26
|
-
return Boolean(
|
|
27
|
-
typeof ArrayBuffer !== 'undefined' &&
|
|
28
|
-
(value instanceof ArrayBuffer ||
|
|
29
|
-
Object.prototype.toString.call(value) === '[object ArrayBuffer]')
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function isBlobValue(value: unknown): value is Blob {
|
|
34
|
-
return Boolean(
|
|
35
|
-
typeof Blob !== 'undefined' &&
|
|
36
|
-
(value instanceof Blob || Object.prototype.toString.call(value) === '[object Blob]') &&
|
|
37
|
-
typeof (value as Blob).arrayBuffer === 'function'
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function readBinaryPayload(value: unknown): BridgeBinaryPayload | undefined {
|
|
42
|
-
if (!isPlainObject(value) || !(BRIDGE_BINARY_PAYLOAD_MARKER in value)) return undefined;
|
|
43
|
-
const payload = value as Partial<BridgeBinaryPayload>;
|
|
44
|
-
if (
|
|
45
|
-
payload[BRIDGE_BINARY_PAYLOAD_MARKER] !== 1 ||
|
|
46
|
-
(payload.type !== 'array-buffer' && payload.type !== 'uint8-array') ||
|
|
47
|
-
typeof payload.data !== 'string'
|
|
48
|
-
) {
|
|
49
|
-
throw invalidBinaryPayload('Invalid bridge binary payload');
|
|
50
|
-
}
|
|
51
|
-
return payload as BridgeBinaryPayload;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function bytesToPayload(bytes: Uint8Array, type: BridgeBinaryPayload['type']): BridgeBinaryPayload {
|
|
55
|
-
return {
|
|
56
|
-
[BRIDGE_BINARY_PAYLOAD_MARKER]: 1,
|
|
57
|
-
data: Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString('base64'),
|
|
58
|
-
type,
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function payloadToBytes(payload: BridgeBinaryPayload): Uint8Array {
|
|
63
|
-
if (payload.data.length % 4 !== 0 || !BASE64_PATTERN.test(payload.data)) {
|
|
64
|
-
throw invalidBinaryPayload('Invalid bridge binary payload data');
|
|
65
|
-
}
|
|
66
|
-
const decoded = Buffer.from(payload.data, 'base64');
|
|
67
|
-
if (decoded.toString('base64') !== payload.data) {
|
|
68
|
-
throw invalidBinaryPayload('Invalid bridge binary payload data');
|
|
69
|
-
}
|
|
70
|
-
return Uint8Array.from(decoded);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async function encodeValue(value: unknown, seen: WeakSet<object>): Promise<unknown> {
|
|
74
|
-
if (isArrayBufferValue(value)) {
|
|
75
|
-
return bytesToPayload(new Uint8Array(value), 'array-buffer');
|
|
76
|
-
}
|
|
77
|
-
if (typeof ArrayBuffer !== 'undefined' && ArrayBuffer.isView(value)) {
|
|
78
|
-
return bytesToPayload(
|
|
79
|
-
new Uint8Array(value.buffer, value.byteOffset, value.byteLength),
|
|
80
|
-
'uint8-array'
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
if (isBlobValue(value)) {
|
|
84
|
-
return bytesToPayload(new Uint8Array(await value.arrayBuffer()), 'uint8-array');
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const encodedPayload = readBinaryPayload(value);
|
|
88
|
-
if (encodedPayload) return encodedPayload;
|
|
89
|
-
if (Array.isArray(value)) {
|
|
90
|
-
if (seen.has(value)) throw invalidBinaryPayload('Circular bridge payload');
|
|
91
|
-
seen.add(value);
|
|
92
|
-
try {
|
|
93
|
-
const encoded: unknown[] = [];
|
|
94
|
-
for (const item of value) {
|
|
95
|
-
encoded.push(await encodeValue(item, seen));
|
|
96
|
-
}
|
|
97
|
-
return encoded.some((item, index) => item !== value[index]) ? encoded : value;
|
|
98
|
-
} finally {
|
|
99
|
-
seen.delete(value);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
if (!isPlainObject(value)) return value;
|
|
103
|
-
if (seen.has(value)) throw invalidBinaryPayload('Circular bridge payload');
|
|
104
|
-
seen.add(value);
|
|
105
|
-
try {
|
|
106
|
-
const entries: [string, unknown][] = [];
|
|
107
|
-
for (const [key, item] of Object.entries(value)) {
|
|
108
|
-
entries.push([key, await encodeValue(item, seen)]);
|
|
109
|
-
}
|
|
110
|
-
return entries.some(([key, item]) => item !== value[key]) ? Object.fromEntries(entries) : value;
|
|
111
|
-
} finally {
|
|
112
|
-
seen.delete(value);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function decodeValue(value: unknown): unknown {
|
|
117
|
-
const payload = readBinaryPayload(value);
|
|
118
|
-
if (payload) {
|
|
119
|
-
const bytes = payloadToBytes(payload);
|
|
120
|
-
return payload.type === 'array-buffer' ? bytes.buffer : bytes;
|
|
121
|
-
}
|
|
122
|
-
if (Array.isArray(value)) {
|
|
123
|
-
const decoded = value.map(decodeValue);
|
|
124
|
-
return decoded.some((item, index) => item !== value[index]) ? decoded : value;
|
|
125
|
-
}
|
|
126
|
-
if (!isPlainObject(value)) return value;
|
|
127
|
-
const entries = Object.entries(value).map(([key, item]) => [key, decodeValue(item)] as const);
|
|
128
|
-
return entries.some(([key, item]) => item !== value[key]) ? Object.fromEntries(entries) : value;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export async function encodeBridgeBinaryPayload(value: unknown): Promise<unknown> {
|
|
132
|
-
return encodeValue(value, new WeakSet());
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export function decodeBridgeBinaryPayload(value: unknown): unknown {
|
|
136
|
-
return decodeValue(value);
|
|
137
|
-
}
|