UNPKG

@metamask/eth-ledger-bridge-keyring

Version:
375 lines 19.1 kB
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); }; var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { if (kind === "m") throw new TypeError("Private method is not writable"); if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; }; var _LedgerDmkBridge_instances, _LedgerDmkBridge_transportMiddleware, _LedgerDmkBridge_sdk, _LedgerDmkBridge_opts, _LedgerDmkBridge_isConnected, _LedgerDmkBridge_sessionState$, _LedgerDmkBridge_sessionSubscription, _LedgerDmkBridge_waitForDeviceAction, _LedgerDmkBridge_toError, _LedgerDmkBridge_startSessionMonitoring; import { CloseAppCommand, DeviceActionStatus, DeviceManagementKitBuilder, DeviceStatus, GetAppAndVersionCommand, isSuccessCommandResult, OpenAppCommand } from "@ledgerhq/device-management-kit"; import { concat, defer, firstValueFrom, of, Subject, Subscription, timer } from "rxjs"; import { catchError, distinctUntilChanged, endWith, filter, map, switchMap } from "rxjs/operators"; import { isDeviceExchangeError, translateDmkError } from "./dmk-error-translator.mjs"; import { EthGetAppConfigurationCommand } from "./eth-get-app-configuration-command.mjs"; import { hexToBytes, stripHexPrefix, stripPathPrefix, toHexString } from "./internal-utils.mjs"; import { LedgerDmkTransportMiddleware } from "./ledger-dmk-transport-middleware.mjs"; /** * LedgerDmkBridge is a bridge between the LedgerKeyring and the * LedgerDmkTransportMiddleware. * It initializes and manages the DeviceManagementKit internally. * The transport factory is injected via constructor, making it platform-agnostic. */ export class LedgerDmkBridge { get onSessionStateChange() { return __classPrivateFieldGet(this, _LedgerDmkBridge_sessionState$, "f").asObservable(); } get isDeviceConnected() { return __classPrivateFieldGet(this, _LedgerDmkBridge_isConnected, "f"); } constructor(opts) { _LedgerDmkBridge_instances.add(this); _LedgerDmkBridge_transportMiddleware.set(this, void 0); _LedgerDmkBridge_sdk.set(this, void 0); _LedgerDmkBridge_opts.set(this, void 0); _LedgerDmkBridge_isConnected.set(this, false); _LedgerDmkBridge_sessionState$.set(this, new Subject()); _LedgerDmkBridge_sessionSubscription.set(this, null); __classPrivateFieldSet(this, _LedgerDmkBridge_sdk, new DeviceManagementKitBuilder() .addTransport(opts.transportFactory) .build(), "f"); __classPrivateFieldSet(this, _LedgerDmkBridge_opts, opts, "f"); __classPrivateFieldSet(this, _LedgerDmkBridge_transportMiddleware, new LedgerDmkTransportMiddleware(__classPrivateFieldGet(this, _LedgerDmkBridge_sdk, "f")), "f"); } /** * Compatibility shim for the shared {@link LedgerBridge} interface. * * DMK session setup happens externally via `updateSessionId` or `connect`, * so this method is a no-op. * * @returns A promise that resolves immediately. */ async init() { return undefined; } /** * Destroys the bridge and cleans up resources. * * Unsubscribes session monitoring, disposes the transport middleware, and * completes the session-state subject so all subscribers are released. A * fresh subject is installed afterward so the bridge can be reconnected * after destroy. State cleanup happens in a `finally` block so the bridge * is marked disconnected even when middleware `dispose()` rejects. * * @returns A promise that resolves when cleanup is complete. */ async destroy() { __classPrivateFieldGet(this, _LedgerDmkBridge_sessionSubscription, "f")?.unsubscribe(); __classPrivateFieldSet(this, _LedgerDmkBridge_sessionSubscription, null, "f"); try { await __classPrivateFieldGet(this, _LedgerDmkBridge_transportMiddleware, "f").dispose(); } finally { __classPrivateFieldSet(this, _LedgerDmkBridge_isConnected, false, "f"); __classPrivateFieldGet(this, _LedgerDmkBridge_sessionState$, "f").next({ connected: false }); __classPrivateFieldGet(this, _LedgerDmkBridge_sessionState$, "f").complete(); __classPrivateFieldSet(this, _LedgerDmkBridge_sessionState$, new Subject(), "f"); } } /** * Returns the bridge options captured at construction. * * Compatibility shim for the shared {@link LedgerBridge} interface. The DMK * bridge does not use runtime-reconfigurable options; this method exists * only to satisfy the interface contract. * * @returns A promise that resolves with the current bridge options. */ async getOptions() { return __classPrivateFieldGet(this, _LedgerDmkBridge_opts, "f"); } /** * Replaces the stored bridge options. * * Compatibility shim for the shared {@link LedgerBridge} interface. Stored * options are not re-applied to the underlying DMK instance or transport * middleware; the values used at construction time remain in effect. * * @param opts - The options to set for the bridge. * @returns A promise that resolves when options are set. */ async setOptions(opts) { __classPrivateFieldSet(this, _LedgerDmkBridge_opts, opts, "f"); } /** * Updates the session ID used for communication. * * @param sessionId - The session ID from DMK. * @returns A promise that resolves with true if the session was updated successfully. */ async updateSessionId(sessionId) { await __classPrivateFieldGet(this, _LedgerDmkBridge_transportMiddleware, "f").setSessionId(sessionId); __classPrivateFieldSet(this, _LedgerDmkBridge_isConnected, true, "f"); __classPrivateFieldGet(this, _LedgerDmkBridge_instances, "m", _LedgerDmkBridge_startSessionMonitoring).call(this, sessionId); return true; } /** * Starts device discovery for the configured DMK transport. * * @param args - Optional DMK discovery options. * @returns An observable that emits discovered devices. */ startDiscovering(...args) { return __classPrivateFieldGet(this, _LedgerDmkBridge_transportMiddleware, "f").startDiscovering(...args); } /** * Connects to a discovered device using the configured DMK transport. * * @param args - The DMK connection arguments. * @returns The created session ID. */ async connect(...args) { const sessionId = await __classPrivateFieldGet(this, _LedgerDmkBridge_transportMiddleware, "f").connect(...args); __classPrivateFieldSet(this, _LedgerDmkBridge_isConnected, true, "f"); __classPrivateFieldGet(this, _LedgerDmkBridge_instances, "m", _LedgerDmkBridge_startSessionMonitoring).call(this, sessionId); return sessionId; } /** * Compatibility shim for the shared {@link LedgerBridge} interface. * * DMK transport selection happens through the injected transport factory * at construction time, so this method is a no-op that always succeeds. * * @param _transportType - The requested transport type (ignored). * @returns A promise that resolves with `true`. */ async updateTransportMethod(_transportType) { return true; } async openEthApp() { const sessionId = __classPrivateFieldGet(this, _LedgerDmkBridge_transportMiddleware, "f").getSessionId(); const result = await __classPrivateFieldGet(this, _LedgerDmkBridge_sdk, "f").sendCommand({ sessionId, command: new OpenAppCommand({ appName: 'Ethereum' }), }); if (!isSuccessCommandResult(result)) { throw __classPrivateFieldGet(this, _LedgerDmkBridge_instances, "m", _LedgerDmkBridge_toError).call(this, result.error); } } async closeApps() { const sessionId = __classPrivateFieldGet(this, _LedgerDmkBridge_transportMiddleware, "f").getSessionId(); const result = await __classPrivateFieldGet(this, _LedgerDmkBridge_sdk, "f").sendCommand({ sessionId, command: new CloseAppCommand(), }); if (!isSuccessCommandResult(result)) { throw __classPrivateFieldGet(this, _LedgerDmkBridge_instances, "m", _LedgerDmkBridge_toError).call(this, result.error); } } /** * Compatibility shim for the shared {@link LedgerBridge} interface. * * The Ethereum signer kit automatically opens the Ethereum app before each * signing operation via DMK's `CallTaskInAppDeviceAction`, so this method * is a no-op that always succeeds. * * @returns A promise that resolves with `true`. */ async attemptMakeApp() { return true; } /** * Retrieves public key/address for a given HD path. * * @param options0 - The parameters object. * @param options0.hdPath - The HD path to derive the public key from. * @returns A promise that resolves with the public key response. */ async getPublicKey({ hdPath, }) { const { observable } = __classPrivateFieldGet(this, _LedgerDmkBridge_transportMiddleware, "f") .getEthSigner() .getAddress(stripPathPrefix(hdPath), { checkOnDevice: false, returnChainCode: true, }); const response = await __classPrivateFieldGet(this, _LedgerDmkBridge_instances, "m", _LedgerDmkBridge_waitForDeviceAction).call(this, observable); return { publicKey: response.publicKey, address: response.address, chainCode: response.chainCode, }; } /** * Signs an Ethereum transaction. * * @param options0 - The parameters object. * @param options0.tx - The transaction hex string to sign. * @param options0.hdPath - The HD path for signing. * @returns A promise that resolves with the transaction signature. */ async deviceSignTransaction({ tx, hdPath, }) { const { observable } = __classPrivateFieldGet(this, _LedgerDmkBridge_transportMiddleware, "f") .getEthSigner() .signTransaction(stripPathPrefix(hdPath), hexToBytes(tx)); const signature = await __classPrivateFieldGet(this, _LedgerDmkBridge_instances, "m", _LedgerDmkBridge_waitForDeviceAction).call(this, observable); return { v: toHexString(signature.v), r: stripHexPrefix(signature.r), s: stripHexPrefix(signature.s), }; } /** * Signs a personal message. * * @param options0 - The parameters object. * @param options0.hdPath - The HD path for signing. * @param options0.message - The message to sign. * @returns A promise that resolves with the message signature. */ async deviceSignMessage({ hdPath, message, }) { const { observable } = __classPrivateFieldGet(this, _LedgerDmkBridge_transportMiddleware, "f") .getEthSigner() .signMessage(stripPathPrefix(hdPath), hexToBytes(message)); const signature = await __classPrivateFieldGet(this, _LedgerDmkBridge_instances, "m", _LedgerDmkBridge_waitForDeviceAction).call(this, observable); return { v: signature.v, r: stripHexPrefix(signature.r), s: stripHexPrefix(signature.s), }; } /** * Signs EIP-712 typed data. * * @param options0 - The parameters object. * @param options0.hdPath - The HD path for signing. * @param options0.message - The typed data message to sign. * @returns A promise that resolves with the typed data signature. */ async deviceSignTypedData({ hdPath, message, }) { const { observable } = __classPrivateFieldGet(this, _LedgerDmkBridge_transportMiddleware, "f") .getEthSigner() .signTypedData(stripPathPrefix(hdPath), message); const signature = await __classPrivateFieldGet(this, _LedgerDmkBridge_instances, "m", _LedgerDmkBridge_waitForDeviceAction).call(this, observable); return { v: signature.v, r: stripHexPrefix(signature.r), s: stripHexPrefix(signature.s), }; } async deviceSignDelegationAuthorization({ hdPath, chainId, contractAddress, nonce, }) { const { observable } = __classPrivateFieldGet(this, _LedgerDmkBridge_transportMiddleware, "f") .getEthSigner() .signDelegationAuthorization(stripPathPrefix(hdPath), chainId, contractAddress, nonce); const signature = await __classPrivateFieldGet(this, _LedgerDmkBridge_instances, "m", _LedgerDmkBridge_waitForDeviceAction).call(this, observable); return { v: toHexString(signature.v), r: stripHexPrefix(signature.r), s: stripHexPrefix(signature.s), }; } /** * Retrieves the current app name and version. * * @returns A promise that resolves with the app name and version. */ async getAppNameAndVersion() { const sessionId = __classPrivateFieldGet(this, _LedgerDmkBridge_transportMiddleware, "f").getSessionId(); const command = new GetAppAndVersionCommand(); const result = await __classPrivateFieldGet(this, _LedgerDmkBridge_sdk, "f").sendCommand({ sessionId, command, }); if (!isSuccessCommandResult(result)) { throw __classPrivateFieldGet(this, _LedgerDmkBridge_instances, "m", _LedgerDmkBridge_toError).call(this, result.error); } return { appName: result.data.name, version: result.data.version, }; } /** * Retrieves the Ethereum app configuration using the eth-specific * GetAppConfiguration APDU command (CLA 0xE0, INS 0x06). * * @returns A promise that resolves with the app configuration. */ async getAppConfiguration() { const sessionId = __classPrivateFieldGet(this, _LedgerDmkBridge_transportMiddleware, "f").getSessionId(); const command = new EthGetAppConfigurationCommand(); const result = await __classPrivateFieldGet(this, _LedgerDmkBridge_sdk, "f").sendCommand({ sessionId, command, }); if (!isSuccessCommandResult(result)) { throw __classPrivateFieldGet(this, _LedgerDmkBridge_instances, "m", _LedgerDmkBridge_toError).call(this, result.error); } const { data } = result; // The `ethapp.adoc` spec for `getAppConfiguration` // (https://github.com/LedgerHQ/app-ethereum/blob/develop/doc/ethapp.adoc#get-app-configuration) // defines four flag bits. This bridge only consumes three (blind signing, // web3 checks enabled, web3 checks opt-in). The fourth — `0x02` *ERC 20 // Token information needs to be provided externally* — is intentionally // mapped to `erc20ProvisioningNecessary: 0` because the rest of the // keyring does not implement the `PROVIDE ERC 20 TOKEN INFORMATION` // provisioning flow. // // TODO: The shared `AppConfigurationResponse` interface also requires // `starkEnabled` and `starkv2Supported`, which are not exposed by the // `getAppConfiguration` APDU at all. They are stubbed to 0 to satisfy // the interface contract. return { arbitraryDataEnabled: data.blindSigningEnabled ? 1 : 0, erc20ProvisioningNecessary: 0, starkEnabled: 0, starkv2Supported: 0, version: data.version, }; } } _LedgerDmkBridge_transportMiddleware = new WeakMap(), _LedgerDmkBridge_sdk = new WeakMap(), _LedgerDmkBridge_opts = new WeakMap(), _LedgerDmkBridge_isConnected = new WeakMap(), _LedgerDmkBridge_sessionState$ = new WeakMap(), _LedgerDmkBridge_sessionSubscription = new WeakMap(), _LedgerDmkBridge_instances = new WeakSet(), _LedgerDmkBridge_waitForDeviceAction = async function _LedgerDmkBridge_waitForDeviceAction(observable) { const state = await firstValueFrom(observable.pipe(catchError((error) => { throw translateDmkError(error); }), filter(({ status }) => status === DeviceActionStatus.Completed || status === DeviceActionStatus.Error))); if (state.status === DeviceActionStatus.Completed) { return state.output; } if (state.status === DeviceActionStatus.Error) { throw translateDmkError(state.error); } // Unreachable: the filter above only lets Completed or Error states // through. Defensive guard retained for type safety. throw new Error('Ledger device action ended without completion.'); }, _LedgerDmkBridge_toError = function _LedgerDmkBridge_toError(error) { if (isDeviceExchangeError(error)) { return translateDmkError(error); } if (error instanceof Error) { return error; } return new Error('Ledger command failed.'); }, _LedgerDmkBridge_startSessionMonitoring = function _LedgerDmkBridge_startSessionMonitoring(sessionId) { __classPrivateFieldGet(this, _LedgerDmkBridge_sessionSubscription, "f")?.unsubscribe(); const MAX_BACKOFF_MS = 30000; const BASE_DELAY_MS = 1000; const MAX_RETRIES = 10; const createMonitoringStream = (retryCount = 0) => __classPrivateFieldGet(this, _LedgerDmkBridge_sdk, "f").getDeviceSessionState({ sessionId }).pipe(map((state) => ({ connected: state.deviceStatus !== DeviceStatus.NOT_CONNECTED, })), catchError(() => { if (retryCount >= MAX_RETRIES) { return of({ connected: false }); } const delay = Math.min(BASE_DELAY_MS * 2 ** retryCount, MAX_BACKOFF_MS); return concat(of({ connected: false }), defer(() => timer(delay).pipe(switchMap(() => createMonitoringStream(retryCount + 1))))); }), endWith({ connected: false })); __classPrivateFieldSet(this, _LedgerDmkBridge_sessionSubscription, createMonitoringStream() .pipe(distinctUntilChanged((a, b) => a.connected === b.connected)) .subscribe((state) => { __classPrivateFieldSet(this, _LedgerDmkBridge_isConnected, state.connected, "f"); __classPrivateFieldGet(this, _LedgerDmkBridge_sessionState$, "f").next(state); }), "f"); }; //# sourceMappingURL=ledger-dmk-bridge.mjs.map