@bytezhang/ledger-adapter 0.0.6 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { IHardwareWallet, IConnector, TransportType, IUiHandler, DeviceInfo, Response, ChainCapability, HardwareEventMap, DeviceEventListener, EvmGetAddressParams, EvmAddress, ProgressCallback, EvmGetPublicKeyParams, EvmPublicKey, EvmSignTxParams, EvmSignedTx, EvmSignMsgParams, EvmSignature, EvmSignTypedDataParams, BtcGetAddressParams, BtcAddress, BtcGetPublicKeyParams, BtcPublicKey, BtcSignTxParams, BtcSignedTx, BtcSignMsgParams, BtcSignature, SolGetAddressParams, SolAddress, SolGetPublicKeyParams, SolPublicKey, SolSignTxParams, SolSignedTx, SolSignMsgParams, SolSignature, DeviceDescriptor, DeviceChangeEvent, HardwareErrorCode } from '@bytezhang/hardware-wallet-core';
1
+ import { IHardwareWallet, IConnector, TransportType, IUiHandler, DeviceInfo, Response, ChainCapability, HardwareEventMap, DeviceEventListener, ChainForFingerprint, EvmGetAddressParams, EvmAddress, ProgressCallback, EvmGetPublicKeyParams, EvmPublicKey, EvmSignTxParams, EvmSignedTx, EvmSignMsgParams, EvmSignature, EvmSignTypedDataParams, BtcGetAddressParams, BtcAddress, BtcGetPublicKeyParams, BtcPublicKey, BtcSignTxParams, BtcSignedTx, BtcSignMsgParams, BtcSignature, SolGetAddressParams, SolAddress, SolGetPublicKeyParams, SolPublicKey, SolSignTxParams, SolSignedTx, SolSignMsgParams, SolSignature, DeviceDescriptor, DeviceChangeEvent, HardwareErrorCode } from '@bytezhang/hardware-wallet-core';
2
2
 
3
3
  /**
4
4
  * Ledger hardware wallet adapter that delegates to an IConnector.
@@ -34,6 +34,18 @@ declare class LedgerAdapter implements IHardwareWallet {
34
34
  off<K extends keyof HardwareEventMap>(event: K, listener: (event: HardwareEventMap[K]) => void): void;
35
35
  off(event: string, listener: DeviceEventListener): void;
36
36
  cancel(connectId: string): void;
37
+ getChainFingerprint(connectId: string, deviceId: string, chain: ChainForFingerprint): Promise<Response<string>>;
38
+ /**
39
+ * Verify that the connected device matches the expected fingerprint.
40
+ *
41
+ * - If deviceId is empty, verification is skipped (returns true).
42
+ * - deviceId is used here as the stored fingerprint to compare against.
43
+ */
44
+ private _verifyDeviceFingerprint;
45
+ /**
46
+ * Derive an address at the fixed testnet path for fingerprint generation.
47
+ */
48
+ private _deriveAddressForFingerprint;
37
49
  evmGetAddress(connectId: string, _deviceId: string, params: EvmGetAddressParams): Promise<Response<EvmAddress>>;
38
50
  evmGetAddresses(connectId: string, deviceId: string, params: EvmGetAddressParams[], onProgress?: ProgressCallback): Promise<Response<EvmAddress[]>>;
39
51
  evmGetPublicKey(connectId: string, _deviceId: string, params: EvmGetPublicKeyParams): Promise<Response<EvmPublicKey>>;
