Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat: sofe derive
  • Loading branch information
ByteZhang1024 committed Aug 15, 2025
commit be93f6986faea5d64a43c632dbbc88445efe0089
3 changes: 3 additions & 0 deletions packages/keyring-eth-onekey/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@
"@onekeyfe/hd-transport": "^1.1.1",
"@onekeyfe/hd-web-sdk": "^1.1.1",
"bytebuffer": "^5.0.1",
"hdkey": "^2.1.0",
"ripple-address-codec": "^5.0.0",
"tslib": "^2.6.2"
},
"devDependencies": {
Expand All @@ -66,6 +68,7 @@
"@metamask/keyring-utils": "workspace:^",
"@ts-bridge/cli": "^0.6.3",
"@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",
Expand Down
7 changes: 0 additions & 7 deletions packages/keyring-eth-onekey/src/onekey-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import type {
EVMSignMessageParams,
EVMSignTypedDataParams,
EVMGetPublicKeyParams,
Features,
} from '@onekeyfe/hd-core';
import type { EthereumMessageSignature } from '@onekeyfe/hd-transport';

Expand Down Expand Up @@ -39,18 +38,12 @@ export type OneKeyBridge = {

updateTransportMethod(transportType: string): Promise<void>;

getDeviceFeatures(): Response<Features>;

// OneKeySdk.getPublicKey has two overloads
// It is not possible to extract them from the library using utility types
getPublicKey(
params: Params<EVMGetPublicKeyParams>,
): Response<{ publicKey: string; chainCode: string }>;

batchGetPublicKey(
params: Params<any> & { bundle: EVMGetPublicKeyParams[] },
): Response<{ pub: string }[]>;

getPassphraseState(): Response<string | undefined>;

ethereumSignTransaction(
Expand Down
167 changes: 81 additions & 86 deletions packages/keyring-eth-onekey/src/onekey-keyring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ import type {
ConnectSettings,
EthereumSignTypedDataMessage,
EthereumSignTypedDataTypes,
EVMGetPublicKeyParams,
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';
Expand Down Expand Up @@ -129,6 +129,8 @@ export class OneKeyKeyring extends EventEmitter {

unlockedAccount = 0;

hdk = new HDKey();

accounts: readonly string[] = [];

accountDetails: Record<string, AccountDetails> = {};
Expand Down Expand Up @@ -194,16 +196,17 @@ export class OneKeyKeyring extends EventEmitter {
this.hdPath = hdPath;
}

lock(): void {
this.hdk = new HDKey();
}

isUnlocked(): boolean {
return false;
return Boolean(this.hdk?.publicKey);
}

async unlock(): Promise<string> {
const features = await this.bridge.getDeviceFeatures();
if (features.success) {
if (features.payload.unlocked) {
return 'already unlocked';
}
if (this.isUnlocked()) {
return 'already unlocked';
}

return new Promise((resolve, reject) => {
Expand All @@ -219,7 +222,24 @@ export class OneKeyKeyring extends EventEmitter {
return;
}
this.passphraseState = passphraseResponse.payload;
resolve('just unlocked');

// 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'));
Expand All @@ -237,41 +257,32 @@ export class OneKeyKeyring extends EventEmitter {
const to = from + numberOfAccounts;
const newAccounts: string[] = [];

const paths: string[] = [];
for (let i = from; i < to; i++) {
paths.push(this.#getPathForIndex(i));
}

// eslint-disable-next-line no-void
void this.#batchGetAddress(paths, this.passphraseState)
.then((addresses) => {
if (addresses.length !== paths.length) {
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');
}

for (let i = 0; i < paths.length; i++) {
const address = addresses[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: paths[i] ?? '',
passphraseState: this.passphraseState,
};
}
this.page = 0;
if (!this.accounts.includes(address)) {
this.accounts = [...this.accounts, address];
newAccounts.push(address);
}
resolve(newAccounts);
})
.catch((error: Error) => {
reject(error);
});
if (!this.accountDetails[address]) {
this.accountDetails[address] = {
index: from + i,
hdPath,
passphraseState: this.passphraseState,
};
}
this.page = 0;
}

resolve(newAccounts);
} catch (error) {
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
reject(error);
}
});
}

Expand Down Expand Up @@ -565,29 +576,15 @@ export class OneKeyKeyring extends EventEmitter {

this.unlock()
.then(async () => {
const paths = [];
for (let i = from; i < to; i++) {
paths.push(this.#getPathForIndex(i));
}

// this.passphraseState = passphraseState;
const addresses = await this.#batchGetAddress(
paths,
this.passphraseState,
);

if (addresses.length !== paths.length) {
throw new Error('Unknown error');
}
for (let i = 0; i < paths.length; i++) {
const address = addresses[i];
const address = this.#addressFromIndex(i);
if (typeof address === 'undefined') {
throw new Error('Unknown error');
}
accounts.push({
index: from + i,
address,
balance: null,
index: from + i,
});
}
resolve(accounts);
Expand All @@ -599,36 +596,6 @@ export class OneKeyKeyring extends EventEmitter {
});
}

async #batchGetAddress(
paths: string[],
passphraseState: string | undefined,
): Promise<string[]> {
const batchParams: EVMGetPublicKeyParams[] = paths.map((path) => ({
path,
showOnOneKey: false,
}));

const response = await this.bridge.batchGetPublicKey({
bundle: batchParams,
useBatch: true,
passphraseState,
useEmptyPassphrase: isEmptyPassphrase(passphraseState),
skipPassphraseCheck: true,
});
if (response.success) {
return response.payload.map((item) => {
const address = ethUtil.publicToAddress(
Buffer.from(item.pub, 'hex'),
true,
);
return ethUtil.toChecksumAddress(
addHexPrefix(Buffer.from(address).toString('hex')),
);
});
}
throw new Error(response.payload?.error || 'Unknown error');
}

#accountDetailsFromAddress(address: string): AccountDetails {
const checksummedAddress = ethUtil.toChecksumAddress(address);
const accountDetails = this.accountDetails[checksummedAddress];
Expand All @@ -638,6 +605,34 @@ export class OneKeyKeyring extends EventEmitter {
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.#isLedgerLegacyHdPath()) {
return `${pathBase}/${index}`;
}
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()) {
Expand Down
66 changes: 1 addition & 65 deletions packages/keyring-eth-onekey/src/onekey-web-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { UI_REQUEST, UI_RESPONSE } from '@onekeyfe/hd-core';
import type {
ConnectSettings,
CoreApi,
EVMGetPublicKeyParams,
EVMSignedTx,
EVMSignMessageParams,
EVMSignTransactionParams,
Expand All @@ -15,10 +14,7 @@ import type {
Unsuccessful,
} from '@onekeyfe/hd-core';
import { HardwareErrorCode } from '@onekeyfe/hd-shared';
import type {
EthereumMessageSignature,
Features,
} from '@onekeyfe/hd-transport';
import type { EthereumMessageSignature } from '@onekeyfe/hd-transport';

import { ONEKEY_HARDWARE_UI_EVENT } from './constants';
import { OneKeyBridge } from './onekey-bridge';
Expand Down Expand Up @@ -130,25 +126,6 @@ export class OneKeyWebBridge implements OneKeyBridge {
return this.model;
}

async getDeviceFeatures(): Promise<
| {
success: true;
payload: Features;
}
| {
success: false;
payload: { error: string; code?: string | number };
}
> {
if (!this.sdk) {
return {
success: false,
payload: { error: 'SDK not initialized', code: 800 },
};
}
return await this.sdk.getFeatures();
}

async getPublicKey(params: {
path: string;
coin: string;
Expand Down Expand Up @@ -188,47 +165,6 @@ export class OneKeyWebBridge implements OneKeyBridge {
});
}

async batchGetPublicKey(
params: Params<any> & { bundle: EVMGetPublicKeyParams[] },
): Promise<
| { success: false; payload: { error: string; code?: string | number } }
| { success: true; payload: { pub: 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) {
if (Array.isArray(result.payload)) {
return {
success: true,
payload: result.payload.map((item) => ({ pub: item.pub })),
};
}
return {
success: false,
payload: { error: 'No public key found', code: 800 },
};
}
this.handleBlockErrorEvent(result);
return {
success: false,
payload: {
error: result?.payload.error ?? '',
code: result?.payload.code ?? undefined,
},
};
});
}

async getPassphraseState(): Promise<
| { success: false; payload: { error: string; code?: string | number } }
| { success: true; payload: string | undefined }
Expand Down
Loading