diff --git a/index.js b/index.js index 9b76a29..673d704 100644 --- a/index.js +++ b/index.js @@ -22,6 +22,36 @@ const TREZOR_CONNECT_MANIFEST = { appUrl: 'https://metamask.io', }; +const oneKeySpecialVersion = 99; +const oneKeyVendor = 'onekey.so'; + +/** + * get the vendor name of the hardware wallet + * @param {object} features + * @returns {'onekey' | 'trezor' | undefined} + */ +function getVendorName(features) { + // If the value of features is null, set vendor to the default value + if (!features) { + return undefined; + } + + // No special field, default is trezor device + if (!features.minor_version || !features.patch_version) { + return 'trezor'; + } + + if ( + features.vendor === oneKeyVendor || + (features.minor_version === oneKeySpecialVersion && + features.patch_version === oneKeySpecialVersion) + ) { + return 'onekey'; + } + + return 'trezor'; +} + function wait(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } @@ -58,6 +88,7 @@ class TrezorKeyring extends EventEmitter { this.perPage = 5; this.unlockedAccount = 0; this.paths = {}; + this.vendor = undefined; this.deserialize(opts); TrezorConnect.on('DEVICE_EVENT', (event) => { @@ -78,6 +109,34 @@ class TrezorKeyring extends EventEmitter { return this.model; } + /** + * Gets the vendor, if known. + * + * @returns {"trezor" | "onekey" | null} + */ + async getVendor() { + return this.vendor || (this.vendor = await this.fetchVendor()); + } + + /** + * fetch vendor by call getFeatures + * @private + * @param {object} features + * @returns {'onekey' | 'trezor' | null} + */ + async fetchVendor() { + try { + const response = await TrezorConnect.getFeatures(); + if (!response.success) { + return null; + } + const vendor = getVendorName(response.payload); + return vendor; + } catch (err) { + return null; + } + } + dispose() { // This removes the Trezor Connect iframe from the DOM // This method is not well documented, but the code it calls can be seen diff --git a/test/test-eth-trezor-keyring.js b/test/test-eth-trezor-keyring.js index 538550f..9f3e41e 100644 --- a/test/test-eth-trezor-keyring.js +++ b/test/test-eth-trezor-keyring.js @@ -694,4 +694,101 @@ describe('TrezorKeyring', function () { } }); }); + + describe('getVendor', function () { + it('should call TrezorConnect.getFeatures if we dont have vendor name', async function () { + sinon + .stub(TrezorConnect, 'getFeatures') + .callsFake(() => Promise.resolve({})); + + await keyring.getVendor(); + assert(TrezorConnect.getFeatures.calledOnce); + }); + + it('should return null when TrezorConnect.getFeatures resolves with an object that does not have a success property', async function () { + sinon + .stub(TrezorConnect, 'getFeatures') + .callsFake(() => Promise.resolve({})); + + const vendor = await keyring.getVendor(); + assert.equal(vendor, null); + }); + + it('should return trezor when getFeatures does not have minor_version or patch_version', async function () { + sinon.stub(TrezorConnect, 'getFeatures').callsFake(() => + Promise.resolve({ + success: true, + payload: { + vendor: 'trezor.io', + }, + }), + ); + + const vendor = await keyring.getVendor(); + assert.equal(vendor, 'trezor'); + }); + + it('should return onekey when minor_version and patch_version is a special version', async function () { + sinon.stub(TrezorConnect, 'getFeatures').callsFake(() => + Promise.resolve({ + success: true, + payload: { + minor_version: 99, + patch_version: 99, + vendor: 'trezor.io', + }, + }), + ); + + const vendor = await keyring.getVendor(); + assert.equal(vendor, 'onekey'); + }); + + it('should return onekey when vendor field is onekey.so', async function () { + sinon.stub(TrezorConnect, 'getFeatures').callsFake(() => + Promise.resolve({ + success: true, + payload: { + minor_version: 1, + patch_version: 1, + vendor: 'onekey.so', + }, + }), + ); + + const vendor = await keyring.getVendor(); + assert.equal(vendor, 'onekey'); + }); + + it('should return null when TrezorConnect.getFeatures rejects with an error', async function () { + sinon.stub(TrezorConnect, 'getFeatures').callsFake(() => + // eslint-disable-next-line prefer-promise-reject-errors + Promise.reject({ + success: false, + payload: { error: 'mock error', code: 'mock error code' }, + }), + ); + + const vendor = await keyring.getVendor(); + assert.equal(vendor, null); + }); + + it('should called once TrezorConnect.getFeatures function when getVendor called more then once', async function () { + sinon.stub(TrezorConnect, 'getFeatures').callsFake(() => + Promise.resolve({ + success: true, + payload: { + minor_version: 1, + patch_version: 1, + vendor: 'onekey.so', + }, + }), + ); + + for (let i = 0; i < 10; i++) { + await keyring.getVendor(); + } + assert(TrezorConnect.getFeatures.calledOnce); + }); + }); });