diff --git a/README.md b/README.md index 0b062de2..83fb8e49 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ This repository contains the following packages [^fn1]: - [`@metamask/account-api`](packages/account-api) - [`@metamask/eth-hd-keyring`](packages/keyring-eth-hd) - [`@metamask/eth-ledger-bridge-keyring`](packages/keyring-eth-ledger-bridge) +- [`@metamask/eth-onekey-keyring`](packages/keyring-eth-onekey) - [`@metamask/eth-qr-keyring`](packages/keyring-eth-qr) - [`@metamask/eth-simple-keyring`](packages/keyring-eth-simple) - [`@metamask/eth-snap-keyring`](packages/keyring-snap-bridge) @@ -40,6 +41,7 @@ linkStyle default opacity:0.5 keyring_api(["@metamask/keyring-api"]); eth_hd_keyring(["@metamask/eth-hd-keyring"]); eth_ledger_bridge_keyring(["@metamask/eth-ledger-bridge-keyring"]); + eth_onekey_keyring(["@metamask/eth-onekey-keyring"]); eth_qr_keyring(["@metamask/eth-qr-keyring"]); eth_simple_keyring(["@metamask/eth-simple-keyring"]); eth_trezor_keyring(["@metamask/eth-trezor-keyring"]); diff --git a/packages/keyring-eth-onekey/CHANGELOG.md b/packages/keyring-eth-onekey/CHANGELOG.md new file mode 100644 index 00000000..7d41f673 --- /dev/null +++ b/packages/keyring-eth-onekey/CHANGELOG.md @@ -0,0 +1,15 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Changed + +- Bump axios +- Init Project + +[Unreleased]: https://github.com/MetaMask/accounts/ diff --git a/packages/keyring-eth-onekey/LICENSE b/packages/keyring-eth-onekey/LICENSE new file mode 100644 index 00000000..b5ed1b9c --- /dev/null +++ b/packages/keyring-eth-onekey/LICENSE @@ -0,0 +1,15 @@ +ISC License + +Copyright (c) 2020 MetaMask + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/packages/keyring-eth-onekey/README.md b/packages/keyring-eth-onekey/README.md new file mode 100644 index 00000000..34cb0357 --- /dev/null +++ b/packages/keyring-eth-onekey/README.md @@ -0,0 +1,41 @@ +# eth-onekey-bridge-keyring + +An implementation of MetaMask's [Keyring interface](https://github.com/MetaMask/eth-simple-keyring#the-keyring-class-protocol), that uses a OneKey hardware wallet for all cryptographic operations. + +In most regards, it works in the same way as +[eth-hd-keyring](https://github.com/MetaMask/eth-hd-keyring), but using a OneKey +device. However there are a number of differences: + +- Because the keys are stored in the device, operations that rely on the device + will fail if there is no OneKey device attached, or a different OneKey device + is attached. + +- It does not support the `signMessage`, `signTypedData` or `exportAccount` + methods, because OneKey devices do not support these operations. + +- Because extensions have limited access to browser features, there's no easy way to interact wth the OneKey Hardware wallet from the MetaMask extension. This library implements a workaround to those restrictions by injecting (on demand) an iframe to the background page of the extension, + +## Usage + +In addition to all the known methods from the [Keyring class protocol](https://github.com/MetaMask/eth-simple-keyring#the-keyring-class-protocol), +there are a few others: + +- **isUnlocked** : Returns true if we have the public key in memory, which allows to generate the list of accounts at any time + +- **unlock** : Connects to the OneKey device and exports the extended public key, which is later used to read the available ethereum addresses inside the OneKey account. + +- **setAccountToUnlock** : the index of the account that you want to unlock in order to use with the signTransaction and signPersonalMessage methods + +- **getFirstPage** : returns the first ordered set of accounts from the OneKey account + +- **getNextPage** : returns the next ordered set of accounts from the OneKey account based on the current page + +- **getPreviousPage** : returns the previous ordered set of accounts from the OneKey account based on the current page + +- **forgetDevice** : removes all the device info from memory so the next interaction with the keyring will prompt the user to connect the OneKey device and export the account information + +## Testing and Linting + +Run `yarn test` to run the tests once. To run tests on file changes, run `yarn test:watch`. + +Run `yarn lint` to run the linter, or run `yarn lint:fix` to run the linter and fix any automatically fixable issues. diff --git a/packages/keyring-eth-onekey/jest.config.js b/packages/keyring-eth-onekey/jest.config.js new file mode 100644 index 00000000..0b3cadbc --- /dev/null +++ b/packages/keyring-eth-onekey/jest.config.js @@ -0,0 +1,32 @@ +/* + * For a detailed explanation regarding each configuration property and type check, visit: + * https://jestjs.io/docs/configuration + */ + +const merge = require('deepmerge'); +const path = require('path'); + +const baseConfig = require('../../jest.config.packages'); + +const displayName = path.basename(__dirname); + +module.exports = merge(baseConfig, { + // The display name when running multiple projects + displayName, + + // An array of regexp pattern strings used to skip coverage collection + coveragePathIgnorePatterns: ['./src/tests'], + + // The glob patterns Jest uses to detect test files + testMatch: ['**/*.test.[jt]s?(x)'], + + // An object that configures minimum threshold enforcement for coverage results + coverageThreshold: { + global: { + branches: 74.31, + functions: 91.25, + lines: 92.83, + statements: 92.88, + }, + }, +}); diff --git a/packages/keyring-eth-onekey/package.json b/packages/keyring-eth-onekey/package.json new file mode 100644 index 00000000..2777609a --- /dev/null +++ b/packages/keyring-eth-onekey/package.json @@ -0,0 +1,108 @@ +{ + "name": "@metamask/eth-onekey-keyring", + "version": "0.1.0", + "description": "A MetaMask compatible keyring, for onekey hardware wallets", + "keywords": [ + "ethereum", + "keyring", + "onekey", + "metamask" + ], + "homepage": "https://github.com/MetaMask/accounts/blob/main/packages/keyring-eth-onekey/README.md", + "bugs": { + "url": "https://github.com/MetaMask/accounts/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/accounts.git" + }, + "license": "ISC", + "exports": { + ".": { + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + } + }, + "main": "./dist/index.cjs", + "types": "./dist/index.d.cts", + "files": [ + "dist/" + ], + "scripts": { + "build": "ts-bridge --project tsconfig.build.json --no-references", + "build:clean": "yarn build --clean", + "build:docs": "typedoc", + "changelog:update": "../../scripts/update-changelog.sh @metamask/eth-onekey-keyring", + "changelog:validate": "../../scripts/validate-changelog.sh @metamask/eth-onekey-keyring", + "publish": "yarn npm publish", + "publish:preview": "yarn npm publish --tag preview", + "test": "jest && jest-it-up", + "test:clean": "jest --clearCache", + "test:watch": "jest --watch" + }, + "dependencies": { + "@ethereumjs/tx": "^5.4.0", + "@ethereumjs/util": "^9.1.0", + "@metamask/eth-sig-util": "^8.2.0", + "@noble/hashes": "^1.7.0", + "@onekeyfe/hd-core": "1.1.6-patch.4", + "@onekeyfe/hd-shared": "1.1.6-patch.4", + "@onekeyfe/hd-transport": "1.1.6-patch.4", + "@onekeyfe/hd-web-sdk": "1.1.6-patch.4", + "hdkey": "^2.1.0" + }, + "devDependencies": { + "@ethereumjs/common": "^4.4.0", + "@lavamoat/allow-scripts": "^3.2.1", + "@lavamoat/preinstall-always-fail": "^2.1.0", + "@metamask/auto-changelog": "^3.4.4", + "@ts-bridge/cli": "^0.6.3", + "@types/bytebuffer": "^5.0.49", + "@types/ethereumjs-tx": "^1.0.1", + "@types/hdkey": "^2.0.1", + "@types/jest": "^29.5.12", + "@types/node": "^20.12.12", + "@types/sinon": "^17.0.3", + "@types/w3c-web-usb": "^1.0.6", + "deepmerge": "^4.2.2", + "depcheck": "^1.4.7", + "ethereumjs-tx": "^1.3.7", + "jest": "^29.5.0", + "jest-environment-jsdom": "^29.7.0", + "jest-it-up": "^3.1.0", + "sinon": "^19.0.2", + "ts-jest": "^29.0.5", + "ts-node": "^10.9.2", + "typedoc": "^0.25.13", + "typescript": "~5.6.3" + }, + "engines": { + "node": "^18.18 || >=20" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "installConfig": { + "hoistingLimits": "workspaces" + }, + "lavamoat": { + "allowScripts": { + "@lavamoat/preinstall-always-fail": false, + "ethereumjs-tx>ethereumjs-util>keccak": false, + "ethereumjs-tx>ethereumjs-util>secp256k1": false, + "hdkey>secp256k1": false, + "ethereumjs-tx>ethereumjs-util>ethereum-cryptography>keccak": false, + "ethereumjs-tx>ethereumjs-util>ethereum-cryptography>secp256k1": false, + "@onekeyfe/hd-transport>protobufjs": false, + "@onekeyfe/hd-core>@onekeyfe/hd-transport>protobufjs": false, + "@onekeyfe/hd-web-sdk>@onekeyfe/hd-core>@onekeyfe/hd-transport>protobufjs": false + } + } +} diff --git a/packages/keyring-eth-onekey/src/constants.ts b/packages/keyring-eth-onekey/src/constants.ts new file mode 100644 index 00000000..e0114f0f --- /dev/null +++ b/packages/keyring-eth-onekey/src/constants.ts @@ -0,0 +1 @@ +export const ONEKEY_HARDWARE_UI_EVENT = 'onekey-hardware-ui-event'; diff --git a/packages/keyring-eth-onekey/src/index.ts b/packages/keyring-eth-onekey/src/index.ts new file mode 100644 index 00000000..9311de0e --- /dev/null +++ b/packages/keyring-eth-onekey/src/index.ts @@ -0,0 +1,4 @@ +export * from './onekey-keyring'; +export type * from './onekey-bridge'; +export * from './onekey-web-bridge'; +export * from './constants'; diff --git a/packages/keyring-eth-onekey/src/onekey-bridge.ts b/packages/keyring-eth-onekey/src/onekey-bridge.ts new file mode 100644 index 00000000..f23e81ac --- /dev/null +++ b/packages/keyring-eth-onekey/src/onekey-bridge.ts @@ -0,0 +1,60 @@ +/* eslint-disable @typescript-eslint/no-redundant-type-constituents */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/naming-convention */ +import type { + ConnectSettings, + Params, + EVMSignedTx, + EVMSignTransactionParams, + EVMSignMessageParams, + EVMSignTypedDataParams, + EVMGetPublicKeyParams, +} from '@onekeyfe/hd-core'; +import type { EthereumMessageSignature } from '@onekeyfe/hd-transport'; + +type Unsuccessful = { + success: false; + payload: { + error: string; + code?: string | number; + }; +}; +type Success = { + success: true; + payload: T; +}; +type Response = Promise | Unsuccessful>; + +export type OneKeyBridge = { + model?: string; + + on(event: string, callback: (event: any) => void): void; + + off(event: string): void; + + init(settings: Partial): Promise; + + dispose(): Promise; + + updateTransportMethod(transportType: string): Promise; + + // OneKeySdk.getPublicKey has two overloads + // It is not possible to extract them from the library using utility types + getPublicKey( + params: Params, + ): Response<{ publicKey: string; chainCode: string }>; + + getPassphraseState(): Response; + + ethereumSignTransaction( + params: Params, + ): Response; + + ethereumSignMessage( + params: Params, + ): Response; + + ethereumSignTypedData( + params: Params, + ): Response; +}; diff --git a/packages/keyring-eth-onekey/src/onekey-keyring.test.ts b/packages/keyring-eth-onekey/src/onekey-keyring.test.ts new file mode 100644 index 00000000..05d2d389 --- /dev/null +++ b/packages/keyring-eth-onekey/src/onekey-keyring.test.ts @@ -0,0 +1,1202 @@ +/* eslint-disable jest/no-conditional-expect */ +import { Common, Chain, Hardfork } from '@ethereumjs/common'; +import type { TypedTransaction } from '@ethereumjs/tx'; +import { + TransactionFactory, + FeeMarketEIP1559Transaction, +} from '@ethereumjs/tx'; +import { Address } from '@ethereumjs/util'; +import { SignTypedDataVersion } from '@metamask/eth-sig-util'; +// eslint-disable-next-line @typescript-eslint/naming-convention +import EthereumTx from 'ethereumjs-tx'; +// eslint-disable-next-line @typescript-eslint/naming-convention +import HDKey from 'hdkey'; +import * as sinon from 'sinon'; + +import type { OneKeyBridge } from './onekey-bridge'; +import type { AccountDetails } from './onekey-keyring'; +import { OneKeyKeyring } from './onekey-keyring'; +import { OneKeyWebBridge } from './onekey-web-bridge'; + +const CONNECT_SRC = 'https://jssdk.onekey.so/1.1.5/'; +const fakeAccounts = [ + '0x73d0385F4d8E00C5e6504C6030F47BF6212736A8', + '0xFA01a39f8Abaeb660c3137f14A310d0b414b2A15', + '0x574BbB36871bA6b78E27f4B4dCFb76eA0091880B', + '0xba98D6a5ac827632E3457De7512d211e4ff7e8bD', + '0x1f815D67006163E502b8eD4947C91ad0A62De24e', + '0xf69619a3dCAA63757A6BA0AF3628f5F6C42c50d2', + '0xA8664Df3D5E74BE57c19fC7005BBcd0F5328041e', + '0xf2252f414e727d652d5a488fE4BFf7e64478737F', + '0x5708Ae081b48ad7bA8c50ca3D4fa0238d544D6FA', + '0x12eF7dfb86f6D5E3e0521b72472ca02D2a3814F4', + '0x9115Fa64b8B9864D6545Fc00d62B6A9Cbb876be7', + '0x8B6cF2eA1A54E054EFC35E4244Ac507c479bb5F6', + '0x6C480ba4409dd5FF29Cbd3ED67152B791750a708', + '0x5f2E5ddEd3DBD431deCc406Ae999F277B625Ba25', + '0x8a143C4CCed2ce826DE598Dbbf7C706cD6DB0Ccd', +] as const; + +const fakeXPubKey = + 'xpub6CNFa58kEQJu2hwMVoofpDEKVVSg6gfwqBqE2zHAianaUnQkrJzJJ42iLDp7Dmg2aP88qCKoFZ4jidk3tECdQuF4567NGHDfe7iBRwHxgke'; +const fakeHdKey = HDKey.fromExtendedKey(fakeXPubKey); +const fakeTx = new EthereumTx({ + nonce: '0x00', + gasPrice: '0x09184e72a000', + gasLimit: '0x2710', + to: '0x0000000000000000000000000000000000000000', + value: '0x00', + data: '0x7f7465737432000000000000000000000000000000000000000000000000000000600057', + // EIP 155 chainId - mainnet: 1, ropsten: 3 + chainId: 1, +}); + +const common = new Common({ chain: 'mainnet' }); +const commonEIP1559 = new Common({ + chain: Chain.Mainnet, + hardfork: Hardfork.London, +}); +const newFakeTx = TransactionFactory.fromTxData( + { + nonce: '0x00', + gasPrice: '0x09184e72a000', + gasLimit: '0x2710', + to: '0x0000000000000000000000000000000000000000', + value: '0x00', + data: '0x7f7465737432000000000000000000000000000000000000000000000000000000600057', + }, + { common, freeze: false }, +); + +const contractDeploymentFakeTx = TransactionFactory.fromTxData( + { + nonce: '0x00', + gasPrice: '0x09184e72a000', + gasLimit: '0x2710', + value: '0x00', + data: '0x7f7465737432000000000000000000000000000000000000000000000000000000600057', + }, + { common, freeze: false }, +); + +const fakeTypeTwoTx = FeeMarketEIP1559Transaction.fromTxData( + { + nonce: '0x00', + maxFeePerGas: '0x19184e72a000', + maxPriorityFeePerGas: '0x09184e72a000', + gasLimit: '0x2710', + to: '0x0000000000000000000000000000000000000000', + value: '0x00', + data: '0x7f7465737432000000000000000000000000000000000000000000000000000000600057', + type: 2, + v: '0x01', + }, + { common: commonEIP1559, freeze: false }, +); + +describe('OneKeyKeyring', function () { + let keyring: OneKeyKeyring; + let bridge: OneKeyBridge; + + beforeEach(async function () { + bridge = new OneKeyWebBridge(); + keyring = new OneKeyKeyring({ bridge }); + keyring.hdk = fakeHdKey; + keyring.accountDetails = { + [fakeAccounts[0]]: { + index: 0, + hdPath: `m/44'/60'/0'/0/0`, + passphraseState: '', + }, + }; + }); + + afterEach(function () { + sinon.restore(); + }); + + describe('Keyring.type', function () { + it('is a class property that returns the type string.', function () { + const { type } = OneKeyKeyring; + expect(typeof type).toBe('string'); + }); + + it('returns the correct value', function () { + const { type } = keyring; + const correct = OneKeyKeyring.type; + expect(type).toBe(correct); + }); + }); + + describe('constructor', function () { + it('constructs', async function () { + const keyringInstance = new OneKeyKeyring({ bridge }); + expect(typeof keyringInstance).toBe('object'); + const accounts = await keyringInstance.getAccounts(); + expect(Array.isArray(accounts)).toBe(true); + }); + + it('throws if a bridge is not provided', async function () { + expect( + () => + new OneKeyKeyring({ + bridge: undefined as unknown as OneKeyBridge, + }), + ).toThrow('Bridge is a required dependency for the keyring'); + }); + }); + + describe('init', function () { + it('initialises the bridge', async function () { + const initStub = sinon.stub().resolves(); + bridge.init = initStub; + + await keyring.init({ + fetchConfig: true, + connectSrc: CONNECT_SRC, + env: 'web', + }); + + expect(initStub.calledOnce).toBe(true); + sinon.assert.calledWithExactly(initStub, { + fetchConfig: true, + connectSrc: CONNECT_SRC, + env: 'web', + }); + }); + }); + + describe('destroy', function () { + it('calls dispose on bridge', async function () { + const disposeStub = sinon.stub().resolves(); + bridge.dispose = disposeStub; + + await keyring.destroy(); + + expect(disposeStub.calledOnce).toBe(true); + sinon.assert.calledWithExactly(disposeStub); + }); + }); + + describe('serialize', function () { + it('serializes an instance', async function () { + const output = await keyring.serialize(); + expect(output.page).toBe(0); + expect(output.hdPath).toBe(`m/44'/60'/0'/0`); + expect(Array.isArray(output.accounts)).toBe(true); + expect(output.accounts).toHaveLength(0); + }); + }); + + describe('deserialize', function () { + it('serializes what it deserializes', async function () { + const someHdPath = `m/44'/60'/0'/1`; + await keyring.deserialize({ + page: 10, + hdPath: someHdPath, + accounts: [], + }); + const serialized = await keyring.serialize(); + expect(serialized.accounts).toHaveLength(0); + expect(serialized.page).toBe(10); + expect(serialized.hdPath).toBe(someHdPath); + }); + }); + + describe('isUnlocked', function () { + it('should return true if we have a public key', function () { + expect(keyring.isUnlocked()).toBe(true); + }); + }); + + describe('unlock', function () { + it('should resolve if we have a public key', async function () { + expect(async () => { + await keyring.unlock(); + }).not.toThrow(); + }); + + it('should call OneKeyWebBridge.getPublicKey if we dont have a public key', async function () { + const getPassphraseStateStub = sinon.stub().resolves({ + success: true, + payload: '', + }); + const getPublicKeyStub = sinon.stub().resolves({ + success: true, + payload: { + publicKey: fakeHdKey.publicKey.toString('hex'), + chainCode: fakeHdKey.chainCode.toString('hex'), + }, + }); + bridge.getPassphraseState = getPassphraseStateStub; + bridge.getPublicKey = getPublicKeyStub; + + keyring.hdk = new HDKey(); + try { + await keyring.unlock(); + } catch { + // Since we only care about ensuring our function gets called, + // we want to ignore warnings due to stub data + } + + expect(getPublicKeyStub.calledOnce).toBe(true); + sinon.assert.calledWithExactly(getPublicKeyStub, { + showOnOneKey: false, + chainId: 1, + path: "m/44'/60'/0'", + passphraseState: '', + }); + }); + }); + + describe('setAccountToUnlock', function () { + it('should set unlockedAccount', function () { + keyring.setAccountToUnlock(3); + expect(keyring.unlockedAccount).toBe(3); + }); + }); + + describe('addAccounts', function () { + describe('with no arguments', function () { + it('returns a single account', async function () { + keyring.setAccountToUnlock(0); + const accounts = await keyring.addAccounts(1); + expect(accounts).toHaveLength(1); + }); + + it('returns the custom accounts desired', async function () { + keyring.setAccountToUnlock(0); + await keyring.addAccounts(1); + keyring.setAccountToUnlock(2); + await keyring.addAccounts(1); + + const accounts = await keyring.getAccounts(); + expect(accounts[0]).toBe(fakeAccounts[0]); + expect(accounts[1]).toBe(fakeAccounts[2]); + }); + }); + + describe('with a numeric argument', function () { + it('returns that number of accounts', async function () { + keyring.setAccountToUnlock(0); + const firstBatch = await keyring.addAccounts(3); + keyring.setAccountToUnlock(3); + const secondBatch = await keyring.addAccounts(2); + + expect(firstBatch).toHaveLength(3); + expect(secondBatch).toHaveLength(2); + }); + + it('returns the expected accounts', async function () { + keyring.setAccountToUnlock(0); + const firstBatch = await keyring.addAccounts(3); + keyring.setAccountToUnlock(3); + const secondBatch = await keyring.addAccounts(2); + + expect(firstBatch).toStrictEqual([ + fakeAccounts[0], + fakeAccounts[1], + fakeAccounts[2], + ]); + expect(secondBatch).toStrictEqual([fakeAccounts[3], fakeAccounts[4]]); + }); + }); + }); + + describe('removeAccount', function () { + describe('if the account exists', function () { + it('should remove that account', async function () { + keyring.setAccountToUnlock(0); + const accounts = await keyring.addAccounts(1); + expect(accounts).toHaveLength(1); + keyring.removeAccount(fakeAccounts[0]); + const accountsAfterRemoval = await keyring.getAccounts(); + expect(accountsAfterRemoval).toHaveLength(0); + }); + + it('should remove only the account requested', async function () { + keyring.setAccountToUnlock(0); + await keyring.addAccounts(1); + keyring.setAccountToUnlock(1); + await keyring.addAccounts(1); + + let accounts = await keyring.getAccounts(); + expect(accounts).toHaveLength(2); + + keyring.removeAccount(fakeAccounts[0]); + accounts = await keyring.getAccounts(); + + expect(accounts).toHaveLength(1); + expect(accounts[0]).toBe(fakeAccounts[1]); + }); + }); + + describe('if the account does not exist', function () { + it('should throw an error', function () { + const unexistingAccount = '0x0000000000000000000000000000000000000000'; + expect(() => { + keyring.removeAccount(unexistingAccount); + }).toThrow(`Address ${unexistingAccount} not found in this keyring`); + }); + }); + }); + + describe('getFirstPage', function () { + it('should set the currentPage to 1', async function () { + await keyring.getFirstPage(); + expect(keyring.page).toBe(1); + }); + + it('should return the list of accounts for current page', async function () { + const accounts = await keyring.getFirstPage(); + + expect(accounts).toHaveLength(keyring.perPage); + expect(accounts[0]?.address).toBe(fakeAccounts[0]); + expect(accounts[1]?.address).toBe(fakeAccounts[1]); + expect(accounts[2]?.address).toBe(fakeAccounts[2]); + expect(accounts[3]?.address).toBe(fakeAccounts[3]); + expect(accounts[4]?.address).toBe(fakeAccounts[4]); + }); + }); + + describe('getNextPage', function () { + it('should return the list of accounts for current page', async function () { + const accounts = await keyring.getNextPage(); + expect(accounts).toHaveLength(keyring.perPage); + expect(accounts[0]?.address).toBe(fakeAccounts[0]); + expect(accounts[1]?.address).toBe(fakeAccounts[1]); + expect(accounts[2]?.address).toBe(fakeAccounts[2]); + expect(accounts[3]?.address).toBe(fakeAccounts[3]); + expect(accounts[4]?.address).toBe(fakeAccounts[4]); + }); + + it('should be able to advance to the next page', async function () { + // manually advance 1 page + await keyring.getNextPage(); + + const accounts = await keyring.getNextPage(); + expect(accounts).toHaveLength(keyring.perPage); + expect(accounts[0]?.address).toBe(fakeAccounts[keyring.perPage + 0]); + expect(accounts[1]?.address).toBe(fakeAccounts[keyring.perPage + 1]); + expect(accounts[2]?.address).toBe(fakeAccounts[keyring.perPage + 2]); + expect(accounts[3]?.address).toBe(fakeAccounts[keyring.perPage + 3]); + expect(accounts[4]?.address).toBe(fakeAccounts[keyring.perPage + 4]); + }); + }); + + describe('getPreviousPage', function () { + it('should return the list of accounts for current page', async function () { + // manually advance 1 page + await keyring.getNextPage(); + const accounts = await keyring.getPreviousPage(); + + expect(accounts).toHaveLength(keyring.perPage); + expect(accounts[0]?.address).toBe(fakeAccounts[0]); + expect(accounts[1]?.address).toBe(fakeAccounts[1]); + expect(accounts[2]?.address).toBe(fakeAccounts[2]); + expect(accounts[3]?.address).toBe(fakeAccounts[3]); + expect(accounts[4]?.address).toBe(fakeAccounts[4]); + }); + + it('should be able to go back to the previous page', async function () { + // manually advance 1 page + await keyring.getNextPage(); + const accounts = await keyring.getPreviousPage(); + + expect(accounts).toHaveLength(keyring.perPage); + expect(accounts[0]?.address).toBe(fakeAccounts[0]); + expect(accounts[1]?.address).toBe(fakeAccounts[1]); + expect(accounts[2]?.address).toBe(fakeAccounts[2]); + expect(accounts[3]?.address).toBe(fakeAccounts[3]); + expect(accounts[4]?.address).toBe(fakeAccounts[4]); + }); + }); + + describe('getAccounts', function () { + const accountIndex = 5; + let accounts: string[] = []; + beforeEach(async function () { + keyring.setAccountToUnlock(accountIndex); + await keyring.addAccounts(1); + accounts = (await keyring.getAccounts()) as string[]; + }); + + it('returns an array of accounts', function () { + expect(Array.isArray(accounts)).toBe(true); + expect(accounts).toHaveLength(1); + }); + + it('returns the expected', function () { + const expectedAccount = fakeAccounts[accountIndex]; + expect(accounts[0]).toBe(expectedAccount); + }); + }); + + describe('signTransaction', function () { + it('should pass serialized transaction to onekey and return signed tx', async function () { + const ethereumSignTransactionStub = sinon.stub().resolves({ + success: true, + payload: { v: '0x1', r: '0x0', s: '0x0' }, + }); + bridge.ethereumSignTransaction = ethereumSignTransactionStub; + + sinon.stub(fakeTx, 'verifySignature').callsFake(() => true); + sinon + .stub(fakeTx, 'getSenderAddress') + .callsFake(() => + Buffer.from(Address.fromString(fakeAccounts[0]).bytes), + ); + + const returnedTx = await keyring.signTransaction(fakeAccounts[0], fakeTx); + // assert that the v,r,s values got assigned to tx. + expect(returnedTx.v).toBeDefined(); + expect(returnedTx.r).toBeDefined(); + expect(returnedTx.s).toBeDefined(); + // ensure we get a older version transaction back + expect((returnedTx as EthereumTx).getChainId()).toBe(1); + expect((returnedTx as TypedTransaction).common).toBeUndefined(); + expect(ethereumSignTransactionStub.calledOnce).toBe(true); + }); + + it('should pass serialized newer transaction to onekey and return signed tx', async function () { + sinon.stub(TransactionFactory, 'fromTxData').callsFake(() => { + // without having a private key/public key pair in this test, we have + // mock out this method and return the original tx because we can't + // replicate r and s values without the private key. + return newFakeTx; + }); + + const ethereumSignTransactionStub = sinon.stub().resolves({ + success: true, + payload: { v: '0x25', r: '0x0', s: '0x0' }, + }); + bridge.ethereumSignTransaction = ethereumSignTransactionStub; + + sinon + .stub(newFakeTx, 'getSenderAddress') + .callsFake(() => Address.fromString(fakeAccounts[0])); + sinon.stub(newFakeTx, 'verifySignature').callsFake(() => true); + + const returnedTx = await keyring.signTransaction( + fakeAccounts[0], + newFakeTx, + ); + // ensure we get a new version transaction back + // eslint-disable-next-line @typescript-eslint/unbound-method + expect((returnedTx as EthereumTx).getChainId).toBeUndefined(); + expect( + (returnedTx as TypedTransaction).common.chainId().toString(16), + ).toBe('1'); + expect(ethereumSignTransactionStub.calledOnce).toBe(true); + }); + + it('should pass serialized contract deployment transaction to onekey and return signed tx', async function () { + sinon.stub(TransactionFactory, 'fromTxData').callsFake(() => { + // without having a private key/public key pair in this test, we have + // mock out this method and return the original tx because we can't + // replicate r and s values without the private key. + return contractDeploymentFakeTx; + }); + + const ethereumSignTransactionStub = sinon.stub().resolves({ + success: true, + payload: { v: '0x25', r: '0x0', s: '0x0' }, + }); + bridge.ethereumSignTransaction = ethereumSignTransactionStub; + + sinon + .stub(contractDeploymentFakeTx, 'getSenderAddress') + .callsFake(() => Address.fromString(fakeAccounts[0])); + + sinon + .stub(contractDeploymentFakeTx, 'verifySignature') + .callsFake(() => true); + + const returnedTx = await keyring.signTransaction( + fakeAccounts[0], + contractDeploymentFakeTx, + ); + // ensure we get a new version transaction back + // eslint-disable-next-line @typescript-eslint/unbound-method + expect((returnedTx as EthereumTx).getChainId).toBeUndefined(); + expect( + (returnedTx as TypedTransaction).common.chainId().toString(16), + ).toBe('1'); + expect(ethereumSignTransactionStub.calledOnce).toBe(true); + expect(ethereumSignTransactionStub.getCall(0).args[0]).toStrictEqual({ + passphraseState: '', + useEmptyPassphrase: true, + path: `m/44'/60'/0'/0/0`, + transaction: { + ...contractDeploymentFakeTx.toJSON(), + to: '0x', + chainId: 1, + }, + }); + }); + + it('should pass correctly encoded EIP1559 transaction to onekey and return signed tx', async function () { + // Copied from @MetaMask/eth-ledger-bridge-keyring + // Generated by signing fakeTypeTwoTx with an unknown private key + const expectedRSV = { + v: '0x0', + r: '0x5ffb3adeaec80e430e7a7b02d95c5108b6f09a0bdf3cf69869dc1b38d0fb8d3a', + s: '0x28b234a5403d31564e18258df84c51a62683e3f54fa2b106fdc1a9058006a112', + }; + // Override actual address of 0x391535104b6e0Ea6dDC2AD0158aB3Fbd7F04ed1B + const fromTxDataStub = sinon.stub(TransactionFactory, 'fromTxData'); + fromTxDataStub.callsFake((...args) => { + const tx = fromTxDataStub.wrappedMethod(...args); + sinon + .stub(tx, 'getSenderAddress') + .returns(Address.fromString(fakeAccounts[0])); + return tx; + }); + + const ethereumSignTransactionStub = sinon.stub().resolves({ + success: true, + payload: expectedRSV, + }); + bridge.ethereumSignTransaction = ethereumSignTransactionStub; + + const returnedTx = await keyring.signTransaction( + fakeAccounts[0], + fakeTypeTwoTx, + ); + + expect(ethereumSignTransactionStub.calledOnce).toBe(true); + sinon.assert.calledWithExactly(ethereumSignTransactionStub, { + passphraseState: '', + useEmptyPassphrase: true, + path: "m/44'/60'/0'/0/0", + transaction: { + type: '0x2', + chainId: 1, + nonce: '0x0', + maxPriorityFeePerGas: '0x9184e72a000', + maxFeePerGas: '0x19184e72a000', + gasLimit: '0x2710', + to: '0x0000000000000000000000000000000000000000', + value: '0x0', + data: '0x7f7465737432000000000000000000000000000000000000000000000000000000600057', + accessList: [], + v: '0x1', + r: undefined, + s: undefined, + }, + }); + + expect(returnedTx.toJSON()).toStrictEqual({ + ...fakeTypeTwoTx.toJSON(), + ...expectedRSV, + }); + }); + }); + + describe('signMessage', function () { + it('should call onekeyConnect.ethereumSignMessage', async function () { + const ethereumSignMessageStub = sinon.stub().resolves({}); + bridge.ethereumSignMessage = ethereumSignMessageStub; + + try { + await keyring.signMessage(fakeAccounts[0], 'some msg'); + } catch { + // Since we only care about ensuring our function gets called, + // we want to ignore warnings due to stub data + } + + expect(ethereumSignMessageStub.calledOnce).toBe(true); + }); + }); + + describe('signPersonalMessage', function () { + it('should call onekeyConnect.ethereumSignMessage', async function () { + const ethereumSignMessageStub = sinon.stub().resolves({}); + bridge.ethereumSignMessage = ethereumSignMessageStub; + + try { + await keyring.signPersonalMessage(fakeAccounts[0], 'some msg'); + } catch { + // Since we only care about ensuring our function gets called, + // we want to ignore warnings due to stub data + } + + expect(ethereumSignMessageStub.calledOnce).toBe(true); + }); + }); + + describe('signTypedData', function () { + it('should call onekeyConnect.ethereumSignTypedData', async function () { + const ethereumSignTypedDataStub = sinon.stub().resolves({ + success: true, + payload: { signature: '0x00', address: fakeAccounts[0] }, + }); + bridge.ethereumSignTypedData = ethereumSignTypedDataStub; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore next-line + // eslint-disable-next-line no-invalid-this + this.timeout = 60000; + await keyring.signTypedData( + fakeAccounts[0], + // Message with missing data that @metamask/eth-sig-util accepts + { + types: { EIP712Domain: [], EmptyMessage: [] }, + primaryType: 'EmptyMessage', + domain: {}, + message: {}, + }, + { version: SignTypedDataVersion.V4 }, + ); + + expect(ethereumSignTypedDataStub.calledOnce).toBe(true); + sinon.assert.calledWithExactly(ethereumSignTypedDataStub, { + passphraseState: '', + useEmptyPassphrase: true, + path: "m/44'/60'/0'/0/0", + data: { + // Empty message that onekey-connect/EIP-712 spec accepts + types: { EIP712Domain: [], EmptyMessage: [] }, + primaryType: 'EmptyMessage', + domain: {}, + message: {}, + }, + metamaskV4Compat: true, + domainHash: + '6192106f129ce05c9075d319c1fa6ea9b3ae37cbd0c1ef92e2be7137bb07baa1', + messageHash: + 'c9e71eb57cf9fa86ec670283b58cb15326bb6933c8d8e2ecb2c0849021b3ef42', + }); + }); + }); + + describe('forgetDevice', function () { + it('should clear the content of the keyring', async function () { + // Add an account + keyring.setAccountToUnlock(0); + await keyring.addAccounts(1); + + // Wipe the keyring + keyring.forgetDevice(); + + const accounts = await keyring.getAccounts(); + + expect(keyring.isUnlocked()).toBe(false); + expect(accounts).toHaveLength(0); + }); + }); + + describe('setHdPath', function () { + const initialProperties = { + hdPath: `m/44'/60'/0'/0` as const, + accounts: [fakeAccounts[0]], + page: 2, + }; + + // hdPath?: string; + // accounts?: string[]; + // accountDetails?: Readonly>; + // page?: number; + // passphraseState?: string; + + const accountToUnlock = 1; + + const mockPaths: Record = { + '0x123': { + index: 0, + hdPath: `m/44'/60'/0'/0`, + passphraseState: '123', + }, + }; + + beforeEach(async function () { + await keyring.deserialize(initialProperties); + // eslint-disable-next-line require-atomic-updates + keyring.accountDetails = mockPaths; + keyring.setAccountToUnlock(accountToUnlock); + }); + + it('should do nothing if passed an hdPath equal to the current hdPath', async function () { + keyring.setHdPath(initialProperties.hdPath); + expect(keyring.hdPath).toBe(initialProperties.hdPath); + expect(keyring.accounts).toStrictEqual(initialProperties.accounts); + expect(keyring.page).toBe(initialProperties.page); + expect(keyring.hdk.publicKey.toString('hex')).toBe( + fakeHdKey.publicKey.toString('hex'), + ); + expect(keyring.unlockedAccount).toBe(accountToUnlock); + expect(keyring.accountDetails).toStrictEqual(mockPaths); + }); + + it('should update the hdPath and reset account and page properties if passed a new hdPath', async function () { + const ledgerLegacyHdPathString = `m/44'/60'/0'/x`; + + keyring.setHdPath(ledgerLegacyHdPathString); + + expect(keyring.hdPath).toBe(ledgerLegacyHdPathString); + expect(keyring.accounts).toStrictEqual([]); + expect(keyring.page).toBe(0); + expect(keyring.perPage).toBe(5); + expect(keyring.hdk.publicKey).toBeNull(); + expect(keyring.unlockedAccount).toBe(0); + expect(keyring.accountDetails).toStrictEqual({}); + }); + + it('should throw an error if passed an ledger live hdPath', async function () { + const unsupportedPath = "m/44'/60'/x'/0/0"; + expect(() => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore next-line + keyring.setHdPath(unsupportedPath); + }).toThrow(`Unknown HD path`); + }); + + it('should throw an error if passed an unsupported hdPath', async function () { + const unsupportedPath = 'unsupported hdPath'; + expect(() => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore next-line + keyring.setHdPath(unsupportedPath); + }).toThrow(`Unknown HD path`); + }); + }); + + describe('error handling and edge cases', function () { + it('should handle signing errors', async function () { + await keyring.addAccounts(1); + const errorResponse = { + success: false, + payload: { error: 'Signing failed' }, + }; + bridge.ethereumSignTransaction = sinon.stub().resolves(errorResponse); + + try { + await keyring.signTransaction(fakeAccounts[0], fakeTx); + throw new Error('Expected error was not thrown'); + } catch (error) { + // eslint-disable-next-line jest/no-conditional-expect + expect((error as Error).message).toContain('Signing failed'); + } + }); + + it('should handle message signing errors', async function () { + await keyring.addAccounts(1); + const errorResponse = { + success: false, + payload: { error: 'Message signing failed' }, + }; + bridge.ethereumSignMessage = sinon.stub().resolves(errorResponse); + + try { + await keyring.signPersonalMessage(fakeAccounts[0], '0x68656c6c6f'); + throw new Error('Expected error was not thrown'); + } catch (error) { + expect((error as Error).message).toContain('Message signing failed'); + } + }); + + it('should handle address verification mismatch in signing', async function () { + await keyring.addAccounts(1); + const wrongAddress = '0x1234567890123456789012345678901234567890'; + const successResponse = { + success: true, + payload: { + v: '0x1', + r: '0x0', + s: '0x0', + address: wrongAddress, + }, + }; + bridge.ethereumSignMessage = sinon.stub().resolves(successResponse); + + try { + await keyring.signPersonalMessage(fakeAccounts[0], '0x68656c6c6f'); + throw new Error('Expected error was not thrown'); + } catch (error) { + expect((error as Error).message).toContain( + 'signature doesnt match the right address', + ); + } + }); + + it('should handle getPreviousPage when already on first page', async function () { + keyring.page = 0; + const accounts = await keyring.getPreviousPage(); + expect(accounts).toHaveLength(keyring.perPage); + expect(keyring.page).toBe(1); // When page <= 0, it gets set to 1 + }); + }); + + describe('HD path validation edge cases', function () { + it('should handle Ledger Live HD path correctly', function () { + const ledgerLiveHdPath = "m/44'/60'/0'/x"; + keyring.setHdPath(ledgerLiveHdPath); + expect(keyring.hdPath).toBe(ledgerLiveHdPath); + }); + + it('should handle standard BIP44 HD path correctly', function () { + const standardHdPath = "m/44'/60'/0'/0/x"; + keyring.setHdPath(standardHdPath); + expect(keyring.hdPath).toBe(standardHdPath); + }); + + it('should handle default HD path correctly', function () { + const defaultPath = "m/44'/60'/0'/0"; + keyring.setHdPath(defaultPath); + expect(keyring.hdPath).toBe(defaultPath); + }); + + it('should handle different HD path formats', function () { + // Test different HD path validations + const ledgerLiveHdPath = "m/44'/60'/0'/x"; + keyring.setHdPath(ledgerLiveHdPath); + expect(keyring.hdPath).toBe(ledgerLiveHdPath); + + const standardHdPath = "m/44'/60'/0'/0/x"; + keyring.setHdPath(standardHdPath); + expect(keyring.hdPath).toBe(standardHdPath); + }); + }); + + describe('account filtering', function () { + beforeEach(async function () { + keyring.setAccountToUnlock(0); + await keyring.addAccounts(5); + }); + + it('should handle removeAccount with all accounts removed', function () { + const allAccounts = [...keyring.accounts]; + + allAccounts.forEach((account) => { + keyring.removeAccount(account); + }); + + expect(keyring.accounts).toHaveLength(0); + expect(Object.keys(keyring.accountDetails)).toHaveLength(0); + }); + + it('should handle removeAccount with non-existent account', function () { + const nonExistentAccount = '0x1234567890123456789012345678901234567890'; + + expect(() => { + keyring.removeAccount(nonExistentAccount); + }).toThrow( + 'Address 0x1234567890123456789012345678901234567890 not found in this keyring', + ); + }); + }); + + describe('transaction serialization edge cases', function () { + it('should handle transaction with empty "to" field (contract deployment)', async function () { + await keyring.addAccounts(1); + + const successResponse = { + success: true, + payload: { v: '0x1', r: '0x0', s: '0x0' }, + }; + const ethereumSignTransactionStub = sinon + .stub() + .resolves(successResponse); + bridge.ethereumSignTransaction = ethereumSignTransactionStub; + + sinon.stub(fakeTx, 'verifySignature').callsFake(() => true); + sinon + .stub(fakeTx, 'getSenderAddress') + .callsFake(() => + Buffer.from(Address.fromString(fakeAccounts[0]).bytes), + ); + + // Simulate a contract deployment transaction by setting to to null + const originalTo = fakeTx.to; + // @ts-expect-error - for testing purposes + fakeTx.to = null; + + const result = await keyring.signTransaction(fakeAccounts[0], fakeTx); + expect(result).toBeDefined(); + + const call = ethereumSignTransactionStub.getCall(0); + expect(call.args[0].transaction.to).toBe('0x'); + + // Restore original to value + // eslint-disable-next-line require-atomic-updates + fakeTx.to = originalTo; + }); + + it('should test hex prefix utilities', async function () { + // Test the serialize method + const serialized = await keyring.serialize(); + expect(serialized.hdPath).toMatch(/^m\//u); // Should start with m/ + }); + }); + + describe('HD path private method coverage', function () { + it('should handle standard BIP44 path variations', async function () { + keyring.setHdPath("m/44'/60'/0'/0/x"); + await keyring.unlock(); + await keyring.addAccounts(1); + expect(keyring.accounts).toHaveLength(1); + + keyring.setHdPath("m/44'/60'/0'/0"); + await keyring.unlock(); + await keyring.addAccounts(1); + expect(keyring.accounts.length).toBeGreaterThan(0); + }); + + it('should handle HD path comparison logic', async function () { + expect(keyring.hdPath).toBe("m/44'/60'/0'/0"); + expect(keyring.accounts).toHaveLength(0); + + await keyring.addAccounts(2); + keyring.setHdPath("m/44'/60'/0'/0/x"); + + expect(keyring.hdPath).toBe("m/44'/60'/0'/0/x"); + expect(keyring.accounts).toHaveLength(2); + + keyring.setHdPath("m/44'/60'/0'/0"); + expect(keyring.hdPath).toBe("m/44'/60'/0'/0"); + expect(keyring.accounts).toHaveLength(2); + + keyring.setHdPath("m/44'/60'/0'/x"); + expect(keyring.hdPath).toBe("m/44'/60'/0'/x"); + expect(keyring.accounts).toHaveLength(0); + }); + }); + + describe('additional edge cases for coverage', function () { + it('should handle forgetDevice', function () { + keyring.forgetDevice(); + expect(keyring.accounts).toHaveLength(0); + expect(keyring.page).toBe(0); + expect(keyring.unlockedAccount).toBe(0); + expect(Object.keys(keyring.accountDetails)).toHaveLength(0); + }); + + it('should handle exportAccount error', function () { + expect(() => { + keyring.exportAccount(); + }).toThrow('Not supported on this device'); + }); + + it('should handle isUnlocked when not unlocked', function () { + keyring.hdk = new HDKey(); + expect(keyring.isUnlocked()).toBe(false); + }); + + it('should handle different passphrase states', function () { + keyring.passphraseState = ''; + expect(keyring.passphraseState).toBe(''); + + keyring.passphraseState = undefined; + expect(keyring.passphraseState).toBeUndefined(); + }); + + it('should handle addHexPrefix utility function', function () { + const testMessage = 'hello world'; + const messageHex = Buffer.from(testMessage, 'utf8').toString('hex'); + + // These calls will use add hex prefix indirectly + // eslint-disable-next-line jest/no-restricted-matchers + expect(messageHex).toBeTruthy(); + }); + + it('should handle getName method', function () { + expect(keyring.getName()).toBe('OneKey Hardware'); + }); + + it('should handle init bridge method', async function () { + const initSpy = sinon.stub(bridge, 'init').resolves(); + + await keyring.init({ debug: true }); + expect(initSpy.calledOnce).toBe(true); + + // Test destroy method by calling keyring destroy + await keyring.destroy(); + }); + + it('should handle getNextPage and getPreviousPage correctly', async function () { + const nextPageAccounts = await keyring.getNextPage(); + expect(nextPageAccounts).toHaveLength(keyring.perPage); + expect(keyring.page).toBeGreaterThan(0); + + const prevPageAccounts = await keyring.getPreviousPage(); + expect(prevPageAccounts).toHaveLength(keyring.perPage); + }); + + it('should handle signMessage method', async function () { + await keyring.addAccounts(1); + const expectedSignature = '0xsignature123'; + const signPersonalMessageStub = sinon + .stub(keyring, 'signPersonalMessage') + .resolves(expectedSignature); + + const result = await keyring.signMessage(fakeAccounts[0], '0x68656c6c6f'); + expect(result).toBe(expectedSignature); + expect( + signPersonalMessageStub.calledWith(fakeAccounts[0], '0x68656c6c6f'), + ).toBe(true); + }); + + it('should handle transaction signing with address verification', async function () { + await keyring.addAccounts(1); + const successResponse = { + success: true, + payload: { v: '0x1', r: '0x0', s: '0x0' }, + }; + bridge.ethereumSignTransaction = sinon.stub().resolves(successResponse); + + // Mock the transaction verification to pass + sinon.stub(fakeTx, 'verifySignature').callsFake(() => true); + sinon + .stub(fakeTx, 'getSenderAddress') + .callsFake(() => + Buffer.from(Address.fromString(fakeAccounts[0]).bytes), + ); + + const result = await keyring.signTransaction(fakeAccounts[0], fakeTx); + expect(result).toBeDefined(); + expect(result.v).toBeDefined(); + expect(result.r).toBeDefined(); + expect(result.s).toBeDefined(); + }); + + it('should handle basic unlock scenarios', async function () { + const accounts = await keyring.getAccounts(); + expect(accounts).toStrictEqual([]); + }); + + it('should handle addAccounts error scenarios', async function () { + const unlockStub = sinon + .stub(keyring, 'unlock') + .rejects(new Error('Unlock failed')); + + try { + await keyring.addAccounts(1); + throw new Error('Expected error was not thrown'); + } catch (error) { + expect((error as Error).message).toContain('Unlock failed'); + } + + unlockStub.restore(); + }); + + it('should handle bridge constructor error', function () { + expect(() => { + // eslint-disable-next-line no-new + new OneKeyKeyring({ bridge: undefined as unknown as OneKeyBridge }); + }).toThrow('Bridge is a required dependency for the keyring'); + }); + + it('should handle event emission from bridge', function () { + const eventSpy = sinon.stub(keyring, 'emit'); + + expect(keyring.bridge).toBe(bridge); + expect(eventSpy.called).toBe(false); + }); + + describe('HD path variations', function () { + it('should handle Ledger Legacy HD path in setHdPath', function () { + // Cover branches in #isSameHdPath for Ledger Legacy + keyring.setHdPath("m/44'/60'/0'/x"); + expect(keyring.hdPath).toBe("m/44'/60'/0'/x"); + + // Setting the same path should trigger #isSameHdPath but not reset + const originalHdk = keyring.hdk; + keyring.setHdPath("m/44'/60'/0'/x"); // This should call #isSameHdPath and return true + expect(keyring.hdk).toBe(originalHdk); // HDKey should not be reset + }); + + it('should handle path comparison between different Ledger Legacy paths', function () { + // Cover line 687-688 in #isSameHdPath + keyring.setHdPath("m/44'/60'/0'/x"); + + // Change to different Ledger Legacy path - should reset HDKey + keyring.setHdPath("m/44'/60'/0'/x"); + expect(keyring.hdPath).toBe("m/44'/60'/0'/x"); + }); + + it('should handle Standard BIP44 path variations in setHdPath', function () { + // Cover branches in #isSameHdPath for standard BIP44 + keyring.setHdPath("m/44'/60'/0'/0/x"); + expect(keyring.hdPath).toBe("m/44'/60'/0'/0/x"); + + // Test equivalence with defaultHdPath - should trigger #isSameHdPath + keyring.setHdPath("m/44'/60'/0'/0"); // Should be considered same as m/44'/60'/0'/0/x + expect(keyring.hdPath).toBe("m/44'/60'/0'/0"); + }); + + it('should handle default path comparison in #isSameHdPath', function () { + // Cover line 694: return this.hdPath === newHdPath; + // Directly set hdPath to test custom path logic + keyring.hdPath = "m/44'/60'/1'/2/3"; // Custom path not in predefined categories + + const originalHdk = keyring.hdk; + keyring.setHdPath("m/44'/60'/0'/0"); // Different path should reset + expect(keyring.hdPath).toBe("m/44'/60'/0'/0"); + expect(keyring.hdk).not.toBe(originalHdk); // HDKey should be reset + }); + + it('should handle Ledger Legacy path in addAccounts workflow', async function () { + // This will trigger #getPathForIndex with Ledger Legacy path (line 660) + keyring.setHdPath("m/44'/60'/0'/x"); + + // Set up successful unlock + const unlockResult = 'unlocked'; + const unlockSpy = sinon.stub(keyring, 'unlock').resolves(unlockResult); + + keyring.hdk = fakeHdKey; + + const accounts = await keyring.addAccounts(1); + expect(accounts).toHaveLength(1); + + unlockSpy.restore(); + }); + + it('should handle custom path in getPathForIndex', async function () { + // Cover line 668: return `${this.hdPath}/${index}`; + // Directly set hdPath to bypass ALLOWED_HD_PATHS check + keyring.hdPath = "m/44'/60'/1'/2"; // Custom path that doesn't match predefined patterns + + const unlockResult = 'unlocked'; + const unlockSpy = sinon.stub(keyring, 'unlock').resolves(unlockResult); + + keyring.hdk = fakeHdKey; + + const accounts = await keyring.addAccounts(1); + expect(accounts).toHaveLength(1); + + unlockSpy.restore(); + }); + + it('should handle same custom path in #isSameHdPath', function () { + // Cover line 694: return this.hdPath === newHdPath; when custom paths are the same + keyring.hdPath = "m/44'/60'/5'/6"; // Custom path + + const originalHdk = keyring.hdk; + keyring.setHdPath("m/44'/60'/0'/0"); // Change to allowed path + expect(keyring.hdPath).toBe("m/44'/60'/0'/0"); + expect(keyring.hdk).not.toBe(originalHdk); // HDKey should be reset + }); + + it('should handle Ledger Live HD path errors', async function () { + // Cover lines 641 and 648: throw new Error('Ledger Live is not supported'); + keyring.hdPath = "m/44'/60'/x'/0/0"; // Ledger Live path (not in ALLOWED_HD_PATHS but we set directly) + + const unlockSpy = sinon.stub(keyring, 'unlock').resolves('unlocked'); + keyring.hdk = fakeHdKey; + + // This should trigger the Ledger Live error paths during addAccounts + try { + await keyring.addAccounts(1); + // If we get here, the test setup was wrong + expect(true).toBe(false); + } catch (error) { + expect((error as Error).message).toContain( + 'Ledger Live is not supported', + ); + } + + unlockSpy.restore(); + }); + }); + }); +}); diff --git a/packages/keyring-eth-onekey/src/onekey-keyring.ts b/packages/keyring-eth-onekey/src/onekey-keyring.ts new file mode 100644 index 00000000..e8ceafdd --- /dev/null +++ b/packages/keyring-eth-onekey/src/onekey-keyring.ts @@ -0,0 +1,699 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import type { TypedTransaction, TypedTxData } from '@ethereumjs/tx'; +import { TransactionFactory } from '@ethereumjs/tx'; +import * as ethUtil from '@ethereumjs/util'; +import type { MessageTypes, TypedMessage } from '@metamask/eth-sig-util'; +import { SignTypedDataVersion, TypedDataUtils } from '@metamask/eth-sig-util'; +import type { + ConnectSettings, + EthereumSignTypedDataMessage, + EthereumSignTypedDataTypes, + EVMSignedTx, + EVMSignTransactionParams, +} from '@onekeyfe/hd-core'; +// eslint-disable-next-line @typescript-eslint/no-shadow, n/prefer-global/buffer +import { Buffer } from 'buffer'; +import type OldEthJsTransaction from 'ethereumjs-tx'; +import { EventEmitter } from 'events'; +import HDKey from 'hdkey'; + +import { ONEKEY_HARDWARE_UI_EVENT } from './constants'; +import type { OneKeyBridge } from './onekey-bridge'; + +const pathBase = 'm'; +const defaultHdPath = `${pathBase}/44'/60'/0'/0`; +const keyringType = 'OneKey Hardware'; + +const hdPathString = `m/44'/60'/0'/0/x`; +const ledgerLegacyHdPathString = `m/44'/60'/0'/x`; + +const ALLOWED_HD_PATHS: Record = { + [defaultHdPath]: true, + [hdPathString]: true, + [ledgerLegacyHdPathString]: true, +} as const; + +enum NetworkApiUrls { + Ropsten = 'https://api-ropsten.etherscan.io', + Kovan = 'https://api-kovan.etherscan.io', + Rinkeby = 'https://api-rinkeby.etherscan.io', + Mainnet = `https://api.etherscan.io`, +} + +export type AccountDetails = { + index?: number; + hdPath: string; + passphraseState?: string | undefined; +}; + +export type AccountPageEntry = { + address: string; + balance: number | null; + index: number; +}; + +export type AccountPage = AccountPageEntry[]; + +export type OneKeyControllerOptions = { + hdPath?: string; + accounts?: string[]; + accountDetails?: Readonly>; + page?: number; + passphraseState?: string; +}; + +export type OneKeyControllerState = { + hdPath: string; + accounts: readonly string[]; + accountDetails: Readonly>; + page: number; + passphraseState?: string; +}; + +/** + * Check if the given transaction is made with ethereumjs-tx or @ethereumjs/tx + * + * Transactions built with older versions of ethereumjs-tx have a + * getChainId method that newer versions do not. + * Older versions are mutable + * while newer versions default to being immutable. + * Expected shape and type + * of data for v, r and s differ (Buffer (old) vs BN (new)). + * + * @param tx - Transaction to check, instance of either ethereumjs-tx or @ethereumjs/tx. + * @returns Returns `true` if tx is an old-style ethereumjs-tx transaction. + */ +function isOldStyleEthereumjsTx( + tx: TypedTransaction | OldEthJsTransaction, +): tx is OldEthJsTransaction { + return 'getChainId' in tx && typeof tx.getChainId === 'function'; +} + +/** + * Check if the given value has a hex prefix. + * + * @param value - The value to check. + * @returns Returns `true` if the value has a hex prefix. + */ +function hasHexPrefix(value: string): boolean { + return value.startsWith('0x'); +} + +/** + * Add a hex prefix to the given value. + * + * @param value - The value to add a hex prefix to. + * @returns Returns the value with a hex prefix. + */ +function addHexPrefix(value: string): string { + if (hasHexPrefix(value)) { + return value; + } + return `0x${value}`; +} + +/** + * Check if the passphrase state is empty. + * + * @param passphraseState - The passphrase state to check. + * @returns Returns `true` if the passphrase state is empty. + */ +function isEmptyPassphrase(passphraseState: string | undefined): boolean { + return ( + passphraseState === null || + passphraseState === undefined || + passphraseState === '' + ); +} + +export class OneKeyKeyring extends EventEmitter { + readonly type: string = keyringType; + + static type: string = keyringType; + + page = 0; + + perPage = 5; + + unlockedAccount = 0; + + hdk = new HDKey(); + + accounts: readonly string[] = []; + + accountDetails: Record = {}; + + passphraseState: string | undefined; + + hdPath = defaultHdPath; + + network: NetworkApiUrls = NetworkApiUrls.Mainnet; + + implementFullBIP44 = false; + + bridge: OneKeyBridge; + + constructor({ bridge }: { bridge: OneKeyBridge }) { + super(); + + if (!bridge) { + throw new Error('Bridge is a required dependency for the keyring'); + } + + this.bridge = bridge; + this.bridge.on(ONEKEY_HARDWARE_UI_EVENT, (_event: any) => { + this.emit(ONEKEY_HARDWARE_UI_EVENT, _event); + }); + } + + async init(settings: Partial): Promise { + return this.bridge.init(settings); + } + + async destroy(): Promise { + this.bridge.off(ONEKEY_HARDWARE_UI_EVENT); + return this.bridge.dispose(); + } + + async serialize(): Promise { + return Promise.resolve({ + hdPath: this.hdPath, + accounts: this.accounts, + accountDetails: this.accountDetails, + page: this.page, + }); + } + + async deserialize(opts: OneKeyControllerOptions = {}): Promise { + this.hdPath = opts.hdPath ?? defaultHdPath; + this.accounts = opts.accounts ?? []; + this.accountDetails = opts.accountDetails ?? {}; + this.page = opts.page ?? 0; + return Promise.resolve(); + } + + getModel(): string | undefined { + return this.bridge.model; + } + + setAccountToUnlock(index: number): void { + this.unlockedAccount = index; + } + + setHdPath(hdPath: string): void { + if (!ALLOWED_HD_PATHS[hdPath]) { + throw new Error('Unknown HD path'); + } + + // Reset HDKey if the path changes + if (!this.#isSameHdPath(hdPath)) { + this.hdk = new HDKey(); + this.accounts = []; + this.page = 0; + this.perPage = 5; + this.unlockedAccount = 0; + this.accountDetails = {}; + } + this.hdPath = hdPath; + } + + lock(): void { + this.hdk = new HDKey(); + } + + isUnlocked(): boolean { + return Boolean(this.hdk?.publicKey); + } + + async unlock(): Promise { + if (this.isUnlocked()) { + return 'already unlocked'; + } + + return new Promise((resolve, reject) => { + // eslint-disable-next-line no-void + void this.bridge + .getPassphraseState() + .then((passphraseResponse) => { + if (passphraseResponse.success) { + this.passphraseState = passphraseResponse.payload; + } + if (!passphraseResponse.success) { + reject(new Error('getPassphraseState failed')); + return; + } + this.passphraseState = passphraseResponse.payload; + + // eslint-disable-next-line no-void + void this.bridge + .getPublicKey({ + showOnOneKey: false, + chainId: 1, + path: this.#getBasePath(), + passphraseState: this.passphraseState ?? '', + }) + .then(async (res) => { + if (res.success) { + this.hdk.publicKey = Buffer.from(res.payload.publicKey, 'hex'); + this.hdk.chainCode = Buffer.from(res.payload.chainCode, 'hex'); + resolve('just unlocked'); + } else { + reject(new Error('getPublicKey failed')); + } + }); + }) + .catch((error) => { + reject(new Error(error?.toString() || 'Unknown error')); + }); + }); + } + + async addAccounts(numberOfAccounts = 1): Promise { + await this.unlock().catch((error) => { + throw new Error(error?.toString() || 'Unknown error'); + }); + + return new Promise((resolve, reject) => { + const from = this.unlockedAccount; + const to = from + numberOfAccounts; + const newAccounts: string[] = []; + + try { + for (let i = from; i < to; i++) { + const address = this.#addressFromIndex(i); + const hdPath = this.#getPathForIndex(i); + if (typeof address === 'undefined') { + throw new Error('Unknown error'); + } + if (!this.accounts.includes(address)) { + this.accounts = [...this.accounts, address]; + newAccounts.push(address); + } + if (!this.accountDetails[address]) { + this.accountDetails[address] = { + index: i, + hdPath, + passphraseState: this.passphraseState, + }; + } + this.page = 0; + } + + resolve(newAccounts); + } catch (error) { + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors + reject(error); + } + }); + } + + getName(): string { + return keyringType; + } + + async getFirstPage(): Promise { + this.page = 0; + return this.#getPage(1); + } + + async getNextPage(): Promise { + return this.#getPage(1); + } + + async getPreviousPage(): Promise { + return this.#getPage(-1); + } + + async getAccounts(): Promise { + return Promise.resolve(this.accounts.slice()); + } + + removeAccount(address: string): void { + const filteredAccounts = this.accounts.filter( + (a) => a.toLowerCase() !== address.toLowerCase(), + ); + + if (filteredAccounts.length === this.accounts.length) { + throw new Error(`Address ${address} not found in this keyring`); + } + + this.accounts = filteredAccounts; + delete this.accountDetails[ethUtil.toChecksumAddress(address)]; + } + + async updateTransportMethod( + transportType: ConnectSettings['env'], + ): Promise { + return this.bridge.updateTransportMethod(transportType); + } + + #normalize(buffer: Buffer): string { + return ethUtil.bytesToHex(buffer); + } + + /** + * Signs a transaction using OneKey. + * + * Accepts either an ethereumjs-tx or @ethereumjs/tx transaction, and returns + * the same type. + * + * @param address - Hex string address. + * @param tx - Instance of either new-style or old-style ethereumjs transaction. + * @returns The signed transaction, an instance of either new-style or old-style + * ethereumjs transaction. + */ + async signTransaction( + address: string, + tx: TypedTransaction | OldEthJsTransaction, + ): Promise { + if (isOldStyleEthereumjsTx(tx)) { + // In this version of ethereumjs-tx we must add the chainId in hex format + // to the initial v value. The chainId must be included in the serialized + // transaction which is only communicated to ethereumjs-tx in this + // value. In newer versions the chainId is communicated via the 'Common' + // object. + return this.#signTransaction( + address, + // @types/ethereumjs-tx and old ethereumjs-tx versions document + // this function return value as Buffer, but the actual + // Transaction._chainId will always be a number. + // See https://github.com/ethereumjs/ethereumjs-tx/blob/v1.3.7/index.js#L126 + tx.getChainId() as unknown as number, + tx, + (payload) => { + tx.v = Buffer.from(payload.v, 'hex'); + tx.r = Buffer.from(payload.r, 'hex'); + tx.s = Buffer.from(payload.s, 'hex'); + return tx; + }, + ); + } + return this.#signTransaction( + address, + Number(tx.common.chainId()), + tx, + (payload) => { + // Because tx will be immutable, first get a plain javascript object that + // represents the transaction. Using txData here as it aligns with the + // nomenclature of ethereumjs/tx. + const txData: TypedTxData = tx.toJSON(); + // The fromTxData utility expects a type to support transactions with a type other than 0 + txData.type = tx.type; + // The fromTxData utility expects v,r and s to be hex prefixed + txData.v = ethUtil.addHexPrefix(payload.v); + txData.r = ethUtil.addHexPrefix(payload.r); + txData.s = ethUtil.addHexPrefix(payload.s); + // Adopt the 'common' option from the original transaction and set the + // returned object to be frozen if the original is frozen. + return TransactionFactory.fromTxData(txData, { + common: tx.common, + freeze: Object.isFrozen(tx), + }); + }, + ); + } + + async #signTransaction( + address: string, + chainId: number, + tx: T, + handleSigning: (tx: EVMSignedTx) => T, + ): Promise { + let transaction: EVMSignTransactionParams['transaction']; + if (isOldStyleEthereumjsTx(tx)) { + // legacy transaction from ethereumjs-tx package has no .toJSON() function, + // so we need to convert to hex-strings manually manually + transaction = { + to: this.#normalize(tx.to), + value: this.#normalize(tx.value), + data: this.#normalize(tx.data), + chainId, + nonce: this.#normalize(tx.nonce), + gasLimit: this.#normalize(tx.gasLimit), + gasPrice: this.#normalize(tx.gasPrice), + }; + } else { + // new-style transaction from @ethereumjs/tx package + // we can just copy tx.toJSON() for everything except chainId, which must be a number + transaction = { + ...tx.toJSON(), + chainId, + to: this.#normalize(Buffer.from(tx.to?.bytes ?? [])), + } as unknown as EVMSignTransactionParams['transaction']; + } + + try { + const details = this.#accountDetailsFromAddress(address); + const response = await this.bridge.ethereumSignTransaction({ + path: details.hdPath, + passphraseState: details.passphraseState ?? '', + useEmptyPassphrase: isEmptyPassphrase(details.passphraseState), + transaction, + }); + if (response.success) { + const newOrMutatedTx = handleSigning(response.payload); + + const addressSignedWith = ethUtil.toChecksumAddress( + ethUtil.addHexPrefix( + newOrMutatedTx.getSenderAddress().toString('hex'), + ), + ); + const correctAddress = ethUtil.toChecksumAddress(address); + if (addressSignedWith !== correctAddress) { + throw new Error("signature doesn't match the right address"); + } + + return newOrMutatedTx; + } + throw new Error(response.payload?.error || 'Unknown error'); + } catch (error) { + throw new Error(error?.toString() ?? 'Unknown error'); + } + } + + async signMessage(withAccount: string, data: string): Promise { + return this.signPersonalMessage(withAccount, data); + } + + // For personal_sign, we need to prefix the message: + async signPersonalMessage( + withAccount: string, + message: string, + ): Promise { + return new Promise((resolve, reject) => { + const details = this.#accountDetailsFromAddress(withAccount); + this.bridge + .ethereumSignMessage({ + path: details.hdPath, + passphraseState: details.passphraseState ?? '', + useEmptyPassphrase: isEmptyPassphrase(details.passphraseState), + messageHex: ethUtil.stripHexPrefix(message), + }) + .then((response) => { + if (response.success) { + if ( + response.payload.address !== + ethUtil.toChecksumAddress(withAccount) + ) { + reject(new Error('signature doesnt match the right address')); + } + const signature = addHexPrefix(response.payload.signature); + // eslint-disable-next-line promise/no-multiple-resolved + resolve(signature); + } else { + reject(new Error(response.payload?.error || 'Unknown error')); + } + }) + .catch((error) => { + reject(new Error(error?.toString() || 'Unknown error')); + }); + }); + } + + // EIP-712 Sign Typed Data + async signTypedData( + address: string, + data: TypedMessage, + { version }: { version?: SignTypedDataVersion }, + ): Promise { + // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison + const useV4 = version === SignTypedDataVersion.V4; + const dataVersion = useV4 + ? SignTypedDataVersion.V4 + : SignTypedDataVersion.V3; + const typedData = TypedDataUtils.sanitizeData(data); + const domainHash = TypedDataUtils.hashStruct( + 'EIP712Domain', + typedData.domain, + typedData.types, + dataVersion, + ).toString('hex'); + const messageHash = TypedDataUtils.hashStruct( + typedData.primaryType as string, + typedData.message, + typedData.types, + dataVersion, + ).toString('hex'); + + const details = this.#accountDetailsFromAddress(address); + const response = await this.bridge.ethereumSignTypedData({ + path: details.hdPath, + passphraseState: details.passphraseState ?? '', + useEmptyPassphrase: isEmptyPassphrase(details.passphraseState), + data: data as EthereumSignTypedDataMessage, + domainHash, + messageHash, + metamaskV4Compat: Boolean(useV4), // eslint-disable-line camelcase + }); + + if (response.success) { + if (ethUtil.toChecksumAddress(address) !== response.payload.address) { + throw new Error('signature doesnt match the right address'); + } + return addHexPrefix(response.payload.signature); + } + + throw new Error(response.payload?.error || 'Unknown error'); + } + + exportAccount(): never { + throw new Error('Not supported on this device'); + } + + forgetDevice(): void { + this.hdk = new HDKey(); + this.accounts = []; + this.page = 0; + this.unlockedAccount = 0; + this.accountDetails = {}; + this.passphraseState = undefined; + } + + async getPassphraseState( + _index: number, + _hdPath: string, + ): Promise { + // TODO: implement + return Promise.resolve(undefined); + } + + async #getPage( + increment: number, + ): Promise<{ address: string; balance: number | null; index: number }[]> { + this.page += increment; + + if (this.page <= 0) { + this.page = 1; + } + + return new Promise((resolve, reject) => { + const from = (this.page - 1) * this.perPage; + const to = from + this.perPage; + + const accounts: { + address: string; + balance: number | null; + index: number; + }[] = []; + + this.unlock() + .then(async () => { + for (let i = from; i < to; i++) { + const address = this.#addressFromIndex(i); + if (typeof address === 'undefined') { + throw new Error('Unknown error'); + } + accounts.push({ + index: i, + address, + balance: null, + }); + } + resolve(accounts); + }) + .catch((error) => { + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors + reject(error); + }); + }); + } + + #accountDetailsFromAddress(address: string): AccountDetails { + const checksummedAddress = ethUtil.toChecksumAddress(address); + const accountDetails = this.accountDetails[checksummedAddress]; + if (typeof accountDetails === 'undefined') { + throw new Error('Unknown address'); + } + return accountDetails; + } + + #addressFromIndex(i: number): string { + const dkey = this.hdk.derive(this.#getDerivePath(i)); + const address = ethUtil.bytesToHex( + ethUtil.publicToAddress(dkey.publicKey, true), + ); + return ethUtil.toChecksumAddress(address); + } + + #getDerivePath(index: number): string { + if (this.#isLedgerLiveHdPath()) { + throw new Error('Ledger Live is not supported'); + } + if (this.#isStandardBip44HdPath()) { + return `${pathBase}/0/${index}`; + } + return `${pathBase}/${index}`; + } + + #getBasePath(): string { + if (this.#isLedgerLiveHdPath()) { + throw new Error('Ledger Live is not supported'); + } + return "m/44'/60'/0'"; + } + + #getPathForIndex(index: number): string { + // Check if the path is BIP 44 (Ledger Live) + if (this.#isLedgerLiveHdPath()) { + return `m/44'/60'/${index}'/0/0`; + } + + if (this.#isLedgerLegacyHdPath()) { + return `m/44'/60'/0'/${index}`; + } + + if (this.#isStandardBip44HdPath()) { + return `m/44'/60'/0'/0/${index}`; + } + + // default path: m/44'/60'/0'/0/x + return `${this.hdPath}/${index}`; + } + + #isLedgerLiveHdPath(): boolean { + return this.hdPath === `m/44'/60'/x'/0/0`; + } + + #isLedgerLegacyHdPath(): boolean { + return this.hdPath === `m/44'/60'/0'/x`; + } + + #isStandardBip44HdPath(): boolean { + return this.hdPath === `m/44'/60'/0'/0/x` || this.hdPath === defaultHdPath; + } + + #isSameHdPath(newHdPath: string): boolean { + if (this.#isLedgerLiveHdPath()) { + return newHdPath === `m/44'/60'/x'/0/0`; + } + if (this.#isLedgerLegacyHdPath()) { + return newHdPath === `m/44'/60'/0'/x`; + } + if (this.#isStandardBip44HdPath()) { + return newHdPath === `m/44'/60'/0'/0/x` || newHdPath === defaultHdPath; + } + + return this.hdPath === newHdPath; + } +} diff --git a/packages/keyring-eth-onekey/src/onekey-web-bridge.test.ts b/packages/keyring-eth-onekey/src/onekey-web-bridge.test.ts new file mode 100644 index 00000000..a074fe10 --- /dev/null +++ b/packages/keyring-eth-onekey/src/onekey-web-bridge.test.ts @@ -0,0 +1,636 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { UI_REQUEST, UI_RESPONSE } from '@onekeyfe/hd-core'; +import { HardwareErrorCode } from '@onekeyfe/hd-shared'; + +import { ONEKEY_HARDWARE_UI_EVENT } from './constants'; +import { OneKeyWebBridge } from './onekey-web-bridge'; + +// Mock the dynamic import +const mockHardwareWebSdk = { + init: jest.fn(), + on: jest.fn(), + uiResponse: jest.fn(), + dispose: jest.fn(), + switchTransport: jest.fn(), + evmGetPublicKey: jest.fn(), + getPassphraseState: jest.fn(), + evmSignTransaction: jest.fn(), + evmSignMessage: jest.fn(), + evmSignTypedData: jest.fn(), +}; + +const mockHardwareSDKLowLevel = {}; + +// Mock the dynamic import at module level +jest.mock('@onekeyfe/hd-web-sdk', () => ({ + HardwareWebSdk: mockHardwareWebSdk, + HardwareSDKLowLevel: mockHardwareSDKLowLevel, +})); + +describe('OneKeyWebBridge', function () { + let bridge: OneKeyWebBridge; + + beforeEach(function () { + bridge = new OneKeyWebBridge(); + jest.clearAllMocks(); + }); + + describe('init', function () { + it('should initialize SDK and set up event handlers', async function () { + mockHardwareWebSdk.init.mockResolvedValue(undefined); + + await bridge.init(); + + expect(mockHardwareWebSdk.init).toHaveBeenCalledTimes(1); + expect(mockHardwareWebSdk.init).toHaveBeenCalledWith( + { + debug: true, + fetchConfig: false, + connectSrc: 'https://jssdk.onekey.so/1.1.0/', + env: 'webusb', + }, + mockHardwareSDKLowLevel, + ); + expect(bridge.isSDKInitialized).toBe(true); + expect(bridge.sdk).toBe(mockHardwareWebSdk); + expect(mockHardwareWebSdk.on).toHaveBeenCalledWith( + 'UI_EVENT', + expect.any(Function), + ); + }); + + it('should not initialize again if already initialized', async function () { + bridge.isSDKInitialized = true; + + await bridge.init(); + + expect(mockHardwareWebSdk.init).not.toHaveBeenCalled(); + }); + + it('should handle initialization failure', async function () { + mockHardwareWebSdk.init.mockRejectedValue(new Error('Init failed')); + + await bridge.init(); + + expect(bridge.isSDKInitialized).toBe(false); + expect(bridge.sdk).toBeUndefined(); + }); + + it('should handle PIN request in UI event', async function () { + let uiEventCallback: any; + mockHardwareWebSdk.on.mockImplementation( + (event: string, callback: any) => { + if (event === 'UI_EVENT') { + uiEventCallback = callback; + } + }, + ); + mockHardwareWebSdk.init.mockResolvedValue(undefined); + + await bridge.init(); + + // Simulate PIN request + uiEventCallback({ type: UI_REQUEST.REQUEST_PIN }); + + expect(mockHardwareWebSdk.uiResponse).toHaveBeenCalledWith({ + type: UI_RESPONSE.RECEIVE_PIN, + payload: '@@ONEKEY_INPUT_PIN_IN_DEVICE', + }); + }); + + it('should handle passphrase request in UI event', async function () { + let uiEventCallback: any; + mockHardwareWebSdk.on.mockImplementation( + (event: string, callback: any) => { + if (event === 'UI_EVENT') { + uiEventCallback = callback; + } + }, + ); + mockHardwareWebSdk.init.mockResolvedValue(undefined); + + await bridge.init(); + + // Simulate passphrase request + uiEventCallback({ type: UI_REQUEST.REQUEST_PASSPHRASE }); + + expect(mockHardwareWebSdk.uiResponse).toHaveBeenCalledWith({ + type: UI_RESPONSE.RECEIVE_PASSPHRASE, + payload: { + value: '', + passphraseOnDevice: true, + save: false, + }, + }); + }); + }); + + describe('destroy', function () { + it('should destroy SDK', async function () { + bridge.sdk = mockHardwareWebSdk as any; + bridge.isSDKInitialized = true; + + await bridge.destroy(); + + expect(bridge.isSDKInitialized).toBe(false); + expect(bridge.sdk).toBeUndefined(); + }); + }); + + describe('dispose', function () { + it('should call dispose on SDK', async function () { + bridge.sdk = mockHardwareWebSdk as any; + + await bridge.dispose(); + + expect(mockHardwareWebSdk.dispose).toHaveBeenCalledTimes(1); + }); + + it('should handle dispose when SDK is not initialized', async function () { + bridge.sdk = undefined; + + // eslint-disable-next-line jest/no-restricted-matchers + await expect(bridge.dispose()).resolves.toBeUndefined(); + }); + }); + + describe('updateTransportMethod', function () { + it('should switch transport when SDK is initialized', async function () { + bridge.sdk = mockHardwareWebSdk as any; + + await bridge.updateTransportMethod('webusb'); + + expect(mockHardwareWebSdk.switchTransport).toHaveBeenCalledTimes(1); + expect(mockHardwareWebSdk.switchTransport).toHaveBeenCalledWith('webusb'); + }); + + it('should not switch transport when SDK is not initialized', async function () { + bridge.sdk = undefined; + + await bridge.updateTransportMethod('webusb'); + + expect(mockHardwareWebSdk.switchTransport).not.toHaveBeenCalled(); + }); + }); + + describe('getPublicKey', function () { + it('should call evmGetPublicKey', async function () { + const expectedResult = { + success: true, + payload: { + pub: '0x123', + // eslint-disable-next-line @typescript-eslint/naming-convention + node: { chain_code: 'abc123' }, + }, + }; + mockHardwareWebSdk.evmGetPublicKey.mockResolvedValue(expectedResult); + bridge.sdk = mockHardwareWebSdk as any; + + const params = { + path: "m/44'/60'/0'/0/0", + coin: 'eth', + }; + const result = await bridge.getPublicKey(params); + + expect(mockHardwareWebSdk.evmGetPublicKey).toHaveBeenCalledTimes(1); + expect(mockHardwareWebSdk.evmGetPublicKey).toHaveBeenCalledWith('', '', { + ...params, + skipPassphraseCheck: true, + }); + expect(result).toStrictEqual({ + success: true, + payload: { + publicKey: '0x123', + chainCode: 'abc123', + }, + }); + }); + + it('should handle public key error response', async function () { + const errorResult = { + success: false, + payload: { + error: 'Device not found', + code: 404, + }, + }; + mockHardwareWebSdk.evmGetPublicKey.mockResolvedValue(errorResult); + bridge.sdk = mockHardwareWebSdk as any; + const handleBlockErrorEventSpy = jest + .spyOn(bridge, 'handleBlockErrorEvent') + .mockImplementation(); + + const result = await bridge.getPublicKey({ + path: "m/44'/60'/0'/0/0", + coin: 'eth', + }); + + expect(result).toStrictEqual({ + success: false, + payload: { + error: 'Device not found', + code: 404, + }, + }); + expect(handleBlockErrorEventSpy).toHaveBeenCalledWith(errorResult); + }); + + it('should handle error without code', async function () { + const errorResult = { + success: false, + payload: { + error: 'Some error', + }, + }; + mockHardwareWebSdk.evmGetPublicKey.mockResolvedValue(errorResult); + bridge.sdk = mockHardwareWebSdk as any; + + const result = await bridge.getPublicKey({ + path: "m/44'/60'/0'/0/0", + coin: 'eth', + }); + + expect(result).toStrictEqual({ + success: false, + payload: { + error: 'Some error', + code: undefined, + }, + }); + }); + + it('should return error when SDK is not initialized', async function () { + bridge.sdk = undefined; + + const result = await bridge.getPublicKey({ + path: "m/44'/60'/0'/0/0", + coin: 'eth', + }); + + expect(result).toStrictEqual({ + success: false, + payload: { + error: 'SDK not initialized', + code: 800, + }, + }); + }); + }); + + describe('getPassphraseState', function () { + it('should call getPassphraseState', async function () { + const expectedResult = { + success: true, + payload: 'some-state', + }; + mockHardwareWebSdk.getPassphraseState.mockResolvedValue(expectedResult); + bridge.sdk = mockHardwareWebSdk as any; + + const result = await bridge.getPassphraseState(); + + expect(mockHardwareWebSdk.getPassphraseState).toHaveBeenCalledTimes(1); + expect(mockHardwareWebSdk.getPassphraseState).toHaveBeenCalledWith(''); + expect(result).toBe(expectedResult); + }); + + it('should handle passphrase state error response and call handleBlockErrorEvent', async function () { + const errorResult = { + success: false, + payload: { + error: 'Failed to get passphrase state', + }, + }; + mockHardwareWebSdk.getPassphraseState.mockResolvedValue(errorResult); + bridge.sdk = mockHardwareWebSdk as any; + const handleBlockErrorEventSpy = jest + .spyOn(bridge, 'handleBlockErrorEvent') + .mockImplementation(); + + const result = await bridge.getPassphraseState(); + + expect(result).toBe(errorResult); + expect(handleBlockErrorEventSpy).toHaveBeenCalledWith(errorResult); + }); + + it('should return error when SDK is not initialized', async function () { + bridge.sdk = undefined; + + const result = await bridge.getPassphraseState(); + + expect(result).toStrictEqual({ + success: false, + payload: { + error: 'SDK not initialized', + code: 800, + }, + }); + }); + }); + + describe('ethereumSignTransaction', function () { + it('should call evmSignTransaction', async function () { + const expectedResult = { + success: true, + payload: { signature: '0xsignature' }, + }; + mockHardwareWebSdk.evmSignTransaction.mockResolvedValue(expectedResult); + bridge.sdk = mockHardwareWebSdk as any; + + const params = { + path: "m/44'/60'/0'/0/0", + transaction: { + to: '0x123', + value: '0x0', + gasLimit: '0x5208', + gasPrice: '0x1', + nonce: '0x0', + data: '0x', + chainId: 1, + }, + }; + const result = await bridge.ethereumSignTransaction(params); + + expect(mockHardwareWebSdk.evmSignTransaction).toHaveBeenCalledTimes(1); + expect(mockHardwareWebSdk.evmSignTransaction).toHaveBeenCalledWith( + '', + '', + { + ...params, + skipPassphraseCheck: true, + }, + ); + expect(result).toBe(expectedResult); + }); + + it('should handle transaction signing error response and call handleBlockErrorEvent', async function () { + const errorResult = { + success: false, + payload: { + error: 'Transaction signing failed', + code: 500, + }, + }; + mockHardwareWebSdk.evmSignTransaction.mockResolvedValue(errorResult); + bridge.sdk = mockHardwareWebSdk as any; + const handleBlockErrorEventSpy = jest + .spyOn(bridge, 'handleBlockErrorEvent') + .mockImplementation(); + + const params = { + path: "m/44'/60'/0'/0/0", + transaction: { + to: '0x123', + value: '0x0', + gasLimit: '0x5208', + gasPrice: '0x1', + nonce: '0x0', + data: '0x', + chainId: 1, + }, + }; + const result = await bridge.ethereumSignTransaction(params); + + expect(result).toBe(errorResult); + expect(handleBlockErrorEventSpy).toHaveBeenCalledWith(errorResult); + }); + + it('should return error when SDK is not initialized', async function () { + bridge.sdk = undefined; + + const params = { + path: "m/44'/60'/0'/0/0", + transaction: { + to: '0x123', + value: '0x0', + gasLimit: '0x5208', + gasPrice: '0x1', + nonce: '0x0', + data: '0x', + chainId: 1, + }, + }; + const result = await bridge.ethereumSignTransaction(params); + + expect(result).toStrictEqual({ + success: false, + payload: { + error: 'SDK not initialized', + code: 800, + }, + }); + }); + }); + + describe('ethereumSignMessage', function () { + it('should call evmSignMessage', async function () { + const expectedResult = { + success: true, + payload: { signature: '0xsignature' }, + }; + mockHardwareWebSdk.evmSignMessage.mockResolvedValue(expectedResult); + bridge.sdk = mockHardwareWebSdk as any; + + const params = { + path: "m/44'/60'/0'/0/0", + messageHex: '48656c6c6f20576f726c64', + }; + const result = await bridge.ethereumSignMessage(params); + + expect(mockHardwareWebSdk.evmSignMessage).toHaveBeenCalledTimes(1); + expect(mockHardwareWebSdk.evmSignMessage).toHaveBeenCalledWith('', '', { + ...params, + skipPassphraseCheck: true, + }); + expect(result).toBe(expectedResult); + }); + + it('should handle message signing error response and call handleBlockErrorEvent', async function () { + const errorResult = { + success: false, + payload: { + error: 'Message signing failed', + code: 600, + }, + }; + mockHardwareWebSdk.evmSignMessage.mockResolvedValue(errorResult); + bridge.sdk = mockHardwareWebSdk as any; + const handleBlockErrorEventSpy = jest + .spyOn(bridge, 'handleBlockErrorEvent') + .mockImplementation(); + + const params = { + path: "m/44'/60'/0'/0/0", + messageHex: '48656c6c6f20576f726c64', + }; + const result = await bridge.ethereumSignMessage(params); + + expect(result).toBe(errorResult); + expect(handleBlockErrorEventSpy).toHaveBeenCalledWith(errorResult); + }); + + it('should return error when SDK is not initialized', async function () { + bridge.sdk = undefined; + + const params = { + path: "m/44'/60'/0'/0/0", + messageHex: '48656c6c6f20576f726c64', + }; + const result = await bridge.ethereumSignMessage(params); + + expect(result).toStrictEqual({ + success: false, + payload: { + error: 'SDK not initialized', + code: 800, + }, + }); + }); + }); + + describe('ethereumSignTypedData', function () { + it('should call evmSignTypedData', async function () { + const expectedResult = { + success: true, + payload: { signature: '0xsignature' }, + }; + mockHardwareWebSdk.evmSignTypedData.mockResolvedValue(expectedResult); + bridge.sdk = mockHardwareWebSdk as any; + + const params = { + path: "m/44'/60'/0'/0/0", + data: { + types: { + EIP712Domain: [{ name: 'name', type: 'string' }], + }, + primaryType: 'EIP712Domain', + domain: { name: 'Test' }, + message: {}, + }, + metamaskV4Compat: true, + }; + const result = await bridge.ethereumSignTypedData(params); + + expect(mockHardwareWebSdk.evmSignTypedData).toHaveBeenCalledTimes(1); + expect(mockHardwareWebSdk.evmSignTypedData).toHaveBeenCalledWith('', '', { + ...params, + skipPassphraseCheck: true, + }); + expect(result).toBe(expectedResult); + }); + + it('should handle typed data signing error response and call handleBlockErrorEvent', async function () { + const errorResult = { + success: false, + payload: { + error: 'Typed data signing failed', + code: 700, + }, + }; + mockHardwareWebSdk.evmSignTypedData.mockResolvedValue(errorResult); + bridge.sdk = mockHardwareWebSdk as any; + const handleBlockErrorEventSpy = jest + .spyOn(bridge, 'handleBlockErrorEvent') + .mockImplementation(); + + const params = { + path: "m/44'/60'/0'/0/0", + data: { + types: { + EIP712Domain: [{ name: 'name', type: 'string' }], + }, + primaryType: 'EIP712Domain', + domain: { name: 'Test' }, + message: {}, + }, + metamaskV4Compat: true, + }; + const result = await bridge.ethereumSignTypedData(params); + + expect(result).toBe(errorResult); + expect(handleBlockErrorEventSpy).toHaveBeenCalledWith(errorResult); + }); + + it('should return error when SDK is not initialized', async function () { + bridge.sdk = undefined; + + const params = { + path: "m/44'/60'/0'/0/0", + data: { + types: { + EIP712Domain: [{ name: 'name', type: 'string' }], + }, + primaryType: 'EIP712Domain', + domain: { name: 'Test' }, + message: {}, + }, + metamaskV4Compat: true, + }; + const result = await bridge.ethereumSignTypedData(params); + + expect(result).toStrictEqual({ + success: false, + payload: { + error: 'SDK not initialized', + code: 800, + }, + }); + }); + }); + + describe('event handling', function () { + it('should add and remove event listeners', function () { + const callback = jest.fn(); + + bridge.on('test-event', callback); + expect(bridge.eventListeners.has('test-event')).toBe(true); + expect(bridge.eventListeners.get('test-event')).toBe(callback); + + bridge.off('test-event'); + expect(bridge.eventListeners.has('test-event')).toBe(false); + }); + + it('should handle block error event with matching error codes', function () { + const callback = jest.fn(); + bridge.on(ONEKEY_HARDWARE_UI_EVENT, callback); + + const payload = { + success: false as const, + payload: { + error: 'Device not found', + code: HardwareErrorCode.WebDeviceNotFoundOrNeedsPermission, + }, + }; + + bridge.handleBlockErrorEvent(payload); + expect(callback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledWith(payload.payload); + }); + + it('should not handle block error event with non-matching error codes', function () { + const callback = jest.fn(); + bridge.on(ONEKEY_HARDWARE_UI_EVENT, callback); + + const payload = { + success: false as const, + payload: { + error: 'Some other error', + code: 999, + }, + }; + + bridge.handleBlockErrorEvent(payload); + expect(callback).not.toHaveBeenCalled(); + }); + }); + + describe('model management', function () { + it('should return model', function () { + bridge.model = 'OneKey Pro'; + expect(bridge.getModel()).toBe('OneKey Pro'); + }); + + it('should return undefined when model is not set', function () { + expect(bridge.getModel()).toBeUndefined(); + }); + }); +}); diff --git a/packages/keyring-eth-onekey/src/onekey-web-bridge.ts b/packages/keyring-eth-onekey/src/onekey-web-bridge.ts new file mode 100644 index 00000000..d5d3b37a --- /dev/null +++ b/packages/keyring-eth-onekey/src/onekey-web-bridge.ts @@ -0,0 +1,257 @@ +/* eslint-disable @typescript-eslint/no-redundant-type-constituents */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/consistent-type-imports */ +import { UI_REQUEST, UI_RESPONSE } from '@onekeyfe/hd-core'; +import type { + ConnectSettings, + CoreApi, + EVMSignedTx, + EVMSignMessageParams, + EVMSignTransactionParams, + EVMSignTypedDataParams, + Params, + UiEvent, + Unsuccessful, +} from '@onekeyfe/hd-core'; +import { HardwareErrorCode } from '@onekeyfe/hd-shared'; +import type { EthereumMessageSignature } from '@onekeyfe/hd-transport'; + +import { ONEKEY_HARDWARE_UI_EVENT } from './constants'; +import { OneKeyBridge } from './onekey-bridge'; + +export type OneKeyIframeBridgeOptions = { + bridgeUrl: string; +}; + +export class OneKeyWebBridge implements OneKeyBridge { + isSDKInitialized = false; + + sdk: CoreApi | undefined = undefined; + + eventListeners: Map void> = new Map(); + + model?: string | undefined; + + on(_event: string, callback: (event: any) => void): void { + this.eventListeners.set(_event, callback); + } + + off(_event: string): void { + this.eventListeners.delete(_event); + } + + handleBlockErrorEvent(payload: Unsuccessful): void { + const { code } = payload.payload; + const errorCodes: number[] = [ + HardwareErrorCode.WebDeviceNotFoundOrNeedsPermission, + HardwareErrorCode.BridgeNotInstalled, + HardwareErrorCode.NewFirmwareForceUpdate, + HardwareErrorCode.NotAllowInBootloaderMode, + HardwareErrorCode.CallMethodNeedUpgradeFirmware, + HardwareErrorCode.DeviceCheckPassphraseStateError, + HardwareErrorCode.DeviceCheckUnlockTypeError, + HardwareErrorCode.SelectDevice, + ]; + + if (code && typeof code === 'number' && errorCodes.includes(code)) { + this.eventListeners.get(ONEKEY_HARDWARE_UI_EVENT)?.(payload.payload); + } + } + + async updateTransportMethod( + transportType: ConnectSettings['env'], + ): Promise { + if (!this.sdk) { + return; + } + await this.sdk.switchTransport(transportType); + } + + async init(): Promise { + if (this.isSDKInitialized) { + return; + } + const sdkLib = await import('@onekeyfe/hd-web-sdk'); + const { HardwareWebSdk, HardwareSDKLowLevel } = sdkLib as any; + const settings: Partial = { + debug: true, + fetchConfig: false, + connectSrc: 'https://jssdk.onekey.so/1.1.0/', + env: 'webusb', + }; + try { + await HardwareWebSdk.init(settings, HardwareSDKLowLevel); + this.isSDKInitialized = true; + this.sdk = HardwareWebSdk as unknown as CoreApi; + + // eslint-disable-next-line id-length + this.sdk?.on('UI_EVENT', (e: any) => { + const originEvent = e as UiEvent; + if (originEvent.type === UI_REQUEST.REQUEST_PIN) { + this.sdk?.uiResponse({ + type: UI_RESPONSE.RECEIVE_PIN, + payload: '@@ONEKEY_INPUT_PIN_IN_DEVICE', + }); + } + if (originEvent.type === UI_REQUEST.REQUEST_PASSPHRASE) { + this.sdk?.uiResponse({ + type: UI_RESPONSE.RECEIVE_PASSPHRASE, + payload: { + value: '', + passphraseOnDevice: true, + save: false, + }, + }); + } + }); + } catch { + this.isSDKInitialized = false; + } + } + + async destroy(): Promise { + this.isSDKInitialized = false; + this.sdk = undefined; + } + + async dispose(): Promise { + this.sdk?.dispose(); + return Promise.resolve(); + } + + getModel(): string | undefined { + return this.model; + } + + async getPublicKey(params: { + path: string; + coin: string; + }): Promise< + | { success: false; payload: { error: string; code?: string | number } } + | { success: true; payload: { publicKey: string; chainCode: string } } + > { + if (!this.sdk) { + return { + success: false, + payload: { error: 'SDK not initialized', code: 800 }, + }; + } + return await this.sdk + .evmGetPublicKey('', '', { ...params, skipPassphraseCheck: true }) + .then((result) => { + if (result?.success) { + return { + success: true, + payload: { + publicKey: result.payload.pub, + chainCode: result.payload.node.chain_code, + }, + }; + } + this.handleBlockErrorEvent(result); + return { + success: false, + payload: { + error: result?.payload.error ?? '', + code: + typeof result?.payload?.code === 'number' + ? result?.payload?.code + : undefined, + }, + }; + }); + } + + async getPassphraseState(): Promise< + | { success: false; payload: { error: string; code?: string | number } } + | { success: true; payload: string | undefined } + > { + if (!this.sdk) { + return { + success: false, + payload: { error: 'SDK not initialized', code: 800 }, + }; + } + return await this.sdk.getPassphraseState('').then((result) => { + if (!result?.success) { + this.handleBlockErrorEvent(result); + } + return result; + }); + } + + async ethereumSignTransaction( + params: Params, + ): Promise< + | { success: false; payload: { error: string; code?: string | number } } + | { success: true; payload: EVMSignedTx } + > { + if (!this.sdk) { + return { + success: false, + payload: { error: 'SDK not initialized', code: 800 }, + }; + } + return await this.sdk + .evmSignTransaction('', '', { + ...params, + skipPassphraseCheck: true, + }) + .then((result) => { + if (!result?.success) { + this.handleBlockErrorEvent(result); + } + return result; + }); + } + + async ethereumSignMessage( + params: Params, + ): Promise< + | { success: false; payload: { error: string; code?: string | number } } + | { success: true; payload: EthereumMessageSignature } + > { + if (!this.sdk) { + return { + success: false, + payload: { error: 'SDK not initialized', code: 800 }, + }; + } + return await this.sdk + .evmSignMessage('', '', { + ...params, + skipPassphraseCheck: true, + }) + .then((result) => { + if (!result?.success) { + this.handleBlockErrorEvent(result); + } + return result; + }); + } + + async ethereumSignTypedData( + params: Params, + ): Promise< + | { success: false; payload: { error: string; code?: string | number } } + | { success: true; payload: EthereumMessageSignature } + > { + if (!this.sdk) { + return { + success: false, + payload: { error: 'SDK not initialized', code: 800 }, + }; + } + return await this.sdk + .evmSignTypedData('', '', { + ...params, + skipPassphraseCheck: true, + }) + .then((result) => { + if (!result?.success) { + this.handleBlockErrorEvent(result); + } + return result; + }); + } +} diff --git a/packages/keyring-eth-onekey/tsconfig.build.json b/packages/keyring-eth-onekey/tsconfig.build.json new file mode 100644 index 00000000..9bcd3d13 --- /dev/null +++ b/packages/keyring-eth-onekey/tsconfig.build.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "dist", + "rootDir": "src", + "exactOptionalPropertyTypes": false, + "skipLibCheck": true, + "lib": ["ES2020"], + "target": "es2017" + }, + "references": [{ "path": "../keyring-utils/tsconfig.build.json" }], + "include": ["./src/**/*.ts"], + "exclude": ["./src/**/*.test.ts"] +} diff --git a/packages/keyring-eth-onekey/tsconfig.json b/packages/keyring-eth-onekey/tsconfig.json new file mode 100644 index 00000000..5ad645d0 --- /dev/null +++ b/packages/keyring-eth-onekey/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./", + "exactOptionalPropertyTypes": false, + "skipLibCheck": true, + "lib": ["ES2020"], + "target": "es2017" + }, + "references": [{ "path": "../keyring-utils" }], + "include": ["./src"], + "exclude": ["./dist/**/*"] +} diff --git a/packages/keyring-eth-onekey/typedoc.json b/packages/keyring-eth-onekey/typedoc.json new file mode 100644 index 00000000..b527b625 --- /dev/null +++ b/packages/keyring-eth-onekey/typedoc.json @@ -0,0 +1,6 @@ +{ + "entryPoints": ["./src/index.ts"], + "excludePrivate": true, + "hideGenerator": true, + "out": "docs" +} diff --git a/tsconfig.build.json b/tsconfig.build.json index 60377db1..95d164ed 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -4,6 +4,7 @@ { "path": "./packages/keyring-internal-api/tsconfig.build.json" }, { "path": "./packages/keyring-eth-ledger-bridge/tsconfig.build.json" }, { "path": "./packages/keyring-eth-qr/tsconfig.build.json" }, + { "path": "./packages/keyring-eth-onekey/tsconfig.build.json" }, { "path": "./packages/keyring-eth-simple/tsconfig.build.json" }, { "path": "./packages/keyring-eth-trezor/tsconfig.build.json" }, { "path": "./packages/keyring-eth-hd/tsconfig.build.json" }, diff --git a/yarn.lock b/yarn.lock index 773e6162..1dad96d3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1742,6 +1742,45 @@ __metadata: languageName: unknown linkType: soft +"@metamask/eth-onekey-keyring@workspace:packages/keyring-eth-onekey": + version: 0.0.0-use.local + resolution: "@metamask/eth-onekey-keyring@workspace:packages/keyring-eth-onekey" + dependencies: + "@ethereumjs/common": "npm:^4.4.0" + "@ethereumjs/tx": "npm:^5.4.0" + "@ethereumjs/util": "npm:^9.1.0" + "@lavamoat/allow-scripts": "npm:^3.2.1" + "@lavamoat/preinstall-always-fail": "npm:^2.1.0" + "@metamask/auto-changelog": "npm:^3.4.4" + "@metamask/eth-sig-util": "npm:^8.2.0" + "@noble/hashes": "npm:^1.7.0" + "@onekeyfe/hd-core": "npm:1.1.6-patch.4" + "@onekeyfe/hd-shared": "npm:1.1.6-patch.4" + "@onekeyfe/hd-transport": "npm:1.1.6-patch.4" + "@onekeyfe/hd-web-sdk": "npm:1.1.6-patch.4" + "@ts-bridge/cli": "npm:^0.6.3" + "@types/bytebuffer": "npm:^5.0.49" + "@types/ethereumjs-tx": "npm:^1.0.1" + "@types/hdkey": "npm:^2.0.1" + "@types/jest": "npm:^29.5.12" + "@types/node": "npm:^20.12.12" + "@types/sinon": "npm:^17.0.3" + "@types/w3c-web-usb": "npm:^1.0.6" + deepmerge: "npm:^4.2.2" + depcheck: "npm:^1.4.7" + ethereumjs-tx: "npm:^1.3.7" + hdkey: "npm:^2.1.0" + jest: "npm:^29.5.0" + jest-environment-jsdom: "npm:^29.7.0" + jest-it-up: "npm:^3.1.0" + sinon: "npm:^19.0.2" + ts-jest: "npm:^29.0.5" + ts-node: "npm:^10.9.2" + typedoc: "npm:^0.25.13" + typescript: "npm:~5.6.3" + languageName: unknown + linkType: soft + "@metamask/eth-qr-keyring@workspace:packages/keyring-eth-qr": version: 0.0.0-use.local resolution: "@metamask/eth-qr-keyring@workspace:packages/keyring-eth-qr" @@ -2511,7 +2550,7 @@ __metadata: languageName: node linkType: hard -"@noble/hashes@npm:1.8.0, @noble/hashes@npm:^1.0.0, @noble/hashes@npm:^1.1.2, @noble/hashes@npm:^1.2.0, @noble/hashes@npm:^1.3.1, @noble/hashes@npm:^1.3.2, @noble/hashes@npm:^1.4.0, @noble/hashes@npm:^1.6.1, @noble/hashes@npm:^1.7.1, @noble/hashes@npm:~1.8.0": +"@noble/hashes@npm:1.8.0, @noble/hashes@npm:^1.0.0, @noble/hashes@npm:^1.1.2, @noble/hashes@npm:^1.2.0, @noble/hashes@npm:^1.3.1, @noble/hashes@npm:^1.3.2, @noble/hashes@npm:^1.4.0, @noble/hashes@npm:^1.6.1, @noble/hashes@npm:^1.7.0, @noble/hashes@npm:^1.7.1, @noble/hashes@npm:~1.8.0": version: 1.8.0 resolution: "@noble/hashes@npm:1.8.0" checksum: 10/474b7f56bc6fb2d5b3a42132561e221b0ea4f91e590f4655312ca13667840896b34195e2b53b7f097ec080a1fdd3b58d902c2a8d0fbdf51d2e238b53808a177e @@ -2656,6 +2695,114 @@ __metadata: languageName: node linkType: hard +"@onekeyfe/cross-inpage-provider-core@npm:^0.0.17": + version: 0.0.17 + resolution: "@onekeyfe/cross-inpage-provider-core@npm:0.0.17" + dependencies: + "@onekeyfe/cross-inpage-provider-errors": "npm:^0.0.17" + "@onekeyfe/cross-inpage-provider-events": "npm:^0.0.17" + "@onekeyfe/cross-inpage-provider-types": "npm:^0.0.17" + events: "npm:^3.3.0" + lodash: "npm:^4.17.21" + ms: "npm:^2.1.3" + checksum: 10/655305db565093b245d42c65f02f951cc4f064469aa201e9acea6c4b9e14e5acc2944815134b18773865ef05afff9bb78face94e6f730ef9e9dd5b0ccd0d72b4 + languageName: node + linkType: hard + +"@onekeyfe/cross-inpage-provider-errors@npm:^0.0.17": + version: 0.0.17 + resolution: "@onekeyfe/cross-inpage-provider-errors@npm:0.0.17" + dependencies: + fast-safe-stringify: "npm:^2.1.1" + checksum: 10/f9a37acaaff97581d5344651b3d72c91cdf537d88c452564db962b8cc214c32c97246de9d5adeeef225ef1ccc5d804545e84755d8c9a95e63af2fc94a90dd4fb + languageName: node + linkType: hard + +"@onekeyfe/cross-inpage-provider-events@npm:^0.0.17": + version: 0.0.17 + resolution: "@onekeyfe/cross-inpage-provider-events@npm:0.0.17" + checksum: 10/f98304e1d98c1b3fc9b2952056019dcd2123de8bf555d9039d1d93a953ceb2937a97a91c1061bd4b971d6efa3a017ed9c68915a3454a7c0b8f9bdcefa0d11d84 + languageName: node + linkType: hard + +"@onekeyfe/cross-inpage-provider-types@npm:^0.0.17": + version: 0.0.17 + resolution: "@onekeyfe/cross-inpage-provider-types@npm:0.0.17" + checksum: 10/4dbf5bc6b4467a8324f2e438757fccd934573e081d88532a75d1588b32120bde3c9f39229ac6678fcc25d4970622fbec4e128f01550b4f6925b798c93ad642a5 + languageName: node + linkType: hard + +"@onekeyfe/hd-core@npm:1.1.6-patch.4": + version: 1.1.6-patch.4 + resolution: "@onekeyfe/hd-core@npm:1.1.6-patch.4" + dependencies: + "@onekeyfe/hd-shared": "npm:1.1.6-patch.4" + "@onekeyfe/hd-transport": "npm:1.1.6-patch.4" + axios: "npm:^0.30.1" + bignumber.js: "npm:^9.0.2" + bytebuffer: "npm:^5.0.1" + jszip: "npm:^3.10.1" + parse-uri: "npm:^1.0.7" + semver: "npm:^7.3.7" + peerDependencies: + "@noble/hashes": ^1.1.3 + checksum: 10/ce1f551deb0c4ac87ced4b994d3e2b7da3c2bd7be7a919f40fa9e435085d1606757830047fa5738c01d2a7034c814cc0c466e53bcfe5c2107ad0bf538e12f89a + languageName: node + linkType: hard + +"@onekeyfe/hd-shared@npm:1.1.6-patch.4": + version: 1.1.6-patch.4 + resolution: "@onekeyfe/hd-shared@npm:1.1.6-patch.4" + checksum: 10/458aa1305ce98ed1229cb04f0372b69a5811cae2b19e768864ba5161a5e832b9da7023bd5ae9050a9fdbe7416fffab016c160bee3fba8e05b9f175a362a0a072 + languageName: node + linkType: hard + +"@onekeyfe/hd-transport-http@npm:1.1.6-patch.4": + version: 1.1.6-patch.4 + resolution: "@onekeyfe/hd-transport-http@npm:1.1.6-patch.4" + dependencies: + "@onekeyfe/hd-shared": "npm:1.1.6-patch.4" + "@onekeyfe/hd-transport": "npm:1.1.6-patch.4" + axios: "npm:^0.30.1" + secure-json-parse: "npm:^4.0.0" + checksum: 10/b29fb4cbd2138dd40ff9c38c9928e5a87ca35b2648c2252afbdf61a681f12d06e846d2d6eb743dc5c7b42945e3c0445b35eeeacddf42bde448414a298d0a1672 + languageName: node + linkType: hard + +"@onekeyfe/hd-transport-web-device@npm:1.1.6-patch.4": + version: 1.1.6-patch.4 + resolution: "@onekeyfe/hd-transport-web-device@npm:1.1.6-patch.4" + dependencies: + "@onekeyfe/hd-shared": "npm:1.1.6-patch.4" + "@onekeyfe/hd-transport": "npm:1.1.6-patch.4" + checksum: 10/34848a6f0716cd8f8d89e6b0eb11ecb739bda52e30fa0d2bacfa92480271a18ca28559f0d88eeff74d27efc8c7c4de05000c5654fa22247b2f9a8ba5dca43f3f + languageName: node + linkType: hard + +"@onekeyfe/hd-transport@npm:1.1.6-patch.4": + version: 1.1.6-patch.4 + resolution: "@onekeyfe/hd-transport@npm:1.1.6-patch.4" + dependencies: + bytebuffer: "npm:^5.0.1" + long: "npm:^4.0.0" + protobufjs: "npm:^6.11.2" + checksum: 10/285e8a1abf2663bf3914b14f59a7273116432704688115b5de70218a08e8bc06b9579558bfcbf966bd225c7960babe764d32a21dd4bc8b6d0fbb03cfd7532fa1 + languageName: node + linkType: hard + +"@onekeyfe/hd-web-sdk@npm:1.1.6-patch.4": + version: 1.1.6-patch.4 + resolution: "@onekeyfe/hd-web-sdk@npm:1.1.6-patch.4" + dependencies: + "@onekeyfe/cross-inpage-provider-core": "npm:^0.0.17" + "@onekeyfe/hd-core": "npm:1.1.6-patch.4" + "@onekeyfe/hd-shared": "npm:1.1.6-patch.4" + "@onekeyfe/hd-transport-http": "npm:1.1.6-patch.4" + "@onekeyfe/hd-transport-web-device": "npm:1.1.6-patch.4" + checksum: 10/b8ab1e72b789dc54f7e529fd98bbed8c24c135779d04e451dea21769ce0ad28b36fe055b13923e9bae14f537cac668b32e2e726d244e379035cbf77173d2a374 + languageName: node + linkType: hard + "@pkgjs/parseargs@npm:^0.11.0": version: 0.11.0 resolution: "@pkgjs/parseargs@npm:0.11.0" @@ -3908,6 +4055,16 @@ __metadata: languageName: node linkType: hard +"@types/bytebuffer@npm:^5.0.49": + version: 5.0.49 + resolution: "@types/bytebuffer@npm:5.0.49" + dependencies: + "@types/long": "npm:^3.0.0" + "@types/node": "npm:*" + checksum: 10/31eb2521d2710f256c3d17a3e8d87f04394f335b29f7276c31c054ddbf4795146f2663effa3b6e910442da69238e994d2db9f7d5918eead4313e3f9e29165932 + languageName: node + linkType: hard + "@types/color-name@npm:^1.1.1": version: 1.1.1 resolution: "@types/color-name@npm:1.1.1" @@ -4029,6 +4186,20 @@ __metadata: languageName: node linkType: hard +"@types/long@npm:^3.0.0": + version: 3.0.32 + resolution: "@types/long@npm:3.0.32" + checksum: 10/cc5422875a085b49b74ffeb5c60a8681d30f700859a8931012b4a58c5c6005cdacb4d3ce3e5af7a7f579ee20d5c2e442a773a83b3a4f7a2d39795a7a8e9a962d + languageName: node + linkType: hard + +"@types/long@npm:^4.0.1": + version: 4.0.2 + resolution: "@types/long@npm:4.0.2" + checksum: 10/68afa05fb20949d88345876148a76f6ccff5433310e720db51ac5ca21cb8cc6714286dbe04713840ddbd25a8b56b7a23aa87d08472fabf06463a6f2ed4967707 + languageName: node + linkType: hard + "@types/minimatch@npm:^3.0.3": version: 3.0.5 resolution: "@types/minimatch@npm:3.0.5" @@ -4868,6 +5039,17 @@ __metadata: languageName: node linkType: hard +"axios@npm:^0.30.1": + version: 0.30.1 + resolution: "axios@npm:0.30.1" + dependencies: + follow-redirects: "npm:^1.15.4" + form-data: "npm:^4.0.4" + proxy-from-env: "npm:^1.1.0" + checksum: 10/ec5fcbb1cd7827e62028772f421ba558316ae0c2e1bef16823c79ce8f6aa7a65c60671c8b3f505c930abd3f7ca4c55604e86f21925887b762b71a934f0df58a1 + languageName: node + linkType: hard + "axios@npm:^1.8.4": version: 1.10.0 resolution: "axios@npm:1.10.0" @@ -5100,10 +5282,10 @@ __metadata: languageName: node linkType: hard -"bignumber.js@npm:^9.0.0, bignumber.js@npm:^9.0.1, bignumber.js@npm:^9.1.2, bignumber.js@npm:^9.3.0": - version: 9.3.0 - resolution: "bignumber.js@npm:9.3.0" - checksum: 10/60b79efcf7b56b925fca8eebd10d1f4b70aa2bf6eade7f5af0266f0092226dd2abcd9a3ee315ecb39459750d5a630ce3980b707e5d7bea32c97ffd378e8cc159 +"bignumber.js@npm:^9.0.0, bignumber.js@npm:^9.0.1, bignumber.js@npm:^9.0.2, bignumber.js@npm:^9.1.2, bignumber.js@npm:^9.3.0": + version: 9.3.1 + resolution: "bignumber.js@npm:9.3.1" + checksum: 10/1be0372bf0d6d29d0a49b9e6a9cefbd54dad9918232ad21fcd4ec39030260773abf0c76af960c6b3b98d3115a3a71e61c6a111812d1395040a039cfa178e0245 languageName: node linkType: hard @@ -5363,6 +5545,15 @@ __metadata: languageName: node linkType: hard +"bytebuffer@npm:^5.0.1": + version: 5.0.1 + resolution: "bytebuffer@npm:5.0.1" + dependencies: + long: "npm:~3" + checksum: 10/f3e9739ed9ab30e19d985fc3dadfdbd631d030874bbb313feefddac756f21ac10957257737e630fd9959744318e6e8b7d8c35b797519693bf1897be16c560970 + languageName: node + linkType: hard + "cacache@npm:^16.1.0": version: 16.1.1 resolution: "cacache@npm:16.1.1" @@ -5409,6 +5600,16 @@ __metadata: languageName: node linkType: hard +"call-bind-apply-helpers@npm:^1.0.1, call-bind-apply-helpers@npm:^1.0.2": + version: 1.0.2 + resolution: "call-bind-apply-helpers@npm:1.0.2" + dependencies: + es-errors: "npm:^1.3.0" + function-bind: "npm:^1.1.2" + checksum: 10/00482c1f6aa7cfb30fb1dbeb13873edf81cfac7c29ed67a5957d60635a56b2a4a480f1016ddbdb3395cc37900d46037fb965043a51c5c789ffeab4fc535d18b5 + languageName: node + linkType: hard + "call-bind@npm:^1.0.0, call-bind@npm:^1.0.2, call-bind@npm:^1.0.7": version: 1.0.7 resolution: "call-bind@npm:1.0.7" @@ -5739,6 +5940,13 @@ __metadata: languageName: node linkType: hard +"core-util-is@npm:~1.0.0": + version: 1.0.3 + resolution: "core-util-is@npm:1.0.3" + checksum: 10/9de8597363a8e9b9952491ebe18167e3b36e7707569eed0ebf14f8bba773611376466ae34575bca8cfe3c767890c859c74056084738f09d4e4a6f902b2ad7d99 + languageName: node + linkType: hard + "cosmiconfig@npm:9.0.0": version: 9.0.0 resolution: "cosmiconfig@npm:9.0.0" @@ -6159,6 +6367,17 @@ __metadata: languageName: node linkType: hard +"dunder-proto@npm:^1.0.1": + version: 1.0.1 + resolution: "dunder-proto@npm:1.0.1" + dependencies: + call-bind-apply-helpers: "npm:^1.0.1" + es-errors: "npm:^1.3.0" + gopd: "npm:^1.2.0" + checksum: 10/5add88a3d68d42d6e6130a0cac450b7c2edbe73364bbd2fc334564418569bea97c6943a8fcd70e27130bf32afc236f30982fc4905039b703f23e9e0433c29934 + languageName: node + linkType: hard + "eastasianwidth@npm:^0.2.0": version: 0.2.0 resolution: "eastasianwidth@npm:0.2.0" @@ -6292,12 +6511,10 @@ __metadata: languageName: node linkType: hard -"es-define-property@npm:^1.0.0": - version: 1.0.0 - resolution: "es-define-property@npm:1.0.0" - dependencies: - get-intrinsic: "npm:^1.2.4" - checksum: 10/f66ece0a887b6dca71848fa71f70461357c0e4e7249696f81bad0a1f347eed7b31262af4a29f5d726dc026426f085483b6b90301855e647aa8e21936f07293c6 +"es-define-property@npm:^1.0.0, es-define-property@npm:^1.0.1": + version: 1.0.1 + resolution: "es-define-property@npm:1.0.1" + checksum: 10/f8dc9e660d90919f11084db0a893128f3592b781ce967e4fccfb8f3106cb83e400a4032c559184ec52ee1dbd4b01e7776c7cd0b3327b1961b1a4a7008920fe78 languageName: node linkType: hard @@ -6308,6 +6525,27 @@ __metadata: languageName: node linkType: hard +"es-object-atoms@npm:^1.0.0, es-object-atoms@npm:^1.1.1": + version: 1.1.1 + resolution: "es-object-atoms@npm:1.1.1" + dependencies: + es-errors: "npm:^1.3.0" + checksum: 10/54fe77de288451dae51c37bfbfe3ec86732dc3778f98f3eb3bdb4bf48063b2c0b8f9c93542656986149d08aa5be3204286e2276053d19582b76753f1a2728867 + languageName: node + linkType: hard + +"es-set-tostringtag@npm:^2.1.0": + version: 2.1.0 + resolution: "es-set-tostringtag@npm:2.1.0" + dependencies: + es-errors: "npm:^1.3.0" + get-intrinsic: "npm:^1.2.6" + has-tostringtag: "npm:^1.0.2" + hasown: "npm:^2.0.2" + checksum: 10/86814bf8afbcd8966653f731415888019d4bc4aca6b6c354132a7a75bb87566751e320369654a101d23a91c87a85c79b178bcf40332839bd347aff437c4fb65f + languageName: node + linkType: hard + "escalade@npm:^3.1.1, escalade@npm:^3.1.2": version: 3.1.2 resolution: "escalade@npm:3.1.2" @@ -7049,7 +7287,7 @@ __metadata: languageName: node linkType: hard -"fast-safe-stringify@npm:^2.0.6": +"fast-safe-stringify@npm:^2.0.6, fast-safe-stringify@npm:^2.1.1": version: 2.1.1 resolution: "fast-safe-stringify@npm:2.1.1" checksum: 10/dc1f063c2c6ac9533aee14d406441f86783a8984b2ca09b19c2fe281f9ff59d315298bc7bc22fd1f83d26fe19ef2f20e2ddb68e96b15040292e555c5ced0c1e4 @@ -7184,13 +7422,13 @@ __metadata: languageName: node linkType: hard -"follow-redirects@npm:^1.15.6": - version: 1.15.9 - resolution: "follow-redirects@npm:1.15.9" +"follow-redirects@npm:^1.15.4, follow-redirects@npm:^1.15.6": + version: 1.15.11 + resolution: "follow-redirects@npm:1.15.11" peerDependenciesMeta: debug: optional: true - checksum: 10/e3ab42d1097e90d28b913903841e6779eb969b62a64706a3eb983e894a5db000fbd89296f45f08885a0e54cd558ef62e81be1165da9be25a6c44920da10f424c + checksum: 10/07372fd74b98c78cf4d417d68d41fdaa0be4dcacafffb9e67b1e3cf090bc4771515e65020651528faab238f10f9b9c0d9707d6c1574a6c0387c5de1042cde9ba languageName: node linkType: hard @@ -7213,14 +7451,16 @@ __metadata: languageName: node linkType: hard -"form-data@npm:^4.0.0": - version: 4.0.0 - resolution: "form-data@npm:4.0.0" +"form-data@npm:^4.0.0, form-data@npm:^4.0.4": + version: 4.0.4 + resolution: "form-data@npm:4.0.4" dependencies: asynckit: "npm:^0.4.0" combined-stream: "npm:^1.0.8" + es-set-tostringtag: "npm:^2.1.0" + hasown: "npm:^2.0.2" mime-types: "npm:^2.1.12" - checksum: 10/7264aa760a8cf09482816d8300f1b6e2423de1b02bba612a136857413fdc96d7178298ced106817655facc6b89036c6e12ae31c9eb5bdc16aabf502ae8a5d805 + checksum: 10/a4b62e21932f48702bc468cc26fb276d186e6b07b557e3dd7cc455872bdbb82db7db066844a64ad3cf40eaf3a753c830538183570462d3649fdfd705601cbcfb languageName: node linkType: hard @@ -7312,16 +7552,21 @@ __metadata: languageName: node linkType: hard -"get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.4": - version: 1.2.4 - resolution: "get-intrinsic@npm:1.2.4" +"get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.6": + version: 1.3.0 + resolution: "get-intrinsic@npm:1.3.0" dependencies: + call-bind-apply-helpers: "npm:^1.0.2" + es-define-property: "npm:^1.0.1" es-errors: "npm:^1.3.0" + es-object-atoms: "npm:^1.1.1" function-bind: "npm:^1.1.2" - has-proto: "npm:^1.0.1" - has-symbols: "npm:^1.0.3" - hasown: "npm:^2.0.0" - checksum: 10/85bbf4b234c3940edf8a41f4ecbd4e25ce78e5e6ad4e24ca2f77037d983b9ef943fd72f00f3ee97a49ec622a506b67db49c36246150377efcda1c9eb03e5f06d + get-proto: "npm:^1.0.1" + gopd: "npm:^1.2.0" + has-symbols: "npm:^1.1.0" + hasown: "npm:^2.0.2" + math-intrinsics: "npm:^1.1.0" + checksum: 10/6e9dd920ff054147b6f44cb98104330e87caafae051b6d37b13384a45ba15e71af33c3baeac7cb630a0aaa23142718dcf25b45cfdd86c184c5dcb4e56d953a10 languageName: node linkType: hard @@ -7339,6 +7584,16 @@ __metadata: languageName: node linkType: hard +"get-proto@npm:^1.0.1": + version: 1.0.1 + resolution: "get-proto@npm:1.0.1" + dependencies: + dunder-proto: "npm:^1.0.1" + es-object-atoms: "npm:^1.0.0" + checksum: 10/4fc96afdb58ced9a67558698b91433e6b037aaa6f1493af77498d7c85b141382cf223c0e5946f334fb328ee85dfe6edd06d218eaf09556f4bc4ec6005d7f5f7b + languageName: node + linkType: hard + "get-stdin@npm:^9.0.0": version: 9.0.0 resolution: "get-stdin@npm:9.0.0" @@ -7518,12 +7773,10 @@ __metadata: languageName: node linkType: hard -"gopd@npm:^1.0.1": - version: 1.0.1 - resolution: "gopd@npm:1.0.1" - dependencies: - get-intrinsic: "npm:^1.1.3" - checksum: 10/5fbc7ad57b368ae4cd2f41214bd947b045c1a4be2f194a7be1778d71f8af9dbf4004221f3b6f23e30820eb0d052b4f819fe6ebe8221e2a3c6f0ee4ef173421ca +"gopd@npm:^1.0.1, gopd@npm:^1.2.0": + version: 1.2.0 + resolution: "gopd@npm:1.2.0" + checksum: 10/94e296d69f92dc1c0768fcfeecfb3855582ab59a7c75e969d5f96ce50c3d201fd86d5a2857c22565764d5bb8a816c7b1e58f133ec318cd56274da36c5e3fb1a1 languageName: node linkType: hard @@ -7564,17 +7817,10 @@ __metadata: languageName: node linkType: hard -"has-proto@npm:^1.0.1": - version: 1.0.1 - resolution: "has-proto@npm:1.0.1" - checksum: 10/eab2ab0ed1eae6d058b9bbc4c1d99d2751b29717be80d02fd03ead8b62675488de0c7359bc1fdd4b87ef6fd11e796a9631ad4d7452d9324fdada70158c2e5be7 - languageName: node - linkType: hard - -"has-symbols@npm:^1.0.3": - version: 1.0.3 - resolution: "has-symbols@npm:1.0.3" - checksum: 10/464f97a8202a7690dadd026e6d73b1ceeddd60fe6acfd06151106f050303eaa75855aaa94969df8015c11ff7c505f196114d22f7386b4a471038da5874cf5e9b +"has-symbols@npm:^1.0.3, has-symbols@npm:^1.1.0": + version: 1.1.0 + resolution: "has-symbols@npm:1.1.0" + checksum: 10/959385c98696ebbca51e7534e0dc723ada325efa3475350951363cce216d27373e0259b63edb599f72eb94d6cde8577b4b2375f080b303947e560f85692834fa languageName: node linkType: hard @@ -7615,7 +7861,7 @@ __metadata: languageName: node linkType: hard -"hasown@npm:^2.0.0, hasown@npm:^2.0.2": +"hasown@npm:^2.0.2": version: 2.0.2 resolution: "hasown@npm:2.0.2" dependencies: @@ -7800,6 +8046,13 @@ __metadata: languageName: node linkType: hard +"immediate@npm:~3.0.5": + version: 3.0.6 + resolution: "immediate@npm:3.0.6" + checksum: 10/f9b3486477555997657f70318cc8d3416159f208bec4cca3ff3442fd266bc23f50f0c9bd8547e1371a6b5e82b821ec9a7044a4f7b944798b25aa3cc6d5e63e62 + languageName: node + linkType: hard + "immer@npm:^9.0.6": version: 9.0.21 resolution: "immer@npm:9.0.21" @@ -7860,7 +8113,7 @@ __metadata: languageName: node linkType: hard -"inherits@npm:2, inherits@npm:^2.0.1, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.4": +"inherits@npm:2, inherits@npm:^2.0.1, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.3, inherits@npm:~2.0.4": version: 2.0.4 resolution: "inherits@npm:2.0.4" checksum: 10/cd45e923bee15186c07fa4c89db0aace24824c482fb887b528304694b2aa6ff8a898da8657046a5dcf3e46cd6db6c61629551f9215f208d7c3f157cf9b290521 @@ -8128,6 +8381,13 @@ __metadata: languageName: node linkType: hard +"isarray@npm:~1.0.0": + version: 1.0.0 + resolution: "isarray@npm:1.0.0" + checksum: 10/f032df8e02dce8ec565cf2eb605ea939bdccea528dbcf565cdf92bfa2da9110461159d86a537388ef1acef8815a330642d7885b29010e8f7eac967c9993b65ab + languageName: node + linkType: hard + "isexe@npm:^2.0.0": version: 2.0.0 resolution: "isexe@npm:2.0.0" @@ -8895,6 +9155,18 @@ __metadata: languageName: node linkType: hard +"jszip@npm:^3.10.1": + version: 3.10.1 + resolution: "jszip@npm:3.10.1" + dependencies: + lie: "npm:~3.3.0" + pako: "npm:~1.0.2" + readable-stream: "npm:~2.3.6" + setimmediate: "npm:^1.0.5" + checksum: 10/bfbfbb9b0a27121330ac46ab9cdb3b4812433faa9ba4a54742c87ca441e31a6194ff70ae12acefa5fe25406c432290e68003900541d948a169b23d30c34dd984 + languageName: node + linkType: hard + "just-extend@npm:^6.2.0": version: 6.2.0 resolution: "just-extend@npm:6.2.0" @@ -8945,6 +9217,15 @@ __metadata: languageName: node linkType: hard +"lie@npm:~3.3.0": + version: 3.3.0 + resolution: "lie@npm:3.3.0" + dependencies: + immediate: "npm:~3.0.5" + checksum: 10/f335ce67fe221af496185d7ce39c8321304adb701e122942c495f4f72dcee8803f9315ee572f5f8e8b08b9e8d7195da91b9fad776e8864746ba8b5e910adf76e + languageName: node + linkType: hard + "lines-and-columns@npm:^1.1.6": version: 1.1.6 resolution: "lines-and-columns@npm:1.1.6" @@ -9025,6 +9306,20 @@ __metadata: languageName: node linkType: hard +"long@npm:^4.0.0": + version: 4.0.0 + resolution: "long@npm:4.0.0" + checksum: 10/8296e2ba7bab30f9cfabb81ebccff89c819af6a7a78b4bb5a70ea411aa764ee0532f7441381549dfa6a1a98d72abe9138bfcf99f4fa41238629849bc035b845b + languageName: node + linkType: hard + +"long@npm:~3": + version: 3.2.0 + resolution: "long@npm:3.2.0" + checksum: 10/ffc685ec458ddf71a830d6deb62ff7dc551a736d47473350d9e077c22db96ec88c8a3554c11ffce7d7f2291b0c30da36629e4d0a97c29b5360dc977533c96d28 + languageName: node + linkType: hard + "loose-envify@npm:^1.1.0": version: 1.4.0 resolution: "loose-envify@npm:1.4.0" @@ -9192,6 +9487,13 @@ __metadata: languageName: node linkType: hard +"math-intrinsics@npm:^1.1.0": + version: 1.1.0 + resolution: "math-intrinsics@npm:1.1.0" + checksum: 10/11df2eda46d092a6035479632e1ec865b8134bdfc4bd9e571a656f4191525404f13a283a515938c3a8de934dbfd9c09674d9da9fa831e6eb7e22b50b197d2edd + languageName: node + linkType: hard + "md5.js@npm:^1.3.4": version: 1.3.5 resolution: "md5.js@npm:1.3.5" @@ -9476,7 +9778,7 @@ __metadata: languageName: node linkType: hard -"ms@npm:^2.0.0, ms@npm:^2.1.1": +"ms@npm:^2.0.0, ms@npm:^2.1.1, ms@npm:^2.1.3": version: 2.1.3 resolution: "ms@npm:2.1.3" checksum: 10/aa92de608021b242401676e35cfa5aa42dd70cbdc082b916da7fb925c542173e36bce97ea3e804923fe92c0ad991434e4a38327e15a1b5b5f945d66df615ae6d @@ -9947,6 +10249,13 @@ __metadata: languageName: node linkType: hard +"pako@npm:~1.0.2": + version: 1.0.11 + resolution: "pako@npm:1.0.11" + checksum: 10/1ad07210e894472685564c4d39a08717e84c2a68a70d3c1d9e657d32394ef1670e22972a433cbfe48976cb98b154ba06855dcd3fcfba77f60f1777634bec48c0 + languageName: node + linkType: hard + "parent-module@npm:^1.0.0": version: 1.0.1 resolution: "parent-module@npm:1.0.1" @@ -9975,6 +10284,13 @@ __metadata: languageName: node linkType: hard +"parse-uri@npm:^1.0.7": + version: 1.0.16 + resolution: "parse-uri@npm:1.0.16" + checksum: 10/5fd915fefd81bda753e7dbfdc887a5f8c88b6e4d1a23a4ac4f447f37cbff7fcdcabef047da56ad099c5418087ab7adb4df2f960f12d802467356ee136791bdae + languageName: node + linkType: hard + "parse5@npm:^7.0.0, parse5@npm:^7.1.1": version: 7.1.2 resolution: "parse5@npm:7.1.2" @@ -10194,6 +10510,13 @@ __metadata: languageName: node linkType: hard +"process-nextick-args@npm:~2.0.0": + version: 2.0.1 + resolution: "process-nextick-args@npm:2.0.1" + checksum: 10/1d38588e520dab7cea67cbbe2efdd86a10cc7a074c09657635e34f035277b59fbb57d09d8638346bf7090f8e8ebc070c96fa5fd183b777fff4f5edff5e9466cf + languageName: node + linkType: hard + "process@npm:^0.11.10": version: 0.11.10 resolution: "process@npm:0.11.10" @@ -10248,6 +10571,30 @@ __metadata: languageName: node linkType: hard +"protobufjs@npm:^6.11.2": + version: 6.11.4 + resolution: "protobufjs@npm:6.11.4" + dependencies: + "@protobufjs/aspromise": "npm:^1.1.2" + "@protobufjs/base64": "npm:^1.1.2" + "@protobufjs/codegen": "npm:^2.0.4" + "@protobufjs/eventemitter": "npm:^1.1.0" + "@protobufjs/fetch": "npm:^1.1.0" + "@protobufjs/float": "npm:^1.0.2" + "@protobufjs/inquire": "npm:^1.1.0" + "@protobufjs/path": "npm:^1.1.2" + "@protobufjs/pool": "npm:^1.1.0" + "@protobufjs/utf8": "npm:^1.1.0" + "@types/long": "npm:^4.0.1" + "@types/node": "npm:>=13.7.0" + long: "npm:^4.0.0" + bin: + pbjs: bin/pbjs + pbts: bin/pbts + checksum: 10/6b7fd7540d74350d65c38f69f398c9995ae019da070e79d9cd464a458c6d19b40b07c9a026be4e10704c824a344b603307745863310c50026ebd661ce4da0663 + languageName: node + linkType: hard + "proxy-from-env@npm:^1.1.0": version: 1.1.0 resolution: "proxy-from-env@npm:1.1.0" @@ -10421,6 +10768,21 @@ __metadata: languageName: node linkType: hard +"readable-stream@npm:~2.3.6": + version: 2.3.8 + resolution: "readable-stream@npm:2.3.8" + dependencies: + core-util-is: "npm:~1.0.0" + inherits: "npm:~2.0.3" + isarray: "npm:~1.0.0" + process-nextick-args: "npm:~2.0.0" + safe-buffer: "npm:~5.1.1" + string_decoder: "npm:~1.1.1" + util-deprecate: "npm:~1.0.1" + checksum: 10/8500dd3a90e391d6c5d889256d50ec6026c059fadee98ae9aa9b86757d60ac46fff24fafb7a39fa41d54cb39d8be56cc77be202ebd4cd8ffcf4cb226cbaa40d4 + languageName: node + linkType: hard + "readable-web-to-node-stream@npm:^3.0.2": version: 3.0.2 resolution: "readable-web-to-node-stream@npm:3.0.2" @@ -10693,7 +11055,7 @@ __metadata: languageName: node linkType: hard -"safe-buffer@npm:~5.1.1": +"safe-buffer@npm:~5.1.0, safe-buffer@npm:~5.1.1": version: 5.1.2 resolution: "safe-buffer@npm:5.1.2" checksum: 10/7eb5b48f2ed9a594a4795677d5a150faa7eb54483b2318b568dc0c4fc94092a6cce5be02c7288a0500a156282f5276d5688bce7259299568d1053b2150ef374a @@ -10744,6 +11106,13 @@ __metadata: languageName: node linkType: hard +"secure-json-parse@npm:^4.0.0": + version: 4.0.0 + resolution: "secure-json-parse@npm:4.0.0" + checksum: 10/c36c9dec9afaf4ef929a5469995d70d2f20d3d89b57219f22e0349b342715987283dbc1a80ab6f39e0bb28f8c3f3f073ce5363765c20c8d003ac243b4a89bd3d + languageName: node + linkType: hard + "semver-compare@npm:^1.0.0": version: 1.0.0 resolution: "semver-compare@npm:1.0.0" @@ -11192,6 +11561,15 @@ __metadata: languageName: node linkType: hard +"string_decoder@npm:~1.1.1": + version: 1.1.1 + resolution: "string_decoder@npm:1.1.1" + dependencies: + safe-buffer: "npm:~5.1.0" + checksum: 10/7c41c17ed4dea105231f6df208002ebddd732e8e9e2d619d133cecd8e0087ddfd9587d2feb3c8caf3213cbd841ada6d057f5142cae68a4e62d3540778d9819b4 + languageName: node + linkType: hard + "strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": version: 6.0.1 resolution: "strip-ansi@npm:6.0.1" @@ -11897,7 +12275,7 @@ __metadata: languageName: node linkType: hard -"util-deprecate@npm:^1.0.1": +"util-deprecate@npm:^1.0.1, util-deprecate@npm:~1.0.1": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2" checksum: 10/474acf1146cb2701fe3b074892217553dfcf9a031280919ba1b8d651a068c9b15d863b7303cb15bd00a862b498e6cf4ad7b4a08fb134edd5a6f7641681cb54a2