UNPKG

@metamask/eth-ledger-bridge-keyring

Version:
379 lines 20.1 kB
"use strict"; 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; Object.defineProperty(exports, "__esModule", { value: true }); exports.LedgerDmkBridge = void 0; const device_management_kit_1 = require("@ledgerhq/device-management-kit"); const rxjs_1 = require("rxjs"); const operators_1 = require("rxjs/operators"); const dmk_error_translator_1 = require("./dmk-error-translator.cjs"); const eth_get_app_configuration_command_1 = require("./eth-get-app-configuration-command.cjs"); const internal_utils_1 = require("./internal-utils.cjs"); const ledger_dmk_transport_middleware_1 = require("./ledger-dmk-transport-middleware.cjs"); /** * 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. */ 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 rxjs_1.Subject()); _LedgerDmkBridge_sessionSubscription.set(this, null); __classPrivateFieldSet(this, _LedgerDmkBridge_sdk, new device_management_kit_1.DeviceManagementKitBuilder() .addTransport(opts.transportFactory) .build(), "f"); __classPrivateFieldSet(this, _LedgerDmkBridge_opts, opts, "f"); __classPrivateFieldSet(this, _LedgerDmkBridge_transportMiddleware, new ledger_dmk_transport_middleware_1.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 rxjs_1.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 device_management_kit_1.OpenAppCommand({ appName: 'Ethereum' }), }); if (!(0, device_management_kit_1.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 device_management_kit_1.CloseAppCommand(), }); if (!(0, device_management_kit_1.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((0, internal_utils_1.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((0, internal_utils_1.stripPathPrefix)(hdPath), (0, internal_utils_1.hexToBytes)(tx)); const signature = await __classPrivateFieldGet(this, _LedgerDmkBridge_instances, "m", _LedgerDmkBridge_waitForDeviceAction).call(this, observable); return { v: (0, internal_utils_1.toHexString)(signature.v), r: (0, internal_utils_1.stripHexPrefix)(signature.r), s: (0, internal_utils_1.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((0, internal_utils_1.stripPathPrefix)(hdPath), (0, internal_utils_1.hexToBytes)(message)); const signature = await __classPrivateFieldGet(this, _LedgerDmkBridge_instances, "m", _LedgerDmkBridge_waitForDeviceAction).call(this, observable); return { v: signature.v, r: (0, internal_utils_1.stripHexPrefix)(signature.r), s: (0, internal_utils_1.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((0, internal_utils_1.stripPathPrefix)(hdPath), message); const signature = await __classPrivateFieldGet(this, _LedgerDmkBridge_instances, "m", _LedgerDmkBridge_waitForDeviceAction).call(this, observable); return { v: signature.v, r: (0, internal_utils_1.stripHexPrefix)(signature.r), s: (0, internal_utils_1.stripHexPrefix)(signature.s), }; } async deviceSignDelegationAuthorization({ hdPath, chainId, contractAddress, nonce, }) { const { observable } = __classPrivateFieldGet(this, _LedgerDmkBridge_transportMiddleware, "f") .getEthSigner() .signDelegationAuthorization((0, internal_utils_1.stripPathPrefix)(hdPath), chainId, contractAddress, nonce); const signature = await __classPrivateFieldGet(this, _LedgerDmkBridge_instances, "m", _LedgerDmkBridge_waitForDeviceAction).call(this, observable); return { v: (0, internal_utils_1.toHexString)(signature.v), r: (0, internal_utils_1.stripHexPrefix)(signature.r), s: (0, internal_utils_1.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 device_management_kit_1.GetAppAndVersionCommand(); const result = await __classPrivateFieldGet(this, _LedgerDmkBridge_sdk, "f").sendCommand({ sessionId, command, }); if (!(0, device_management_kit_1.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 eth_get_app_configuration_command_1.EthGetAppConfigurationCommand(); const result = await __classPrivateFieldGet(this, _LedgerDmkBridge_sdk, "f").sendCommand({ sessionId, command, }); if (!(0, device_management_kit_1.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, }; } } exports.LedgerDmkBridge = LedgerDmkBridge; _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 (0, rxjs_1.firstValueFrom)(observable.pipe((0, operators_1.catchError)((error) => { throw (0, dmk_error_translator_1.translateDmkError)(error); }), (0, operators_1.filter)(({ status }) => status === device_management_kit_1.DeviceActionStatus.Completed || status === device_management_kit_1.DeviceActionStatus.Error))); if (state.status === device_management_kit_1.DeviceActionStatus.Completed) { return state.output; } if (state.status === device_management_kit_1.DeviceActionStatus.Error) { throw (0, dmk_error_translator_1.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 ((0, dmk_error_translator_1.isDeviceExchangeError)(error)) { return (0, dmk_error_translator_1.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((0, operators_1.map)((state) => ({ connected: state.deviceStatus !== device_management_kit_1.DeviceStatus.NOT_CONNECTED, })), (0, operators_1.catchError)(() => { if (retryCount >= MAX_RETRIES) { return (0, rxjs_1.of)({ connected: false }); } const delay = Math.min(BASE_DELAY_MS * 2 ** retryCount, MAX_BACKOFF_MS); return (0, rxjs_1.concat)((0, rxjs_1.of)({ connected: false }), (0, rxjs_1.defer)(() => (0, rxjs_1.timer)(delay).pipe((0, operators_1.switchMap)(() => createMonitoringStream(retryCount + 1))))); }), (0, operators_1.endWith)({ connected: false })); __classPrivateFieldSet(this, _LedgerDmkBridge_sessionSubscription, createMonitoringStream() .pipe((0, operators_1.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.cjs.map