-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Expand file tree
/
Copy pathLedger.ts
More file actions
285 lines (264 loc) · 8.64 KB
/
Ledger.ts
File metadata and controls
285 lines (264 loc) · 8.64 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
import type BleTransport from '@ledgerhq/react-native-hw-transport-ble';
import {
KeyringMetadata,
SignTypedDataVersion,
} from '@metamask/keyring-controller';
import ExtendedKeyringTypes from '../../constants/keyringTypes';
import Engine from '../Engine';
import {
LedgerKeyring,
LedgerMobileBridge,
} from '@metamask/eth-ledger-bridge-keyring';
import {
LEDGER_BIP44_PATH,
LEDGER_LEGACY_PATH,
LEDGER_LIVE_PATH,
} from './constants';
import PAGINATION_OPERATIONS from '../../constants/pagination';
import { strings } from '../../../locales/i18n';
import { keyringTypeToName } from '@metamask/accounts-controller';
import { removeAccountsFromPermissions } from '../Permissions';
import { isEthAppNotOpenError, isDisconnectError } from './ledgerErrors';
/**
* Perform an operation with the Ledger keyring.
*
* If no Ledger keyring is found, one is created.
*
* Note that the `operation` function should only be used for interactions with the ledger keyring.
* If you call KeyringController methods within this function, it could result in a deadlock.
*
* @param operation - The keyring operation to perform.
* @returns The stored Ledger Keyring
*/
export const withLedgerKeyring = async <CallbackResult = void>(
operation: (selectedKeyring: {
keyring: LedgerKeyring;
metadata: KeyringMetadata;
}) => Promise<CallbackResult>,
): Promise<CallbackResult> => {
const keyringController = Engine.context.KeyringController;
return await keyringController.withKeyring(
{ type: ExtendedKeyringTypes.ledger },
operation,
// TODO: Refactor this to stop creating the keyring on-demand
// Instead create it only in response to an explicit user action, and do
// not allow Ledger interactions until after that has been done.
{ createIfMissing: true },
);
};
/**
* Connects to the ledger device by requesting some metadata from it.
*
* @param transport - The transport to use to connect to the device
* @param deviceId - The device ID to connect to
* @returns The name of the currently open application on the device
*/
export const connectLedgerHardware = async (
transport: BleTransport,
deviceId: string,
): Promise<string> => {
const appAndVersion = await withLedgerKeyring(async ({ keyring }) => {
keyring.setHdPath(LEDGER_LIVE_PATH);
keyring.setDeviceId(deviceId);
const bridge = keyring.bridge as LedgerMobileBridge;
await bridge.updateTransportMethod(transport);
return await bridge.getAppNameAndVersion();
});
return appAndVersion.appName;
};
/**
* Automatically opens the Ethereum app on the Ledger device.
*/
export const openEthereumAppOnLedger = async (): Promise<void> => {
await withLedgerKeyring(async ({ keyring }) => {
const bridge = keyring.bridge as LedgerMobileBridge;
await bridge.openEthApp();
});
};
/**
* Automatically closes the current app on the Ledger device.
*/
export const closeRunningAppOnLedger = async (): Promise<void> => {
await withLedgerKeyring(async ({ keyring }) => {
const bridge = keyring.bridge as LedgerMobileBridge;
await bridge.closeApps();
});
};
/**
* Forgets the ledger device.
*/
export const forgetLedger = async (): Promise<void> => {
await withLedgerKeyring(async ({ keyring }) => {
// Permissions need to be updated before the hardware wallet is forgotten.
// This is because `removeAccountsFromPermissions` relies on the account
// existing in AccountsController in order to resolve a hex address
// back into CAIP Account Id. Hex addresses are used in
// `removeAccountsFromPermissions` because too many places in the UI still
// operate on hex addresses rather than CAIP Account Id.
const accounts = await keyring.getAccounts();
removeAccountsFromPermissions(accounts);
keyring.forgetDevice();
});
};
/**
* Get DeviceId from Ledger Keyring
*
* @returns The DeviceId
*/
export const getDeviceId = async (): Promise<string> =>
await withLedgerKeyring(async ({ keyring }) => keyring.getDeviceId());
/**
* Check if the path is valid
* @param path - The HD Path to check
* @returns Whether the path is valid
*/
export const isValidPath = (path: string): boolean => {
if (!path) return false;
if (!path.startsWith("m/44'/60'")) return false;
switch (path) {
case LEDGER_LIVE_PATH:
case LEDGER_LEGACY_PATH:
case LEDGER_BIP44_PATH:
return true;
default:
return false;
}
};
/**
* Set HD Path for Ledger Keyring
* @param path - The HD Path to set
*/
export const setHDPath = async (path: string) => {
await withLedgerKeyring(async ({ keyring }) => {
if (isValidPath(path)) {
keyring.setHdPath(path);
} else {
throw new Error(strings('ledger.hd_path_error', { path }));
}
});
};
/**
* Get HD Path from Ledger Keyring
*
* @returns The HD Path
*/
export const getHDPath = async (): Promise<string> =>
await withLedgerKeyring(async ({ keyring }) => keyring.hdPath);
/**
* Get Ledger Accounts
* @returns The Ledger Accounts
*/
export const getLedgerAccounts = async (): Promise<string[]> =>
await withLedgerKeyring(async ({ keyring }) => keyring.getAccounts());
/**
* Unlock Ledger Accounts by page
* @param operation - the operation number, <br> 0: Get First Page<br> 1: Get Next Page <br> -1: Get Previous Page
* @return The Ledger Accounts
*/
export const getLedgerAccountsByOperation = async (
operation: number,
): Promise<{ balance: string; address: string; index: number }[]> => {
try {
const accounts = await withLedgerKeyring(async ({ keyring }) => {
switch (operation) {
case PAGINATION_OPERATIONS.GET_PREVIOUS_PAGE:
return await keyring.getPreviousPage();
case PAGINATION_OPERATIONS.GET_NEXT_PAGE:
return await keyring.getNextPage();
default:
return await keyring.getFirstPage();
}
});
return accounts.map((account) => ({
...account,
balance: '0x0',
}));
} catch (e) {
/* istanbul ignore next */
if (isEthAppNotOpenError(e)) {
throw new Error(strings('ledger.eth_app_not_open_message'));
}
if (isDisconnectError(e)) {
throw new Error(strings('ledger.ledger_disconnected'));
}
throw new Error(strings('ledger.unspecified_error_during_connect'));
}
};
/**
* signTypedMessage from Ledger Keyring
*
* @returns signTypedMessage
*/
export const ledgerSignTypedMessage = async (
messageParams: {
from: string;
data: string | Record<string, unknown> | Record<string, unknown>[];
},
version: SignTypedDataVersion,
): Promise<string> => {
await withLedgerKeyring(async () => {
// This is just to trigger the keyring to get created if it doesn't exist already
});
const keyringController = Engine.context.KeyringController;
return await keyringController.signTypedMessage(
{
from: messageParams.from,
// @ts-expect-error TODO: Fix types
data: messageParams.data,
},
version,
);
};
/**
* Check if account name exists in the accounts list
*
* @param accountName - The account name to check
*/
export const checkAccountNameExists = async (accountName: string) => {
const accountsController = Engine.context.AccountsController;
const accounts = Object.values(
accountsController.state.internalAccounts.accounts,
);
const existingAccount = accounts.find(
(account) => account.metadata.name === accountName,
);
return !!existingAccount;
};
/**
* Unlock Ledger Wallet Account with index, and add it that account to metamask
*
* @param index - The index of the account to unlock
*/
export const unlockLedgerWalletAccount = async (index: number) => {
const accountsController = Engine.context.AccountsController;
try {
const { unlockAccount, name } = await withLedgerKeyring(
async ({ keyring }) => {
const existingAccounts = await keyring.getAccounts();
const keyringName = keyringTypeToName(ExtendedKeyringTypes.ledger);
const accountName = `${keyringName} ${existingAccounts.length + 1}`;
if (await checkAccountNameExists(accountName)) {
throw new Error(
strings('ledger.account_name_existed', { accountName }),
);
}
keyring.setAccountToUnlock(index);
const accounts = await keyring.addAccounts(1);
return {
unlockAccount: accounts[accounts.length - 1],
name: accountName,
};
},
);
const account = accountsController.getAccountByAddress(unlockAccount);
if (account && name !== account.metadata.name) {
accountsController.setAccountName(account.id, name);
}
Engine.setSelectedAddress(unlockAccount);
} catch (e) {
if (isEthAppNotOpenError(e)) {
throw new Error(strings('ledger.eth_app_not_open_message'));
}
throw e;
}
};