@metamask/eth-ledger-bridge-keyring
Version:
A MetaMask compatible keyring, for ledger hardware wallets
379 lines • 20.1 kB
JavaScript
"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