@@ -45,9 +57,7 @@ declare class LedgerAdapter implements IHardwareWallet {
45
57
  btcGetPublicKey(connectId: string, _deviceId: string, params: BtcGetPublicKeyParams): Promise<Response<BtcPublicKey>>;
46
58
  btcSignTransaction(connectId: string, _deviceId: string, params: BtcSignTxParams): Promise<Response<BtcSignedTx>>;
47
59
  btcSignMessage(_connectId: string, _deviceId: string, _params: BtcSignMsgParams): Promise<Response<BtcSignature>>;
48
- btcGetMasterFingerprint(connectId: string, _deviceId: string, params?: {
49
- skipOpenApp?: boolean;
50
- }): Promise<Response<{
60
+ btcGetMasterFingerprint(connectId: string, _deviceId: string): Promise<Response<{
51
61
  masterFingerprint: string;
52
62
  }>>;
53
63
  solGetAddress(_connectId: string, _deviceId: string, _params: SolGetAddressParams): Promise<Response<SolAddress>>;
@@ -64,6 +74,7 @@ declare class LedgerAdapter implements IHardwareWallet {
64
74
  */
65
75
  private static readonly MAX_DEVICE_RETRY;
66
76
  private _deviceConnectResolve;
77
+ private _connectingPromise;
67
78
  private static readonly DEVICE_CONNECT_TIMEOUT_MS;
68
79
  /**
69
80
  * Wait for user to connect and unlock device.
@@ -79,6 +90,7 @@ declare class LedgerAdapter implements IHardwareWallet {
79
90
  */
80
91
  deviceConnectResponse(type: 'confirm' | 'cancel'): void;
81
92
  private ensureConnected;
93
+ private _doConnect;
82
94
  private _connectFirstOrSelect;
83
95
  /**
84
96
  * Call the connector with automatic session resolution and disconnect retry.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { IHardwareWallet, IConnector, TransportType, IUiHandler, DeviceInfo, Response, ChainCapability, HardwareEventMap, DeviceEventListener, EvmGetAddressParams, EvmAddress, ProgressCallback, EvmGetPublicKeyParams, EvmPublicKey, EvmSignTxParams, EvmSignedTx, EvmSignMsgParams, EvmSignature, EvmSignTypedDataParams, BtcGetAddressParams, BtcAddress, BtcGetPublicKeyParams, BtcPublicKey, BtcSignTxParams, BtcSignedTx, BtcSignMsgParams, BtcSignature, SolGetAddressParams, SolAddress, SolGetPublicKeyParams, SolPublicKey, SolSignTxParams, SolSignedTx, SolSignMsgParams, SolSignature, DeviceDescriptor, DeviceChangeEvent, HardwareErrorCode } from '@bytezhang/hardware-wallet-core';
1
+ import { IHardwareWallet, IConnector, TransportType, IUiHandler, DeviceInfo, Response, ChainCapability, HardwareEventMap, DeviceEventListener, ChainForFingerprint, EvmGetAddressParams, EvmAddress, ProgressCallback, EvmGetPublicKeyParams, EvmPublicKey, EvmSignTxParams, EvmSignedTx, EvmSignMsgParams, EvmSignature, EvmSignTypedDataParams, BtcGetAddressParams, BtcAddress, BtcGetPublicKeyParams, BtcPublicKey, BtcSignTxParams, BtcSignedTx, BtcSignMsgParams, BtcSignature, SolGetAddressParams, SolAddress, SolGetPublicKeyParams, SolPublicKey, SolSignTxParams, SolSignedTx, SolSignMsgParams, SolSignature, DeviceDescriptor, DeviceChangeEvent, HardwareErrorCode } from '@bytezhang/hardware-wallet-core';
2
2
 
3
3
  /**
4
4
  * Ledger hardware wallet adapter that delegates to an IConnector.
@@ -34,6 +34,18 @@ declare class LedgerAdapter implements IHardwareWallet {
34
34
  off<K extends keyof HardwareEventMap>(event: K, listener: (event: HardwareEventMap[K]) => void): void;
35
35
  off(event: string, listener: DeviceEventListener): void;
36
36
  cancel(connectId: string): void;
37
+ getChainFingerprint(connectId: string, deviceId: string, chain: ChainForFingerprint): Promise<Response<string>>;
38
+ /**
39
+ * Verify that the connected device matches the expected fingerprint.
40
+ *
41
+ * - If deviceId is empty, verification is skipped (returns true).
42
+ * - deviceId is used here as the stored fingerprint to compare against.
43
+ */
44
+ private _verifyDeviceFingerprint;
45
+ /**
46
+ * Derive an address at the fixed testnet path for fingerprint generation.
47
+ */
48
+ private _deriveAddressForFingerprint;
37
49
  evmGetAddress(connectId: string, _deviceId: string, params: EvmGetAddressParams): Promise<Response<EvmAddress>>;
38
50
  evmGetAddresses(connectId: string, deviceId: string, params: EvmGetAddressParams[], onProgress?: ProgressCallback): Promise<Response<EvmAddress[]>>;
39
51
  evmGetPublicKey(connectId: string, _deviceId: string, params: EvmGetPublicKeyParams): Promise<Response<EvmPublicKey>>;
@@ -45,9 +57,7 @@ declare class LedgerAdapter implements IHardwareWallet {
45
57
  btcGetPublicKey(connectId: string, _deviceId: string, params: BtcGetPublicKeyParams): Promise<Response<BtcPublicKey>>;
46
58
  btcSignTransaction(connectId: string, _deviceId: string, params: BtcSignTxParams): Promise<Response<BtcSignedTx>>;
47
59
  btcSignMessage(_connectId: string, _deviceId: string, _params: BtcSignMsgParams): Promise<Response<BtcSignature>>;
48
- btcGetMasterFingerprint(connectId: string, _deviceId: string, params?: {
49
- skipOpenApp?: boolean;
50
- }): Promise<Response<{
60
+ btcGetMasterFingerprint(connectId: string, _deviceId: string): Promise<Response<{
51
61
  masterFingerprint: string;
52
62
  }>>;
53
63
  solGetAddress(_connectId: string, _deviceId: string, _params: SolGetAddressParams): Promise<Response<SolAddress>>;
@@ -64,6 +74,7 @@ declare class LedgerAdapter implements IHardwareWallet {
64
74
  */
65
75
  private static readonly MAX_DEVICE_RETRY;
66
76
  private _deviceConnectResolve;
77
+ private _connectingPromise;
67
78
  private static readonly DEVICE_CONNECT_TIMEOUT_MS;
68
79
  /**
69
80
  * Wait for user to connect and unlock device.
@@ -79,6 +90,7 @@ declare class LedgerAdapter implements IHardwareWallet {
79
90
  */
80
91
  deviceConnectResponse(type: 'confirm' | 'cancel'): void;
81
92
  private ensureConnected;
93
+ private _doConnect;
82
94
  private _connectFirstOrSelect;
83
95
  /**
84
96
  * Call the connector with automatic session resolution and disconnect retry.
package/dist/index.js CHANGED
@@ -169,6 +169,8 @@ var _LedgerAdapter = class _LedgerAdapter {
169
169
  this._sessions = /* @__PURE__ */ new Map();
170
170
  // Pending device-connect resolve — set by _waitForDeviceConnect, resolved by uiResponse
171
171
  this._deviceConnectResolve = null;
172
+ // Mutex for ensureConnected — prevents concurrent calls from establishing duplicate connections
173
+ this._connectingPromise = null;
172
174
  // ---------------------------------------------------------------------------
173
175
  // Event translation
174
176
  // ---------------------------------------------------------------------------
@@ -222,6 +224,8 @@ var _LedgerAdapter = class _LedgerAdapter {
222
224
  async init(_config) {
223
225
  }
224
226
  async dispose() {
227
+ this._deviceConnectResolve?.(true);
228
+ this._deviceConnectResolve = null;
225
229
  this.unregisterEventListeners();
226
230
  this.connector.reset();
227
231
  this._uiHandler = null;
@@ -293,10 +297,74 @@ var _LedgerAdapter = class _LedgerAdapter {
293
297
  void this.connector.cancel(sessionId);
294
298
  }
295
299
  // ---------------------------------------------------------------------------
300
+ // Chain fingerprint
301
+ // ---------------------------------------------------------------------------
302
+ async getChainFingerprint(connectId, deviceId, chain) {
303
+ await this._ensureDevicePermission(connectId, deviceId);
304
+ try {
305
+ const address = await this._deriveAddressForFingerprint(connectId, chain);
306
+ return (0, import_hardware_wallet_core2.success)((0, import_hardware_wallet_core2.deriveDeviceFingerprint)(address));
307
+ } catch (err) {
308
+ return this.errorToFailure(err);
309
+ }
310
+ }
311
+ /**
312
+ * Verify that the connected device matches the expected fingerprint.
313
+ *
314
+ * - If deviceId is empty, verification is skipped (returns true).
315
+ * - deviceId is used here as the stored fingerprint to compare against.
316
+ */
317
+ async _verifyDeviceFingerprint(connectId, deviceId, chain) {
318
+ if (!deviceId) return true;
319
+ try {
320
+ const address = await this._deriveAddressForFingerprint(connectId, chain);
321
+ const fingerprint = (0, import_hardware_wallet_core2.deriveDeviceFingerprint)(address);
322
+ return fingerprint === deviceId;
323
+ } catch (err) {
324
+ const mapped = mapLedgerError(err);
325
+ if (mapped.code === import_hardware_wallet_core2.HardwareErrorCode.WrongApp || mapped.code === import_hardware_wallet_core2.HardwareErrorCode.DeviceLocked) {
326
+ return true;
327
+ }
328
+ throw err;
329
+ }
330
+ }
331
+ /**
332
+ * Derive an address at the fixed testnet path for fingerprint generation.
333
+ */
334
+ async _deriveAddressForFingerprint(connectId, chain) {
335
+ const path = import_hardware_wallet_core2.CHAIN_FINGERPRINT_PATHS[chain];
336
+ if (chain === "evm") {
337
+ const result = await this.connectorCall(connectId, "evmGetAddress", {
338
+ path,
339
+ showOnDevice: false
340
+ });
341
+ return result.address;
342
+ }
343
+ if (chain === "btc") {
344
+ const result = await this.connectorCall(connectId, "btcGetAddress", {
345
+ path,
346
+ showOnDevice: false,
347
+ coin: "Testnet"
348
+ });
349
+ return result.address;
350
+ }
351
+ if (chain === "sol") {
352
+ const result = await this.connectorCall(connectId, "solGetAddress", {
353
+ path,
354
+ showOnDevice: false
355
+ });
356
+ return result.address;
357
+ }
358
+ throw new Error(`Unsupported chain for fingerprint: ${chain}`);
359
+ }
360
+ // ---------------------------------------------------------------------------
296
361
  // EVM methods
297
362
  // ---------------------------------------------------------------------------
298
363
  async evmGetAddress(connectId, _deviceId, params) {
299
364
  await this._ensureDevicePermission(connectId, _deviceId);
365
+ if (!await this._verifyDeviceFingerprint(connectId, _deviceId, "evm")) {
366
+ return (0, import_hardware_wallet_core2.failure)(import_hardware_wallet_core2.HardwareErrorCode.DeviceMismatch, "Wrong device connected");
367
+ }
300
368
  try {
301
369
  const result = await this.connectorCall(connectId, "evmGetAddress", {
302
370
  path: params.path,
@@ -320,6 +388,9 @@ var _LedgerAdapter = class _LedgerAdapter {
320
388
  }
321
389
  async evmGetPublicKey(connectId, _deviceId, params) {
322
390
  await this._ensureDevicePermission(connectId, _deviceId);
391
+ if (!await this._verifyDeviceFingerprint(connectId, _deviceId, "evm")) {
392
+ return (0, import_hardware_wallet_core2.failure)(import_hardware_wallet_core2.HardwareErrorCode.DeviceMismatch, "Wrong device connected");
393
+ }
323
394
  try {
324
395
  const result = await this.connectorCall(connectId, "evmGetAddress", {
325
396
  path: params.path,
@@ -335,21 +406,19 @@ var _LedgerAdapter = class _LedgerAdapter {
335
406
  }
336
407
  async evmSignTransaction(connectId, _deviceId, params) {
337
408
  await this._ensureDevicePermission(connectId, _deviceId);
409
+ if (!await this._verifyDeviceFingerprint(connectId, _deviceId, "evm")) {
410
+ return (0, import_hardware_wallet_core2.failure)(import_hardware_wallet_core2.HardwareErrorCode.DeviceMismatch, "Wrong device connected");
411
+ }
338
412
  try {
413
+ if (!params.serializedTx) {
414
+ return (0, import_hardware_wallet_core2.failure)(
415
+ import_hardware_wallet_core2.HardwareErrorCode.InvalidParams,
416
+ "Ledger requires a pre-serialized transaction (serializedTx). Provide an RLP-encoded hex string."
417
+ );
418
+ }
339
419
  const result = await this.connectorCall(connectId, "evmSignTransaction", {
340
420
  path: params.path,
341
- transaction: {
342
- to: params.to,
343
- value: params.value,
344
- chainId: params.chainId,
345
- nonce: params.nonce,
346
- gasLimit: params.gasLimit,
347
- gasPrice: params.gasPrice,
348
- maxFeePerGas: params.maxFeePerGas,
349
- maxPriorityFeePerGas: params.maxPriorityFeePerGas,
350
- accessList: params.accessList,
351
- data: params.data
352
- }
421
+ serializedTx: params.serializedTx
353
422
  });
354
423
  return (0, import_hardware_wallet_core2.success)({
355
424
  v: ensure0x(result.v),
@@ -362,6 +431,9 @@ var _LedgerAdapter = class _LedgerAdapter {
362
431
  }
363
432
  async evmSignMessage(connectId, _deviceId, params) {
364
433
  await this._ensureDevicePermission(connectId, _deviceId);
434
+ if (!await this._verifyDeviceFingerprint(connectId, _deviceId, "evm")) {
435
+ return (0, import_hardware_wallet_core2.failure)(import_hardware_wallet_core2.HardwareErrorCode.DeviceMismatch, "Wrong device connected");
436
+ }
365
437
  try {
366
438
  const result = await this.connectorCall(connectId, "evmSignMessage", {
367
439
  path: params.path,
@@ -376,6 +448,9 @@ var _LedgerAdapter = class _LedgerAdapter {
376
448
  }
377
449
  async evmSignTypedData(connectId, _deviceId, params) {
378
450
  await this._ensureDevicePermission(connectId, _deviceId);
451
+ if (!await this._verifyDeviceFingerprint(connectId, _deviceId, "evm")) {
452
+ return (0, import_hardware_wallet_core2.failure)(import_hardware_wallet_core2.HardwareErrorCode.DeviceMismatch, "Wrong device connected");
453
+ }
379
454
  if (params.mode === "hash") {
380
455
  return (0, import_hardware_wallet_core2.failure)(
381
456
  import_hardware_wallet_core2.HardwareErrorCode.MethodNotSupported,
@@ -399,6 +474,9 @@ var _LedgerAdapter = class _LedgerAdapter {
399
474
  // ---------------------------------------------------------------------------
400
475
  async btcGetAddress(connectId, _deviceId, params) {
401
476
  await this._ensureDevicePermission(connectId, _deviceId);
477
+ if (!await this._verifyDeviceFingerprint(connectId, _deviceId, "btc")) {
478
+ return (0, import_hardware_wallet_core2.failure)(import_hardware_wallet_core2.HardwareErrorCode.DeviceMismatch, "Wrong device connected");
479
+ }
402
480
  try {
403
481
  const result = await this.connectorCall(connectId, "btcGetAddress", {
404
482
  path: params.path,
@@ -423,6 +501,9 @@ var _LedgerAdapter = class _LedgerAdapter {
423
501
  }
424
502
  async btcGetPublicKey(connectId, _deviceId, params) {
425
503
  await this._ensureDevicePermission(connectId, _deviceId);
504
+ if (!await this._verifyDeviceFingerprint(connectId, _deviceId, "btc")) {
505
+ return (0, import_hardware_wallet_core2.failure)(import_hardware_wallet_core2.HardwareErrorCode.DeviceMismatch, "Wrong device connected");
506
+ }
426
507
  try {
427
508
  const result = await this.connectorCall(connectId, "btcGetPublicKey", {
428
509
  path: params.path,
@@ -443,6 +524,9 @@ var _LedgerAdapter = class _LedgerAdapter {
443
524
  }
444
525
  async btcSignTransaction(connectId, _deviceId, params) {
445
526
  await this._ensureDevicePermission(connectId, _deviceId);
527
+ if (!await this._verifyDeviceFingerprint(connectId, _deviceId, "btc")) {
528
+ return (0, import_hardware_wallet_core2.failure)(import_hardware_wallet_core2.HardwareErrorCode.DeviceMismatch, "Wrong device connected");
529
+ }
446
530
  if (!params.psbt) {
447
531
  return (0, import_hardware_wallet_core2.failure)(
448
532
  import_hardware_wallet_core2.HardwareErrorCode.InvalidParams,
@@ -463,12 +547,13 @@ var _LedgerAdapter = class _LedgerAdapter {
463
547
  // ---------------------------------------------------------------------------
464
548
  // Device fingerprint
465
549
  // ---------------------------------------------------------------------------
466
- async btcGetMasterFingerprint(connectId, _deviceId, params) {
550
+ async btcGetMasterFingerprint(connectId, _deviceId) {
467
551
  await this._ensureDevicePermission(connectId, _deviceId);
552
+ if (!await this._verifyDeviceFingerprint(connectId, _deviceId, "btc")) {
553
+ return (0, import_hardware_wallet_core2.failure)(import_hardware_wallet_core2.HardwareErrorCode.DeviceMismatch, "Wrong device connected");
554
+ }
468
555
  try {
469
- const result = await this.connectorCall(connectId, "btcGetMasterFingerprint", {
470
- skipOpenApp: params?.skipOpenApp
471
- });
556
+ const result = await this.connectorCall(connectId, "btcGetMasterFingerprint", {});
472
557
  return (0, import_hardware_wallet_core2.success)({ masterFingerprint: result.masterFingerprint });
473
558
  } catch (err) {
474
559
  return this.errorToFailure(err);
@@ -515,7 +600,10 @@ var _LedgerAdapter = class _LedgerAdapter {
515
600
  clearTimeout(timer);
516
601
  this._deviceConnectResolve = null;
517
602
  if (cancelled) {
518
- reject(new Error("User cancelled Ledger connection"));
603
+ reject(Object.assign(
604
+ new Error("User cancelled Ledger connection"),
605
+ { _tag: "DeviceNotRecognizedError" }
606
+ ));
519
607
  } else {
520
608
  resolve();
521
609
  }
@@ -546,6 +634,17 @@ var _LedgerAdapter = class _LedgerAdapter {
546
634
  if (this._sessions.size > 0) {
547
635
  return this._sessions.keys().next().value;
548
636
  }
637
+ if (this._connectingPromise) {
638
+ return this._connectingPromise;
639
+ }
640
+ this._connectingPromise = this._doConnect();
641
+ try {
642
+ return await this._connectingPromise;
643
+ } finally {
644
+ this._connectingPromise = null;
645
+ }
646
+ }
647
+ async _doConnect() {
549
648
  for (let attempt = 0; attempt < _LedgerAdapter.MAX_DEVICE_RETRY; attempt++) {
550
649
  const devices = await this.searchDevices();
551
650
  if (devices.length > 0) {
@@ -779,7 +878,8 @@ var LedgerDeviceManager = class {
779
878
  return new Promise((resolve) => {
780
879
  let resolved = false;
781
880
  let syncResult = null;
782
- const sub = this._dmk.listenToAvailableDevices().subscribe({
881
+ let sub = null;
882
+ sub = this._dmk.listenToAvailableDevices().subscribe({
783
883
  next: (devices) => {
784
884
  if (resolved) return;
785
885
  resolved = true;
@@ -787,7 +887,12 @@ var LedgerDeviceManager = class {
787
887
  for (const d of devices) {
788
888
  this._discovered.set(d.id, d);
789
889
  }
790
- syncResult = devices;
890
+ if (sub) {
891
+ sub.unsubscribe();
892
+ resolve(devices.map((d) => ({ path: d.id, type: d.deviceModel.id })));
893
+ } else {
894
+ syncResult = devices;
895
+ }
791
896
  },
792
897
  error: () => {
793
898
  if (!resolved) {
@@ -799,7 +904,7 @@ var LedgerDeviceManager = class {
799
904
  if (syncResult !== null) {
800
905
  sub.unsubscribe();
801
906
  const devices = syncResult;
802
- resolve(devices.map((d) => ({ path: d.id, type: d.deviceModel.name })));
907
+ resolve(devices.map((d) => ({ path: d.id, type: d.deviceModel.id })));
803
908
  }
804
909
  });
805
910
  }
@@ -820,7 +925,7 @@ var LedgerDeviceManager = class {
820
925
  name: d.name
821
926
  }));
822
927
  if (!previousIds.has(d.id)) {
823
- onChange({ type: "device-connected", descriptor: { path: d.id, type: d.deviceModel.name } });
928
+ onChange({ type: "device-connected", descriptor: { path: d.id, type: d.deviceModel.id } });
824
929
  }
825
930
  }
826
931
  for (const id of previousIds) {